- Be concise and direct - Avoid repetitive explanations
- Focus on actions, not discussions - Show progress through tool use, not lengthy descriptions
- Skip conversational phrases - No "Great!", "Certainly!", "Okay!" - get straight to the point
- One clear message per concept - Don't repeat the same information in different ways
- Use a concise Conventional Commit-style PR title:
type(scope): imperative summary, or omit the scope when none adds useful context. - Describe the final branch state, not the sequence of commits used to reach it.
CRITICAL: These commands MUST pass before any implementation is considered complete:
Exception: if the change is markdown/docs-only and does not modify Go code, generated manifests,
Helm/chart behavior, Taskfiles, CI, shell scripts, or any executable/test configuration, you do
not need to run the full validation suite below. In that case run task lint-docs, which is the
whole gate for such a change, and follow docs/style-guide.md.
Run the e2e commands sequentially, not in parallel!
task lint # Must pass golangci-lint checks
task test # Must pass all unit tests + the coverage ratchet (see TESTING REQUIREMENTS)
task test-e2e # Must pass end-to-end teststask lint also runs actionlint on every workflow under .github/workflows/ (via the
lint-actions task) and hadolint on the Dockerfiles (via lint-dockerfiles), so a
workflow or Dockerfile change is covered by the normal lint gate; you can also run
task lint-actions or task lint-dockerfiles directly. actionlint, hadolint, and
golangci-lint all ship in the devcontainer image.
trivy ships there too, behind task scan-image — the same command CI runs to gate the
built image, so a scan failure can be reproduced locally instead of by pushing:
task scan-image SCAN_ARCHIVE=project-image.tar # a docker-archive tarball
task scan-image SCAN_IMAGE=ghcr.io/example/img:tag # or an image referenceIt is not part of task lint: it needs an image to scan, which a lint run has no reason
to build. Suppressions live in .trivyignore.yaml, each with a
justification and an expiry date. A devcontainer built before trivy was added to the
image does not have it — rebuild the container if the task reports it missing.
It also runs the documentation checks via task lint-docs, which is three tasks:
lint-doc-links (hack/doccheck, every tracked file), lint-markdown (markdownlint-cli2,
every tracked file), and lint-prose (Vale, against
docs/style-guide.md). Only prose gates a subset — the files
.docs-lint-scope lists — so editing an unlisted document fails no prose
check; run vale on what you touch anyway.
task lint-markdown-fix applies the mechanical half. Both tools ship in the devcontainer image.
See Documentation checks.
- Check Docker availability for e2e tests: Before running
task test-e2e, verify Docker is running withdocker infoor ask user to start Docker daemon if needed - Always read project context first: Use
read_fileto understand existing patterns in target directories - Search for similar implementations: Use
search_filesto find existing patterns before writing new code - Follow established architecture: Maintain consistency with
internal/directory structure
- Follow Go naming conventions and add godoc comments for exports
- Maintain 120-character line limit (enforced by
.golangci.yml) - Use consistent error handling patterns from existing codebase
- Cover new code with tests; total coverage must not regress (see TESTING REQUIREMENTS)
- Write table-driven tests where appropriate
- Follow kubebuilder patterns and annotations
- Implement idempotent reconciliation logic
- Add appropriate RBAC markers
- Handle finalizers for cleanup
- Implement admission webhook interface correctly
- Add proper validation/mutation logic
- Update webhook configuration in
config/webhook/
- Add kubebuilder validation tags
- Include JSON tags and field descriptions
- Run
task manifeststo update CRDs - Test CRD installation and usage
- Keep CRD descriptions to the contract. A doc comment on a type or field becomes the
kubectl explaintext, so it should say what the field does, what its values mean, and the gotchas — not why it was designed that way. Put design rationale in a comment block separated from the doc comment by a blank line: Go does not treat it as the doc comment, so controller-gen leaves it out of the schema while it stays in the source. - Where that block goes matters, and getting it wrong fails silently. On a FIELD, put it above
the doc comment. On a TYPE, put it above the
+kubebuilder:marker block, never between the markers and the doc — that displaces the markers and they are dropped without any error (a+kubebuilder:resource:scope=Clusterwas silently lost this way, flipping a CRD to Namespaced). After editing API comments, confirm only descriptions moved: regenerate into a scratch dir withcontroller-gen crd paths=./api/... output:crd:artifacts:config=<dir>, then compare againstconfig/crd/baseswith everydescriptionkey stripped from both.
- Handle Git errors gracefully
- Implement proper conflict resolution
- Add race condition protection
- Use temporary directories for testing
- After editing any workflow, run
task lint-actionsto catch errors withactionlintbefore pushing - A workflow-only change still counts as a CI/config change, so it is not covered by the markdown/docs-only validation exception
- Cover new code with tests (both positive and negative cases)
- Add integration tests for complex workflows
- Follow naming convention:
TestFunctionName_Scenario(t *testing.T)
task testrunscover-check, a self-ratcheting gate: it fails if total unit coverage drops more than a small tolerance below.coverage-baseline(a committed high-water mark).- When coverage improves,
cover-checkauto-raises.coverage-baseline. Commit the bumped file so the floor advances; otherwise the change is just discarded. - e2e coverage of the deployed controller:
E2E_COVERAGE=1 task test-e2ethentask e2e-coverage-collect(writese2e-cover.out). - On PRs, Codecov reports the merged
unit+e2ecoverage (codecov.yml); its project status is a non-regression ratchet (compared to the base commit).
- Update README.md for user-facing changes
- Add/update godoc comments for all exports
- Update API documentation if modifying webhook behavior
- Update API documentation if modifying CRDs
Nobody reads an upgrade guide before the version exists. Write every entry in the present
tense, describing the release the reader is upgrading to as something that already is:
"schemaVersion is gone; apiVersion replaces it", never "will be removed" or "now
gains". Do not mark an entry Unreleased or (next minor) — by the time it is read, both
are false, and a heading that says "Unreleased" about a change shipped six versions ago is
worse than no heading. Name what changed, not when it is going to.
For markdown/docs-only edits, skip this full sequence unless the documentation change depends on or describes behavior you also changed in code/config during the same task.
task fmt- Format codetask generate- Update generated code (if needed)task manifests- Update CRDs (if API changes)task vet- Run go vettask lint- Run golangci-lint (MANDATORY)task test- Run unit tests (MANDATORY)task test-e2e- Run e2e tests (MANDATORY)
- If
task lintfails: Runtask lint-fixfirst - If tests fail: Fix issues; keep total coverage at or above
.coverage-baseline(commit the bump when it auto-raises) - If e2e fails: Check k3d cluster setup and Docker availability
- If Docker not available: Ask user to start Docker daemon before running e2e tests
- Contributing guide:
CONTRIBUTING.md - In the devcontainer, agents may use
ghin read-only mode when the repo-root.envsetsGH_TOKEN. That.envfile is optional, local-only, and must never be committed.