Owner ruling, sneak 2026-08-09: every lint run happens inside a Docker container, invoked through the script/ entrypoint. Docker is always available. Linting runs independently and does not need a cache. He has directed a PR for every repo not already set up this way.
Reference implementation is sneak/homoicon — copy its shape: a root Dockerfile.lint built FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, which COPYs the repo in and runs golangci-lint run --config .golangci.yml ./... as a build step, with script/lint reduced to building it. Linting as a build step means a successful build IS a clean lint.
This supersedes the version-pinning approach scoped for script/lint here, and it also removes the escape-into-another-worktree problem observed in this repo — a host lint run reported findings whose paths pointed into a different agent's worktree entirely.
Note this repo already builds lint in a separate hash-pinned Docker stage; the change is to make script/lint the only path and drop any host invocation, not to introduce Docker where there was none.
Two things to get right, both of which would otherwise ship a false green:
A cached build lints nothing. A lint build on an unchanged tree returns success in well under a second having run no linter. Caching is explicitly waived here, so force the lint layers to execute.
golangci-lint config verify fetches its JSON schema over an unpinned live HTTPS call. Decide deliberately whether to include it.
Definition of done
script/lint runs the linter only in Docker; no host golangci-lint path remains.
Two consecutive script/lint runs on an unchanged tree both demonstrably execute the linter.
Negative control: introduce a deliberate lint violation, confirm it fails with that specific finding, revert, confirm clean.
SPLIT 2026-09-03:#104 could not be completed inside a single agent run (three attempts died mid-run having pushed nothing). Split into:
issue #107 — add hash-pinned Dockerfile.lint, make script/lint Docker-only (mechanical)
issue #108 — prove the gate actually lints: layer-cache defeat + negative control (blocked on 107)
This issue stays open as the parent tracker and should be closed by hand once both children merge. Do not dispatch work directly against #104.
Owner ruling, sneak 2026-08-09: every lint run happens inside a Docker container, invoked through the `script/` entrypoint. Docker is always available. Linting runs independently and does not need a cache. He has directed a PR for every repo not already set up this way.
Reference implementation is `sneak/homoicon` — copy its shape: a root `Dockerfile.lint` built `FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240`, which COPYs the repo in and runs `golangci-lint run --config .golangci.yml ./...` as a build step, with `script/lint` reduced to building it. Linting as a build step means a successful build IS a clean lint.
This supersedes the version-pinning approach scoped for `script/lint` here, and it also removes the escape-into-another-worktree problem observed in this repo — a host lint run reported findings whose paths pointed into a different agent's worktree entirely.
Note this repo already builds lint in a separate hash-pinned Docker stage; the change is to make `script/lint` the only path and drop any host invocation, not to introduce Docker where there was none.
Two things to get right, both of which would otherwise ship a false green:
1. **A cached build lints nothing.** A lint build on an unchanged tree returns success in well under a second having run no linter. Caching is explicitly waived here, so force the lint layers to execute.
2. **`golangci-lint config verify` fetches its JSON schema over an unpinned live HTTPS call.** Decide deliberately whether to include it.
## Definition of done
- `script/lint` runs the linter only in Docker; no host golangci-lint path remains.
- Two consecutive `script/lint` runs on an unchanged tree both demonstrably execute the linter.
- Negative control: introduce a deliberate lint violation, confirm it fails with that specific finding, revert, confirm clean.
- `make check` still green.
Canonical tracking issue: https://git.eeqj.de/sneak/prompts/issues/40
---
**SPLIT 2026-09-03:** #104 could not be completed inside a single agent run (three attempts died mid-run having pushed nothing). Split into:
- [issue #107](https://git.eeqj.de/sneak/pixa/issues/107) — add hash-pinned `Dockerfile.lint`, make `script/lint` Docker-only (mechanical)
- [issue #108](https://git.eeqj.de/sneak/pixa/issues/108) — prove the gate actually lints: layer-cache defeat + negative control (blocked on 107)
This issue stays open as the parent tracker and should be closed by hand once both children merge. Do not dispatch work directly against #104.
Plan. The two sub-issues were closed as out-of-fleet, so this issue is dispatched directly as one unit, with their definitions of done folded in: Dockerfile.lint hash-pinned, script/lint reduced to building it, the lint layers forced to run every time, a negative control, and the config verify question decided in the PR body.
One interaction the sub-issues did not name: once script/lint is Docker-only, the lint stage inside Dockerfile can no longer run make lint (there is no Docker inside a build). Copy sneak/homoicon's shape for that stage as well, so the same linter runs the same way in CI and locally.
Base branch next. Runs after nothing; #101 follows it because both change how the checks run in Docker.
model: claude-fable-5
Plan. The two sub-issues were closed as out-of-fleet, so this issue is dispatched directly as one unit, with their definitions of done folded in: `Dockerfile.lint` hash-pinned, `script/lint` reduced to building it, the lint layers forced to run every time, a negative control, and the `config verify` question decided in the PR body.
One interaction the sub-issues did not name: once `script/lint` is Docker-only, the lint stage inside `Dockerfile` can no longer run `make lint` (there is no Docker inside a build). Copy `sneak/homoicon`'s shape for that stage as well, so the same linter runs the same way in CI and locally.
Base branch `next`. Runs after nothing; https://git.eeqj.de/sneak/pixa/issues/101 follows it because both change how the checks run in Docker.
model: claude-fable-5
Add root Dockerfile.lint built from the pinned golangci/golangci-lint:v2.12.2-alpine image this repo's Dockerfile already uses, not the Debian digest the issue quotes from homoicon: pixa is CGO/libvips, so the lint image must carry vips-dev/libheif-dev/build-base/pkgconfig for the type-aware linters to compile, and reusing the one digest already pinned here avoids two v2.12.2 images drifting.
script/lint becomes a single docker build -f Dockerfile.lint; no host or nix-shell golangci-lint path remains.
Cache defeat: script/lint passes a unique CACHEBUST build-arg and Dockerfile.lint fails the build when it is unset, so the lint layer re-executes every run and cannot silently serve a cached, un-run result.
The Dockerfile lint stage can no longer call make lint (no Docker inside a build), so it invokes golangci-lint run directly, same image and config.
golangci-lint config verify is excluded: it fetches its schema over an unpinned live HTTPS call, which REPO_POLICIES forbids without exception.
Negative control and the two-run cache-defeat check are run and their outcome recorded on the PR; full transcripts go to the manager, per the no-passing-logs rule.
model: claude-opus-4-8
Plan for https://git.eeqj.de/sneak/pixa/issues/104, folding in https://git.eeqj.de/sneak/pixa/issues/107 and https://git.eeqj.de/sneak/pixa/issues/108.
- Add root `Dockerfile.lint` built from the pinned `golangci/golangci-lint:v2.12.2-alpine` image this repo's `Dockerfile` already uses, not the Debian digest the issue quotes from `homoicon`: pixa is CGO/libvips, so the lint image must carry `vips-dev`/`libheif-dev`/`build-base`/`pkgconfig` for the type-aware linters to compile, and reusing the one digest already pinned here avoids two `v2.12.2` images drifting.
- `script/lint` becomes a single `docker build -f Dockerfile.lint`; no host or nix-shell golangci-lint path remains.
- Cache defeat: `script/lint` passes a unique `CACHEBUST` build-arg and `Dockerfile.lint` fails the build when it is unset, so the lint layer re-executes every run and cannot silently serve a cached, un-run result.
- The `Dockerfile` lint stage can no longer call `make lint` (no Docker inside a build), so it invokes `golangci-lint run` directly, same image and config.
- `golangci-lint config verify` is excluded: it fetches its schema over an unpinned live HTTPS call, which REPO_POLICIES forbids without exception.
- Negative control and the two-run cache-defeat check are run and their outcome recorded on the PR; full transcripts go to the manager, per the no-passing-logs rule.
model: claude-opus-4-8
All linting now runs only inside Docker: script/lint builds a hash-pinned root Dockerfile.lint, and the host/nix-shell golangci-lint invocation is removed. A per-run CACHEBUST build-arg forces the lint step to re-execute every run, so an unchanged tree cannot pass having linted nothing. Dockerfile's lint stage now runs golangci-lint directly, since make lint builds a container and there is no Docker inside a build. golangci-lint config verify stays out: it fetches its schema over an unpinned live HTTPS call.
Verified: two consecutive script/lint runs both re-ran the linter; the negative control failed with its specific finding and was clean after revert; the authoritative gate docker build --target builder . passed.
Model: opus-4-8
Implemented as https://git.eeqj.de/sneak/pixa/pulls/122 (base `next`).
All linting now runs only inside Docker: `script/lint` builds a hash-pinned root `Dockerfile.lint`, and the host/nix-shell `golangci-lint` invocation is removed. A per-run `CACHEBUST` build-arg forces the lint step to re-execute every run, so an unchanged tree cannot pass having linted nothing. `Dockerfile`'s lint stage now runs `golangci-lint` directly, since `make lint` builds a container and there is no Docker inside a build. `golangci-lint config verify` stays out: it fetches its schema over an unpinned live HTTPS call.
Verified: two consecutive `script/lint` runs both re-ran the linter; the negative control failed with its specific finding and was clean after revert; the authoritative gate `docker build --target builder .` passed.
Model: opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Owner ruling, sneak 2026-08-09: every lint run happens inside a Docker container, invoked through the
script/entrypoint. Docker is always available. Linting runs independently and does not need a cache. He has directed a PR for every repo not already set up this way.Reference implementation is
sneak/homoicon— copy its shape: a rootDockerfile.lintbuiltFROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240, which COPYs the repo in and runsgolangci-lint run --config .golangci.yml ./...as a build step, withscript/lintreduced to building it. Linting as a build step means a successful build IS a clean lint.This supersedes the version-pinning approach scoped for
script/linthere, and it also removes the escape-into-another-worktree problem observed in this repo — a host lint run reported findings whose paths pointed into a different agent's worktree entirely.Note this repo already builds lint in a separate hash-pinned Docker stage; the change is to make
script/lintthe only path and drop any host invocation, not to introduce Docker where there was none.Two things to get right, both of which would otherwise ship a false green:
golangci-lint config verifyfetches its JSON schema over an unpinned live HTTPS call. Decide deliberately whether to include it.Definition of done
script/lintruns the linter only in Docker; no host golangci-lint path remains.script/lintruns on an unchanged tree both demonstrably execute the linter.make checkstill green.Canonical tracking issue: sneak/prompts#40
SPLIT 2026-09-03: #104 could not be completed inside a single agent run (three attempts died mid-run having pushed nothing). Split into:
Dockerfile.lint, makescript/lintDocker-only (mechanical)This issue stays open as the parent tracker and should be closed by hand once both children merge. Do not dispatch work directly against #104.
clawbot referenced this issue2026-09-03 20:06:21 +02:00
Plan. The two sub-issues were closed as out-of-fleet, so this issue is dispatched directly as one unit, with their definitions of done folded in:
Dockerfile.linthash-pinned,script/lintreduced to building it, the lint layers forced to run every time, a negative control, and theconfig verifyquestion decided in the PR body.One interaction the sub-issues did not name: once
script/lintis Docker-only, the lint stage insideDockerfilecan no longer runmake lint(there is no Docker inside a build). Copysneak/homoicon's shape for that stage as well, so the same linter runs the same way in CI and locally.Base branch
next. Runs after nothing; #101 follows it because both change how the checks run in Docker.model: claude-fable-5
Plan for #104, folding in #107 and #108.
Dockerfile.lintbuilt from the pinnedgolangci/golangci-lint:v2.12.2-alpineimage this repo'sDockerfilealready uses, not the Debian digest the issue quotes fromhomoicon: pixa is CGO/libvips, so the lint image must carryvips-dev/libheif-dev/build-base/pkgconfigfor the type-aware linters to compile, and reusing the one digest already pinned here avoids twov2.12.2images drifting.script/lintbecomes a singledocker build -f Dockerfile.lint; no host or nix-shell golangci-lint path remains.script/lintpasses a uniqueCACHEBUSTbuild-arg andDockerfile.lintfails the build when it is unset, so the lint layer re-executes every run and cannot silently serve a cached, un-run result.Dockerfilelint stage can no longer callmake lint(no Docker inside a build), so it invokesgolangci-lint rundirectly, same image and config.golangci-lint config verifyis excluded: it fetches its schema over an unpinned live HTTPS call, which REPO_POLICIES forbids without exception.model: claude-opus-4-8
Implemented as #122 (base
next).All linting now runs only inside Docker:
script/lintbuilds a hash-pinned rootDockerfile.lint, and the host/nix-shellgolangci-lintinvocation is removed. A per-runCACHEBUSTbuild-arg forces the lint step to re-execute every run, so an unchanged tree cannot pass having linted nothing.Dockerfile's lint stage now runsgolangci-lintdirectly, sincemake lintbuilds a container and there is no Docker inside a build.golangci-lint config verifystays out: it fetches its schema over an unpinned live HTTPS call.Verified: two consecutive
script/lintruns both re-ran the linter; the negative control failed with its specific finding and was clean after revert; the authoritative gatedocker build --target builder .passed.Model: opus-4-8
clawbot referenced this issue2026-09-28 14:27:50 +02:00