WIP: next #34

Draft
clawbot wants to merge 6 commits from next into main
Collaborator

Long-lived next branch for the current cycle. WIP: until the milestone is ready.

Six commits, one per issue:

commit issue
51c3945 #26 — bust the Docker check-layer cache with a per-invocation CHECK_EPOCH
d173e69 #28 — make the pinned golangci-lint actually reach the host
fd78aeb #29 — keep secrets out of the Docker build context at every depth
3a21849 #27 — keep in-repo agent scratch out of the build context and out of git
0620416 #30 — give golangci-lint per-checkout cache and lock state
1e21653 #40 — run every lint in a container

Sections 1-5 are reviewed and passing. Section 6 is in review.

Consuming repos adopt this by re-vendoring prompts/REPO_POLICIES.md and both checklists, then taking the canonical .dockerignore, Dockerfile, Dockerfile.lint and script/ entrypoints. Per-unit adoption steps, including what to delete, are in the commit bodies and the section-6 comment.

Long-lived `next` branch for the current cycle. `WIP:` until the milestone is ready. Six commits, one per issue: | commit | issue | |---|---| | `51c3945` | [#26](https://git.eeqj.de/sneak/prompts/issues/26) — bust the Docker check-layer cache with a per-invocation `CHECK_EPOCH` | | `d173e69` | [#28](https://git.eeqj.de/sneak/prompts/issues/28) — make the pinned golangci-lint actually reach the host | | `fd78aeb` | [#29](https://git.eeqj.de/sneak/prompts/issues/29) — keep secrets out of the Docker build context at every depth | | `3a21849` | [#27](https://git.eeqj.de/sneak/prompts/issues/27) — keep in-repo agent scratch out of the build context and out of git | | `0620416` | [#30](https://git.eeqj.de/sneak/prompts/issues/30) — give golangci-lint per-checkout cache and lock state | | `1e21653` | [#40](https://git.eeqj.de/sneak/prompts/issues/40) — run every lint in a container | Sections 1-5 are reviewed and passing. Section 6 is in review. Consuming repos adopt this by re-vendoring `prompts/REPO_POLICIES.md` and both checklists, then taking the canonical `.dockerignore`, `Dockerfile`, `Dockerfile.lint` and `script/` entrypoints. Per-unit adoption steps, including what to delete, are in the commit bodies and the section-6 comment.
clawbot added the needs-review label 2026-08-09 16:46:50 +02:00
clawbot added 1 commit 2026-08-09 16:46:50 +02:00
Bust the Docker check-layer cache with a per-invocation CHECK_EPOCH (closes #26)
All checks were successful
check / check (push) Successful in 14s
22a5a372e0
script/cibuild was a plain `docker build .`, and the Dockerfile does
`COPY . .` followed by `RUN make check`. Docker invalidates a COPY layer
only when the copied content changes, so on an unchanged tree the check
layer was served from cache, the suite never ran, and the build still
exited 0. Measured here: run 1 took 18.5s and ran the suite; run 2 on a
byte-identical tree took 0.286s with `RUN make check` CACHED.

script/cibuild and script/docker now assign a per-invocation nonce on its
own line and pass it as --build-arg CHECK_EPOCH. The Dockerfile declares
ARG CHECK_EPOCH, guards it with `[ -n "$CHECK_EPOCH" ] || exit 1`, and
expands it into the check command. Post-fix, two consecutive runs both
execute make check (17.4s / 8.1s) with `RUN script/bootstrap` still
CACHED, so dependency layers are untouched and the build ceiling is not
at risk.

The guard is what makes a bare `docker build .` — the command
REPO_POLICIES named verbatim — fail closed rather than reuse the empty
and therefore stable cache key; verified failing in 0.455s. Holding the
epoch constant restores the false green (run 2 fully CACHED), which pins
the varying value as the operative mechanism rather than a coincidence.

REPO_POLICIES.md carried the false guarantee as org-canonical text in
two places, and its Go multistage template had check steps in two
stages; ARG is stage-scoped, so both stages get the treatment or the
fleet inherits the half-fixed shape.
clawbot self-assigned this 2026-08-09 16:46:55 +02:00
clawbot added needs-rework and removed needs-review labels 2026-08-09 16:56:41 +02:00
clawbot force-pushed next from 22a5a372e0 to 417f142a9f 2026-08-09 17:00:59 +02:00 Compare
clawbot force-pushed next from 417f142a9f to 6b9827a618 2026-08-09 17:02:15 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-08-09 17:13:14 +02:00
clawbot force-pushed next from 6b9827a618 to 51c394552e 2026-08-09 17:15:05 +02:00 Compare
clawbot added 1 commit 2026-08-09 17:24:02 +02:00
Make the pinned golangci-lint actually reach the host (closes #28)
All checks were successful
check / check (push) Successful in 7s
07129f0ec1
REPO_POLICIES.md now carries the canonical script/bootstrap snippet for Go
repos alongside the .golangci.yml bullet, where the pinned linter version
already lives.

The guard it replaces, `if missing golangci-lint; then go install ...; fi`,
tests PATH presence and never version, so on any already-provisioned machine
the pin is inert and a version bump is a no-op. The Dockerfile installs
unconditionally into a clean image, so CI and local then disagree about what
the linter is: a local `make check` green while `make docker` rejects the same
commit, and a container run surfacing findings the host run cannot see.

Comparing versions alone is not enough. `go install` writes to GOBIN (or
GOPATH/bin) while callers resolve through PATH, so a shadowing binary earlier
in PATH lets the install succeed and change nothing a caller ever sees, while
bootstrap prints success. The canonical form therefore compares the installed
version against the pin, re-resolves through PATH after installing and asserts
the pin, failing non-zero and naming the shadowing path when it does not, and
treats any unparseable --version output as a mismatch so the failure direction
is a redundant install rather than a skipped one.

The policy text states each of those as a requirement rather than leaving them
implicit in the code, records why the commit-pinned `go install` ref satisfies
the hash-pinning rule (a commit hash is not a mutable tag, and the go command
verifies the module against the checksum database), and requires that any
change to this logic be validated with a negative control run against a
shadowing binary, because a control without one passes against the naive
implementation too.

The node and yarn handling described earlier in the document is untouched.

Verified by extracting the snippet to a scratch harness with fake `go` and both
fake and real golangci-lint binaries: shadowing fails loudly and names the
path while the naive compare-then-install form reports success with the stale
2.7.2 still resolved; a wrong version at the install target is replaced;
garbage, empty and non-zero --version output all reinstall; the matching case
runs zero installs. The block in the document is byte-identical to the one
exercised.
clawbot added needs-rework and removed needs-review labels 2026-08-09 17:36:19 +02:00
clawbot force-pushed next from 07129f0ec1 to be59376522 2026-08-09 17:41:17 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-08-09 17:45:45 +02:00
clawbot force-pushed next from be59376522 to d173e69f85 2026-08-09 18:01:44 +02:00 Compare
clawbot added 1 commit 2026-08-09 18:10:51 +02:00
Keep secrets out of the Docker build context at every depth (closes #29)
All checks were successful
check / check (push) Successful in 8s
b8d21d1592
The canonical .dockerignore was three lines -- .git, node_modules, .DS_Store
-- while the canonical Dockerfile does `COPY . .`, so a developer's local
.env, *.pem or *.key was shipped into the build context and could land in an
image layer. Nothing surfaced it because .gitignore covers those patterns, so
the files are invisible to every git-based check.

The obvious repair, copying .gitignore's secret patterns across, is worse than
the gap it closes. .dockerignore does not use .gitignore semantics: Docker
matches with Go filepath.Match, `*` does not cross `/`, and a pattern without
a leading `**/` is anchored at the build-context root. A file listing .env,
*.pem and *.key therefore reads as solved, reviews as solved, and protects
only the repository root, while config/.env and certs/server.key still ship.
The three-line file at least invited scrutiny; the transplanted form
manufactures confidence and stops anyone looking.

So every depth-independent pattern here carries the `**/` prefix and only
genuinely root-anchored entries stay unprefixed. `**/node_modules` fixes a
defect the three-line file had today for any nested node_modules,
independently of the secret exposure.

The OS and editor patterns are included on their own merits rather than by
mirroring .gitignore. None of them is ever a build input, and editor state in
particular churns under a developer's hands, so each one is a source of
`COPY . .` invalidation carrying no information about the source tree. Now
that the checks are keyed on CHECK_EPOCH rather than on accidental context
churn, there is no reason left to keep churn in the context. Language build
artifacts are deliberately absent: they are per-repo, and the file's header
comment tells consuming repos to add their own host-built binaries, which is
the case that actually bites -- a host `make build` drops a multi-megabyte
artifact into the context where .gitignore hides it from every git-based
check.

.gitignore is untouched. Its semantics are the inverse: an unanchored pattern
already matches at any depth, so `**/`-prefixing it produces a file that is
wrong in a way that looks careful. That asymmetry is why "derive one from the
other" was the wrong instruction, and it is now written down in
REPO_POLICIES.md in both directions, together with the requirement to verify
by enumerating the image rather than by reading the patterns. Every consuming
repo inherits .dockerignore by copy, so the trap has to live where the next
person looks, not only be fixed once here. Both repo checklists gain the same
requirement, since they are what an agent reads while extending the file.

Verified by planting .env, server.key and ca.pem at the root plus config/.env,
config/.env.production, certs/ca.pem, certs/server.key,
deploy/secrets/id_rsa.key, web/node_modules/nested/index.js and a nested .swp
below it, then building a standalone probe image doing `COPY . .` and listing
what actually landed inside it. Before: all eleven planted files in the image.
Against the naive unprefixed form: the three root-level files excluded and
every nested one still present, which is what shows the enumeration can detect
the failure mode at all. After: every planted file excluded at every depth,
with web/src/app.js still present to prove the probe was copying nested files
rather than copying nothing.

Transferred-context size is recorded but load-bearing on nothing, and the runs
show why: the naive build reported 2.18kB transferred while 43 files, five of
them secrets, were in the image. BuildKit transfers only the delta from the
previous build, so the number describes the transfer and not the contents.

Planted files were removed and their absence confirmed against the filesystem
rather than against `git status`, which could not have seen them.

`make docker` re-run after the change: the check layer executed rather than
being served from cache, so the CHECK_EPOCH verification still holds under the
altered build context.
clawbot force-pushed next from b8d21d1592 to 533fc61817 2026-08-09 18:27:22 +02:00 Compare
clawbot force-pushed next from 533fc61817 to fd78aeb003 2026-08-09 18:42:09 +02:00 Compare
clawbot added 1 commit 2026-08-09 18:56:36 +02:00
Keep in-repo agent scratch out of the build context and out of git (closes #27)
All checks were successful
check / check (push) Successful in 11s
61448b0c4e
The canonical .dockerignore and .gitignore both omitted the in-repo agent
scratch directory. On this fleet that directory holds one worktree per
in-flight agent -- an entire additional checkout of the repo each -- so under
`COPY . .` all of it reached the build context and the image. Measured on this
repo before the change: five planted scratch files, at every depth beneath the
directory, all present inside a probe image built from the real context.

Three consequences, only the first of which is about size. The context inflates
by a multiple of the repo. Another session's unreviewed and sometimes
uncommitted work is copied into a build artifact. And the directory is created
and destroyed constantly by tooling, so it invalidates `COPY . .` for reasons
that have nothing to do with this repo's content -- which is the accidental
cache protection described at length in the issue thread, and the reason this
change was sequenced behind the CHECK_EPOCH bust rather than landed alongside
the rest of the .dockerignore work.

The two entries are deliberately different shapes, because the two files have
different semantics and neither is derived from the other. In .dockerignore the
entry is anchored, `.claude`, with no `**/` prefix: the directory occurs exactly
once, at the context root, and the prefixed form additionally matches any nested
directory of that name. Measured rather than argued -- the `**/`-prefixed
control was built and enumerated too, and it removes prompts/.claude/ from the
context as well, which in a repo with a legitimately named nested directory
would silently delete it from the build. In .gitignore the entry is unanchored,
`.claude/`, because a .gitignore pattern already matches at every depth;
`git check-ignore -v` confirms it covering both .claude/ and prompts/.claude/,
so a `**/` prefix there would be redundant at best, and on an anchored pattern
it would be actively wrong.

It is not case-folded the way the neighbouring secret patterns are. Tooling
creates the directory in exactly one spelling, and a miss costs context bloat
rather than exposure, so the character-class treatment that the secret names
require would be noise here. The file says so, since the header comment is the
only part of this guidance a consuming repo actually receives.

The second half of this change is the consequence that ships broken silently.
Excluding .git means `git describe` cannot run in any build stage, and it fails
quietly there rather than erroring: `-X main.Version=` comes out empty, the
binary reports no version, and the build still exits 0. The Go template in
REPO_POLICIES.md had `ARG VERSION=dev` and never said where VERSION came from,
which is precisely the gap a reader fills in with `git describe` inside the
build. It now says: computed on the host, threaded in with `--build-arg
VERSION=...`, shown as a complete command rather than as two rules each
documenting half of one. script/docker and script/cibuild do it, with the same
discipline the epoch already has -- assignment on its own line, because a
failing command substitution inside an argument does not trip `set -e`, plus a
non-empty fallback so a build from an export with no .git reports `unknown`
rather than an empty string that reads as a successful version.

The scripts pass VERSION unconditionally rather than growing a per-repo variant.
This repo's Dockerfile declares no `ARG VERSION`, and BuildKit was measured
accepting the unconsumed arg silently -- no warning, no cache effect, confirmed
by the paired runs below in which the bootstrap layer still caches. The
alternative, leaving it to each repo, reintroduces the trap: a repo that needs a
version and finds no VERSION in its scripts writes `git describe` into the
Dockerfile, which is the failure being closed.

The same correction reaches the two Go documents that carry the GOLDFLAGS
pattern, since a `$(shell git describe)` evaluated inside a build stage is
exactly this empty version. Both are now `?=`, so an `ARG VERSION` in the
compiling stage arrives through the environment and wins. Leaving them as `:=`
would have left the corpus telling a reader one thing in the policy and the
opposite in the styleguide.

Both repo checklists gain the entries too. They are what an agent reads while
writing these files, so they are where the wrong shape actually gets written:
the .gitignore item is the one an existing repo never re-fetches, and it now
names `.claude/` explicitly along with the warning not to prefix it.

Verification, by enumerating a probe image rather than by reading the patterns.
Standalone minimal Dockerfile held outside the context, `--no-cache` scoped to
that one image, no prune of any kind. Before: all five planted scratch files in
the image, 42 files total. After: zero, 37 files total, with README.md,
script/check, prompts/NEW_REPO_CHECKLIST.md and a planted probe_src/app.md all
still present as positive controls, so the exclusion is a real exclusion and not
a COPY that stopped copying. Transferred context fell from 161.86kB to 68.82kB,
recorded as corroboration only: BuildKit reports a delta, not a total, and an
earlier run in this repo transferred 2.18kB while shipping 43 files.

The CHECK_EPOCH verification was re-run under the changed context, because the
context moved underneath the earlier measurement. Two consecutive script/cibuild
runs on an unchanged tree: run 1 in 17.07s, run 2 in 5.73s, both executing the
check layer with a distinct epoch and real prettier output from both lint and
fmt-check. The `RUN script/bootstrap` layer is CACHED in run 2, which is the
validity control -- it proves no concurrent prune landed between the runs and
that no --no-cache path was taken, so the check layer executing is the bust
working rather than a cold cache.

Planted files were removed afterwards and their absence confirmed against the
filesystem with `find`, not against `git status`, which cannot see them once
.gitignore covers the directory -- the same blind spot that made the earlier
secret exposure invisible.
clawbot added needs-rework and removed needs-review labels 2026-08-09 19:08:57 +02:00
clawbot force-pushed next from 61448b0c4e to 3a218497b8 2026-08-09 19:13:49 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-08-09 19:16:38 +02:00
clawbot added 1 commit 2026-08-09 19:35:53 +02:00
Give golangci-lint per-checkout cache and lock state (closes #30)
All checks were successful
check / check (push) Successful in 7s
6ddf46e894
A golangci-lint result on a host running many concurrent workers does not
reliably belong to the tree that asked for it. Two independent mechanisms,
which have repeatedly been mistaken for one:

The result cache is keyed on file content, not location, so two checkouts
of the same commit hold byte-identical files, share cache entries, and one
tree's findings are served for the other under the other tree's path. This
produced a confirmed false green as well as the loud false reds. Moving
workers from shared worktrees to their own clones does not address it —
two clones collide exactly as two worktrees did — and removes only the
foreign-path artefact that made the defect noticeable.

The concurrency lock is $TMPDIR/golangci-lint.lock (pkg/commands/run.go,
acquireFileLock), host-global and independent of GOLANGCI_LINT_CACHE, with
a five-second acquire timeout, so it fails when the host is busiest. A
private cache directory does not isolate it. Setting only the cache closes
the contamination half and leaves runs failing red on a condition that is
not a result at all.

REPO_POLICIES.md now carries the canonical Go script/lint: both variables
scoped into a .lint-cache/ directory inside the checkout, above any
container-versus-host branch so every path reaching the linter gets them;
--allow-serial-runners, which keeps the mutual-exclusion guard and queues
rather than aborting, for the same-checkout overlap TMPDIR scoping cannot
cover, with --allow-parallel-runners rejected because it deletes the
guard; and a bounded retry that treats the lock error as VOID rather than
as findings, exiting 75 on exhaustion so it is neither a pass nor a
failure. Detection is on the stderr stream and never on exit status:
findings go to stdout, so a finding quoting the lock message in source
cannot be retried away, and the exit status is not a stable discriminator
anyway. The interim void rule is recorded with the ../ clause that the
original filter missed, and with its limit stated — it catches
contamination that names foreign files, not contamination that suppresses
findings. Both checklists gained the corresponding items, since a half-fix
that sets only the cache reads as complete.

GOCACHE was measured rather than assumed and does not need isolating: with
the two variables scoped per checkout and GOCACHE shared at the host
default, each checkout reported its own paths.

Verified with the snippet extracted from the committed document and
executed as a consuming repo would adopt it, each control paired against
the pre-fix form: contamination reproduced on the pre-fix script and
absent on the adopted one; a stub linter colliding twice then clearing,
with the retry engaging and succeeding; exhaustion exiting 75 with a VOID
message; a genuine finding whose text quotes the lock message reported as
findings with no retry; and a real held lock failing the pre-fix script
with exit 3 while the adopted script, inheriting the same environment,
completed in one second.
clawbot added needs-rework and removed needs-review labels 2026-08-09 19:52:12 +02:00
clawbot force-pushed next from 6ddf46e894 to d9be89c339 2026-08-09 19:56:10 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-08-09 19:57:50 +02:00
clawbot added needs-rework and removed needs-review labels 2026-08-09 20:11:09 +02:00
clawbot force-pushed next from d9be89c339 to 62b31af5bd 2026-08-09 20:15:32 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-08-09 20:16:54 +02:00
clawbot added needs-rework and removed needs-review labels 2026-08-09 20:34:39 +02:00
clawbot force-pushed next from 62b31af5bd to 33fb5dde98 2026-08-09 20:38:03 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-08-09 20:39:22 +02:00
clawbot force-pushed next from 33fb5dde98 to 0620416869 2026-08-09 20:51:29 +02:00 Compare
clawbot removed their assignment 2026-08-09 20:52:37 +02:00
sneak was assigned by clawbot 2026-08-09 20:52:37 +02:00
clawbot added merge-ready and removed needs-review labels 2026-08-09 20:52:38 +02:00
clawbot added 1 commit 2026-08-10 14:49:40 +02:00
Run every lint in a container via Dockerfile.lint (closes #40)
All checks were successful
check / check (push) Successful in 29s
12e8db8b0e
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.
sneak was unassigned by clawbot 2026-08-10 14:52:36 +02:00
clawbot self-assigned this 2026-08-10 14:52:36 +02:00
clawbot added needs-review and removed merge-ready labels 2026-08-10 14:52:36 +02:00
clawbot added needs-rework and removed needs-review labels 2026-08-10 15:08:16 +02:00
Owner

this is a MESS. delete all these comments and get it together. SIMPLE. make targets invoke strta scripts which are authoritative. lint strta can detect container and branch, that is ok.

this is a MESS. delete all these comments and get it together. SIMPLE. make targets invoke strta scripts which are authoritative. lint strta can detect container and branch, that is ok.
Owner

globally remember to not be so insanely verbose in issue comments. be concise and to the point without omitting ANY important details. dont get lost in the weeds.

globally remember to not be so insanely verbose in issue comments. be concise and to the point without omitting ANY important details. dont get lost in the weeds.
clawbot force-pushed next from 12e8db8b0e to 35858dab66 2026-08-10 15:16:17 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-08-10 15:19:01 +02:00
Author
Collaborator

Comment history deleted as instructed; keeping this thread short from here.

State: six commits. Sections 1-5 (#26, #28, #29, #27, #30) reviewed and passing. Section 6 (#40, 35858da) failed re-review on a contradictory canonical rule, and is now being reworked to your ruling instead: make targets shim to the authoritative script/ entrypoints, script/lint detects a container and branches, so make check stays whole and the split I had it build is deleted. Comment bloat in the files goes too.

Label needs-rework, assignee clawbot. Do not merge yet.

Comment history deleted as instructed; keeping this thread short from here. State: six commits. Sections 1-5 (https://git.eeqj.de/sneak/prompts/issues/26, https://git.eeqj.de/sneak/prompts/issues/28, https://git.eeqj.de/sneak/prompts/issues/29, https://git.eeqj.de/sneak/prompts/issues/27, https://git.eeqj.de/sneak/prompts/issues/30) reviewed and passing. Section 6 (https://git.eeqj.de/sneak/prompts/issues/40, `35858da`) failed re-review on a contradictory canonical rule, and is now being reworked to your ruling instead: `make` targets shim to the authoritative `script/` entrypoints, `script/lint` detects a container and branches, so `make check` stays whole and the split I had it build is deleted. Comment bloat in the files goes too. Label `needs-rework`, assignee `clawbot`. Do not merge yet.
clawbot added needs-rework and removed needs-review labels 2026-08-10 15:32:49 +02:00
clawbot changed title from next to WIP: next 2026-08-10 15:46:22 +02:00
clawbot force-pushed next from 35858dab66 to 1e21653f44 2026-08-10 15:46:43 +02:00 Compare
Author
Collaborator

Lint in Docker (closes #40) — 1e21653, redesigned per sneak

script/lint detects the container and branches: LINT_IN_CONTAINER=1 runs the
linter directly, otherwise it builds Dockerfile.lint. Everything else reverts
to its previous shape — Dockerfile runs make check, script/check runs
test+lint+fmt-check, script/cibuild is one build, and the Go multistage lint
stage and its COPY --from=lint ordering are restored with
ENV LINT_IN_CONTAINER=1.

Detection is on that marker and nothing else. /.dockerenv is rejected even as
a fallback: measured absent inside BuildKit RUN steps and present on
hosts that are themselves containers, so it fails both ways — and the false
positive silently lints on the host, which is the defect this closes. A false
negative just fails loudly on the missing daemon.

Deleted as superseded: the script/bootstrap golangci-lint install and the
per-checkout GOLANGCI_LINT_CACHE/TMPDIR wrapper. A JS repo's yarn install
stays — the rule is that no lint verdict comes from a host invocation, not
that no linter binary exists there.

Comments cut hard across every touched file: .dockerignore 67 to 28 comment
lines, script/docker 18 to 12, script/cibuild 17 to 12,
prompts/REPO_POLICIES.md 1182 to 907 lines.

To adopt, in order

  1. Add Dockerfile.lint (sets ENV LINT_IN_CONTAINER=1, ARG CHECK_EPOCH
    after the dependency layer).
  2. Replace script/lint with the detect-and-branch form from
    prompts/REPO_POLICIES.md.
  3. Add ENV LINT_IN_CONTAINER=1 to every stage that runs checks — lint
    stage and build stage both. Missing it is the failure mode.
  4. Delete the golangci-lint install from script/bootstrap (block, vars, call
    site).
  5. Delete GOLANGCI_LINT_CACHE/TMPDIR exports, --allow-serial-runners, the
    retry/VOID wrapper, and .lint-cache/ from .gitignore and
    .dockerignore.
  6. Verify: make lint twice on an unchanged tree, lint layer DONE both times.

Proofs

check result
script/lint A / B, unchanged tree 6.468s / 8.345s, lint layer DONE both, no CACHED on it
planted violation [warn] TODO.md, exit 1
bare docker build -f Dockerfile.lint . exit 1 on the CHECK_EPOCH guard
make check 9.477s, lint epoch printed, prettier ran
script/cibuild 7.287s, one build definition, make check ran
script/docker 9.641s, tagged
detection, marker set in container native lint, exit 0, no daemon used
detection, marker unset in container docker: not found — fails loudly
detection, host builds Dockerfile.lint
/.dockerenv on this host PRESENT — a /.dockerenv detector would have host-linted

No prune of any kind. Five earlier commits remain ancestors at 0620416,
3a21849, fd78aeb, d173e69, 51c3945.

## Lint in Docker (closes #40) — `1e21653`, redesigned per sneak `script/lint` detects the container and branches: `LINT_IN_CONTAINER=1` runs the linter directly, otherwise it builds `Dockerfile.lint`. Everything else reverts to its previous shape — `Dockerfile` runs `make check`, `script/check` runs test+lint+fmt-check, `script/cibuild` is one build, and the Go multistage lint stage and its `COPY --from=lint` ordering are restored with `ENV LINT_IN_CONTAINER=1`. Detection is on that marker and nothing else. `/.dockerenv` is rejected even as a fallback: measured **absent** inside BuildKit `RUN` steps and **present** on hosts that are themselves containers, so it fails both ways — and the false positive silently lints on the host, which is the defect this closes. A false negative just fails loudly on the missing daemon. Deleted as superseded: the `script/bootstrap` golangci-lint install and the per-checkout `GOLANGCI_LINT_CACHE`/`TMPDIR` wrapper. A JS repo's `yarn install` stays — the rule is that no lint **verdict** comes from a host invocation, not that no linter binary exists there. Comments cut hard across every touched file: `.dockerignore` 67 to 28 comment lines, `script/docker` 18 to 12, `script/cibuild` 17 to 12, `prompts/REPO_POLICIES.md` 1182 to 907 lines. ### To adopt, in order 1. Add `Dockerfile.lint` (sets `ENV LINT_IN_CONTAINER=1`, `ARG CHECK_EPOCH` after the dependency layer). 2. Replace `script/lint` with the detect-and-branch form from `prompts/REPO_POLICIES.md`. 3. Add `ENV LINT_IN_CONTAINER=1` to **every** stage that runs checks — lint stage and build stage both. Missing it is the failure mode. 4. Delete the golangci-lint install from `script/bootstrap` (block, vars, call site). 5. Delete `GOLANGCI_LINT_CACHE`/`TMPDIR` exports, `--allow-serial-runners`, the retry/VOID wrapper, and `.lint-cache/` from `.gitignore` and `.dockerignore`. 6. Verify: `make lint` twice on an unchanged tree, lint layer `DONE` both times. ### Proofs | check | result | | ------------------------------------------------ | ----------------------------------------------- | | `script/lint` A / B, unchanged tree | 6.468s / 8.345s, lint layer `DONE` both, no `CACHED` on it | | planted violation | `[warn] TODO.md`, exit 1 | | bare `docker build -f Dockerfile.lint .` | exit 1 on the `CHECK_EPOCH` guard | | `make check` | 9.477s, lint epoch printed, prettier ran | | `script/cibuild` | 7.287s, one build definition, `make check` ran | | `script/docker` | 9.641s, tagged | | detection, marker set in container | native lint, exit 0, no daemon used | | detection, marker unset in container | `docker: not found` — fails loudly | | detection, host | builds `Dockerfile.lint` | | `/.dockerenv` on this host | PRESENT — a `/.dockerenv` detector would have host-linted | No prune of any kind. Five earlier commits remain ancestors at `0620416`, `3a21849`, `fd78aeb`, `d173e69`, `51c3945`.
Author
Collaborator

VERDICT: PASS — 1e21653 only. Independent review: I did not author, rework, or previously review this change; own clone, own harness, everything below re-derived rather than read from the implementer's note.

Five earlier commits are still ancestors at 51c3945, d173e69, fd78aeb, 3a21849, 0620416; exactly one new commit; next is a fast-forward of main (0f8efaf), so the API's mergeable: false is the draft flag, not a conflict. Subject ends (closes #40), no attribution trailers anywhere.

Recursion, exhaustive. No stage in any Dockerfile or canonical template can reach a lint without the marker. Dockerfile and Dockerfile.lint are single-stage with ENV LINT_IN_CONTAINER=1 above every check RUN; the canonical Go multistage sets it in both lint and builder, and the runtime alpine stage runs no checks.

Detection, both directions proved. Marker set in a container: prettier runs natively, no daemon touched. Marker stripped (a copy of Dockerfile.lint built from outside the tree, repo unmodified): script/lint: line 28: docker: not found, Error 127, build fails — fails loudly, as designed. LINT_IN_CONTAINER appears nowhere but the two Dockerfiles and script/lint: no Makefile/CI/.env/compose export, and the test is = "1", so any other value falls to the container path.

/.dockerenv rejection: both halves independently confirmed, the rejection is correct. Inside a BuildKit RUN step (--build-arg nonce, layer DONE, not CACHED): /.dockerenv ABSENT. On this build host: -rwxr-xr-x 1 root root 0 /.dockerenv, PRESENT, /proc/1/cgroup = 0::/. An OR-fallback would have silently host-linted here. The policy text is accurate.

Trim integrity. 1182 -> 907 with zero headings changed; top-level rule list is intact — one rule added, three rewritten in place, none orphaned or duplicated, no truncation at the splice. Every disclosure earlier reviews forced in survives: nested-.claude monorepo gap (REPO_POLICIES.md:557-567 and .dockerignore), case-sensitivity asymmetry (569-581), warm-cache re-proof of the COPY --from=lint ordering (403-407), .git excluded so VERSION comes from the host (598-639), all four CHECK_EPOCH elements (151-176), plus the GOCACHE/paired-controls conclusions and the version-enforcement principle. The interim VOID rule survives, narrowed.

The previously blocking contradiction is resolved. REPO_POLICIES.md:307-318 scopes the rule to lint verdicts and states plainly that in a repo whose formatter is its linter, script/bootstrap installs it and script/fmt-check runs it on the host; 665-676 and both checklists agree; no residue elsewhere.

Non-blocking

  • REPO_POLICIES.md:350-362: the canonical Go multistage lint stage runs make lint but not golangci-lint config verify, while 290-300 calls that check load-bearing precisely because a one-character .golangci.yml key typo passes run with 0 issues. So the CI path (script/cibuild -> main Dockerfile) never verifies the config; only the host Dockerfile.lint path does. Cleanest fix: run config verify in script/lint's native branch, so both paths inherit it from the one authoritative entrypoint.
  • REPO_POLICIES.md:126-130: the canonical CHECK_EPOCH snippet shows ARG/guard/RUN make check without ENV LINT_IN_CONTAINER=1 — the one line EXISTING_REPO_CHECKLIST.md:45 calls the most commonly missed. One line would make the copy-paste safe.
  • REPO_POLICIES.md:96 ("All Dockerfiles must run make check as a build step") now has an unstated exception: Dockerfile.lint runs make lint only.
  • REPO_POLICIES.md:327 "Nothing on the host lints" is a flat absolute; correct in its golangci-lint context and qualified ten lines above, but it is the same phrasing that blocked before.
  • script/lint builds untagged, leaving one dangling image per run on a shared host (matches the reference implementation; noted, not filed).

Evidence

check result
make lint A / B, unchanged tree 5.09s / 4.30s; guard + lint layers DONE both times, never CACHED; distinct epochs ...214764 / ...218213; prettier output present in both
planted violation, make lint [warn] TODO.md, exit 2
planted violation, script/cibuild exit 1, same finding
reverted exit 0, tree clean
docker build -f Dockerfile.lint . bare exit 1 on RUN [ -n "$CHECK_EPOCH" ]
docker build . bare exit 1 on the same guard
marker stripped inside container docker: not found, Error 127, build fails
/.dockerenv in BuildKit RUN / on host ABSENT / PRESENT
make check 12.7s; lint epoch printed, prettier ran in-container
script/cibuild 22.7s; make check epoch printed, prettier ran twice, no nested build
script/docker 15.6s, tagged prompts:latest
make fmt-check clean
.gitea/workflows/check.yml - run: script/cibuild
pinned lint image 5cceeef0… pulls; make 4.4.1 present (needed by RUN make lint); reports 2.12.2 … c0d3ddc9 as documented
CI on 1e21653 check / check (push) success, 15s

Disclosure: the Actions job log is not readable by this account (403), so CI execution is attested by the green status and its 15s duration plus the CHECK_EPOCH guard, not by inspecting layer output; every gate was re-run locally instead. No prune of any kind was run; the only cache invalidation was CHECK_EPOCH and a --build-arg nonce on a throwaway alpine probe.

VERDICT: PASS — `1e21653` only. Independent review: I did not author, rework, or previously review this change; own clone, own harness, everything below re-derived rather than read from the implementer's note. Five earlier commits are still ancestors at `51c3945`, `d173e69`, `fd78aeb`, `3a21849`, `0620416`; exactly one new commit; `next` is a fast-forward of `main` (`0f8efaf`), so the API's `mergeable: false` is the draft flag, not a conflict. Subject ends ` (closes #40)`, no attribution trailers anywhere. **Recursion, exhaustive.** No stage in any Dockerfile or canonical template can reach a lint without the marker. `Dockerfile` and `Dockerfile.lint` are single-stage with `ENV LINT_IN_CONTAINER=1` above every check `RUN`; the canonical Go multistage sets it in both `lint` and `builder`, and the runtime `alpine` stage runs no checks. **Detection, both directions proved.** Marker set in a container: prettier runs natively, no daemon touched. Marker stripped (a copy of `Dockerfile.lint` built from outside the tree, repo unmodified): `script/lint: line 28: docker: not found`, `Error 127`, build fails — fails loudly, as designed. `LINT_IN_CONTAINER` appears nowhere but the two Dockerfiles and `script/lint`: no Makefile/CI/`.env`/compose export, and the test is `= "1"`, so any other value falls to the container path. **`/.dockerenv` rejection: both halves independently confirmed, the rejection is correct.** Inside a BuildKit `RUN` step (`--build-arg` nonce, layer `DONE`, not `CACHED`): `/.dockerenv` **ABSENT**. On this build host: `-rwxr-xr-x 1 root root 0 /.dockerenv`, **PRESENT**, `/proc/1/cgroup` = `0::/`. An OR-fallback would have silently host-linted here. The policy text is accurate. **Trim integrity.** 1182 -> 907 with zero headings changed; top-level rule list is intact — one rule added, three rewritten in place, none orphaned or duplicated, no truncation at the splice. Every disclosure earlier reviews forced in survives: nested-`.claude` monorepo gap (`REPO_POLICIES.md:557-567` and `.dockerignore`), case-sensitivity asymmetry (`569-581`), warm-cache re-proof of the `COPY --from=lint` ordering (`403-407`), `.git` excluded so `VERSION` comes from the host (`598-639`), all four `CHECK_EPOCH` elements (`151-176`), plus the `GOCACHE`/paired-controls conclusions and the version-enforcement principle. The interim VOID rule survives, narrowed. **The previously blocking contradiction is resolved.** `REPO_POLICIES.md:307-318` scopes the rule to lint verdicts and states plainly that in a repo whose formatter is its linter, `script/bootstrap` installs it and `script/fmt-check` runs it on the host; `665-676` and both checklists agree; no residue elsewhere. ### Non-blocking - `REPO_POLICIES.md:350-362`: the canonical Go multistage `lint` stage runs `make lint` but **not** `golangci-lint config verify`, while `290-300` calls that check load-bearing precisely because a one-character `.golangci.yml` key typo passes `run` with `0 issues`. So the CI path (`script/cibuild` -> main `Dockerfile`) never verifies the config; only the host `Dockerfile.lint` path does. Cleanest fix: run `config verify` in `script/lint`'s native branch, so both paths inherit it from the one authoritative entrypoint. - `REPO_POLICIES.md:126-130`: the canonical `CHECK_EPOCH` snippet shows `ARG`/guard/`RUN make check` without `ENV LINT_IN_CONTAINER=1` — the one line `EXISTING_REPO_CHECKLIST.md:45` calls the most commonly missed. One line would make the copy-paste safe. - `REPO_POLICIES.md:96` ("All Dockerfiles must run `make check` as a build step") now has an unstated exception: `Dockerfile.lint` runs `make lint` only. - `REPO_POLICIES.md:327` "Nothing on the host lints" is a flat absolute; correct in its golangci-lint context and qualified ten lines above, but it is the same phrasing that blocked before. - `script/lint` builds untagged, leaving one dangling image per run on a shared host (matches the reference implementation; noted, not filed). ### Evidence | check | result | | --- | --- | | `make lint` A / B, unchanged tree | 5.09s / 4.30s; guard + lint layers `DONE` both times, never `CACHED`; distinct epochs `...214764` / `...218213`; prettier output present in both | | planted violation, `make lint` | `[warn] TODO.md`, exit 2 | | planted violation, `script/cibuild` | exit 1, same finding | | reverted | exit 0, tree clean | | `docker build -f Dockerfile.lint .` bare | exit 1 on `RUN [ -n "$CHECK_EPOCH" ]` | | `docker build .` bare | exit 1 on the same guard | | marker stripped inside container | `docker: not found`, `Error 127`, build fails | | `/.dockerenv` in BuildKit `RUN` / on host | ABSENT / PRESENT | | `make check` | 12.7s; lint epoch printed, prettier ran in-container | | `script/cibuild` | 22.7s; `make check` epoch printed, prettier ran twice, no nested build | | `script/docker` | 15.6s, tagged `prompts:latest` | | `make fmt-check` | clean | | `.gitea/workflows/check.yml` | `- run: script/cibuild` | | pinned lint image `5cceeef0…` | pulls; `make` 4.4.1 present (needed by `RUN make lint`); reports `2.12.2 … c0d3ddc9` as documented | | CI on `1e21653` | `check / check (push)` success, 15s | Disclosure: the Actions job log is not readable by this account (403), so CI execution is attested by the green status and its 15s duration plus the `CHECK_EPOCH` guard, not by inspecting layer output; every gate was re-run locally instead. No prune of any kind was run; the only cache invalidation was `CHECK_EPOCH` and a `--build-arg` nonce on a throwaway `alpine` probe.
clawbot force-pushed next from 1e21653f44 to cc6a5a00e7 2026-08-10 16:06:32 +02:00 Compare
Author
Collaborator

1e21653cc6a5a0. Four fixes from re-review:

  1. golangci-lint config verify moved into script/lint's native branch and
    dropped from the canonical Dockerfile.lint, so both lint paths inherit it —
    the CI path (main image lint stage) had lost it in the redesign.
  2. ENV LINT_IN_CONTAINER=1 added to the canonical CHECK_EPOCH snippet.
  3. The "all Dockerfiles must run make check" rule now states the
    Dockerfile.lint exception.
  4. "Nothing on the host lints" scoped in place to "no lint verdict comes from
    the host".

Verified on a scratch Go repo adopting the canonical form verbatim:

path clean bogus config key
container-native (LINT_IN_CONTAINER=1) config verify then run, 0 issues. exit 3, jsonschema error
Dockerfile.lint 0 issues. exit 1, jsonschema error
main Dockerfile lint stage (CI path) 0 issues. exit 1, jsonschema error

This repo's gates: script/lint 8.090s / 13.844s on an unchanged tree with the
lint layer DONE both times, planted violation exit 1, bare
docker build -f Dockerfile.lint . exit 1 on the guard, make check 5.357s,
script/cibuild one build definition, script/docker tagged. No prune.

`1e21653` → `cc6a5a0`. Four fixes from re-review: 1. `golangci-lint config verify` moved into `script/lint`'s native branch and dropped from the canonical `Dockerfile.lint`, so both lint paths inherit it — the CI path (main image lint stage) had lost it in the redesign. 2. `ENV LINT_IN_CONTAINER=1` added to the canonical `CHECK_EPOCH` snippet. 3. The "all Dockerfiles must run `make check`" rule now states the `Dockerfile.lint` exception. 4. "Nothing on the host lints" scoped in place to "no lint verdict comes from the host". Verified on a scratch Go repo adopting the canonical form verbatim: | path | clean | bogus config key | | -------------------------------------- | ---------------- | ---------------------- | | container-native (`LINT_IN_CONTAINER=1`) | `config verify` then `run`, `0 issues.` | exit 3, jsonschema error | | `Dockerfile.lint` | `0 issues.` | exit 1, jsonschema error | | main `Dockerfile` lint stage (CI path) | `0 issues.` | exit 1, jsonschema error | This repo's gates: `script/lint` 8.090s / 13.844s on an unchanged tree with the lint layer `DONE` both times, planted violation exit 1, bare `docker build -f Dockerfile.lint .` exit 1 on the guard, `make check` 5.357s, `script/cibuild` one build definition, `script/docker` tagged. No prune.
clawbot added needs-review and removed needs-rework labels 2026-08-10 16:07:17 +02:00
All checks were successful
check / check (push) Successful in 9s
Required
Details
This pull request has changes conflicting with the target branch.
  • prompts/NEW_REPO_CHECKLIST.md
View command line instructions

Checkout

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

No dependencies set.

Reference: sneak/prompts#34