Close three gaps between the containerised-lint rule and its first adopters #43
Reference in New Issue
Block a user
Delete Branch "lint-policy-reconcile"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This is speculative and awaiting your ruling. Nothing here is merged; close
it and delete the branch if you disagree, and no other repo is touched.
First, the thing worth knowing before reading the diff
I was sent to fix a contradiction in
prompts/REPO_POLICIES.md: the clauserequiring every Dockerfile to run
make checkas a build step, and the clauseheaded "Dockerfiles must use a separate lint stage for fail-fast feedback" with
its
COPY --from=lint /src/go.sum /dev/nullordering trick. Both areunsatisfiable under the ruling in
#40.
Both were already rewritten, in
12e8db8onnext, which is in#34. Opening a second PR rewriting them
again would have been a duplicate that conflicts with that one, so I did not.
The contradiction is visible on
mainonly becausenexthas not merged yet.That is why this PR is based on
nextand targetsnextrather than thedefault branch: based on
mainit would have to re-do the whole rewrite.What follows is the residual delta I found by checking that rewrite against the
two repos that have actually implemented it —
sneak/homoiconandsneak/quak— rather than against the tracker.What changed
Three narrow amendments to
prompts/REPO_POLICIES.md, with matching items inboth checklists.
1.
.dockerignoreexcluding the agent scratch directory is a correctnessprecondition of containerised linting, not a context-size measure. The policy
already requires the
.claudeentry, justified on build-context bloat and onanother session's unreviewed work reaching an image layer. Both are true and
neither is the load-bearing one now.
Dockerfile.lintlints whateverCOPY . .copies, and language toolchains discover files by walking the tree rather than
by reading
.gitignore—./...,eslint .andprettier --check .alldescend into a nested worktree.
sneak/quakmeasured exactly this on its testrunner: a nested
.claude/worktree took the discovered test count from 210 to1050 (sneak/quak#30). So a repo that containerises
its lint and skips the
.dockerignoreentry re-creates inside the containerthe foreign-tree false reds that containerising was adopted to end — and in the
convincing form, where the findings are real and simply belong to another
checkout.
2. The cache-bust build arg is
CHECK_EPOCHinDockerfile.linttoo. Thecanonical text already says so;
sneak/quak'sDockerfile.lintnames itLINT_EPOCH. Functionally its guard is correct, so this is drift rather than adefect — but a per-file name is invisible to the grep that proves every build
in a repo is cache-busted, which makes a renamed guard and an absent guard read
identically without opening both Dockerfiles. Stated explicitly so the next
implementer copying quak does not conclude the doc is the thing that is wrong.
3. The formatting check runs in exactly one of the two images, and either
placement is allowed. The policy currently requires the main
Dockerfiletorun
script/testandscript/fmt-check.sneak/quakinstead runsmake test+make buildinDockerfile, andeslint .+prettier --check .inDockerfile.lint. Read literally, quak is out ofcompliance. I think the doc should move, not quak: where the formatter is the
same pinned dependency as the linter —
prettierout ofnode_modules— runningit in the lint image takes the last host toolchain off the checked path, for
exactly the reason the linter came off it. What must not happen is it running in
neither image, and that is the live risk, because splitting lint out of the
Dockerfileis precisely the momentfmt-checkgets dropped from both. So therule is now "exactly one, never neither, never both", with the epoch guard on
whichever image runs it and
script/checkstill running all three targets forthe developer.
Alternatives I rejected
make checkin the mainDockerfileand havingscript/lintskipthe container via an env flag when it detects it is already inside one. This
is the tempting one because it keeps
make checkwhole and keeps the sentence"a successful build implies all checks pass" true of a single file. It
requires the linter installed in the app image, which is the host install the
ruling deletes, wearing a different hat — and it reintroduces version skew in
the one place nobody looks for it. It also makes the guarantee conditional on
a detection heuristic being right.
Dockerfilelint stage for non-Go repos only. Therecursion argument is not Go-specific: the stage ran
make lint, andmake lintis adocker buildin every language. A JS repo keeping the stagenests a build just as a Go repo does. It would also leave two canonical
patterns standing, and consuming repos read this document literally.
fmt-checkin the mainDockerfileand marking quaknon-compliant. Defensible, and it keeps one shape. I rejected it because the
reason it would be enforced (uniformity) is weaker than the reason to allow
the other placement (the host formatter is the same class of defect as the
host linter), and because forcing it back would mean quak runs prettier from
the host toolchain in one image and the pinned one in the other.
mainwith the full rewrite. Rejected as a duplicateof #34; see above.
Verification
make checkgreen in a fresh clone — 12.9s, with the lint layer executingagainst a fresh epoch rather than reporting
CACHED, andmake fmtrun beforecommitting so the markdown is prettier-clean.
last_modifiedon all threetouched documents already reads
2026-08-10, which is today, so it is correctas-is rather than unbumped.
The open question
Section 3 in particular is a decision, not a cleanup: it is either the doc that
moves or quak that moves. Asked plainly on
#40.
unmergeable i should not be assigned. dockerfile can invoke the strta entries instead of make but must do lint and fmt check and test before building so any failure prevents a root docker build.
Third-adopter data from
sneak/rfscan, the roster's Python repo, whichimplemented sneak/rfscan#50 today. Posting
here rather than opening a competing PR, since this PR already owns the
.dockerignoreclause and a second one would just conflict.Your three sections, checked against a non-Go, non-JS adopter:
Dockerfilenowruns
make test fmt-checkandscript/cibuilddrivesscript/lintfirst, then the main build. I rejected the env-flag escape hatch for
the same reason you did, and a reviewer verified the ordering fails
closed: with a lint violation present,
script/cibuildexits 1 andthe main
Dockerfileis never loaded.LINT_EPOCH, so that is two of threeadopters drifting the same way rather than one. Your grep argument
convinces me; if this lands, rfscan renames to
CHECK_EPOCH.fmt-checkin the main image, not the lint image — theother allowed placement under your "exactly one, never neither" rule,
so the rule as written covers it. Worth noting the live risk you
name is real and near: splitting lint out of the
Dockerfileisexactly the edit where
fmt-checkgets dropped from both images, andit was one line away from happening here.
A fourth gap this PR does not cover, measured today.
Section 1 argues
.dockerignorehygiene from foreign trees reaching thelint. There is a second, subtler mechanism in the same family:
dropping
.gitfrom the build context silently changes which filesthe linter walks. Linters that honour
.gitignore— ruff, eslint,prettier — do so by finding the repository, and a
COPY . .buildcontext has no
.git. So the lint set inside the image is not the lintset on the developer's machine.
rfscan's reviewer isolated it with
.gitas the only variable, sameimage, same tree: without
.git, a file underbuild/producedF401 ... build/x.py:1:8and exit 1; with.gitrestored, clean andexit 0.
build/,dist/,.tox/,.nox/,htmlcov/and*.egg-info/are gitignored in that repo but not dockerignored, so anydeveloper with build artifacts on disk gets a red
script/lintforfiles git is ignoring. CI is unaffected — it clones clean — which is
what makes it nasty: it fails only locally, only for some people, and
looks like a real finding.
Note this cuts the opposite way from your section 1. That one is about
extra files reaching the image; this one is about the image losing the
mechanism that would have excluded them. Adding
.claudeto.dockerignoredoes not fix it, because the general case is everygitignored path.
Two candidate rules, and I do not think it is my call which:
.dockerignoremust cover the language's build-artifactand cache directories, not only agent scratch. Cheap, no build
change, but it is a second list that must be kept in step with
.gitignoreby hand — the drift you are objecting to in section 2,in a different file.
Dockerfile.lintgets.git(dockerignore-negated for thatbuild) so the linter honours
.gitignorethe way it does everywhereelse. Single source of truth, at the cost of
.gitchurn touchingthe lint layer's cache key — which matters much less here than
elsewhere, since the epoch guard already forces that layer to run.
I lean to the second: it makes the containerised lint set identical to
the local one by construction rather than by two lists agreeing. But it
partially re-opens the
.git-in-context question thatsneak/rfscan#35 closed for the main image, so
it deserves a deliberate answer rather than my picking it.
rfscan is filing its own local fix against whichever way this goes; it
does not block on it.
One smaller note for the record, not an objection:
make dockerand abare
docker build .no longer lint in an adopting repo. That followsdirectly from the ruling and is documented in rfscan's
Dockerfileheader, but
script/dockeris a published entrypoint whose meaningquietly changed, and the policy text does not currently say so.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.