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:
- 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.
- 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.
- 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.
| Command | Behavior |
|---|---|
task run | Run the application locally with race detection and compiler optimizations disabled |
task run:docker | Run the application in a Linux Go container with the checkout mounted |
task debug | Start the headless Delve debugger on port 2345 |
task build | Build .build/app with race detection and compiler optimizations disabled |
task image | Build 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:
| Manifest | Contents |
|---|---|
| .go42x/.vendor.yaml | Go and Google style references |
| api/proto/third_party/.vendor.yaml | Protovalidate definitions |
| static/swagger/.vendor.yaml | Swagger 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.
- Define the transport contract in OpenAPI or Protobuf. Specify validation, authorization, response fields, and error behavior.
- 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 propagatetxCtxto required outbox writes. - 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.
- 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.
- Register a new adapter or dependency in cmd/app.
RegisterV1supplies the HTTP/api/v1prefix; the gRPC adapter registers its generated service. For new background work, define its startup and shutdown owner. - Run the generation workflow. A new HTTP contract also needs generator directives in api/generate.go; the existing directives name the authentication contract explicitly.
- 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.
- 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 type | Formatter | Check |
|---|---|---|
| Go | task fmt:go -- "$FILE" | task lint:go -- "$PACKAGE" |
| Plain YAML, including workflows | task fmt:yaml -- "$FILE" | task lint:yaml -- "$FILE" |
| JSON | task fmt:json -- "$FILE" | task lint:json -- "$FILE" |
| TOML | task fmt:toml -- "$FILE" | task lint:toml -- "$FILE" |
| Markdown | task fmt:markdown -- "$FILE" | task lint:markdown -- "$FILE" and task lint:prose -- "$FILE" |
| Protobuf | task fmt:proto -- "$FILE" | task lint:proto checks the entire api module |
| SQL migrations | task 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
| Change | Additional checks |
|---|---|
| Workflow YAML | task lint:actions -- "$FILE" runs Actionlint and Zizmor; omit files for composite actions or shared workflow interfaces |
| OpenAPI | task lint:openapi and task lint:openapi-breaking |
| Protobuf | task lint:proto-breaking, in addition to the full-module checks above |
| Dockerfile | task lint:docker |
| Helm | task lint:helm checks tag and digest image references; format only plain chart YAML, preserving Go template syntax |
| Standalone shell | task 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, argumentsfmt: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:
- Review the final diff, including generated outputs. Update affected documentation through its change workflow.
- 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.
- Link relevant issues, requirements, decisions, and project documentation. Keep durable rationale and operating instructions in the project documentation.
- 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. - Add a
Deploymentsection 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
| Setting | Source |
|---|---|
| Go version | go.mod |
| Tool versions and platform locks | etc/mise.toml and etc/mise.lock |
| Commands | Taskfile.yaml |
| Formatter and linter configuration | etc/ |
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.