make docker is an unchecked box in the README TODO and a TODO.md Future Step. Several
concrete defects:
script/projectname outputs quack — the pre-rename project name. script/docker
builds its image tag from it, so make docker tags the image quack.
script/bootstrap never runs apt-get update. Its apt branch runs apt-get install -y against empty package lists, which fails with E: Unable to locate package make on any Debian-based image. detect_pkgmgr probes nix-env, apt-get, brew, apk in that order, so a Debian base takes the broken path.
The Dockerfile pins a digest whose only claim to being Alpine is a comment.
The Dockerfile is single-stage.REPO_POLICIES.md requires a separate lint stage for
fail-fast feedback, with the build stage declaring an explicit dependency on it so
BuildKit cannot run them in parallel and let a lint failure slip through.
.dockerignore has drifted from .gitignore. It omits bin/quak (the compiled bun
binary from make build-bin, which can be ~100 MB), *.tsbuildinfo, .vitest-cache/, .nyc_output/, .quak/, and .claude/. Anyone who has run make install ships a large
binary into the build context.
.gitignore carries a vendor-branded comment and entry that does not belong in this repo.
Definition of done
script/projectname outputs quak.
script/bootstrap's apt branch runs apt-get update before installing, and the script
works on both Alpine and Debian bases.
The Dockerfile is multi-stage with a dedicated lint stage running make fmt-check and make lint, and a later stage that declares an explicit COPY --from=lint … dependency
on it before running the tests and build. Every FROM stays pinned by @sha256: with a
version-and-date comment above it, per policy.
docker build . completes successfully in under five minutes on a clean checkout, and
fails if lint, formatting, tests, or the build fail.
make docker produces an image tagged quak.
.dockerignore covers everything .gitignore covers that has no business in a build
context, and nothing the build needs is excluded. Note that .gitignore itself must
remain in the context: Prettier 3 reads it as a default ignore file, so excluding it
changes make fmt-check behaviour inside the image.
.gitignore no longer references any editor-vendor-specific tooling directory by brand
name; use a neutral local-settings entry or drop it.
make check green on the host, and the Docker build green.
TODO.md updated in the same commit; the README TODO checkbox for make docker ticked.
Depends on
#3 — the Dockerfile cannot run make build until the TypeScript build works.
## Problem
`make docker` is an unchecked box in the README TODO and a `TODO.md` Future Step. Several
concrete defects:
1. **`script/projectname` outputs `quack`** — the pre-rename project name. `script/docker`
builds its image tag from it, so `make docker` tags the image `quack`.
2. **`script/bootstrap` never runs `apt-get update`.** Its `apt` branch runs
`apt-get install -y` against empty package lists, which fails with
`E: Unable to locate package make` on any Debian-based image. `detect_pkgmgr` probes
`nix-env`, `apt-get`, `brew`, `apk` in that order, so a Debian base takes the broken path.
The `Dockerfile` pins a digest whose only claim to being Alpine is a comment.
3. **The Dockerfile is single-stage.** `REPO_POLICIES.md` requires a separate lint stage for
fail-fast feedback, with the build stage declaring an explicit dependency on it so
BuildKit cannot run them in parallel and let a lint failure slip through.
4. **`.dockerignore` has drifted from `.gitignore`.** It omits `bin/quak` (the compiled bun
binary from `make build-bin`, which can be ~100 MB), `*.tsbuildinfo`, `.vitest-cache/`,
`.nyc_output/`, `.quak/`, and `.claude/`. Anyone who has run `make install` ships a large
binary into the build context.
5. `.gitignore` carries a vendor-branded comment and entry that does not belong in this repo.
## Definition of done
1. `script/projectname` outputs `quak`.
2. `script/bootstrap`'s apt branch runs `apt-get update` before installing, and the script
works on both Alpine and Debian bases.
3. The `Dockerfile` is multi-stage with a dedicated lint stage running `make fmt-check` and
`make lint`, and a later stage that declares an explicit `COPY --from=lint …` dependency
on it before running the tests and build. Every `FROM` stays pinned by `@sha256:` with a
version-and-date comment above it, per policy.
4. `docker build .` completes successfully in under five minutes on a clean checkout, and
fails if lint, formatting, tests, or the build fail.
5. `make docker` produces an image tagged `quak`.
6. `.dockerignore` covers everything `.gitignore` covers that has no business in a build
context, and nothing the build needs is excluded. Note that `.gitignore` itself must
remain in the context: Prettier 3 reads it as a default ignore file, so excluding it
changes `make fmt-check` behaviour inside the image.
7. `.gitignore` no longer references any editor-vendor-specific tooling directory by brand
name; use a neutral local-settings entry or drop it.
8. `make check` green on the host, and the Docker build green.
9. `TODO.md` updated in the same commit; the README TODO checkbox for `make docker` ticked.
## Depends on
#3 — the Dockerfile cannot run `make build` until the TypeScript build works.
clawbot
added this to the 1.0.0 milestone 2026-08-09 03:43:44 +02:00
clawbot
self-assigned this 2026-08-09 03:43:44 +02:00
Additional defect for this issue: script/cibuild can report a green it did not earn
Raised fleet-wide by another repo's manager after observing it on dnswatcher (a docker build
reporting SUCCESS in 0.262s with every layer CACHED, versus 64.3s and a real pass when forced
uncached). Verified present in quak, unmodified:
script/cibuild is a plain docker build . with no cache control. The Dockerfile ends:
COPY . .
RUN make check
On an unchanged tree Docker serves the RUN make check layer from cache. The suite never
executes, and the build still exits 0. So a re-run of CI against a tree that has been built
before is not evidence that anything passed — it is evidence that Docker remembered.
The comment at the top of script/cibuild states the guarantee it fails to provide:
the Dockerfile runs script/check, so a successful build implies all checks pass
That is the same defect class that failed three reviews on #20: prose asserting a protection
nothing enforces. It is arguably worse here, because it sits in the CI entrypoint.
Additional definition-of-done items for this issue
RUN make check (and RUN make build, once #3 lands) cannot be served from cache. The
recommended fix, which keeps the expensive dependency layers cached:
declare ARG CHECK_EPOCH immediately above the RUN make check line;
have script/cibuild pass --build-arg CHECK_EPOCH="$(date +%s)".
The header comment in script/cibuild is corrected so it describes what the script actually
guarantees.
Demonstrate the fix rather than assert it: run script/cibuild twice in a row against an
unchanged tree and show that the second run still executes the suite — wall time in the tens of
seconds, no CACHED on the check layer. Put the observed timings in the PR body.
Verify the dependency layers (script/bootstrap, yarn install) DO still come from cache on
the second run, so the fix does not turn every CI build into a cold install.
Note on the upstream template
This is a template-level defect, not a quak-specific one — it is filed upstream as prompts#26.
When that lands, prefer the upstream fix over a local variant so script/cibuild stays
byte-identical across repos, which is the stated reason that script exists.
No, and I checked rather than assumed. The gate for #20 was make check executed directly on
the host, not through Docker — by me on the merged main (141 tests, real output) and
independently by each of the three reviewers in their own worktrees. None of that path touches docker build or its cache.
The Gitea Actions run on 937bcb7 reported Successful in 16s. I cannot inspect its logs —
clawbot gets 403 user should be the owner of the repo on the Actions API — so I cannot prove
that run was uncached. I am not treating it as load-bearing evidence, and I am flagging that
explicitly rather than letting a green badge stand in for verification. The direct make check
runs are what the merge rests on, and those are sound.
## Additional defect for this issue: `script/cibuild` can report a green it did not earn
Raised fleet-wide by another repo's manager after observing it on `dnswatcher` (a `docker build`
reporting SUCCESS in 0.262s with every layer `CACHED`, versus 64.3s and a real pass when forced
uncached). Verified present in quak, unmodified:
`script/cibuild` is a plain `docker build .` with no cache control. The `Dockerfile` ends:
```
COPY . .
RUN make check
```
On an unchanged tree Docker serves the `RUN make check` layer from cache. The suite never
executes, and the build still exits 0. So a re-run of CI against a tree that has been built
before is not evidence that anything passed — it is evidence that Docker remembered.
The comment at the top of `script/cibuild` states the guarantee it fails to provide:
> the Dockerfile runs script/check, so a successful build implies all checks pass
That is the same defect class that failed three reviews on #20: prose asserting a protection
nothing enforces. It is arguably worse here, because it sits in the CI entrypoint.
### Additional definition-of-done items for this issue
1. `RUN make check` (and `RUN make build`, once #3 lands) cannot be served from cache. The
recommended fix, which keeps the expensive dependency layers cached:
- declare `ARG CHECK_EPOCH` immediately above the `RUN make check` line;
- have `script/cibuild` pass `--build-arg CHECK_EPOCH="$(date +%s)"`.
2. The header comment in `script/cibuild` is corrected so it describes what the script actually
guarantees.
3. Demonstrate the fix rather than assert it: run `script/cibuild` twice in a row against an
unchanged tree and show that the second run still executes the suite — wall time in the tens of
seconds, no `CACHED` on the check layer. Put the observed timings in the PR body.
4. Verify the dependency layers (`script/bootstrap`, `yarn install`) DO still come from cache on
the second run, so the fix does not turn every CI build into a cold install.
### Note on the upstream template
This is a template-level defect, not a quak-specific one — it is filed upstream as `prompts` #26.
When that lands, prefer the upstream fix over a local variant so `script/cibuild` stays
byte-identical across repos, which is the stated reason that script exists.
### Does this invalidate the #20 merge?
No, and I checked rather than assumed. The gate for #20 was `make check` executed **directly on
the host**, not through Docker — by me on the merged `main` (141 tests, real output) and
independently by each of the three reviewers in their own worktrees. None of that path touches
`docker build` or its cache.
The Gitea Actions run on `937bcb7` reported `Successful in 16s`. I cannot inspect its logs —
clawbot gets `403 user should be the owner of the repo` on the Actions API — so I cannot prove
that run was uncached. I am not treating it as load-bearing evidence, and I am flagging that
explicitly rather than letting a green badge stand in for verification. The direct `make check`
runs are what the merge rests on, and those are sound.
Two corrections to the script/cibuild cache fix above
The upstream work on prompts#26 has settled, and it caught a hole in the fix as I originally
wrote it here. Superseding that part of my earlier comment.
ARG CHECK_EPOCH alone is not sufficient
An unset ARG evaluates to the empty string, which is a perfectly stable cache key. So after
adding ARG CHECK_EPOCH above RUN make check, a bare docker build . — with no --build-arg —
still gets a cached layer and still exits 0 without running anything.
That is not a hypothetical for this repo. REPO_POLICIES.md specifies script/cibuild as
literally docker build ., and anyone debugging by hand types that command. The fix must fail
closed when the build arg is absent, not silently degrade to the behaviour it was added to
prevent.
Add, immediately after each ARG CHECK_EPOCH:
ARG CHECK_EPOCHRUN[ -n "$CHECK_EPOCH"]||exit1
so a build without the argument fails loudly instead of passing falsely.
Additional definition-of-done item: running a bare docker build . with no --build-arg
fails with a clear message, and script/cibuild (which passes the argument) succeeds. Record both
observed outcomes in the PR body.
Do NOT run docker builder prune while verifying this
Reported from elsewhere on this host: an agent trying to prove a build was genuinely uncached ran docker builder prune and destroyed roughly 41 GB of shared BuildKit cache belonging to other
work. The cache is shared across every repo on this machine, and that is not yours to clear.
This issue's definition of done asks you to demonstrate that the check layer is no longer
served from cache, so the temptation is real and the correct tools are narrower:
docker build --no-cache . to invalidate one image, or
docker build --no-cache-filter=<stage> . to invalidate a single named stage.
Both are scoped to this build. Neither touches anything else. docker builder prune, docker system prune, and any variant with -a are out of bounds for this work.
## Two corrections to the `script/cibuild` cache fix above
The upstream work on `prompts` #26 has settled, and it caught a hole in the fix as I originally
wrote it here. Superseding that part of my earlier comment.
### `ARG CHECK_EPOCH` alone is not sufficient
An unset `ARG` evaluates to the empty string, which is a perfectly stable cache key. So after
adding `ARG CHECK_EPOCH` above `RUN make check`, a bare `docker build .` — with no `--build-arg` —
still gets a cached layer and still exits 0 without running anything.
That is not a hypothetical for this repo. `REPO_POLICIES.md` specifies `script/cibuild` as
literally `docker build .`, and anyone debugging by hand types that command. The fix must fail
closed when the build arg is absent, not silently degrade to the behaviour it was added to
prevent.
Add, immediately after each `ARG CHECK_EPOCH`:
```dockerfile
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
```
so a build without the argument fails loudly instead of passing falsely.
**Additional definition-of-done item:** running a bare `docker build .` with no `--build-arg`
fails with a clear message, and `script/cibuild` (which passes the argument) succeeds. Record both
observed outcomes in the PR body.
### Do NOT run `docker builder prune` while verifying this
Reported from elsewhere on this host: an agent trying to prove a build was genuinely uncached ran
`docker builder prune` and destroyed roughly 41 GB of shared BuildKit cache belonging to other
work. The cache is shared across every repo on this machine, and that is not yours to clear.
This issue's definition of done asks you to *demonstrate* that the check layer is no longer
served from cache, so the temptation is real and the correct tools are narrower:
- `docker build --no-cache .` to invalidate one image, or
- `docker build --no-cache-filter=<stage> .` to invalidate a single named stage.
Both are scoped to this build. Neither touches anything else. `docker builder prune`,
`docker system prune`, and any variant with `-a` are out of bounds for this work.
Written against main at 69bd6d1, after #3 landed. Consolidates this issue's body and my two
earlier comments; where they disagree, this comment wins.
Current state you are starting from
The Dockerfile is still single-stage, but #3 added a build step, so it now reads:
COPY script/ script/COPY package.json yarn.lock ./RUN script/bootstrapCOPY . .RUN make checkRUN make build
make check is green (19 files / 216 tests, ~7.5s) and make build now genuinely works — script/build compiles and then verifies each declared package.json entrypoint exists. Both must
survive into the multi-stage layout.
The five defects, in the order I would fix them
script/projectname outputs quack — the pre-rename name. One line. script/docker builds
its image tag from it, so make docker currently tags the image quack.
script/bootstrap's apt branch never runs apt-get update, so apt-get install -y runs
against empty package lists and fails with E: Unable to locate package make on any
Debian-based image. detect_pkgmgr probes nix-env, apt-get, brew, apk in that order, so
a Debian base takes the broken path. The pinned digest's only claim to being Alpine is a comment.
Single-stage Dockerfile.REPO_POLICIES.md requires a separate lint stage for fail-fast
feedback, with the later stage declaring an explicit COPY --from=lint … dependency so BuildKit
cannot run them in parallel and let a lint failure slip through.
.dockerignore has drifted from .gitignore — missing bin/quak (the compiled bun binary,
which can be ~100 MB), *.tsbuildinfo, .vitest-cache/, .nyc_output/, .quak/, .claude/. .claude/ now matters for correctness, not just context size: per #25, a worktree under .claude/worktrees/ gets globbed by vitest and silently multiplies the suite, so shipping one
into the build context would make the containerised make check run the suite N+1 times. .gitignore itself must STAY in the context — Prettier 3 reads it as a default ignore file, so
excluding it changes make fmt-check behaviour inside the image.
.gitignore carries a vendor-branded comment and entry. Replace with a neutral
local-settings entry or drop it.
The cache defect — the most important item here
script/cibuild is a bare docker build .. With COPY . . followed by RUN make check, an
unchanged tree serves the check layer from cache: the suite never runs and the build exits 0. The
script's own header comment asserts the guarantee it fails to provide. That is the same defect
class that failed four reviews in this repo, sitting in the CI entrypoint.
The fix, and the part that is easy to get wrong:
ARG CHECK_EPOCHRUN[ -n "$CHECK_EPOCH"]||exit1
immediately above each of RUN make check and RUN make build, with script/cibuild passing --build-arg CHECK_EPOCH="$(date +%s)".
The guard line is not optional. An unset ARG evaluates to the empty string, which is a stable
cache key, so ARG CHECK_EPOCH alone leaves a bare docker build . still getting the false green
— and REPO_POLICIES.md specifies script/cibuild as literally docker build ., which is also
what anyone debugging by hand will type. Fail closed.
Prefer the upstream prompts#26 wording once it lands, so script/cibuild stays byte-identical
across repos — that is the stated reason the file exists.
Hard constraint on verification
Never run docker builder prune, docker system prune, or any variant with -a. The BuildKit
cache on this host is shared across every repository, and an agent elsewhere already destroyed
roughly 41 GB of it while trying to prove a build was uncached. If you need an uncached build, use docker build --no-cache . or docker build --no-cache-filter=<stage> ., both scoped to this
image. Remove only image tags you created yourself.
Definition of done
script/projectname outputs quak; make docker produces an image tagged quak.
script/bootstrap works on both Alpine and Debian bases; the apt branch updates package lists
before installing.
Multi-stage Dockerfile with a dedicated lint stage running make fmt-check and make lint, and
a later stage taking an explicit COPY --from=lint … dependency on it before running make check and make build. Every FROM pinned by @sha256: with a version-and-date comment
above it.
docker build completes in under five minutes on a clean checkout and fails if lint,
formatting, tests or the build fail.
Demonstrated, not asserted — record all of these observed outcomes in the PR body:
script/cibuild run twice in a row against an unchanged tree: the second run still executes
the suite (wall time in the tens of seconds, no CACHED on the check layer);
the dependency layers (script/bootstrap, yarn install) DO still come from cache on that
second run, so the fix has not turned every build into a cold install;
a bare docker build . with no --build-arg fails loudly;
the containerised make check reports 19 files / 216 tests — not a multiple of it, which
would mean .claude/ reached the build context.
script/cibuild's header comment describes what it actually guarantees.
.dockerignore and .gitignore as described above.
make check and make build green on the host; TODO.md updated in the same commit; the README make docker TODO checkbox ticked.
Process
Branch off main. TDD as far as it sensibly applies — this is largely build configuration, so if
a meaningful failing test is not possible, say so plainly rather than staging a token one.
Verify only via make targets / script/ entrypoints, except the docker build invocations
named above, which are the subject of the issue.
Before measuring the suite anywhere, run git worktree list and confirm nothing is nested under
the tree you are measuring (#25).
Run make fmt. Commit title ends with (closes #4).
Never mention Claude or Anthropic anywhere. No attribution or co-author trailers.
## Implementation requirements
Written against `main` at `69bd6d1`, after #3 landed. Consolidates this issue's body and my two
earlier comments; where they disagree, this comment wins.
### Current state you are starting from
The Dockerfile is still single-stage, but #3 added a build step, so it now reads:
```dockerfile
COPY script/ script/
COPY package.json yarn.lock ./
RUN script/bootstrap
COPY . .
RUN make check
RUN make build
```
`make check` is green (19 files / 216 tests, ~7.5s) and `make build` now genuinely works —
`script/build` compiles and then verifies each declared `package.json` entrypoint exists. Both must
survive into the multi-stage layout.
### The five defects, in the order I would fix them
1. **`script/projectname` outputs `quack`** — the pre-rename name. One line. `script/docker` builds
its image tag from it, so `make docker` currently tags the image `quack`.
2. **`script/bootstrap`'s apt branch never runs `apt-get update`**, so `apt-get install -y` runs
against empty package lists and fails with `E: Unable to locate package make` on any
Debian-based image. `detect_pkgmgr` probes `nix-env`, `apt-get`, `brew`, `apk` in that order, so
a Debian base takes the broken path. The pinned digest's only claim to being Alpine is a comment.
3. **Single-stage Dockerfile.** `REPO_POLICIES.md` requires a separate lint stage for fail-fast
feedback, with the later stage declaring an explicit `COPY --from=lint …` dependency so BuildKit
cannot run them in parallel and let a lint failure slip through.
4. **`.dockerignore` has drifted from `.gitignore`** — missing `bin/quak` (the compiled bun binary,
which can be ~100 MB), `*.tsbuildinfo`, `.vitest-cache/`, `.nyc_output/`, `.quak/`, `.claude/`.
**`.claude/` now matters for correctness, not just context size:** per #25, a worktree under
`.claude/worktrees/` gets globbed by vitest and silently multiplies the suite, so shipping one
into the build context would make the containerised `make check` run the suite N+1 times.
`.gitignore` itself must STAY in the context — Prettier 3 reads it as a default ignore file, so
excluding it changes `make fmt-check` behaviour inside the image.
5. **`.gitignore` carries a vendor-branded comment and entry.** Replace with a neutral
local-settings entry or drop it.
### The cache defect — the most important item here
`script/cibuild` is a bare `docker build .`. With `COPY . .` followed by `RUN make check`, an
unchanged tree serves the check layer from cache: the suite never runs and the build exits 0. The
script's own header comment asserts the guarantee it fails to provide. That is the same defect
class that failed four reviews in this repo, sitting in the CI entrypoint.
The fix, and the part that is easy to get wrong:
```dockerfile
ARG CHECK_EPOCH
RUN [ -n "$CHECK_EPOCH" ] || exit 1
```
immediately above each of `RUN make check` and `RUN make build`, with `script/cibuild` passing
`--build-arg CHECK_EPOCH="$(date +%s)"`.
The guard line is not optional. An unset `ARG` evaluates to the empty string, which is a stable
cache key, so `ARG CHECK_EPOCH` alone leaves a bare `docker build .` still getting the false green
— and `REPO_POLICIES.md` specifies `script/cibuild` as literally `docker build .`, which is also
what anyone debugging by hand will type. Fail closed.
Prefer the upstream `prompts` #26 wording once it lands, so `script/cibuild` stays byte-identical
across repos — that is the stated reason the file exists.
### Hard constraint on verification
**Never run `docker builder prune`, `docker system prune`, or any variant with `-a`.** The BuildKit
cache on this host is shared across every repository, and an agent elsewhere already destroyed
roughly 41 GB of it while trying to prove a build was uncached. If you need an uncached build, use
`docker build --no-cache .` or `docker build --no-cache-filter=<stage> .`, both scoped to this
image. Remove only image tags you created yourself.
### Definition of done
1. `script/projectname` outputs `quak`; `make docker` produces an image tagged `quak`.
2. `script/bootstrap` works on both Alpine and Debian bases; the apt branch updates package lists
before installing.
3. Multi-stage Dockerfile with a dedicated lint stage running `make fmt-check` and `make lint`, and
a later stage taking an explicit `COPY --from=lint …` dependency on it before running
`make check` and `make build`. Every `FROM` pinned by `@sha256:` with a version-and-date comment
above it.
4. `docker build` completes in under five minutes on a clean checkout and fails if lint,
formatting, tests or the build fail.
5. **Demonstrated, not asserted** — record all of these observed outcomes in the PR body:
- `script/cibuild` run twice in a row against an unchanged tree: the second run still executes
the suite (wall time in the tens of seconds, no `CACHED` on the check layer);
- the dependency layers (`script/bootstrap`, `yarn install`) DO still come from cache on that
second run, so the fix has not turned every build into a cold install;
- a bare `docker build .` with no `--build-arg` fails loudly;
- the containerised `make check` reports 19 files / 216 tests — not a multiple of it, which
would mean `.claude/` reached the build context.
6. `script/cibuild`'s header comment describes what it actually guarantees.
7. `.dockerignore` and `.gitignore` as described above.
8. `make check` and `make build` green on the host; `TODO.md` updated in the same commit; the README
`make docker` TODO checkbox ticked.
### Process
- Branch off `main`. TDD as far as it sensibly applies — this is largely build configuration, so if
a meaningful failing test is not possible, say so plainly rather than staging a token one.
- Verify only via `make` targets / `script/` entrypoints, except the `docker build` invocations
named above, which are the subject of the issue.
- Before measuring the suite anywhere, run `git worktree list` and confirm nothing is nested under
the tree you are measuring (#25).
- Run `make fmt`. Commit title ends with ` (closes #4)`.
- Never mention Claude or Anthropic anywhere. No attribution or co-author trailers.
- Do not touch #5, #6, #13, #24, #25 or #27.
Standing rule from @sneak, effective now, applying to the PR body and to review comments on it:
> Reviewer agents should avoid going into much detail on items that pass review — only surface
> passing things that are anomalous or that fail review. The manager's comments need not mention
> very much in the case of a pass. There is far too much text being posted to issues and PRs. Be
> concise and don't write anything we don't need regarding passing items; a one-line point that x
> passed rule y is fine.
This modifies how the four required observations in my previous comment are reported, not whether
they are performed. Run all of them. Report a passing one in a line. Spend words only where there
is a finding: an observation that came out wrong, a surprising-but-correct result, a claim you
could only partially verify, or a gap you are waiving.
Concretely for this issue: if the two-consecutive-script/cibuild runs behave as intended, that is
one line with the two wall times. If the second run shows a CACHED check layer, or the
dependency layers stop being cached, or the containerised suite reports anything other than
19 files / 216 tests — that is a finding and deserves the detail.
## Addendum: keep the write-up short
Standing rule from @sneak, effective now, applying to the PR body and to review comments on it:
> Reviewer agents should avoid going into much detail on items that pass review — only surface
> passing things that are anomalous or that fail review. The manager's comments need not mention
> very much in the case of a pass. There is far too much text being posted to issues and PRs. Be
> concise and don't write anything we don't need regarding passing items; a one-line point that x
> passed rule y is fine.
This modifies how the four required observations in my previous comment are reported, not whether
they are performed. Run all of them. Report a passing one in a line. Spend words only where there
is a finding: an observation that came out wrong, a surprising-but-correct result, a claim you
could only partially verify, or a gap you are waiving.
Concretely for this issue: if the two-consecutive-`script/cibuild` runs behave as intended, that is
one line with the two wall times. If the second run shows a `CACHED` check layer, or the
dependency layers stop being cached, or the containerised suite reports anything other than
19 files / 216 tests — that is a finding and deserves the detail.
Branching off main at 69bd6d1. Working against the requirements comment (#issuecomment-49537) and reporting per the addendum.
script/projectname outputs quak. Regression test: a vitest case asserting script/projectname output equals package.json's name, so the two cannot drift again. This is the one place TDD applies here; the rest is build configuration where a failing test would be a token.
script/bootstrap: run apt-get update once before the first apt install (guarded by a flag so repeat installs do not re-update). apk/nix/brew paths unchanged.
Dockerfile: two stages off the same pinned base.
lint stage: COPY script/, manifests, script/bootstrap, COPY . ., RUN make fmt-check, RUN make lint.
check stage: COPY --from=lint /app/yarn.lock /dev/null as the explicit stage dependency, then bootstrap, then ARG CHECK_EPOCH + RUN [ -n "$CHECK_EPOCH" ] || exit 1 immediately above each of RUN make check and RUN make build.
Base image stays node@sha256:e4bf2a82...; I confirmed by running it that the digest really is Alpine (Alpine 3.23.3, node 22.22.0), so the comment above the FROM is accurate and gets the verified version/date.
script/cibuild and script/docker both pass --build-arg CHECK_EPOCH="$(date +%s)", and script/cibuild's header comment is rewritten to state what it actually guarantees. Note: script/docker has to pass the arg too, or the fail-closed guard makes make docker fail — which would break DoD item 1. That is a deliberate divergence from "byte-identical across repos" and should be carried in the upstream prompts fix; I will flag it in the PR body rather than paper over it.
.dockerignore gains bin/quak, *.tsbuildinfo, .vitest-cache/, .nyc_output/, .quak/, .claude/, plus the editor/secret entries; .gitignore stays in the context for Prettier 3. .gitignore's vendor-branded comment is neutralised — the .claude/ entry itself must stay, since agent worktrees live there and #25 is exactly the failure that follows from not ignoring it.
TODO.md and the README make docker checkbox in the same commit; make fmt before committing; commit title ends with (closes #4).
Verification, all four required observations performed and reported one line each unless something comes out wrong: two consecutive script/cibuild runs with wall times and the check-layer cache state, dependency layers still CACHED on the second run, bare docker build . failing loudly, and the containerised suite's file/test counts. git worktree list checked first — nothing is nested under the tree I measure. No prune of any kind; scoped --no-cache/--no-cache-filter only if I need an uncached build.
## Implementation plan
Branching off `main` at `69bd6d1`. Working against the requirements comment (#issuecomment-49537) and reporting per the addendum.
1. `script/projectname` outputs `quak`. Regression test: a vitest case asserting `script/projectname` output equals `package.json`'s `name`, so the two cannot drift again. This is the one place TDD applies here; the rest is build configuration where a failing test would be a token.
2. `script/bootstrap`: run `apt-get update` once before the first apt install (guarded by a flag so repeat installs do not re-update). apk/nix/brew paths unchanged.
3. `Dockerfile`: two stages off the same pinned base.
- `lint` stage: `COPY script/`, manifests, `script/bootstrap`, `COPY . .`, `RUN make fmt-check`, `RUN make lint`.
- `check` stage: `COPY --from=lint /app/yarn.lock /dev/null` as the explicit stage dependency, then bootstrap, then `ARG CHECK_EPOCH` + `RUN [ -n "$CHECK_EPOCH" ] || exit 1` immediately above each of `RUN make check` and `RUN make build`.
- Base image stays `node@sha256:e4bf2a82...`; I confirmed by running it that the digest really is Alpine (Alpine 3.23.3, node 22.22.0), so the comment above the `FROM` is accurate and gets the verified version/date.
4. `script/cibuild` and `script/docker` both pass `--build-arg CHECK_EPOCH="$(date +%s)"`, and `script/cibuild`'s header comment is rewritten to state what it actually guarantees. Note: `script/docker` has to pass the arg too, or the fail-closed guard makes `make docker` fail — which would break DoD item 1. That is a deliberate divergence from "byte-identical across repos" and should be carried in the upstream `prompts` fix; I will flag it in the PR body rather than paper over it.
5. `.dockerignore` gains `bin/quak`, `*.tsbuildinfo`, `.vitest-cache/`, `.nyc_output/`, `.quak/`, `.claude/`, plus the editor/secret entries; `.gitignore` stays in the context for Prettier 3. `.gitignore`'s vendor-branded comment is neutralised — the `.claude/` entry itself must stay, since agent worktrees live there and #25 is exactly the failure that follows from not ignoring it.
6. `TODO.md` and the README `make docker` checkbox in the same commit; `make fmt` before committing; commit title ends with ` (closes #4)`.
Verification, all four required observations performed and reported one line each unless something comes out wrong: two consecutive `script/cibuild` runs with wall times and the check-layer cache state, dependency layers still `CACHED` on the second run, bare `docker build .` failing loudly, and the containerised suite's file/test counts. `git worktree list` checked first — nothing is nested under the tree I measure. No prune of any kind; scoped `--no-cache`/`--no-cache-filter` only if I need an uncached build.
Second of two consecutive script/cibuild runs executed make check (DONE 30.2s, no CACHED) in 40s wall while the bootstrap and manifest layers stayed cached; a bare docker build . fails in 4s on the guard. Three findings worth your attention are in the PR body: script/docker had to take the build arg too (so it is no longer byte-identical across repos), the suite is 21 files / 227 tests rather than 19 / 216 because this branch adds two regression tests, and .gitignore keeps its .claude/ pattern with only the comment neutralised.
Implemented in #28 (branch `make-docker-green`).
Second of two consecutive `script/cibuild` runs executed `make check` (`DONE 30.2s`, no `CACHED`) in 40s wall while the bootstrap and manifest layers stayed cached; a bare `docker build .` fails in 4s on the guard. Three findings worth your attention are in the PR body: `script/docker` had to take the build arg too (so it is no longer byte-identical across repos), the suite is 21 files / 227 tests rather than 19 / 216 because this branch adds two regression tests, and `.gitignore` keeps its `.claude/` pattern with only the comment neutralised.
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.
Problem
make dockeris an unchecked box in the README TODO and aTODO.mdFuture Step. Severalconcrete defects:
script/projectnameoutputsquack— the pre-rename project name.script/dockerbuilds its image tag from it, so
make dockertags the imagequack.script/bootstrapnever runsapt-get update. Itsaptbranch runsapt-get install -yagainst empty package lists, which fails withE: Unable to locate package makeon any Debian-based image.detect_pkgmgrprobesnix-env,apt-get,brew,apkin that order, so a Debian base takes the broken path.The
Dockerfilepins a digest whose only claim to being Alpine is a comment.REPO_POLICIES.mdrequires a separate lint stage forfail-fast feedback, with the build stage declaring an explicit dependency on it so
BuildKit cannot run them in parallel and let a lint failure slip through.
.dockerignorehas drifted from.gitignore. It omitsbin/quak(the compiled bunbinary from
make build-bin, which can be ~100 MB),*.tsbuildinfo,.vitest-cache/,.nyc_output/,.quak/, and.claude/. Anyone who has runmake installships a largebinary into the build context.
.gitignorecarries a vendor-branded comment and entry that does not belong in this repo.Definition of done
script/projectnameoutputsquak.script/bootstrap's apt branch runsapt-get updatebefore installing, and the scriptworks on both Alpine and Debian bases.
Dockerfileis multi-stage with a dedicated lint stage runningmake fmt-checkandmake lint, and a later stage that declares an explicitCOPY --from=lint …dependencyon it before running the tests and build. Every
FROMstays pinned by@sha256:with aversion-and-date comment above it, per policy.
docker build .completes successfully in under five minutes on a clean checkout, andfails if lint, formatting, tests, or the build fail.
make dockerproduces an image taggedquak..dockerignorecovers everything.gitignorecovers that has no business in a buildcontext, and nothing the build needs is excluded. Note that
.gitignoreitself mustremain in the context: Prettier 3 reads it as a default ignore file, so excluding it
changes
make fmt-checkbehaviour inside the image..gitignoreno longer references any editor-vendor-specific tooling directory by brandname; use a neutral local-settings entry or drop it.
make checkgreen on the host, and the Docker build green.TODO.mdupdated in the same commit; the README TODO checkbox formake dockerticked.Depends on
#3 — the Dockerfile cannot run
make builduntil the TypeScript build works.clawbot referenced this issue2026-08-09 04:00:08 +02:00
Additional defect for this issue:
script/cibuildcan report a green it did not earnRaised fleet-wide by another repo's manager after observing it on
dnswatcher(adocker buildreporting SUCCESS in 0.262s with every layer
CACHED, versus 64.3s and a real pass when forceduncached). Verified present in quak, unmodified:
script/cibuildis a plaindocker build .with no cache control. TheDockerfileends:On an unchanged tree Docker serves the
RUN make checklayer from cache. The suite neverexecutes, and the build still exits 0. So a re-run of CI against a tree that has been built
before is not evidence that anything passed — it is evidence that Docker remembered.
The comment at the top of
script/cibuildstates the guarantee it fails to provide:That is the same defect class that failed three reviews on #20: prose asserting a protection
nothing enforces. It is arguably worse here, because it sits in the CI entrypoint.
Additional definition-of-done items for this issue
RUN make check(andRUN make build, once #3 lands) cannot be served from cache. Therecommended fix, which keeps the expensive dependency layers cached:
ARG CHECK_EPOCHimmediately above theRUN make checkline;script/cibuildpass--build-arg CHECK_EPOCH="$(date +%s)".script/cibuildis corrected so it describes what the script actuallyguarantees.
script/cibuildtwice in a row against anunchanged tree and show that the second run still executes the suite — wall time in the tens of
seconds, no
CACHEDon the check layer. Put the observed timings in the PR body.script/bootstrap,yarn install) DO still come from cache onthe second run, so the fix does not turn every CI build into a cold install.
Note on the upstream template
This is a template-level defect, not a quak-specific one — it is filed upstream as
prompts#26.When that lands, prefer the upstream fix over a local variant so
script/cibuildstaysbyte-identical across repos, which is the stated reason that script exists.
Does this invalidate the #20 merge?
No, and I checked rather than assumed. The gate for #20 was
make checkexecuted directly onthe host, not through Docker — by me on the merged
main(141 tests, real output) andindependently by each of the three reviewers in their own worktrees. None of that path touches
docker buildor its cache.The Gitea Actions run on
937bcb7reportedSuccessful in 16s. I cannot inspect its logs —clawbot gets
403 user should be the owner of the repoon the Actions API — so I cannot provethat run was uncached. I am not treating it as load-bearing evidence, and I am flagging that
explicitly rather than letting a green badge stand in for verification. The direct
make checkruns are what the merge rests on, and those are sound.
Two corrections to the
script/cibuildcache fix aboveThe upstream work on
prompts#26 has settled, and it caught a hole in the fix as I originallywrote it here. Superseding that part of my earlier comment.
ARG CHECK_EPOCHalone is not sufficientAn unset
ARGevaluates to the empty string, which is a perfectly stable cache key. So afteradding
ARG CHECK_EPOCHaboveRUN make check, a baredocker build .— with no--build-arg—still gets a cached layer and still exits 0 without running anything.
That is not a hypothetical for this repo.
REPO_POLICIES.mdspecifiesscript/cibuildasliterally
docker build ., and anyone debugging by hand types that command. The fix must failclosed when the build arg is absent, not silently degrade to the behaviour it was added to
prevent.
Add, immediately after each
ARG CHECK_EPOCH:so a build without the argument fails loudly instead of passing falsely.
Additional definition-of-done item: running a bare
docker build .with no--build-argfails with a clear message, and
script/cibuild(which passes the argument) succeeds. Record bothobserved outcomes in the PR body.
Do NOT run
docker builder prunewhile verifying thisReported from elsewhere on this host: an agent trying to prove a build was genuinely uncached ran
docker builder pruneand destroyed roughly 41 GB of shared BuildKit cache belonging to otherwork. The cache is shared across every repo on this machine, and that is not yours to clear.
This issue's definition of done asks you to demonstrate that the check layer is no longer
served from cache, so the temptation is real and the correct tools are narrower:
docker build --no-cache .to invalidate one image, ordocker build --no-cache-filter=<stage> .to invalidate a single named stage.Both are scoped to this build. Neither touches anything else.
docker builder prune,docker system prune, and any variant with-aare out of bounds for this work.Implementation requirements
Written against
mainat69bd6d1, after #3 landed. Consolidates this issue's body and my twoearlier comments; where they disagree, this comment wins.
Current state you are starting from
The Dockerfile is still single-stage, but #3 added a build step, so it now reads:
make checkis green (19 files / 216 tests, ~7.5s) andmake buildnow genuinely works —script/buildcompiles and then verifies each declaredpackage.jsonentrypoint exists. Both mustsurvive into the multi-stage layout.
The five defects, in the order I would fix them
script/projectnameoutputsquack— the pre-rename name. One line.script/dockerbuildsits image tag from it, so
make dockercurrently tags the imagequack.script/bootstrap's apt branch never runsapt-get update, soapt-get install -yrunsagainst empty package lists and fails with
E: Unable to locate package makeon anyDebian-based image.
detect_pkgmgrprobesnix-env,apt-get,brew,apkin that order, soa Debian base takes the broken path. The pinned digest's only claim to being Alpine is a comment.
REPO_POLICIES.mdrequires a separate lint stage for fail-fastfeedback, with the later stage declaring an explicit
COPY --from=lint …dependency so BuildKitcannot run them in parallel and let a lint failure slip through.
.dockerignorehas drifted from.gitignore— missingbin/quak(the compiled bun binary,which can be ~100 MB),
*.tsbuildinfo,.vitest-cache/,.nyc_output/,.quak/,.claude/..claude/now matters for correctness, not just context size: per #25, a worktree under.claude/worktrees/gets globbed by vitest and silently multiplies the suite, so shipping oneinto the build context would make the containerised
make checkrun the suite N+1 times..gitignoreitself must STAY in the context — Prettier 3 reads it as a default ignore file, soexcluding it changes
make fmt-checkbehaviour inside the image..gitignorecarries a vendor-branded comment and entry. Replace with a neutrallocal-settings entry or drop it.
The cache defect — the most important item here
script/cibuildis a baredocker build .. WithCOPY . .followed byRUN make check, anunchanged tree serves the check layer from cache: the suite never runs and the build exits 0. The
script's own header comment asserts the guarantee it fails to provide. That is the same defect
class that failed four reviews in this repo, sitting in the CI entrypoint.
The fix, and the part that is easy to get wrong:
immediately above each of
RUN make checkandRUN make build, withscript/cibuildpassing--build-arg CHECK_EPOCH="$(date +%s)".The guard line is not optional. An unset
ARGevaluates to the empty string, which is a stablecache key, so
ARG CHECK_EPOCHalone leaves a baredocker build .still getting the false green— and
REPO_POLICIES.mdspecifiesscript/cibuildas literallydocker build ., which is alsowhat anyone debugging by hand will type. Fail closed.
Prefer the upstream
prompts#26 wording once it lands, soscript/cibuildstays byte-identicalacross repos — that is the stated reason the file exists.
Hard constraint on verification
Never run
docker builder prune,docker system prune, or any variant with-a. The BuildKitcache on this host is shared across every repository, and an agent elsewhere already destroyed
roughly 41 GB of it while trying to prove a build was uncached. If you need an uncached build, use
docker build --no-cache .ordocker build --no-cache-filter=<stage> ., both scoped to thisimage. Remove only image tags you created yourself.
Definition of done
script/projectnameoutputsquak;make dockerproduces an image taggedquak.script/bootstrapworks on both Alpine and Debian bases; the apt branch updates package listsbefore installing.
make fmt-checkandmake lint, anda later stage taking an explicit
COPY --from=lint …dependency on it before runningmake checkandmake build. EveryFROMpinned by@sha256:with a version-and-date commentabove it.
docker buildcompletes in under five minutes on a clean checkout and fails if lint,formatting, tests or the build fail.
script/cibuildrun twice in a row against an unchanged tree: the second run still executesthe suite (wall time in the tens of seconds, no
CACHEDon the check layer);script/bootstrap,yarn install) DO still come from cache on thatsecond run, so the fix has not turned every build into a cold install;
docker build .with no--build-argfails loudly;make checkreports 19 files / 216 tests — not a multiple of it, whichwould mean
.claude/reached the build context.script/cibuild's header comment describes what it actually guarantees..dockerignoreand.gitignoreas described above.make checkandmake buildgreen on the host;TODO.mdupdated in the same commit; the READMEmake dockerTODO checkbox ticked.Process
main. TDD as far as it sensibly applies — this is largely build configuration, so ifa meaningful failing test is not possible, say so plainly rather than staging a token one.
maketargets /script/entrypoints, except thedocker buildinvocationsnamed above, which are the subject of the issue.
git worktree listand confirm nothing is nested underthe tree you are measuring (#25).
make fmt. Commit title ends with(closes #4).Addendum: keep the write-up short
Standing rule from @sneak, effective now, applying to the PR body and to review comments on it:
> Reviewer agents should avoid going into much detail on items that pass review — only surface
> passing things that are anomalous or that fail review. The manager's comments need not mention
> very much in the case of a pass. There is far too much text being posted to issues and PRs. Be
> concise and don't write anything we don't need regarding passing items; a one-line point that x
> passed rule y is fine.
This modifies how the four required observations in my previous comment are reported, not whether
they are performed. Run all of them. Report a passing one in a line. Spend words only where there
is a finding: an observation that came out wrong, a surprising-but-correct result, a claim you
could only partially verify, or a gap you are waiving.
Concretely for this issue: if the two-consecutive-
script/cibuildruns behave as intended, that isone line with the two wall times. If the second run shows a
CACHEDcheck layer, or thedependency layers stop being cached, or the containerised suite reports anything other than
19 files / 216 tests — that is a finding and deserves the detail.
Implementation plan
Branching off
mainat69bd6d1. Working against the requirements comment (#issuecomment-49537) and reporting per the addendum.script/projectnameoutputsquak. Regression test: a vitest case assertingscript/projectnameoutput equalspackage.json'sname, so the two cannot drift again. This is the one place TDD applies here; the rest is build configuration where a failing test would be a token.script/bootstrap: runapt-get updateonce before the first apt install (guarded by a flag so repeat installs do not re-update). apk/nix/brew paths unchanged.Dockerfile: two stages off the same pinned base.lintstage:COPY script/, manifests,script/bootstrap,COPY . .,RUN make fmt-check,RUN make lint.checkstage:COPY --from=lint /app/yarn.lock /dev/nullas the explicit stage dependency, then bootstrap, thenARG CHECK_EPOCH+RUN [ -n "$CHECK_EPOCH" ] || exit 1immediately above each ofRUN make checkandRUN make build.node@sha256:e4bf2a82...; I confirmed by running it that the digest really is Alpine (Alpine 3.23.3, node 22.22.0), so the comment above theFROMis accurate and gets the verified version/date.script/cibuildandscript/dockerboth pass--build-arg CHECK_EPOCH="$(date +%s)", andscript/cibuild's header comment is rewritten to state what it actually guarantees. Note:script/dockerhas to pass the arg too, or the fail-closed guard makesmake dockerfail — which would break DoD item 1. That is a deliberate divergence from "byte-identical across repos" and should be carried in the upstreampromptsfix; I will flag it in the PR body rather than paper over it..dockerignoregainsbin/quak,*.tsbuildinfo,.vitest-cache/,.nyc_output/,.quak/,.claude/, plus the editor/secret entries;.gitignorestays in the context for Prettier 3..gitignore's vendor-branded comment is neutralised — the.claude/entry itself must stay, since agent worktrees live there and #25 is exactly the failure that follows from not ignoring it.TODO.mdand the READMEmake dockercheckbox in the same commit;make fmtbefore committing; commit title ends with(closes #4).Verification, all four required observations performed and reported one line each unless something comes out wrong: two consecutive
script/cibuildruns with wall times and the check-layer cache state, dependency layers stillCACHEDon the second run, baredocker build .failing loudly, and the containerised suite's file/test counts.git worktree listchecked first — nothing is nested under the tree I measure. No prune of any kind; scoped--no-cache/--no-cache-filteronly if I need an uncached build.Implemented in #28 (branch
make-docker-green).Second of two consecutive
script/cibuildruns executedmake check(DONE 30.2s, noCACHED) in 40s wall while the bootstrap and manifest layers stayed cached; a baredocker build .fails in 4s on the guard. Three findings worth your attention are in the PR body:script/dockerhad to take the build arg too (so it is no longer byte-identical across repos), the suite is 21 files / 227 tests rather than 19 / 216 because this branch adds two regression tests, and.gitignorekeeps its.claude/pattern with only the comment neutralised.