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, and it works even where the docker daemon is remote and bind mounts are impossible.
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.
Also remove golangci-lint installation from script/bootstrap — nothing runs on the host any more.
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.
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, and it works even where the docker daemon is remote and bind mounts are impossible.
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.
Also remove golangci-lint installation from `script/bootstrap` — nothing runs on the host any more.
## 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
Queued after #10, which changes the same files (Dockerfile, script/bootstrap, .golangci.yml). Dispatch it once that lands on next: one issue-to-pr worker, branch cut from next, PR based on next. The worker reads sneak/homoicon for the shape and decides how make check inside the main Dockerfile handles lint once script/lint itself runs docker build.
Model: opus-5-5
Queued after https://git.eeqj.de/sneak/attrsum/issues/10, which changes the same files (`Dockerfile`, `script/bootstrap`, `.golangci.yml`). Dispatch it once that lands on `next`: one `issue-to-pr` worker, branch cut from `next`, PR based on `next`. The worker reads `sneak/homoicon` for the shape and decides how `make check` inside the main `Dockerfile` handles lint once `script/lint` itself runs `docker build`.
Model: opus-5-5
Plan. Dispatched once #10 is on next. The shape is settled by REPO_POLICIES.md in sneak/prompts at commit dd4027b (the commit #13 re-vendors from), following sneak's ruling on sneak/prompts#40 (2026-08-10). It replaces the Dockerfile.lint shape in the body above: lint and test are phases of the one Dockerfile, there is no separate lint file, and there is no golangci-lint config verify step.
Implementer's brief:
Dockerfile: copy the canonical Go example in REPO_POLICIES.md at dd4027b. A lint stage FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 (v2.12.2, dated comment) that runs golangci-lint run --config .golangci.yml ./...; a test stage on the pinned Debian Go image that runs the tests as an unprivileged user, as today (the permission tests need it); the build stage takes COPY --from=lint /src/go.sum /dev/null and the same from test; the runtime stage stays last. The version step stays as it is on next (it already matches that example).
The build stage no longer runs make check: once script/lint and script/test are docker builds they cannot run inside one.
script/lint and script/test each build only their phase: docker build --no-cache --target <phase> -t "$(script/projectname)-<phase>" .. script/check runs test, lint and script/fmt-check (host). script/cibuild runs script/bootstrap, script/check, then builds the image tagged and with --no-cache.
script/bootstrap: drop golangci-lint; nothing lints on the host.
Not here: .golangci.yml and the move to golangci-lint v2.14.0 change together in #13.
README.md and TODO.md updated in the same commit.
Definition of done: the issue's list, plus: plant a lint violation and script/cibuild fails on it, and a failing test fails script/cibuild too; a plain docker build . still builds the lint and test phases; make check and script/cibuild pass.
Model: opus-5-5
Plan. Dispatched once https://git.eeqj.de/sneak/attrsum/issues/10 is on `next`. The shape is settled by `REPO_POLICIES.md` in `sneak/prompts` at commit `dd4027b` (the commit https://git.eeqj.de/sneak/attrsum/issues/13 re-vendors from), following sneak's ruling on https://git.eeqj.de/sneak/prompts/issues/40 (2026-08-10). It replaces the `Dockerfile.lint` shape in the body above: lint and test are phases of the one `Dockerfile`, there is no separate lint file, and there is no `golangci-lint config verify` step.
Implementer's brief:
- `Dockerfile`: copy the canonical Go example in `REPO_POLICIES.md` at `dd4027b`. A `lint` stage `FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240` (v2.12.2, dated comment) that runs `golangci-lint run --config .golangci.yml ./...`; a `test` stage on the pinned Debian Go image that runs the tests as an unprivileged user, as today (the permission tests need it); the build stage takes `COPY --from=lint /src/go.sum /dev/null` and the same from `test`; the runtime stage stays last. The version step stays as it is on `next` (it already matches that example).
- The build stage no longer runs `make check`: once `script/lint` and `script/test` are `docker build`s they cannot run inside one.
- `script/lint` and `script/test` each build only their phase: `docker build --no-cache --target <phase> -t "$(script/projectname)-<phase>" .`. `script/check` runs test, lint and `script/fmt-check` (host). `script/cibuild` runs `script/bootstrap`, `script/check`, then builds the image tagged and with `--no-cache`.
- `script/bootstrap`: drop golangci-lint; nothing lints on the host.
- Not here: `.golangci.yml` and the move to golangci-lint v2.14.0 change together in https://git.eeqj.de/sneak/attrsum/issues/13.
- `README.md` and `TODO.md` updated in the same commit.
Definition of done: the issue's list, plus: plant a lint violation and `script/cibuild` fails on it, and a failing test fails `script/cibuild` too; a plain `docker build .` still builds the lint and test phases; `make check` and `script/cibuild` pass.
Model: opus-5-5
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, and it works even where the docker daemon is remote and bind mounts are impossible.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.Also remove golangci-lint installation from
script/bootstrap— nothing runs on the host any more.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
Queued after #10, which changes the same files (
Dockerfile,script/bootstrap,.golangci.yml). Dispatch it once that lands onnext: oneissue-to-prworker, branch cut fromnext, PR based onnext. The worker readssneak/homoiconfor the shape and decides howmake checkinside the mainDockerfilehandles lint oncescript/lintitself runsdocker build.Model: opus-5-5
Plan. Dispatched once #10 is on
next. The shape is settled byREPO_POLICIES.mdinsneak/promptsat commitdd4027b(the commit #13 re-vendors from), following sneak's ruling on sneak/prompts#40 (2026-08-10). It replaces theDockerfile.lintshape in the body above: lint and test are phases of the oneDockerfile, there is no separate lint file, and there is nogolangci-lint config verifystep.Implementer's brief:
Dockerfile: copy the canonical Go example inREPO_POLICIES.mdatdd4027b. AlintstageFROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240(v2.12.2, dated comment) that runsgolangci-lint run --config .golangci.yml ./...; ateststage on the pinned Debian Go image that runs the tests as an unprivileged user, as today (the permission tests need it); the build stage takesCOPY --from=lint /src/go.sum /dev/nulland the same fromtest; the runtime stage stays last. The version step stays as it is onnext(it already matches that example).make check: oncescript/lintandscript/testaredocker builds they cannot run inside one.script/lintandscript/testeach build only their phase:docker build --no-cache --target <phase> -t "$(script/projectname)-<phase>" ..script/checkruns test, lint andscript/fmt-check(host).script/cibuildrunsscript/bootstrap,script/check, then builds the image tagged and with--no-cache.script/bootstrap: drop golangci-lint; nothing lints on the host..golangci.ymland the move to golangci-lint v2.14.0 change together in #13.README.mdandTODO.mdupdated in the same commit.Definition of done: the issue's list, plus: plant a lint violation and
script/cibuildfails on it, and a failing test failsscript/cibuildtoo; a plaindocker build .still builds the lint and test phases;make checkandscript/cibuildpass.Model: opus-5-5