All checks were successful
check / check (push) Successful in 13s
The linter is no longer installed on the host and no longer invoked there. script/lint is now `docker build -f Dockerfile.lint .` and nothing else, with the linter running as a build step, so a successful build of that file is a clean lint — and it works unchanged where the docker daemon is remote and bind mounts are impossible. That removes three host-only failure mechanisms rather than mitigating them: the result cache keyed on file content rather than location, which produced a confirmed false green and a string of findings reported against other checkouts; the host-global $TMPDIR/golangci-lint.lock, which fails a run with `parallel golangci-lint is running` in a way no caller can distinguish from findings; and host/container version skew, which hid thirteen findings on one repo. A container per run has its own cache, its own lock and a binary pinned by digest. Resolving the recursion this creates. script/lint is a docker build, so a Dockerfile that runs `make check` would nest a build inside a build step where there is no daemon. Fixed by direction, not detection: the main Dockerfile runs script/test and script/fmt-check individually, with a comment saying why `make check` must not come back, and script/cibuild runs script/lint first for fail-fast feedback. script/check still runs all three, so developers and the pre-commit hook are unaffected. Dockerfile.lint carries the same CHECK_EPOCH guard as the main image, with the ARG placed below the dependency layer so only the lint steps re-run. Blanket --no-cache was rejected: it re-runs the dependency install on every lint and makes linting network-dependent. golangci-lint config verify is kept, on measurement rather than preference. Under the pinned v2.12.2, a bogus top-level key and a bogus key nested 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. The two catch disjoint classes, and `run` alone silently ignores the class where a threshold reads as configured and is not applied. The concern that config verify fetches its JSON schema over live HTTPS does not hold for this version: every case reproduced byte-identically under `docker run --network none`, in a container where `getent hosts golangci-lint.run` exits 2. The schema is embedded in the pinned binary. Two canonical forms are superseded and deleted rather than left standing beside the new one, because consuming repos read these documents literally and two contradictory canonical script/lint forms is worse than either. The script/bootstrap golangci-lint install landed for #28 is removed: nothing invokes a host linter now, so it can only reintroduce the skew it was written to close. Its version-enforcement principle — compare version not presence, re-resolve through PATH after installing, let a mis-parse fall through to reinstall, and call it — stays documented for any other pinned host tool. The per-checkout GOLANGCI_LINT_CACHE/TMPDIR wrapper is removed with it; its entire subject was making a host run trustworthy. Adopting repos delete .lint-cache/ from .gitignore and .dockerignore too. The Go multistage lint stage and its COPY --from=lint ordering trick go the same way: that stage ran `make lint`, which is now a docker build. Corrected everywhere the claim that a successful docker build implies lint passed — REPO_POLICIES.md, both repo checklists, the Go styleguide and the README. The guarantee now belongs to script/cibuild, which runs both container builds; a bare `docker build .` never lints at all. Verified in this repo, not only documented: two consecutive script/lint runs on a byte-identical tree both executed prettier (4.556s and 3.738s, lint layers DONE with a fresh epoch printed, dependency layers CACHED as intended); a planted violation failed the build naming the file, and reverting it went green; a bare `docker build -f Dockerfile.lint .` failed on the guard; make check, script/docker and script/cibuild all green with the check layers demonstrably executing; and the main image build completed without attempting a nested build. Rework, from independent review of this commit. The canonical text is the deliverable here, so a false sentence is a fleet-wide defect: the Dockerfile rule still said the build "fails if the branch is not green", which stopped being true when lint left that image, and the earlier sweep grepped one phrasing rather than the claim. Re-swept on the claim itself — green/red-branch wording, build-fails wording, entailment verbs near build/lint/check, and "linted" asserted as covered — across prompts/, README.md, TODO.md, both Dockerfiles and every script. Two absolutes are narrowed to what is actually true, because seventeen repos adopt this literally. The rule is that no lint VERDICT may come from a host invocation, not that the binary never exists on the host: a JS repo's `yarn install` puts its linter in node_modules on the host unavoidably, and in a repo whose formatter is its linter — this one — `script/fmt-check` runs the same command that Dockerfile.lint runs. That gap is now stated with its bound (the version is pinned in the repo's own node_modules, so no shared cache, no host lock, nothing to skew) and the audit grep keeps its reach, gaining a note on which two hits are expected rather than being weakened. script/lint conflates "found issues" with "could not run": docker build exits 1 for both. The exit-75 VOID machinery is deliberately not restored, and the reasoning is now recorded where a reader looking for it lands. The dangerous direction is already closed, since a build that cannot run fails closed and can never read as clean; BuildKit already names the failing step, where the old lock error went to stderr while findings went to stdout and was easy to lose; and the failure is not transient, so the retry that justified the old machinery would be wrong here. Rebuilding the distinction would mean per-invocation capture files and traps again plus matching on BuildKit's message format, which is not a stable interface, and a mis-match in the "treat as infrastructure" direction would be the false green this rule exists to prevent. What survives is binding as a reading rule: a run that did not reach the lint step is not a verdict. Also corrected: script/lint was listed above a CHECK_EPOCH snippet that does a bare `docker build .` with no -f, which would have built the main image and linted nothing; the canonical Go Dockerfile template used `make fmt-check` / `make test` where every prose rule in the same document says script/, one Makefile edit away from re-entering the recursion; README omitted the mandatory VERSION build arg; script/docker did not say lint had left its image, a comment that propagates fleet-wide; the "byte-identical across repos" claim for script/lint is narrowed to its executable lines; and "--no-cache makes linting network-dependent" is softened to the measured comparative claim.
180 lines
12 KiB
Markdown
180 lines
12 KiB
Markdown
# 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: 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. Two positions are stated
|
|
rather than left as gaps, because canon that omits them gets re-derived
|
|
wrongly: `docker build` returns 1 both for findings and for a build that never
|
|
reached the lint step, and the exit-75 VOID machinery is deliberately not
|
|
restored — the dangerous direction is closed since an unrunnable lint fails
|
|
closed, BuildKit already names the failing step, and the failure is not
|
|
transient, so what survives is the reading rule that a run which did not lint
|
|
is not a verdict. And the rule is about linters: `script/fmt` and
|
|
`script/fmt-check` run on the host by necessity, which in a repo whose
|
|
formatter is its linter — this one — means that exact command does run there,
|
|
recorded as a known and bounded gap rather than papered over. 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).
|