All checks were successful
check / check (push) Successful in 15s
script/lint runs the linter directly when it is already inside a container and otherwise builds Dockerfile.lint, so the linter never runs on a developer host. That closes three host-only mechanisms: the result cache golangci-lint keys on file content rather than location, which produced a confirmed false green and findings reported against other checkouts; the host-global $TMPDIR/golangci-lint.lock, which fails a run in a way no caller can distinguish from findings; and host/container version skew, which hid thirteen findings on one repo. Detection is on LINT_IN_CONTAINER=1, set by every Dockerfile, and on nothing else. The two directions are not symmetric: a false negative inside a container attempts a nested docker build, finds no daemon and fails loudly, while a false positive on a host silently lints there, which is the defect this issue exists to kill. /.dockerenv is therefore rejected even as a fallback -- measured absent inside BuildKit RUN steps and present on any host that is itself a container, so it fails in both directions and one of them is the dangerous one. Nothing else changes shape. The Dockerfile still runs make check, script/check still runs test, lint and fmt-check, script/cibuild is still a single docker build with CHECK_EPOCH and VERSION, and the Go multistage lint stage and its COPY --from=lint ordering dependency survive with ENV LINT_IN_CONTAINER=1 added. Dockerfile.lint is the standalone developer-host path and carries the same CHECK_EPOCH guard, with the ARG below the dependency layer so only the lint re-runs. The script/bootstrap golangci-lint install and the per-checkout GOLANGCI_LINT_CACHE/TMPDIR wrapper are deleted as superseded. Neither has a caller left. A JS repo's yarn install stays: the rule is that no lint verdict may come from a host invocation, not that no linter binary may exist there, and in a repo whose formatter is its linter the formatter necessarily runs on the host. golangci-lint config verify is kept, on measurement. Under the pinned v2.12.2 a bogus top-level key and a bogus key under linters.settings.lll both pass `golangci-lint run` with exit 0 and `0 issues` while config verify exits 3 and names them; an unknown linter name fails run and passes config verify. It needs no network: every case reproduced byte-identically under `docker run --network none`, in a container where `getent hosts golangci-lint.run` exits 2. Comment blocks were cut hard across every file this unit touches. .dockerignore drops from 67 comment lines to 28, script/cibuild from 17 to 12, script/docker from 18 to 12, and prompts/REPO_POLICIES.md from 1182 lines to 907. What remains says why a line is load-bearing; the discovery narratives are gone.
10 KiB
10 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.mdchanges in the same commit as the work) - merge to
mainif 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: Moved every lint run into a container.
script/lintnow runs the linter directly whenLINT_IN_CONTAINER=1and otherwise buildsDockerfile.lint, so the linter never runs on a developer host — closing the content-keyed result cache that produced a confirmed false green, the host-global$TMPDIR/golangci-lint.lock, and host/container version skew. Detection is on that marker alone: a false negative inside a container fails loudly on the missing daemon, while a false positive on a host would silently restore host linting, so/.dockerenvis rejected outright — measured absent inside BuildKitRUNsteps and present on hosts that are themselves containers. Everything else keeps its existing shape:make checkstill runs in the image,script/cibuildis still one build, and the Go multistage lint stage survives withENV LINT_IN_CONTAINER=1.Dockerfile.lintcarries the sameCHECK_EPOCHguard, with theARGbelow the dependency layer so only the lint re-runs. Thescript/bootstrapgolangci-lint install and the per-checkout cache/lock/.lint-cachewrapper are deleted as superseded; a JS repo'syarn installstays, since the rule is about where a verdict comes from, not about which binaries exist.golangci-lint config verifywas kept on measurement: a bogus config key passesgolangci-lint runwith0 issuesand failsconfig verify, and every case reproduced byte-identically under--network none, so the schema is embedded and the line costs no network. Comment blocks across the touched files were cut hard in the same pass. - 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-checkoutGOLANGCI_LINT_CACHEand per-checkoutTMPDIR. 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 thatTMPDIRscoping cannot;--allow-parallel-runnersis 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.GOCACHEwas 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 underCOPY . .all of it was reaching the image: another session's unreviewed, sometimes uncommitted work, inflating the context by a multiple of the repo and invalidatingCOPYfor reasons unrelated to the repo's own content. The.dockerignoreentry 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 shipsservices/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.gitignoreentry is unanchored, because.gitignorepatterns 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.gitmeansgit describecannot run in any build stage and yields an empty version without erroring, soscript/dockerandscript/cibuildnow compute the version on the host and pass--build-arg VERSION, andREPO_POLICIES.mdstates whereVERSIONcomes from instead of leaving the reader to fill the gap withgit describeinside the build. The two Go documents that carry theGOLDFLAGSpattern 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 theCHECK_EPOCHcache verification re-run under the changed build context. - 2026-08-09: Closed the secret exposure in the canonical
.dockerignore: a developer's local.env,*.pemor*.keywas reaching the Docker build context underCOPY . ., invisible to every git-based check because.gitignorecovers it. The patterns are written to.dockerignore's ownmoby/patternmatchersemantics —**/-prefixed so they hold at every depth, which also fixes nestednode_modules— rather than transplanted from.gitignore, whose unprefixed form protects only the repository root while reading as solved. Coverage extends past the.env/.pem/.keytrio to theprod.envconvention,.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 missesServer.Key.REPO_POLICIES.mdand 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/bootstrapsnippet for Go repos, which installs when the installed version does not match the pin (the oldif missingguard tested PATH presence only, so pins were inert on any provisioned machine and CI silently disagreed with local) and then re-resolves the binary throughPATHand 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/cibuildandscript/dockernow pass a per-invocationCHECK_EPOCHnonce, and theDockerfile(plus the Go multistage template in REPO_POLICIES.md, in both its lint and builder stages) declaresARG CHECK_EPOCHwith a guard that makes a baredocker 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-fixscript/cibuildand ended on an acceptance item the guard makes unsatisfiable), and the Go styleguide. - 2026-08-07: Set the canonical
.golangci.ymlto the org-standard v2-schema config already deployed byte-identical across the org's Go repos (settings underlinters.settingsso 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).