Skip to main content

Development workflow

Use this guide to take a change from a fresh checkout to a verified pull request. Start at the documentation index for the application's context and relevant requirements and decisions. Follow the project conventions for engineering rules and the documentation workflow when updating application knowledge alongside the implementation.

Taskfile.yaml defines the executable workflows. Task runs commands; mise manages the project tools and their locked versions. This guide explains which commands to use, their scope, and their prerequisites.

Environment setup​

Install Go and mise using the Go version from go.mod and the minimum mise version from etc/mise.toml. Install Task separately using its installation instructions. Ensure these executables are on PATH, and install Docker for container workflows.

From the repository root, install the project tools:

task setup

This installs all tools locked in etc/mise.lock, including gopls. It also syncs Vale styles and downloads Go modules. Task is an external prerequisite.

In CI, the local setup-tools action installs mise using min_version from etc/mise.toml. It also defines the pinned Task version and installs Task when requested. It configures mise's project paths and caches the project's tools at job completion, including tools installed by later Task steps. Cache entries distinguish selected tools, setup tasks, operating systems, and architectures. Jobs select the mise tools they need; the prepare-env action uses it before running task setup for a full development environment.

The MCP configuration runs the local servers through task tool -- gopls mcp, task tool -- go42x mcp, and task tool -- mise mcp. Start these commands from the repository root to use the project tool versions and environment configured by mise.

Mise MCP exposes tool versions, installation status, environment values, and active configuration files. Its server configuration sets MISE_EXPERIMENTAL=1. Workflows remain in Taskfile; no mise tasks are defined, so the MCP run_task tool cannot execute Taskfile commands directly.

GitHub MCP uses GitHub's hosted HTTP server at https://api.githubcopilot.com/mcp/. Export GITHUB_TOKEN with your PAT in the MCP client's environment; the configuration uses it for bearer authentication and sets X-MCP-Toolsets: all. After changing the MCP configuration, run task x -- agentenv generate and restart the client.

For code generation, task setup:generators installs the generators and their supporting tools from the same lockfile. Run it before task generate to prepare only the tools needed for generation.

For documentation, task setup:docs installs Node.js, Markdownlint, and Vale from the lockfile, then syncs the pinned Vale styles. Run it before task docs:check to prepare only the documentation tools.

Create .env from .env.example if it does not already exist, then adjust the local settings:

if [ ! -f .env ]; then
cp .env.example .env
fi

Task supplies mise's project paths, so Task commands work without shell exports. Tasks invoke managed tools with mise exec --, which also makes the project tools available to child processes. Existing exported mise settings override Taskfile defaults. Go commands use the installed go executable with GOTOOLCHAIN=local. Generation runs through mise so it can find the pinned generators.

Invoking tools directly​

For direct tool calls from a shell or editor, configure the project paths from the repository root:

export MISE_DEFAULT_CONFIG_FILENAME=etc/mise.toml
export MISE_DATA_DIR="$PWD/.tools"
export MISE_CACHE_DIR="$MISE_DATA_DIR/cache"
export MISE_STATE_DIR="$MISE_DATA_DIR/state"
export PATH="$MISE_DATA_DIR/shims:$PATH"

The tool shims load applicable mise settings, including Redocly's update-notice and telemetry settings.

Using Task​

Use group:name for related commands: test:unit, fmt:yaml, lint:go, docs:check, and setup:generators. Keep standalone commands short, such as run, build, generate, and setup. Use hyphens within a name for multiple words, such as generate:migration-id and lint:openapi-breaking. Give each task one name; update its callers and documentation when renaming it.

Run commands from the repository root. Discover tasks and inspect their behavior with:

task help
task --list --sort none --json
task fmt:sql --summary
task --dry test:resilience

task and task help list commands in Taskfile order. Tasks run without caching. Pass file or local Go package arguments after --; put Task variables before that separator:

task fmt:yaml -- .github/workflows/150-load-tests.yaml
task lint:yaml -- Taskfile.yaml .github/workflows/150-load-tests.yaml
task lint:go -- ./internal/metrics
task test:unit -- ./internal/metrics

Use task tool -- COMMAND [ARGS...] to run a command with mise's project versions and environment. Pass the executable name and its arguments after --; the task preserves standard input and output:

task tool -- gopls version
task tool -- node --version
task tool -- go42x kwb build

Formatters require existing individual files. Linters that accept file arguments also reject directories, literal globs, and tool flags. Quote paths containing spaces. Go package arguments use local paths such as ./internal/metrics or ./internal/auth/.... A lint:* task without arguments checks its default project scope; task lint accepts no file or package arguments.

Updating go42x​

After publishing the go42x release and its matching MCP image, update to the latest available release from the repository root:

task x:update

To select a specific release:

task x:update VERSION=1.2.3

The task exports the installed binary's original assets before updating the mise pin and lockfile. Without VERSION, mise resolves the latest release to a concrete version. The new binary's agentenv update --base command uses Git to merge its embedded configuration, templates, and instruction chunks with project customizations and the old export. Upstream references and their vendoring manifest and lockfile come from the release together. Additional project files and local overrides remain in place. The old export must match the release from which the current assets originated; reconcile assets refreshed from source or another version first.

The update command prepares every merge and validates configuration before applying assets. Export and installation failures leave assets unchanged. The task restores the previous mise pin and lockfile when installation fails or the update reports no applied files. The printed .build/go42x-update.* workspace holds the old export, mise files, and update report. Merge errors print candidates under .build/agentenv-update-*/merged/. Reconcile conflicting project customizations and rerun the task. Conflicts include modifying a file deleted upstream or deleting a file changed upstream. Unchanged files deleted locally stay deleted. The installed version must support agentenv export, and the target version must support agentenv update --base; the first upgrade from an older version requires manual reconciliation.

After applying assets, the task generates agent configuration and the gh-aw MCP fragment with the installed release, compiles and lints workflows, and rebuilds the knowledge base. Partial application or an unrecognized failure retains the new pin. For an interrupted write, recover originals from the backup directory identified in the update report under .go42x/backups/updates/; its files/ directory uses project-relative paths. For a generation or lint failure, fix the reported problem and rerun the task. Review all changed files together and restart MCP clients. The task neither commits changes nor pulls an image. Workflow sources, preparation actions, and Taskfile definitions remain separately maintained.

To inspect the running binary's original bundle without updating the project:

task x -- agentenv export --output .build/original-go42x-bundle

The destination must not exist. Export works independently of project configuration and does not modify .go42x or .gitignore.

Go navigation and diagnostics​

The generated agent instructions use the configured gopls tools for Go navigation and feedback during editing:

  1. Before changing a function signature, shared type, or interface, inspect symbol references and read affected callers, implementations, and tests. Request file context or a package's public API when needed to understand unfamiliar code.
  2. After a coherent batch of saved Go edits, request diagnostics for the changed files. Investigate relevant findings, fix errors introduced by the change, and check again after fixes.
  3. Run the applicable lint and test commands before completing the change. Include affected callers when selecting packages; diagnostics supplement those checks.

The configured standalone gopls server reads saved files. Its results reflect the loaded workspace and build configuration; review affected build tags and platforms separately. If gopls is unavailable, use local source inspection and the project's lint and test commands, and report the limitation.

Edit the authored search guidance to change this workflow, then run task x -- agentenv generate to refresh the generated instructions.

Refreshing project knowledge​

The AI preparation action refreshes the knowledge base after generating agent configuration. The preparation actions are maintained in go42x; this blueprint keeps local copies so workflows use files from their own repository. The Copilot setup workflow follows the same approach, passing providers: copilot to generate its configuration once before indexing. Use the same order from the repository root when preparing a local session:

task x -- agentenv generate
task x -- kwb build

The project exclusions omit generated Swagger JavaScript bundles while retaining handwritten JavaScript. The knowledge base also indexes authored .go42x configuration, instruction templates, vendored Markdown, and .env.example. Run task x -- kwb check --json to verify freshness without updating the index. It exits 0 for a complete, fresh scan and 1 for stale, unavailable, or incomplete results. Build again to apply source or exclusion changes. The scan uses recorded build settings and current ignore files.

The go42x MCP server supports --context-doc=project,conventions,documentation to load these authored guidance IDs. Configure it in the MCP configuration, regenerate agent output, and restart the client when changing the server's arguments. Context requests read supplied files directly. Directory paths select documentation impact; use knowledge-base search and gopls to discover source declarations.

GitHub Agentic Workflows​

The PR description workflow updates a generated summary when a non-draft pull request changes. The agent request workflow responds to /agent comments on issues and pull requests from contributors with write, maintain, or admin access. Questions receive a comment; implementation requests produce a draft pull request or update the triggering same-repository branch. Fork pull requests receive comments. The GitHub configuration reference lists required and optional workflow variables and secrets. Initial repository setup belongs to the blueprint adoption instructions.

Keep workflow Markdown in .github/workflows/9XX-aw-*.md. Edit workflow triggers, permissions, engine, and task prompts there. The shared MCP definitions come from go42x configuration; each local server keeps its command and an optional container definition for gh-aw. Refresh and verify from the repository root:

task agentic:compile
task agentic:lint
task lint:actions

agentic:generate runs the mise-installed go42x to generate shared instructions and 900-aw-mcp.md with the exact provider list gh-aw. agentic:compile runs generation and the pinned gh-aw compiler. agentic:lint checks the compiled workflows. Review and commit the Markdown sources, generated MCP fragment, .lock.yml files, and action resolution lock together. The separate agentic-lint CI job repeats generation, compilation, and linting and fails if they leave changes. Plain YAML formatting excludes compiler-owned workflow locks; Actionlint and Zizmor still check them. The .gitattributes entry marks these files as generated in GitHub's diff view.

At runtime, each workflow uses the local AI preparation action to install the full development toolset, generate instructions, and index the checkout. An empty providers input generates shared instructions only; gh-aw supplies its own runtime configuration from the committed workflow. pull-mcp-image: true pulls the tools image from mcp-image-repository, which defaults to ghcr.io/go42-dev/go42x. The action selects the release matching the go42x version chosen by mise and resolves its immutable digest for the MCP containers. Override the repository input when using another GHCR package. Upgrade the mise pin only after its matching image has been published. Local MCP commands continue to run through Task.

The workflows compile with the Codex engine. See the model and budget controls for runtime configuration. Changing the engine requires editing and recompiling the workflow source.

Running and debugging​

The launch tasks require .env through task check:env. They load defaults from .env.example, then local overrides from .env. Start the services required by the selected database, cache, and event settings.

CommandBehavior
task runRun the application locally with race detection and compiler optimizations disabled
task run:dockerRun the application in a Linux Go container with the checkout mounted
task debugStart the headless Delve debugger on port 2345
task buildBuild .build/app with race detection and compiler optimizations disabled
task imageBuild and load a cached development image for the host platform

task run and task run:docker report command failures as task failures.

Pass application arguments after -- to run, run:docker, and debug, and additional Go build flags to build. The image task accepts Docker build flags after --; set IMAGE and VERSION before the separator:

task build -- -tags=development
task image IMAGE=ghcr.io/go42-dev/go42:dev VERSION=dev -- --no-cache

For optional inspection tools, task grpcui requires grpcui and targets plaintext localhost:50051. Enable SERVER_GRPC_REFLECTION_ENABLED=true on the local test application and restart it first; reflection is disabled by default. Business calls still require x-api-key metadata and permissions. The generated integration clients use compiled descriptors and do not need reflection. task generate:dep-graph writes the dependency graph and requires goda and Graphviz's dot. These dependencies are described in Taskfile comments and are installed separately.

Profiling a local process​

Use a host process launched by task run, task debug, or a direct binary for this procedure. Set PPROF_ENABLED=true, PPROF_LISTEN=127.0.0.1:6060, and PPROF_PREFIX=/debug/pprof in its configuration, then restart it. The profiling server is separate from the main HTTP listener and has no application authentication. Use loopback access for local collection and retain profiles as private diagnostic artifacts.

From another terminal, capture a heap sample and ten seconds of CPU activity:

mkdir -p .build
curl --fail --silent --show-error --max-time 15 \
http://127.0.0.1:6060/debug/pprof/heap -o .build/heap.pprof
curl --fail --silent --show-error --max-time 25 \
'http://127.0.0.1:6060/debug/pprof/profile?seconds=10' -o .build/cpu.pprof
go tool pprof -top .build/heap.pprof
go tool pprof -top .build/cpu.pprof

Expect nonempty profile files and a function/sample summary. Exercise the affected workload during CPU collection; an idle sample may contain little evidence. Record the running revision and symptom with the profile, and use a matching binary for source or disassembly analysis. Adjust the URL port if the listener differs, but keep the default prefix: the current custom-prefix handler returns HTML for named profiles such as heap. This recipe does not apply unchanged to task run:docker, whose port mapping expects :port listener values. Disable local profiling again when finished. Its endpoint availability does not establish main-server readiness.

Generated files and dependencies​

Edit source definitions or generator configuration, then run task generate when the change affects generated content. It tidies Go modules, regenerates APIs and mocks, generates .env.example through cmd/cfg2env, and combines the OpenAPI definitions. Review the resulting diff and commit source changes and corresponding tracked outputs together. After setup, running task generate from a clean checkout must produce no changes.

Generated outputs include api/gen/, package mocks/ directories, .env.example, and the combined OpenAPI document. Documentation assembly owns pages/docs/, and the website build owns pages/build/; regenerate those through the documentation commands. Dependency tools own lockfiles such as etc/mise.lock and pages/package-lock.json.

Vendored dependencies use a .vendor.yaml manifest and .vendor.lock.yaml beside their files:

ManifestContents
.go42x/.vendor.yamlGo and Google style references
api/proto/third_party/.vendor.yamlProtovalidate definitions
static/swagger/.vendor.yamlSwagger UI and themes

Run task vendor:check to verify all three against their locks. Run task vendor:sync to restore locked files without selecting newer versions. To upgrade one dependency, edit its source version or URL and checksum in the relevant manifest, then update that manifest, for example:

task x -- vendor update --manifest static/swagger/.vendor.yaml
task vendor:check

Review the changed manifest, lock, and files together. External Proto files and published npm archives use exact release URLs and SHA-256 checksums. Regenerate affected outputs with task generate after changing API definitions. CI checks vendor locks before code generation. Follow tool maintenance when changing tool pins or configuration.

For a new database migration, run task generate:migration-id once to obtain its filename prefix. Use the same filename across database engines and follow the migration conventions.

Implementing a feature​

Use the source map to trace an existing operation with similar behavior. Confirm the desired outcome and failure cases in the relevant requirement; record a significant new choice in a decision when needed.

  1. Define the transport contract in OpenAPI or Protobuf. Specify validation, authorization, response fields, and error behavior.
  2. Implement service inputs and errors in the feature's domain/ package, persistence models and migrations when needed, and repository operations. Keep transaction boundaries in the service and propagate txCtx to required outbox writes.
  3. Update consumer interfaces and implement the service operation. Decide which failures roll back the operation and which side effects can be retried independently. Cover those outcomes in service and repository tests.
  4. Implement each affected adapter. HTTP routes and request/response types are handwritten in adapter.go and models.go. Generating an HTTP SDK does not add a running route. For gRPC, implement the generated service interface and update adapterPermissionMapping. With gRPC authorization enabled, methods without registered permissions are denied.
  5. Register a new adapter or dependency in cmd/app. RegisterV1 supplies the HTTP /api/v1 prefix; the gRPC adapter registers its generated service. For new background work, define its startup and shutdown owner.
  6. Run the generation workflow. A new HTTP contract also needs generator directives in api/generate.go; the existing directives name the authentication contract explicitly.
  7. Test the service and transport behavior, including validation, missing permissions, and dependency failure. When both transports expose an operation, verify both mappings. Run applicable API compatibility checks before review.
  8. Update the owning handbook page, requirement evidence, and any affected operating instructions with the change.

For persisted-data or message-format changes, include the migration handoff.

For the existing authentication feature, a focused unit check is:

task test:unit -- ./internal/auth/... ./internal/api/...

The HTTP integration clients exercise both generated HTTP SDKs; gRPC integration tests exercise the generated gRPC client. Follow the integration prerequisites before running task test:integration.

Formatting and linting​

Format only edited source files and review the diff; linters check without applying fixes. In the commands below, FILE is an existing file and PACKAGE is a local Go package or subtree. Use task TASK --summary for usage.

Source typeFormatterCheck
Gotask fmt:go -- "$FILE"task lint:go -- "$PACKAGE"
Plain YAML, including workflowstask fmt:yaml -- "$FILE"task lint:yaml -- "$FILE"
JSONtask fmt:json -- "$FILE"task lint:json -- "$FILE"
TOMLtask fmt:toml -- "$FILE"task lint:toml -- "$FILE"
Markdowntask fmt:markdown -- "$FILE"task lint:markdown -- "$FILE" and task lint:prose -- "$FILE"
Protobuftask fmt:proto -- "$FILE"task lint:proto checks the entire api module
SQL migrationstask fmt:sql DIALECT="$DIALECT" -- "$FILE"task lint:sql DIALECT="$DIALECT" -- "$FILE"

For SQL, use DIALECT=sqlite for migrate/sqlite/, mysql for migrate/mysql/, and postgres for migrate/pgsql/. Explicit files require a dialect. With no files, task lint:sql checks all three; adding DIALECT selects one.

YAML, JSON, and TOML checks default to tracked files and unignored untracked files, skipping deleted files. YAML skips Helm templates, combined OpenAPI output, and compiled workflow locks; JSON skips package-lock.json; TOML selects *.toml. Markdown and prose default to docs/. Let the owning tools update lockfiles. Markdown formatting excludes vendored references, including when explicit files are passed.

Go formatter settings control line length and import grouping; use go mod edit -fmt for go.mod. TOML tasks load etc/tombi.toml, use offline schemas, and treat lint warnings as errors. Markdown formatting fixes supported issues; remaining diagnostics need manual edits.

JavaScript, TypeScript, CSS, shell, and Dockerfiles have no configured formatter. Follow the surrounding style and .editorconfig; check edited files with task lint:editorconfig -- "$FILE". Checker exceptions live in etc/editorconfig.json: language formatters handle indentation, and vendored upstream snapshots are excluded.

Checks for specific purposes​

ChangeAdditional checks
Workflow YAMLtask lint:actions -- "$FILE" runs Actionlint and Zizmor; omit files for composite actions or shared workflow interfaces
OpenAPItask lint:openapi and task lint:openapi-breaking
Protobuftask lint:proto-breaking, in addition to the full-module checks above
Dockerfiletask lint:docker
Helmtask lint:helm checks tag and digest image references; format only plain chart YAML, preserving Go template syntax
Standalone shelltask tool -- shellcheck "$FILE"

Regenerate affected outputs before API compatibility checks and ensure origin/master is available.

Editor watchers​

Run Task-based watchers from the repository root on the edited file. Make Go and Task available on the editor's PATH and run task setup first.

  • JetBrains: For a Markdown watcher, use program task, arguments fmt:markdown -- "$FilePath$", and working directory $ProjectFileDir$. The shared definitions use project shims.
  • VS Code: Install Run on Save and merge the preset into .vscode/settings.json.

The VS Code preset runs Task commands in sequence, formats Go before package linting, selects SQL dialects, and skips Helm templates for YAML formatting. Per-project .idea/ and .vscode/ settings are ignored.

Keep tool calls aligned with Taskfile and the tool environment. Markdown calls need --no-globs for file-specific fixes; Tombi needs etc/ as its working directory and absolute file paths.

Testing and verification​

Use the testing guide to select suites, prepare dependencies, and check results.

Integration test environment​

Follow integration setup for application settings, test credentials, and backend requirements.

Choosing checks before review​

Follow the review checks and report commands, results, backends, and skips.

Preparing a pull request​

Use Conventional Commits with the types and rules in etc/commitlint.yaml. Branch names must match ^[A-Za-z0-9/_.-]+$, be descriptive, and include a task identifier when applicable. Prefer merge commits over rebasing, and keep rebase disabled. Use .gitkeep to preserve empty directories in Git and the gh client to access GitHub resources. Use Semantic Versioning for releases; follow Release for publishing artifacts.

Destructive operations require user confirmation in interactive mode. In non-interactive mode, allow them only when explicitly requested by the user.

Before opening or updating a pull request:

  1. Review the final diff, including generated outputs. Update affected documentation through its change workflow.
  2. Use the pull request template for PRs created through the web interface or tools. Describe the problem and resulting behavior, with a brief before/after example when useful. Keep the title and description proportional to and aligned with the final change.
  3. Link relevant issues, requirements, decisions, and project documentation. Keep durable rationale and operating instructions in the project documentation.
  4. In Verification, report checks actually performed and their results, including commands, suites, backends, and relevant manual checks. Distinguish completed checks from planned checks, identify anything left unverified, and explain material skips.
  5. Add a Deployment section when adopting the change requires action. Describe relevant migrations, configuration or compatibility changes, rollout requirements, and rollback limitations. Link the relevant deployment instructions.

Maintaining the workflow​

SettingSource
Go versiongo.mod
Tool versions and platform locksetc/mise.toml and etc/mise.lock
CommandsTaskfile.yaml
Formatter and linter configurationetc/

To change a mise-managed tool, select its key from etc/mise.toml and the required version:

task bump TOOL=node VERSION=26.7.0

TOOL and VERSION are required. The task installs the version and updates its exact pin and lock entries for the existing platforms. Review both files together and align any duplicate CI pins.

CI uses the same tool pins and lockfile. Review the environment action and affected workflows when changing setup or dependencies. Adopt shared action changes from go42x into the local copies after reviewing repository-specific customizations. Update Task definitions, CI callers, and this guide together when commands, prerequisites, or check scopes change. Fuzz and load steps need --exit-code to propagate failures.

Upstream reference: Task guide.


Source: docs/handbook/development.md. Revision: 3841815ceb9e.