--- title: Existing Repo Checklist last_modified: 2026-11-06 --- 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` - [ ] 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 - [ ] 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`, `go fmt`) - [ ] Default formatter config, only exception: four-space indents (except Go) - [ ] 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