Files
prompts/TODO.md
sneak dcf75f6c72
All checks were successful
check / check (push) Successful in 22s
Close three gaps between the containerised-lint rule and its first adopters
The rule landed in 12e8db8 is right; these are the three places where the
canonical text and the repos implementing it can diverge without either
side looking wrong.

1. `.dockerignore` excluding the agent scratch directory is now stated as a
   correctness precondition of containerised linting rather than a
   context-size measure. `Dockerfile.lint` lints whatever `COPY . .` copies,
   and language toolchains discover files by walking the tree instead of
   reading `.gitignore`, so a nested worktree in the context puts the
   foreign-tree false reds back inside the container — in the convincing
   form, where the findings are real but belong to another checkout.
   sneak/quak measured the same discovery mechanism taking a test count
   from 210 to 1050.

2. The cache-bust build arg is fixed at `CHECK_EPOCH` in `Dockerfile.lint`
   as well as in `Dockerfile`. A per-file name is invisible to the grep that
   proves every build is busted, which makes a renamed guard and a missing
   guard read identically. sneak/quak's lint file currently names it
   `LINT_EPOCH`.

3. The formatting check must run in exactly one of the two images, and
   either placement is allowed. Splitting lint out of the `Dockerfile` is
   precisely the moment `fmt-check` gets dropped from both, and running the
   formatter beside the linters is the better shape wherever it is the same
   pinned dependency — it takes the last host toolchain off the checked
   path for the reason the linter came off it.

Both checklists carry the matching items, since a repo that satisfies the
policy prose but not the checklist is the drift this is meant to stop.

Refs #40
2026-08-10 12:59:44 +00:00

12 KiB

Workflow

  • branch (from main)
  • do the work in Next Step
  • move Next Step to the top of Completed Steps
  • move the top item of Future Steps into Next Step
  • commit (TODO.md changes in the same commit as the work)
  • merge to main if the branch is not protected, otherwise open a PR
  • push

Status

pre-1.0

Next Step

Finish the two draft prompt documents in the working tree and commit them: prompts/FIXUP_CLEAN.md (currently a near-empty stub) and prompts/FIXUP_REPORT.md (a rough draft). Write the missing content, run make fmt so they pass fmt-check, and commit.

Completed Steps

  • 2026-08-10: Closed three gaps the containerised-lint rule left between the canonical text and the first repos to implement it. .dockerignore excluding the agent scratch directory is now stated as a correctness precondition of that rule rather than a context-size measure: the lint image lints whatever COPY . . copies, and toolchains discover files by walking the tree instead of reading .gitignore, so a nested worktree puts the foreign-tree false reds back inside the container — sneak/quak measured the same discovery mechanism taking a test count from 210 to 1050. The cache-bust arg is fixed at CHECK_EPOCH in Dockerfile.lint as well, because a per-file name is invisible to the grep that proves every build is busted, making a renamed guard indistinguishable from a missing one. And the formatting check is now required to run in exactly one of the two images, with either placement allowed: splitting lint out of the Dockerfile is precisely when fmt-check gets dropped from both, and running the formatter beside the linters is the better shape where it is the same pinned dependency.
  • 2026-08-10: Moved every lint run into a container, on the owner's ruling, and made this repo do it rather than merely document it. script/lint is now docker build -f Dockerfile.lint . and nothing else; the linter is never installed on the host and never invoked there, so a run cannot inherit another checkout's content-keyed result cache, the host-global $TMPDIR/golangci-lint.lock, or a host toolchain that differs from the pinned one — the three mechanisms behind a confirmed false green, a string of findings reported against other agents' checkouts, and a container that saw thirteen findings the host missed. Linting runs as a build step, so a successful build is a clean lint, which also works where the docker daemon is remote and bind mounts are impossible. The recursion this creates is resolved by direction rather than by detection: the main Dockerfile runs the individual non-lint checks instead of make check, and script/cibuild runs script/lint first, so no build ever nests a build. Dockerfile.lint carries the same CHECK_EPOCH guard as the main image, with the ARG below the dependency layer so only the lint steps re-run — blanket --no-cache was rejected because it makes every lint reinstall its dependencies over the network. Two canonical forms were superseded rather than left standing beside the new one, since consuming repos read this document literally: the script/bootstrap golangci-lint install (nothing runs a host linter now, so it can only reintroduce skew; the version-enforcement principle stays documented for other pinned host tools) and the per-checkout cache/lock/.lint-cache wrapper (its whole subject was making a host run trustworthy). The Go multistage lint stage goes with them: it ran make lint, which is now a docker build. golangci-lint config verify was kept on measurement, not preference — a bogus config key passes golangci-lint run with 0 issues and fails config verify, and every case reproduced byte-identically under docker run --network none, so the schema is embedded in the pinned binary and the line costs no network. Verified with two consecutive runs on an unchanged tree both executing the linter, a planted violation caught and reverted, the bare-build guard firing, and the main image building without attempting a nested build.
  • 2026-08-09: Made a golangci-lint result belong to the tree that asked for it. REPO_POLICIES.md now carries the canonical Go script/lint, which gives the linter per-checkout GOLANGCI_LINT_CACHE and per-checkout TMPDIR. The two are separate defects and the second is the one that gets dropped: the result cache is keyed on file content rather than location, so checkouts holding identical files serve each other's findings under the other's path, while the concurrency lock is $TMPDIR/golangci-lint.lock — host-global, independent of the cache, and unaffected by isolating it. Moving workers from worktrees to their own clones does not help either half; it only removes the foreign-path artefact that made the defect visible. The lock error is retried rather than surfaced, because it is not a result: it exits non-zero exactly as findings do, and reporting it as findings sends a correct branch back for rework. Detection is on the stderr stream and never on exit status, so a finding quoting the lock message in source cannot be retried away, and exhaustion exits 75 with a VOID message rather than passing or failing quietly. --allow-serial-runners (which keeps the guard and queues) covers the same-checkout overlap that TMPDIR scoping cannot; --allow-parallel-runners is rejected outright. The stdout and stderr capture files are per invocation rather than per checkout, because serialising the linter does not serialise the shell's redirections: two runs in one checkout — the overlap the flag exists to support — would otherwise truncate and read each other's output, which is the same defect one layer above where it was fixed. Both checklists gained the corresponding items, since a half-fix that sets only the cache reads as complete. GOCACHE was measured and does not need isolating. Verified with the snippet extracted from the committed document and executed as a consuming repo would adopt it, against paired controls: contamination reproduced on the pre-fix form and absent on the adopted one, retry engaged, exhaustion loud, a genuine finding still reported, and a held host lock failing the pre-fix script while leaving the adopted one untouched.
  • 2026-08-09: Kept in-repo agent scratch out of the Docker build context and out of version control. .claude/ holds one worktree — an entire additional checkout of the repo — per in-flight agent, and under COPY . . all of it was reaching the image: another session's unreviewed, sometimes uncommitted work, inflating the context by a multiple of the repo and invalidating COPY for reasons unrelated to the repo's own content. The .dockerignore entry is root-anchored, because the directory occurs exactly once where agents run at the repo root and the **/ form additionally deletes any nested directory of that name — with the residual gap that follows from anchoring (a monorepo running agents in subdirectories still ships services/api/.claude/) stated in the canonical .dockerignore, the policy and the existing-repo checklist, since consuming repos receive the files rather than the tracker; the .gitignore entry is unanchored, because .gitignore patterns already match at every depth, and each file is written to its own semantics rather than derived from the other. Also closed the consequence that ships broken silently: excluding .git means git describe cannot run in any build stage and yields an empty version without erroring, so script/docker and script/cibuild now compute the version on the host and pass --build-arg VERSION, and REPO_POLICIES.md states where VERSION comes from instead of leaving the reader to fill the gap with git describe inside the build. The two Go documents that carry the GOLDFLAGS pattern were corrected in the same pass, from := to ?=, since a $(shell git describe) evaluated inside a build stage is exactly the empty version this closes. Verified by enumerating a probe image before, after, and against the **/-prefixed form, with a positive control and the CHECK_EPOCH cache verification re-run under the changed build context.
  • 2026-08-09: Closed the secret exposure in the canonical .dockerignore: a developer's local .env, *.pem or *.key was reaching the Docker build context under COPY . ., invisible to every git-based check because .gitignore covers it. The patterns are written to .dockerignore's own moby/patternmatcher semantics — **/-prefixed so they hold at every depth, which also fixes nested node_modules — rather than transplanted from .gitignore, whose unprefixed form protects only the repository root while reading as solved. Coverage extends past the .env/.pem/.key trio to the prod.env convention, .envrc, PKCS#12 bundles and extensionless SSH keys, every one of them case-folded with character ranges because matching is case-sensitive and an ALL-CAPS twin per pattern still misses Server.Key. REPO_POLICIES.md and both repo checklists now state that asymmetry and require verification by enumerating the image rather than by reading the patterns. Verified with a probe image before, against three naive forms (unprefixed, lowercase-only, ALL-CAPS-doubled), and after.
  • 2026-08-09: Made the pinned golangci-lint actually propagate: REPO_POLICIES.md now carries the canonical script/bootstrap snippet for Go repos, which installs when the installed version does not match the pin (the old if missing guard tested PATH presence only, so pins were inert on any provisioned machine and CI silently disagreed with local) and then re-resolves the binary through PATH and fails loudly, naming the shadowing path, when the install did not take effect — the failure mode the naive compare-then-install fix leaves behind while reporting success.
  • 2026-08-09: Fixed the false green in the canonical CI gate: script/cibuild and script/docker now pass a per-invocation CHECK_EPOCH nonce, and the Dockerfile (plus the Go multistage template in REPO_POLICIES.md, in both its lint and builder stages) declares ARG CHECK_EPOCH with a guard that makes a bare docker build . fail closed. Corrected the org-canonical text that asserted a successful build implies all checks pass, across every document carrying it: REPO_POLICIES.md, both repo checklists (which still told agents to write the pre-fix script/cibuild and ended on an acceptance item the guard makes unsatisfiable), and the Go styleguide.
  • 2026-08-07: Set the canonical .golangci.yml to the org-standard v2-schema config already deployed byte-identical across the org's Go repos (settings under linters.settings so thresholds like lll/funlen/cyclop/dupl actually apply under golangci-lint v2). Recorded the canonical golangci-lint version (v2.12.2, commit-pinned) in REPO_POLICIES.md.
  • 2026-03-20: Strengthened constructor naming and Params struct rules in the Go styleguide.
  • 2026-03-18: Documented fail-fast Dockerfile lint stage and conditional -v test rerun patterns in REPO_POLICIES.md.
  • 2026-03-11: Added HTTP service hardening policy for 1.0 releases.
  • 2026-03-10: Added policy: no build artifacts in repos.
  • 2026-03-04: Added LLM prose tells reference and copyediting checklist, then several self-applied revision passes.
  • 2026-02-28: Expanded the pre-1.0 schema migration rule; added clawpub reference.
  • 2026-02-23: Added Go style rules (no type-only packages, Stringer for string-based types); template repos section in README.
  • 2026-02-22: Initial policy corpus: REPO_POLICIES.md, code styleguides (general, Go, JS, Python), repo checklists, CI policy, hash pinning, Go HTTP server conventions, repo scaffolding.

Future Steps

  • Finish, format, and commit FIXUP_CLEAN.md and FIXUP_REPORT.md (the Next Step).
  • Commit this TODO.md at the repo root; it is the last missing policy file.
  • Decide the fate of untracked resume.sh: commit it or delete it.
  • Add more prompt templates for common development tasks (from README TODO).