--- title: Existing Repo Checklist last_modified: 2026-11-09 --- Use this checklist when beginning work in a repo that may not yet conform to our repository policies (`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`). Work on a feature branch. Check each item and fix any gaps before proceeding with your task. # Formatting (do this first) - [ ] If the repo has never been formatted to our standards, run `make fmt` and commit the result as a standalone branch/commit/PR before any other changes. Formatting diffs can be large and should not be mixed with functional changes. # Required Files - [ ] `README.md` exists with all required sections (Description, Getting Started, Rationale, Design, TODO, License, Author) - [ ] `LICENSE` file exists and matches the README - [ ] `REPO_POLICIES.md` exists and version date is current — fetch from `https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md` - [ ] Guidance for coding agents, if the repo has any, is one `AGENTS.md` at the root — never a file or directory named after one agent tool, such as `CLAUDE.md` or `.claude/`, and never separate memory files. Move what any such committed file says into `AGENTS.md` and delete it. - [ ] `.gitignore` is comprehensive (OS, editor, agent scratch, secrets, the repo's own build outputs) — fetch from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` if missing. An existing repo usually has a hand-written one that is never re-fetched, so check the entries rather than the file's presence. The file is the canonical content followed by the repo's own entries, such as its binaries; a re-vendor replaces the canonical part and keeps those entries. - [ ] `.editorconfig` exists — fetch from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig`. The file is the canonical content followed by the repo's own sections, such as one for another language it uses; a re-vendor replaces the canonical part and keeps those sections. - [ ] `Dockerfile` and `.dockerignore` exist; the Dockerfile carries a `lint` phase and a `test` phase, and the final stage carries a `COPY --from=` of a harmless file from each — fetch `.dockerignore` from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` - [ ] Nothing has been appended after the final stage, and the gate phases are reachable from it. A stage nothing depends on is built only when `--target` names it, so a lost `COPY --from=` edge leaves `docker build .` passing while the gate never runs. Confirm by planting a violation, not by reading the file. - [ ] In every stage, every step that lints, tests or compiles comes after `ARG CACHEBUST`. `ARG CACHEBUST` comes right after these steps: - the steps that install system packages - the steps that download Go's modules - the step that runs `script/bootstrap` to install a Python `test` phase's packages from `Pipfile.lock` - [ ] A stage with none of these steps declares `ARG CACHEBUST` right after `FROM`. A step that also installs anything else comes after `ARG CACHEBUST` too. A `script/bootstrap` that also installs node packages is one. The `script/` builds pass a new `CACHEBUST` value on every build, which reruns every `RUN` step after that line and leaves the layers above it and the cache mounts in place; `--no-cache` would build those layers again and empty the mounts. A stage without the `ARG` line is served from cache on an unchanged tree, so the scripts and the `ARG` lines are re-vendored in one commit. - [ ] Go: each Go stage downloads the modules (`COPY go.mod go.sum ./`, `RUN go mod download`) above `ARG CACHEBUST`; the steps that compile (`golangci-lint run`, `go test`, `go build`) mount Go's build cache (`--mount=type=cache,target=/root/.cache/go-build`), `golangci-lint run` also mounts golangci-lint's (`--mount=type=cache,target=/root/.cache/golangci-lint`), and no step mounts anything else; both `go test` runs carry `-count=1`; a first run that timed out gets no rerun, as in the canonical Go `Dockerfile` in `REPO_POLICIES.md` - [ ] Python: a `test` phase that installs its packages from `Pipfile.lock` does it above `ARG CACHEBUST`, with `COPY script/ script/`, `COPY Pipfile Pipfile.lock ./` and `RUN script/bootstrap`. When `script/bootstrap` runs in that step, it installs nothing beyond these: - system packages - pipenv and its dependencies - the packages from `Pipfile.lock` It creates a virtual environment at `$HOME/.pipenv` with `python3 -m venv` when it is missing, then uses that environment's `pip` and `pipenv`. On a host, it first installs the distribution's python and venv packages when they are missing. It installs pipenv and its dependencies by hash, with `pip install --require-hashes`. It installs the packages from `Pipfile.lock` with `pipenv install --deploy`, plus `--dev` when the tests need the development packages. It sets `PIP_ONLY_BINARY=:all:` for both installs, so a package with no wheel for the image stops the install. In that step it installs no Python, because a step above it already did. `Pipfile` names no `python_version`, because `--deploy` stops when the distribution's Python differs from it. The tests run after `ARG CACHEBUST`, and every other stage installs its Python packages after it, as in the Python `test` phase example in `REPO_POLICIES.md` - [ ] The gate phases invoke their tools directly, never through `make lint` or `script/test` — those are themselves a `docker build` and would recurse inside a build step - [ ] Every depth-independent pattern in `.dockerignore` carries a `**/` prefix, only genuinely root-anchored entries such as `.claude` are unprefixed, and `.gitignore`'s patterns have not been transplanted unmodified — the transplanted form leaves `config/.env` and `certs/server.key` in the build context while reading as solved - [ ] `.dockerignore` excludes the repo's own host-built artifacts (compiled binaries, test binaries, coverage output), written root-anchored — `/myapp`, never `**/myapp`. An existing repo is where such a binary is likeliest to already be sitting in the build context, invisible to git. - [ ] `.claude/` is in `.gitignore` (unanchored) and `.claude` in `.dockerignore` (anchored, no `**/` prefix). Agent worktrees are entire checkouts of the repo, so they inflate the context by a multiple of it and can copy another session's unreviewed work into an image layer. If agents here run anywhere other than the repo root, the anchored entry misses `services/api/.claude/`: add anchored entries for those directories. - [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into the build context. It keeps out every git `config` at any depth (`**/.git/config`, `**/.git/modules/**/config`): - the repository's own - each submodule's under `.git/modules/` - that of a submodule keeping its own `.git` directory `git describe` does not need them, and each can hold a credential: a password in a remote URL, or the token the CI checkout step stores there. Some submodule names have a `config` segment, as in `config`, `deploy/config` or `config/lib`. Such a submodule loses its whole git directory to `**/.git/modules/**/config`, and Go's version stamping then fails the build. Give it a name without that segment (`git submodule add --name`). The stage that compiles has `git`: the Debian Go image has it, and an alpine one needs `apk add --no-cache git`. That stage takes the version from the `VERSION` build argument when one is given, otherwise from `git describe --tags --always`. That gives: - the tag, on a tagged commit - the tag, the number of commits since it and the short commit (`v1.2.3-4-gabc1234`), on a later commit - the short commit, when no tag is reachable The stage that compiles also marks its working directory safe for git (`git config --system --add safe.directory /src`). A context sent as a tar stream keeps the sender's file owners, and git refuses a checkout owned by another user, so the version would come out empty. `ARG VERSION` has no default. The build fails if the context carries `.git` and the version still comes out empty, `dev` or `unknown`. A plain `docker build .` with no build arguments must succeed; a Dockerfile that refuses an empty build argument drops that refusal and keeps the argument. A checkout whose `.git` is a file is the exception: a linked worktree, or a repository checked out as a submodule. That file points to a git directory outside the build context, so the build cannot read the version and a plain `docker build .` fails. Pass the version with `--build-arg VERSION=...`. `script/docker` and `script/cibuild` already pass the version they compute on the host; it takes precedence. The canonical `.gitea/workflows/check.yml` sets `fetch-depth: 0` on its checkout step, which otherwise clones shallow and fetches no tags, so a CI build finds the tag too. - [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on push, checks out with `persist-credentials: false` and with `fetch-depth: 0` (which fetches the tags `git describe` needs), carries the `concurrency` block that lets a new push cancel only the same branch's older run, and sets `timeout-minutes: 20` on the `check` job so a hung build frees the shared runner — reference `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml` - [ ] Language-specific config: - [ ] Go: `go.mod`, `go.sum`, `.golangci.yml` (fetch from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and, in the same commit, set the lint phase digest to the one named in the `.golangci.yml` paragraph of `REPO_POLICIES.md`) - [ ] JS: `package.json`, `yarn.lock`, `.yarnrc.yml`, `.yarn/releases/`, `.gitattributes`, `.prettierrc`, `.prettierignore` (fetch the last two from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc` and `https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierignore`). - [ ] Any repository with `yarn.lock`: that file is in yarn 4's format. If its second line says `yarn lockfile v1`, convert it with the steps below. Do them in one commit with the re-vendored scripts, because yarn 1 ignores `--immutable` and exits 0 on a tree with no `.yarnrc.yml`. Run them with any distribution's node and yarn, and use `yarnpkg` on Debian and Ubuntu. 1. Read the version from `yarnPath` in `https://git.eeqj.de/sneak/prompts/raw/branch/main/.yarnrc.yml`. 2. Run `yarn set version `. It writes `.yarn/releases/yarn-.cjs`, `yarnPath` in `.yarnrc.yml`, and `packageManager` in `package.json`. 3. Add `nodeLinker: node-modules` and `enableTelemetry: false` to `.yarnrc.yml`. 4. Run `yarn install` once. It converts `yarn.lock`, and each package keeps its version and gains a `checksum`. 5. Delete `approvedGitRepositories`, `enableScripts` and `npmMinimalAgeGate` from `.yarnrc.yml`. The conversion wrote them. They allow install scripts, any git repository and packages of any age, which yarn otherwise refuses. 6. In `package.json`, name any package that must run its install script, as `"dependenciesMeta": { "": { "built": true } }`, and run `yarn install` again. 7. Add `.yarn/*` and `!.yarn/releases` above the secret patterns in `.gitignore` and `.dockerignore`. Docker obeys the last matching line, and the secret patterns must win. Add `.yarn/` to `.prettierignore`, and `.yarn/releases/** binary` to `.gitattributes`. 8. Run `yarn install --immutable`. It must pass and change nothing. - [ ] Python: `pyproject.toml` - [ ] Docs/writing: `.prettierrc`, `.prettierignore` (same URLs as above) # Makefile and script/ Entrypoints - [ ] `Makefile` exists in root — reference `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile` - [ ] Has targets: `test`, `lint`, `fmt`, `fmt-check`, `check`, `docker`, `hooks`, `update-policy` - [ ] Target implementations live in `script/` (scripts-to-rule-them-all); Makefile targets are thin shims calling them — model scripts at `https://git.eeqj.de/sneak/prompts/raw/branch/main/script/` - [ ] `script/precommit` exists and the pre-commit hook (installed by `script/install-precommit`, shimmed by `make hooks`) runs it - [ ] `script/update-policy` exists, identical to the model script, and `make update-policy` shims to it. Before its first use, the repo's own entries in `.gitignore`, `.editorconfig` and `.dockerignore` go below the comment that ends each canonical file. An entry above that comment which the canonical file lacks makes the script stop before it writes anything, naming the file and the entry. - [ ] README has an **Entrypoints** section documenting the `script/` entrypoints and linking the standard - [ ] `script/lint` and `script/test` each run `docker build --build-arg CACHEBUST="$(date +%s)-$$" --target --output type=cacheonly .`, which builds their phase by name and writes no image, and no host invocation anywhere in the repo can produce a lint verdict — grep for the linter's own name across `script/`, the `Makefile` and CI config, not just `script/lint`. A second path is likeliest here: a `make lint-fast`, an older host-versus-container branch, or a CI step calling the binary directly. `script/fmt` and `script/fmt-check` are expected hits and stay on the host. - [ ] No `docker build` in `script/` leaves a dangling image behind: `script/lint` and `script/test` write no image, and `script/docker` and `script/cibuild` tag theirs. A build that writes an untagged image leaves one behind on every run, on every host and CI runner. - [ ] `script/cibuild` runs `script/bootstrap` before `script/check`, and builds the image with a new `CACHEBUST` value on every build, like every `docker build` in `script/`. Without the bootstrap the CI run dies in `script/fmt-check`, which runs the formatter on the host and finds nothing installed. - [ ] `script/bootstrap` installs node, yarn and the node packages only when `yarn.lock` is in the tree. node and yarn come from the distribution when they are missing. It installs the node packages with `yarn install --immutable`. The distribution's yarn only starts the release file that `yarnPath` in `.yarnrc.yml` names, and that file does the work. - [ ] A `Dockerfile` stage that needs yarn may start from any distribution that packages node and yarn. It copies `package.json`, `yarn.lock`, `.yarnrc.yml` and `.yarn/releases/` before it runs `script/bootstrap`. A stage that needs no yarn copies none of them. - [ ] `script/bootstrap`, `script/fmt` and `script/fmt-check` run yarn by whichever name is present: `yarn`, or `yarnpkg` on Debian and Ubuntu. With neither name on `PATH`, `script/fmt` and `script/fmt-check` print one line saying to run `script/bootstrap`, and exit 1. - [ ] `script/bootstrap` installs no linter of its own — delete the block, its version variables and its call site. A JS repo's `yarn install` stays; it brings a linter along with every other dependency, and no verdict is taken from it. - [ ] `make check` does not modify any files in the repo - [ ] `make test` has a 90-second timeout and completes within the 60-second hard cap (over 20 seconds is green but must be filed as an improvement bug) - [ ] `make test` runs real tests, not a no-op (at minimum, import/compile check) - [ ] `make check` passes on current branch # Formatting - [ ] Platform-standard formatter is configured (`black`, `prettier`, and for Go `gofmt` then `golines -m 77`) - [ ] Default formatter config, except four-space indents (not Go), `proseWrap: always` for Markdown, and the Go 77-column limit - [ ] Go: `script/bootstrap` installs `golines` with `go install` pinned to a commit hash; `script/fmt-check` fails when `gofmt` or `golines -m 77` would change a file - [ ] All files pass `make fmt-check` # Git Hygiene - [ ] Pre-commit hook is installed (`make hooks`) - [ ] No secrets in the repo (`.env`, keys, credentials) - [ ] No mutable references (tags, `@latest`) to what the repo downloads and installs itself in scripts and Dockerfiles — all pinned by cryptographic hash with version/date comment. Distribution packages are outside this rule. They need no version and no hash, and updating and upgrading them is fine too. In a `Dockerfile`, the distribution is the pinned base image's. On a host, it is the host's, and the host's package manager stands for it, nix and brew included. A package source that a `Dockerfile` adds itself is not the distribution's own, so its packages are pinned - [ ] node, yarn, npm, python, pip and venv come from the distribution wherever possible, never from nvm, pyenv, corepack or `npm install -g yarn`. If the distribution's version is too old, use a newer distribution. node means the distribution's newest Node package, such as `nodejs-current` on Alpine 3.24 - [ ] yarn's release file is in git under `.yarn/releases/`, written once by `yarn set version` with an exact version (repositories with `yarn.lock`) - [ ] Using `yarn`, not `npm` (JS projects) # Directory Structure - [ ] No unnecessary files in repo root - [ ] Files organized into canonical subdirectories (`bin/`, `cmd/`, `docs/`, `internal/`, `static/`, etc.) - [ ] Go migrations in `internal/db/migrations/` and embedded in binary # HTTP Service Hardening (if targeting 1.0 and the repo is an HTTP/web service) - [ ] Security headers set on all responses (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) - [ ] Request body size limits enforced on all endpoints - [ ] Read/write/idle timeouts configured on the HTTP server (slowloris defense) - [ ] Per-handler execution time limits in place - [ ] Password-based auth endpoints are rate-limited - [ ] CSRF tokens on all state-mutating HTML forms - [ ] Passwords hashed with bcrypt, scrypt, or argon2 - [ ] Session cookies use HttpOnly, Secure, and SameSite attributes - [ ] True client IP correctly detected behind reverse proxy (trusted proxy allowlist configured) - [ ] CORS restricted to explicit origin allowlist for authenticated endpoints - [ ] Error responses do not leak stack traces, SQL queries, or internal paths # Final - [ ] `make check` passes - [ ] `script/cibuild` succeeds in a fresh clone on a host carrying what CI has. That host is the runner image `docker.gitea.com/runner-images:ubuntu-latest`, which has docker, git and node 24. Take any yarn off `PATH` first. The run must show that the checks executed. A sub-second build means nothing ran. So does `CACHED` on a step that lints or tests, one of the `RUN` steps below `ARG CACHEBUST`. Above that line, these steps are expected to show `CACHED`: - the steps that install system packages - the steps that download Go's modules - the step that runs `script/bootstrap` to install a Python `test` phase's packages from `Pipfile.lock` - [ ] A planted lint violation fails both `make lint` and a plain `docker build .`; revert it afterwards - [ ] Commit and merge fixes before starting your actual task