Files
prompts/TODO.md
sneak 3a218497b8
All checks were successful
check / check (push) Successful in 17s
Keep in-repo agent scratch out of the build context and out of git (closes #27)
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.

Anchoring buys that at the cost of a residual exposure, and the vendored files
now say so rather than only asserting the reason to anchor. "Occurs exactly
once, at the context root" is a property of how agents are run, not of the
tooling: the directory is created in the agent's working directory, so a
monorepo running a per-service agent in services/api/ still ships
services/api/.claude/ into the context and the image -- the exact exposure this
change exists to close, left open in the repo shape where it is likeliest. The
.dockerignore header block, the REPO_POLICIES.md bullet and both checklists
state the gap and the remedy (anchored entries for the subdirectories that have
one, or `**/.claude` once no legitimately named nested directory would be
caught). Consuming repos receive the files and not the tracker, so a caveat that
lives only in a PR body is not a caveat.

It is not case-folded the way the neighbouring secret patterns are: tooling
creates the directory in exactly one spelling, so a folded pattern would add no
coverage. The earlier justification -- that a miss costs bloat rather than
exposure -- is gone, because it contradicted this issue's own framing, in which
the cost of a miss is unreviewed work in an image layer.

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.

That fallback is applied in exactly one place, and that place can actually
execute. `git describe ... || true` leaves the value empty when it fails, and
the `[ -n "$version" ]` line is what substitutes `unknown`. Folding the fallback
into the substitution as `|| echo unknown` would have left the guard unreachable
-- harmless in itself, but a guard that cannot fire is indistinguishable from
one that works to every repo that copies it, and canonical text should not carry
a check that is decorative. Measured firing in both sh and dash: not a git
repository -> unknown, repository with no commits yet -> unknown, this
repository -> the describe output. The checklist now carries the guard line as
well, so the vendored guidance and the vendored script no longer disagree.

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 `?=`, and their comments say precisely
when that matters: where a build stage compiles by invoking make, `ARG VERSION`
puts the value in the environment and `?=` defers to it, whereas the canonical
Go template compiles with `go build` directly and uses the Makefile on the host
only -- still `?=`, so that a repo which later moves its build behind make does
not silently start shipping an empty version. 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.
2026-08-09 17:13:10 +00:00

6.4 KiB

Workflow

  • branch (from main)
  • do the work in Next Step
  • move Next Step to the top of Completed Steps
  • move the top item of Future Steps into Next Step
  • commit (TODO.md changes in the same commit as the work)
  • merge to main if the branch is not protected, otherwise open a PR
  • push

Status

pre-1.0

Next Step

Finish the two draft prompt documents in the working tree and commit them: prompts/FIXUP_CLEAN.md (currently a near-empty stub) and prompts/FIXUP_REPORT.md (a rough draft). Write the missing content, run make fmt so they pass fmt-check, and commit.

Completed Steps

  • 2026-08-09: Kept in-repo agent scratch out of the Docker build context and out of version control. .claude/ holds one worktree — an entire additional checkout of the repo — per in-flight agent, and under COPY . . all of it was reaching the image: another session's unreviewed, sometimes uncommitted work, inflating the context by a multiple of the repo and invalidating COPY for reasons unrelated to the repo's own content. The .dockerignore entry is root-anchored, because the directory occurs exactly once where agents run at the repo root and the **/ form additionally deletes any nested directory of that name — with the residual gap that follows from anchoring (a monorepo running agents in subdirectories still ships services/api/.claude/) stated in the canonical .dockerignore, the policy and the existing-repo checklist, since consuming repos receive the files rather than the tracker; the .gitignore entry is unanchored, because .gitignore patterns already match at every depth, and each file is written to its own semantics rather than derived from the other. Also closed the consequence that ships broken silently: excluding .git means git describe cannot run in any build stage and yields an empty version without erroring, so script/docker and script/cibuild now compute the version on the host and pass --build-arg VERSION, and REPO_POLICIES.md states where VERSION comes from instead of leaving the reader to fill the gap with git describe inside the build. The two Go documents that carry the GOLDFLAGS pattern were corrected in the same pass, from := to ?=, since a $(shell git describe) evaluated inside a build stage is exactly the empty version this closes. Verified by enumerating a probe image before, after, and against the **/-prefixed form, with a positive control and the CHECK_EPOCH cache verification re-run under the changed build context.
  • 2026-08-09: Closed the secret exposure in the canonical .dockerignore: a developer's local .env, *.pem or *.key was reaching the Docker build context under COPY . ., invisible to every git-based check because .gitignore covers it. The patterns are written to .dockerignore's own moby/patternmatcher semantics — **/-prefixed so they hold at every depth, which also fixes nested node_modules — rather than transplanted from .gitignore, whose unprefixed form protects only the repository root while reading as solved. Coverage extends past the .env/.pem/.key trio to the prod.env convention, .envrc, PKCS#12 bundles and extensionless SSH keys, every one of them case-folded with character ranges because matching is case-sensitive and an ALL-CAPS twin per pattern still misses Server.Key. REPO_POLICIES.md and both repo checklists now state that asymmetry and require verification by enumerating the image rather than by reading the patterns. Verified with a probe image before, against three naive forms (unprefixed, lowercase-only, ALL-CAPS-doubled), and after.
  • 2026-08-09: Made the pinned golangci-lint actually propagate: REPO_POLICIES.md now carries the canonical script/bootstrap snippet for Go repos, which installs when the installed version does not match the pin (the old if missing guard tested PATH presence only, so pins were inert on any provisioned machine and CI silently disagreed with local) and then re-resolves the binary through PATH and fails loudly, naming the shadowing path, when the install did not take effect — the failure mode the naive compare-then-install fix leaves behind while reporting success.
  • 2026-08-09: Fixed the false green in the canonical CI gate: script/cibuild and script/docker now pass a per-invocation CHECK_EPOCH nonce, and the Dockerfile (plus the Go multistage template in REPO_POLICIES.md, in both its lint and builder stages) declares ARG CHECK_EPOCH with a guard that makes a bare docker build . fail closed. Corrected the org-canonical text that asserted a successful build implies all checks pass, across every document carrying it: REPO_POLICIES.md, both repo checklists (which still told agents to write the pre-fix script/cibuild and ended on an acceptance item the guard makes unsatisfiable), and the Go styleguide.
  • 2026-08-07: Set the canonical .golangci.yml to the org-standard v2-schema config already deployed byte-identical across the org's Go repos (settings under linters.settings so thresholds like lll/funlen/cyclop/dupl actually apply under golangci-lint v2). Recorded the canonical golangci-lint version (v2.12.2, commit-pinned) in REPO_POLICIES.md.
  • 2026-03-20: Strengthened constructor naming and Params struct rules in the Go styleguide.
  • 2026-03-18: Documented fail-fast Dockerfile lint stage and conditional -v test rerun patterns in REPO_POLICIES.md.
  • 2026-03-11: Added HTTP service hardening policy for 1.0 releases.
  • 2026-03-10: Added policy: no build artifacts in repos.
  • 2026-03-04: Added LLM prose tells reference and copyediting checklist, then several self-applied revision passes.
  • 2026-02-28: Expanded the pre-1.0 schema migration rule; added clawpub reference.
  • 2026-02-23: Added Go style rules (no type-only packages, Stringer for string-based types); template repos section in README.
  • 2026-02-22: Initial policy corpus: REPO_POLICIES.md, code styleguides (general, Go, JS, Python), repo checklists, CI policy, hash pinning, Go HTTP server conventions, repo scaffolding.

Future Steps

  • Finish, format, and commit FIXUP_CLEAN.md and FIXUP_REPORT.md (the Next Step).
  • Commit this TODO.md at the repo root; it is the last missing policy file.
  • Decide the fate of untracked resume.sh: commit it or delete it.
  • Add more prompt templates for common development tasks (from README TODO).