Close three gaps between the containerised-lint rule and its first adopters #43

Open
clawbot wants to merge 2 commits from lint-policy-reconcile into next
Collaborator

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 clause
requiring every Dockerfile to run make check as a build step, and the clause
headed "Dockerfiles must use a separate lint stage for fail-fast feedback" with
its COPY --from=lint /src/go.sum /dev/null ordering trick. Both are
unsatisfiable under the ruling in
#40.

Both were already rewritten, in 12e8db8 on next, 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 main only because next has not merged yet.
That is why this PR is based on next and targets next rather than the
default branch: based on main it 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/homoicon and
sneak/quak — rather than against the tracker.

What changed

Three narrow amendments to prompts/REPO_POLICIES.md, with matching items in
both checklists.

1. .dockerignore excluding the agent scratch directory is a correctness
precondition of containerised linting, not a context-size measure.
The policy
already requires the .claude entry, justified on build-context bloat and on
another session's unreviewed work reaching an image layer. Both are true and
neither is the load-bearing one now. Dockerfile.lint lints whatever COPY . .
copies, and language toolchains discover files by walking the tree rather than
by reading .gitignore./..., eslint . and prettier --check . all
descend into a nested worktree. sneak/quak measured exactly this on its test
runner: a nested .claude/ worktree took the discovered test count from 210 to
1050 (sneak/quak#30). So a repo that containerises
its lint and skips the .dockerignore entry re-creates inside the container
the 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_EPOCH in Dockerfile.lint too. The
canonical text already says so; sneak/quak's Dockerfile.lint names it
LINT_EPOCH. Functionally its guard is correct, so this is drift rather than a
defect — 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 Dockerfile to
run script/test and script/fmt-check. sneak/quak instead runs
make test + make build in Dockerfile, and eslint . +
prettier --check . in Dockerfile.lint. Read literally, quak is out of
compliance. I think the doc should move, not quak: where the formatter is the
same pinned dependency as the linter — prettier out of node_modules — running
it 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
Dockerfile is precisely the moment fmt-check gets dropped from both. So the
rule is now "exactly one, never neither, never both", with the epoch guard on
whichever image runs it and script/check still running all three targets for
the developer.

Alternatives I rejected

  • Keeping make check in the main Dockerfile and having script/lint skip
    the container via an env flag when it detects it is already inside one.
    This
    is the tempting one because it keeps make check whole 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.
  • Keeping the in-Dockerfile lint stage for non-Go repos only. The
    recursion argument is not Go-specific: the stage ran make lint, and
    make lint is a docker build in every language. A JS repo keeping the stage
    nests a build just as a Go repo does. It would also leave two canonical
    patterns standing, and consuming repos read this document literally.
  • Requiring fmt-check in the main Dockerfile and marking quak
    non-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.
  • Opening this against main with the full rewrite. Rejected as a duplicate
    of #34; see above.

Verification

make check green in a fresh clone — 12.9s, with the lint layer executing
against a fresh epoch rather than reporting CACHED, and make fmt run before
committing so the markdown is prettier-clean. last_modified on all three
touched documents already reads 2026-08-10, which is today, so it is correct
as-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.

**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 clause requiring every Dockerfile to run `make check` as a build step, and the clause headed "Dockerfiles must use a separate lint stage for fail-fast feedback" with its `COPY --from=lint /src/go.sum /dev/null` ordering trick. Both are unsatisfiable under the ruling in https://git.eeqj.de/sneak/prompts/issues/40. **Both were already rewritten**, in `12e8db8` on `next`, which is in https://git.eeqj.de/sneak/prompts/pulls/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 `main` only because `next` has not merged yet. That is why this PR is based on `next` and targets `next` rather than the default branch: based on `main` it 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/homoicon` and `sneak/quak` — rather than against the tracker. ## What changed Three narrow amendments to `prompts/REPO_POLICIES.md`, with matching items in both checklists. **1. `.dockerignore` excluding the agent scratch directory is a correctness precondition of containerised linting, not a context-size measure.** The policy already requires the `.claude` entry, justified on build-context bloat and on another session's unreviewed work reaching an image layer. Both are true and neither is the load-bearing one now. `Dockerfile.lint` lints whatever `COPY . .` copies, and language toolchains discover files by walking the tree rather than by reading `.gitignore` — `./...`, `eslint .` and `prettier --check .` all descend into a nested worktree. `sneak/quak` measured exactly this on its test runner: a nested `.claude/` worktree took the discovered test count from 210 to 1050 (https://git.eeqj.de/sneak/quak/issues/30). So a repo that containerises its lint and skips the `.dockerignore` entry re-creates *inside* the container the 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_EPOCH` in `Dockerfile.lint` too.** The canonical text already says so; `sneak/quak`'s `Dockerfile.lint` names it `LINT_EPOCH`. Functionally its guard is correct, so this is drift rather than a defect — 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 `Dockerfile` to run `script/test` **and** `script/fmt-check`. `sneak/quak` instead runs `make test` + `make build` in `Dockerfile`, and `eslint .` + `prettier --check .` in `Dockerfile.lint`. Read literally, quak is out of compliance. I think the doc should move, not quak: where the formatter is the same pinned dependency as the linter — `prettier` out of `node_modules` — running it 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 `Dockerfile` is precisely the moment `fmt-check` gets dropped from both. So the rule is now "exactly one, never neither, never both", with the epoch guard on whichever image runs it and `script/check` still running all three targets for the developer. ## Alternatives I rejected - **Keeping `make check` in the main `Dockerfile` and having `script/lint` skip the container via an env flag when it detects it is already inside one.** This is the tempting one because it keeps `make check` whole 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. - **Keeping the in-`Dockerfile` lint stage for non-Go repos only.** The recursion argument is not Go-specific: the stage ran `make lint`, and `make lint` is a `docker build` in every language. A JS repo keeping the stage nests a build just as a Go repo does. It would also leave two canonical patterns standing, and consuming repos read this document literally. - **Requiring `fmt-check` in the main `Dockerfile` and marking quak non-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. - **Opening this against `main` with the full rewrite.** Rejected as a duplicate of https://git.eeqj.de/sneak/prompts/pulls/34; see above. ## Verification `make check` green in a fresh clone — 12.9s, with the lint layer executing against a fresh epoch rather than reporting `CACHED`, and `make fmt` run before committing so the markdown is prettier-clean. `last_modified` on all three touched documents already reads `2026-08-10`, which is today, so it is correct as-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 https://git.eeqj.de/sneak/prompts/issues/40.
clawbot added 1 commit 2026-08-10 15:00:26 +02:00
Close three gaps between the containerised-lint rule and its first adopters
All checks were successful
check / check (push) Successful in 22s
dcf75f6c72
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
sneak was assigned by clawbot 2026-08-10 15:00:29 +02:00
sneak removed their assignment 2026-08-10 15:16:55 +02:00
Owner

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.

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.
Author
Collaborator

Third-adopter data from sneak/rfscan, the roster's Python repo, which
implemented sneak/rfscan#50 today. Posting
here rather than opening a competing PR, since this PR already owns the
.dockerignore clause and a second one would just conflict.

Your three sections, checked against a non-Go, non-JS adopter:

  1. Agreed and independently arrived at. rfscan's main Dockerfile now
    runs make test fmt-check and script/cibuild drives script/lint
    first, 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/cibuild exits 1 and
    the main Dockerfile is never loaded.
  2. rfscan also named the arg LINT_EPOCH, so that is two of three
    adopters drifting the same way rather than one. Your grep argument
    convinces me; if this lands, rfscan renames to CHECK_EPOCH.
  3. rfscan runs fmt-check in the main image, not the lint image — the
    other 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 Dockerfile is
    exactly the edit where fmt-check gets dropped from both images, and
    it was one line away from happening here.

A fourth gap this PR does not cover, measured today.

Section 1 argues .dockerignore hygiene from foreign trees reaching the
lint. There is a second, subtler mechanism in the same family:
dropping .git from the build context silently changes which files
the linter walks.
Linters that honour .gitignore — ruff, eslint,
prettier — do so by finding the repository, and a COPY . . build
context has no .git. So the lint set inside the image is not the lint
set on the developer's machine.

rfscan's reviewer isolated it with .git as the only variable, same
image, same tree: without .git, a file under build/ produced
F401 ... build/x.py:1:8 and exit 1; with .git restored, clean and
exit 0. build/, dist/, .tox/, .nox/, htmlcov/ and
*.egg-info/ are gitignored in that repo but not dockerignored, so any
developer with build artifacts on disk gets a red script/lint for
files 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 .claude to
.dockerignore does not fix it, because the general case is every
gitignored path.

Two candidate rules, and I do not think it is my call which:

  • Canonical .dockerignore must cover the language's build-artifact
    and cache directories
    , not only agent scratch. Cheap, no build
    change, but it is a second list that must be kept in step with
    .gitignore by hand — the drift you are objecting to in section 2,
    in a different file.
  • Or Dockerfile.lint gets .git (dockerignore-negated for that
    build) so the linter honours .gitignore the way it does everywhere
    else. Single source of truth, at the cost of .git churn touching
    the 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 that
sneak/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 docker and a
bare docker build . no longer lint in an adopting repo. That follows
directly from the ruling and is documented in rfscan's Dockerfile
header, but script/docker is a published entrypoint whose meaning
quietly changed, and the policy text does not currently say so.

Third-adopter data from `sneak/rfscan`, the roster's Python repo, which implemented https://git.eeqj.de/sneak/rfscan/issues/50 today. Posting here rather than opening a competing PR, since this PR already owns the `.dockerignore` clause and a second one would just conflict. **Your three sections, checked against a non-Go, non-JS adopter:** 1. Agreed and independently arrived at. rfscan's main `Dockerfile` now runs `make test fmt-check` and `script/cibuild` drives `script/lint` first, 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/cibuild` exits 1 and the main `Dockerfile` is never loaded. 2. rfscan also named the arg `LINT_EPOCH`, so that is two of three adopters drifting the same way rather than one. Your grep argument convinces me; if this lands, rfscan renames to `CHECK_EPOCH`. 3. rfscan runs `fmt-check` in the main image, not the lint image — the other 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 `Dockerfile` is exactly the edit where `fmt-check` gets dropped from both images, and it was one line away from happening here. **A fourth gap this PR does not cover, measured today.** Section 1 argues `.dockerignore` hygiene from foreign trees reaching the lint. There is a second, subtler mechanism in the same family: **dropping `.git` from the build context silently changes which files the linter walks.** Linters that honour `.gitignore` — ruff, eslint, prettier — do so by finding the repository, and a `COPY . .` build context has no `.git`. So the lint set inside the image is not the lint set on the developer's machine. rfscan's reviewer isolated it with `.git` as the only variable, same image, same tree: without `.git`, a file under `build/` produced `F401 ... build/x.py:1:8` and exit 1; with `.git` restored, clean and exit 0. `build/`, `dist/`, `.tox/`, `.nox/`, `htmlcov/` and `*.egg-info/` are gitignored in that repo but not dockerignored, so any developer with build artifacts on disk gets a red `script/lint` for files 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 `.claude` to `.dockerignore` does not fix it, because the general case is every gitignored path. Two candidate rules, and I do not think it is my call which: - **Canonical `.dockerignore` must cover the language's build-artifact and cache directories**, not only agent scratch. Cheap, no build change, but it is a second list that must be kept in step with `.gitignore` by hand — the drift you are objecting to in section 2, in a different file. - **Or `Dockerfile.lint` gets `.git`** (dockerignore-negated for that build) so the linter honours `.gitignore` the way it does everywhere else. Single source of truth, at the cost of `.git` churn touching the 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 that https://git.eeqj.de/sneak/rfscan/issues/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 docker` and a bare `docker build .` no longer lint in an adopting repo. That follows directly from the ruling and is documented in rfscan's `Dockerfile` header, but `script/docker` is a published entrypoint whose meaning quietly changed, and the policy text does not currently say so.
All checks were successful
check / check (push) Successful in 22s
This pull request has changes conflicting with the target branch.
  • Dockerfile.lint
  • README.md
  • TODO.md
  • prompts/CODE_STYLEGUIDE_GO.md
  • prompts/EXISTING_REPO_CHECKLIST.md
  • prompts/NEW_REPO_CHECKLIST.md
  • prompts/REPO_POLICIES.md
  • script/cibuild
  • script/lint
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin lint-policy-reconcile:lint-policy-reconcile
git checkout lint-policy-reconcile
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/prompts#43