A directory can review repositories other than itself: a workbench whose config, env file and records serve several, freezing each with --repo and --out. The run took the working directory for the repository under review: the prompt named it, the calibration was kept under it, and --trees fell back to it. The run now reads the repository from the artifact's freeze.json - the prompt names it and the calibration is kept under it - and --trees defaults to what the record says: the worktree, else the repository when it is not the working directory, else `.`, the same rule that builds the printed run line. The working directory keeps its roles: the config, the env file, the slots, the lens processes' cwd and every logged path. A damaged record stops the run at preflight.
621 lines
40 KiB
Markdown
621 lines
40 KiB
Markdown
# Pre-push procedure
|
||
|
||
Scope: every push of a commit range from a repository set up for this
|
||
pipeline (`crafter-review init`) — a release channel with real users, where a push (or
|
||
the pull request it opens) is the next release. The `/prepush` skill drives
|
||
this file. The mechanics: freeze → hash → gates → the owner; five lenses,
|
||
the functional one as Gate 2 (Jorijn, 2026-10-15), the security lens beside
|
||
the bug-prover in Gate 3 (2026-10-22), the commit-truth lens as Gate 4
|
||
(2026-10-31), every one of them off until the repository's config turns it
|
||
on (2026-10-31).
|
||
|
||
Nothing is pushed without the owner's explicit approval of the exact frozen
|
||
hash. Local commits parked unpushed are fine and normal.
|
||
|
||
Terms: *the pipeline* is crafter-review, the Flatpak
|
||
`net.catcrafts.CrafterReview`; every command below is spelled
|
||
`flatpak run net.catcrafts.CrafterReview <command>`, because the Bash tool sees no
|
||
alias (a native build is run by its path instead). *The repository* is the
|
||
consumer checkout the driver runs in; *the owner* is whoever approves the
|
||
hash at Gate 5.
|
||
|
||
## Inputs — what the lenses see, and nothing else
|
||
|
||
- The frozen artifact: `.review-gates/push/<slug>/*.patch` — `git
|
||
format-patch --base=<base> <base>..HEAD` of the repository, cut by
|
||
`freeze`, inlined into every prompt.
|
||
- The repository at the range HEAD: the checkout itself (`--trees .`; the
|
||
driver verifies HEAD and a clean tree before every launch) or a detached
|
||
worktree under `.review-gates/trees/<repo>-<hash8>/` (`freeze --worktree`;
|
||
gitignored; `--trees` defaults to what the freeze record says, so a
|
||
workbench that froze another repository with `--repo` reads its worktree)
|
||
— content, history and blame; the state before the range via
|
||
`git show <base>:<path>`.
|
||
- `--context-paths` for the evidence dirs the commit messages' claims rest on.
|
||
- The lens charter (`process/lenses/repo-*.md`, system text
|
||
`_common-repo.md`) and the dispositions in force.
|
||
- A lens with the `browser` or `browser+supabase` environment (the
|
||
functional lens by default; `lenses.<name>.environment` in the config):
|
||
the running application (`.review-gates/config.json` `functional.*`: its
|
||
URL, the local origins, the login fixture, the owner's notes), through the
|
||
Playwright MCP server the driver starts for each of its passes, and the
|
||
exact one-time-code command the prompt names (`totp`).
|
||
- A lens with the `supabase` or `browser+supabase` environment: the local
|
||
stack's database (`supabase.dbUrl`, this machine only), through the exact
|
||
command the prompt names (`sql`) — every call one transaction rolled back
|
||
at its end.
|
||
The helper hands the SQL to the driver's broker, which runs it: the URL,
|
||
its password included, stays in the driver, and the pass's process gets
|
||
only the broker's address and its stack's token.
|
||
|
||
Never: `.review-gates/push/` (dumps, manifests, gate-state, freeze.json), the
|
||
pipeline's own files, the network beyond the application, a build tool, a test
|
||
runner, a package manager, a stack command or a database client of its own
|
||
(`psql` is denied: it commits to the database every pass shares) — the
|
||
bug-prover and the security lens prove statically unless their environment
|
||
gives them the database or the application, where a proof may also be run;
|
||
the functional lens observes, it does not build.
|
||
|
||
### How the passes share the stacks
|
||
|
||
A stack is an application with its API, its database and a login. It is
|
||
either the one the config names (`functional.*`, `supabase.dbUrl`, the first
|
||
login fixture) or one of several **slots** the operator brings up side by
|
||
side (`slots.*`). Each instrument is shared in its own way:
|
||
|
||
- **The application** (the browser environments): a pass holds a stack of
|
||
its own from its launch to its end, and no other pass with the browser
|
||
gets that stack meanwhile. So as many such passes run at once as there
|
||
are stacks up — one on the config's single stack — and a sign-out, a
|
||
setting or a row of one pass reaches no pass running beside it. A stack
|
||
is not reset between passes: what keeps a pass apart from the ones before
|
||
and after it is the rule in the APPLICATION UNDER TEST block. Create only
|
||
what you need, name it with your unit id, never change what you did not
|
||
create, and leave the account as you found it. The application's writes
|
||
are real; they last until the stack is reset. With slots, the pass's own
|
||
stack — its URL, origins and login — is named in its user message, so
|
||
the system prompt every pass shares stays the same.
|
||
- **The database** (the supabase environments): every SQL call is its own
|
||
transaction and is rolled back, so a pass never sees another pass's
|
||
writes. The application never sees them either, and nothing outlives the
|
||
call. That is why a pass with the database alone needs no stack of its
|
||
own: it uses the first. A lock a call takes (DDL, an UPDATE) holds until
|
||
that call ends; other passes wait for it, up to the `lock_timeout`
|
||
(`PgWire::LockTimeoutS`, 3 s). The guard is against interference and
|
||
leftovers, not against a lens set on escaping: a second connection opened
|
||
from inside the database (dblink) commits on its own, and a lens may open
|
||
a connection of its own with the local stack's well-known credentials.
|
||
- **Memory:** there is no per-lens number. Before a gate, and before every
|
||
launch, the driver reads the machine's free memory (`MemAvailable`). It
|
||
launches a pass only while the pass fits: free memory at the gate's start
|
||
less the reserve (`REVIEW_GATES_MEM_RESERVE_MB`, by default 10 % of the
|
||
machine), less what the passes in flight were admitted at, with the
|
||
pass's cost still free right now. A pass costs its environment's measured
|
||
peak: the driver samples every pass's own process tree (claude, the
|
||
Playwright server, Chromium), keeps the recent peaks per repository and
|
||
environment (`~/.cache/review-gates/memory.json`), and admits at the
|
||
highest. Before any is recorded it uses the defaults measured on
|
||
2026-10-22. With nothing in flight a pass runs whatever the memory says.
|
||
- **Other drivers on this machine** (2026-10-31): every driver registers
|
||
its passes in flight on one board, `~/.cache/review-gates/seat-board.json`
|
||
(`SeatPool::Board`), each
|
||
with the account it runs on, the memory it was admitted at and the stack
|
||
it holds. A launch is decided and registered in one locked section, so:
|
||
a stack another driver's pass holds is not free here (a stack is its
|
||
application's address, `Pool::StackKey`, so two configs that name one
|
||
URL share it); the other drivers' passes count against the memory budget
|
||
at what they were admitted at, and the budget's free memory is the
|
||
reading from before they ran (`Board::MemoryBase`) — "nothing in flight"
|
||
means nothing on the machine; their passes count in each seat's
|
||
projection, in an account's 128 and in an API key's slots; and
|
||
`--concurrency`, when given, is the machine's width, every driver's
|
||
passes counted. A driver waiting on
|
||
what another holds re-checks every 30 s. A driver that died is swept by
|
||
its lifetime lock under the board's `drivers/`, a pass by its deadline. A
|
||
driver takes its passes with it however it ends: each pass runs under a
|
||
supervisor that kills the pass's whole tree — the Playwright server and
|
||
Chromium included — when the driver goes, SIGKILL included.
|
||
- **Stopping a run:** `flatpak run net.catcrafts.CrafterReview stop <its scratch dir>`, or
|
||
`--hash <hash8>` for every live run of that hash (the scratch dir is the
|
||
last word of the run's gate-0 log). It works from any sandbox: the driver
|
||
looks for the stop file every 2 s and handles it as SIGTERM — its running
|
||
passes killed, no later gate started, the dump written. From outside the
|
||
sandbox a SIGTERM reaches the driver only through the process group it was
|
||
launched in (`kill -TERM -<pgid>` of the `setsid` launch); a SIGTERM to the
|
||
`flatpak run` pid alone does not. `flatpak kill net.catcrafts.CrafterReview`
|
||
is the hard stop: it ends every run of the app at once, without a dump.
|
||
|
||
## The consumer's configuration
|
||
|
||
`.review-gates/config.json` at the repository root, committed; the template
|
||
is what `init` writes (`flatpak run net.catcrafts.CrafterReview doc config` prints it) and
|
||
`flatpak run net.catcrafts.CrafterReview config` prints the merged result with its problems. What it holds: `base` (the integration
|
||
branch a range is measured against) and `rangeFrom` (`upstream`: the push
|
||
delta `@{upstream}..HEAD`, the rule for a repository that pushes to its
|
||
own trunk; `base`: always the merge-base with `base`, the whole pull request
|
||
for a repository whose branches become PRs — their commits must be linear,
|
||
rebased, never a merge of the integration branch), `authors` (the emails the
|
||
freeze accepts; null = `git config user.email`), `aiTrailers` (`forbid` or
|
||
`allow`), `requireSigned`, `allowDirty` (tracked
|
||
files the local stack rewrites in place while the lenses run), `gate0` (the
|
||
commands Gate 0 runs by hand, for the skill), `lenses` (per lens, by its
|
||
name: `run` — every lens is off until it is `true` here (opt-in only,
|
||
2026-10-31); a lens the config does not turn on is out of every run of this
|
||
repository:
|
||
its gate disappears, the units only it reads are listed as not reviewed,
|
||
never as holes, and the run stays a full run; the ruling of record for a
|
||
repository with nothing to exercise, where `--without` is a per-run ruling
|
||
that leaves a hole — and `environment`, what its processes get beyond the
|
||
read-only tools: `none`; `browser` = a Playwright MCP server per pass,
|
||
the browser tools and `curl`, the APPLICATION UNDER TEST block, the probe
|
||
before its gate, a stack of its own per pass and the fail-closed server
|
||
check; `supabase` = the database under test: the `sql` helper as its
|
||
exact command, the DATABASE UNDER TEST block, the probe before its gate
|
||
(the database answers and its applied migrations are the range HEAD's —
|
||
by version, and by content for the migrations the range adds or changes)
|
||
and the fail-closed check that a pass whose SQL calls never reached the
|
||
database is no evidence; `browser+supabase` = both. The functional lens
|
||
is empirical and needs one with the browser while it runs; the static
|
||
lenses default to `none` and may take any. A verdict bought in one
|
||
environment is never reused, resumed or salvaged in another, and a defect
|
||
found in one drops the clean verdicts the lens holds for the same code in
|
||
the others), `supabase`, the database under test for every lens with
|
||
`supabase` or `browser+supabase`: `dbUrl` (the local stack's Postgres —
|
||
this machine only; masked in the dump and the log), `start` / `stop` (how
|
||
the operator brings the stack up when no lens drives the application;
|
||
`functional.start` covers it when one does), `notes` (the schemas that
|
||
matter, the seeded users, what to leave alone), `slots`, stacks side by
|
||
side: `dir` (where the bring-up writes `<n>/slot.json` once slot n is up —
|
||
`{appUrl, apiUrls, dbUrl, login}`; with it set, the driver uses the slots
|
||
that are up and refuses a gate with none), `start` / `stop` (the commands
|
||
for slot `{n}`; the skill runs them), `memMb` (one slot's measured memory
|
||
while a pass uses it: its containers' and processes' own, never the
|
||
machine's — `slots` sizes the slots by it), and
|
||
`functional`, the application under test for every lens with the
|
||
`browser` or `browser+supabase` environment when no slots are configured:
|
||
`appUrl` and `readyPath` (what the driver probes before that lens's gate),
|
||
`apiUrls`, `start` /
|
||
`stop` (how the operator brings the application up from the frozen tree — the
|
||
skill runs them, the driver only probes), `login.fixtures` (JSON files with
|
||
`email`, `password`, `totpSecret`; the first that exists is the stack's
|
||
login; a slot names its own in `slot.json`), `exclude` (path globs whose
|
||
hunks carry no runtime behaviour: tests, CI, docs — mechanical n/a for the
|
||
functional lens; it holds with slots too),
|
||
`extraTools` (`Bash(<prefix>:*)` or `mcp__<server>[__<tool>]` entries a lens
|
||
with the browser may also use; an entry the deny list (`Tools::Deny`) overrides — `psql`, a
|
||
package manager, a stack command — is a config problem, never a silent
|
||
no-op), `mcp` (the Playwright MCP command; null args = the pipeline's pinned
|
||
default), `notes`.
|
||
|
||
## Gate 0 — mechanical preflight (no agents)
|
||
|
||
1. **Build and tests green on the exact HEAD**, the repository's own way (the
|
||
`gate0` commands of the config, run by hand from the checkout). Keep the
|
||
output where a commit message's "tested" claim can point at it
|
||
(`--context-paths`).
|
||
2. **Freeze** — `freeze` checks, all of them, before it
|
||
exports anything: author AND committer are in `authors`; AI attribution
|
||
trailers (`Assisted-by:`, `Co-Authored-By:`) by the `aiTrailers` rule; the
|
||
tracked tree is clean (the `allowDirty` paths aside, warned); no merge
|
||
commit in the range (`git format-patch` drops them silently, so the
|
||
artifact would not be the push); the range is a fast-forward of
|
||
`@{upstream}` when there is one; an unsigned commit warns
|
||
(`requireSigned` makes it fail). The base: `--base`, else by `rangeFrom`:
|
||
`@{upstream}` (the push delta) or the merge-base of HEAD and the
|
||
configured `base` branch (the whole pull request). Then the
|
||
export, the hash, an empty `gate-state/dispositions.json` when none
|
||
exists, `freeze.json`, `run-line.txt`, and with `--worktree` the detached
|
||
checkout.
|
||
|
||
```
|
||
flatpak run net.catcrafts.CrafterReview freeze --slug <branch-or-release> [--worktree] [--base <ref>]
|
||
```
|
||
|
||
The hash is `cat $(ls *.patch | sort) | sha256sum` over the export; every
|
||
pass binds to it. An amend or a rebase changes the `From <sha>` lines and
|
||
therefore the hash: re-freeze with `--replace` into the same dir
|
||
(`gate-state/` survives, so clean unit results are reused from
|
||
`unit-state.json` — what a re-freeze re-runs is under Gates 1–3).
|
||
|
||
3. **The application and the database under test** (before a gate whose
|
||
lens has an environment with either can run — Gate 2 by default; nothing
|
||
to start when no lens that runs has one): start them from the tree the
|
||
lenses read, at the frozen HEAD — the checkout itself when `--trees .`,
|
||
the worktree when `--worktree` — with the config's `functional.start`
|
||
(or `supabase.start` when no lens drives the application). With
|
||
`slots.*`, bring up slot 1 first, then run
|
||
`flatpak run net.catcrafts.CrafterReview slots --artifact <dir>`: it prints
|
||
how many slots fit in free memory — no more than the range's browser
|
||
passes can use at once — and the `slots.start` commands for the missing
|
||
ones, which may run side by side. The driver refuses a gate while a
|
||
stack's application or database does not answer, while its applied
|
||
migrations are not HEAD's (a migration of HEAD not applied, one applied
|
||
that HEAD lacks, or one of the range applied with other content — an
|
||
amend without a reset), or while its login is not in its database; and
|
||
it aborts the run if the tree moves or gets dirty beyond `allowDirty`
|
||
while it runs. After `freeze --replace`, bring every stack up again from
|
||
the new HEAD before the rerun. A fix during a run happens in another
|
||
worktree, never in the tree the lenses read.
|
||
|
||
## Gates 1–4 (the driver) and Gate 5 (the owner)
|
||
|
||
| Gate | Lens | Question |
|
||
|---|---|---|
|
||
| 1 Approach | `repo-better-solution` (whole pass gating code blocks) | an existing mechanism? the right layer? the right commit shape? compatible with what users have? |
|
||
| 2 Functional | `repo-functional` (whole pass gating code blocks; the `browser` or `browser+supabase` environment; absent when the config turns it off) | in the running application, does the range do what its code and messages say, and does it leave the flows next to it working? |
|
||
| 3 Proof | `repo-bug-prover` (code blocks, static; with the `supabase` environment a proof may also be run in the database) | a demonstrated functional defect in the range's code |
|
||
| 3 Proof | `repo-security` (code blocks, static, beside the bug-prover; with the `supabase` or a browser environment a demonstration may also be run in the database or the application) | a demonstrated gap in a protection the repository promises: a check, policy or grant the range's code lacks, weakens or removes |
|
||
| 4 Commit truth | `repo-facts` (message blocks, documentation blocks and the `whole:order` / `whole:absent` slots, static) | every claim in every commit message, and in the documentation the range adds or changes, proven or gone |
|
||
| 5 | the owner | approves this exact hash, then the push happens |
|
||
|
||
Every lens runs on Opus 5.5 at effort `xhigh` (`--effort` overrides).
|
||
There is no width of the machine by default (2026-10-31, Jorijn: "there is
|
||
no need to do a global cap anymore, the cap should be 128 per available
|
||
account"): every OAuth account takes at most 128 passes at once, every
|
||
driver's counted (`REVIEW_GATES_OAUTH_CONCURRENCY`), an API seat its
|
||
`REVIEW_GATES_API_CONCURRENCY`, and free memory the rest; `--concurrency N`
|
||
still caps the machine when given (it was 128 by default from 2026-10-15,
|
||
the width two drivers running at once shared). One full run
|
||
per hash: a second on the same hash is refused while the first is live
|
||
(`State::RunLock`; a test run takes a run number of its own), and no two
|
||
drivers share a scratch dir. `flatpak run net.catcrafts.CrafterReview seats` shows the
|
||
drivers running and what they hold. The seat pool still places
|
||
every process by its seat's projected 5-hour headroom
|
||
(`REVIEW_GATES_LENS_COST_PCT`, `REVIEW_GATES_SEAT_MAX_UTIL`) — or, on an
|
||
account whose 5-hour limit Anthropic has removed, by its weekly window
|
||
under the 99 % cutoff — at most 128 passes per account, every driver's
|
||
counted (`REVIEW_GATES_OAUTH_CONCURRENCY`), and a pass
|
||
launches only when its environment's instrument is free and it fits in
|
||
free memory (How the passes share the stacks): the driver logs per gate
|
||
how many passes of each environment memory and the stacks allow. Run
|
||
from the repository root in a detached background shell (`setsid nohup … &` —
|
||
a launch bound to a tool call's timeout dies with it) and poll
|
||
`~/.cache/review-gates/<hash8>-run-<n>/progress.json`; `freeze` prints the
|
||
exact line and writes it to `run-line.txt` — launch from that file verbatim,
|
||
never retyped (2026-09-29: a hand-copied base cut at 120 columns did not
|
||
resolve and nobody noticed for a round).
|
||
|
||
```
|
||
flatpak run net.catcrafts.CrafterReview run --artifact .review-gates/push/<slug>/ --hash <sha256> \
|
||
--base <base sha> --trees . --dispositions .review-gates/push/<slug>/gate-state/dispositions.json \
|
||
[--context-paths <dirs>] --date <YYYY-MM-DD>
|
||
```
|
||
|
||
The lens processes run on the seats in the repository's `.env`
|
||
(`CLAUDE_CODE_OAUTH_TOKEN1..6` Max plans, `ANTHROPIC_API_KEY1..6` API-credit
|
||
organisations: API seats first, up to `REVIEW_GATES_API_BUDGET_USD` per seat
|
||
per cycle, then OAuth seats by 5-hour headroom), else this machine's own
|
||
login; `flatpak run net.catcrafts.CrafterReview seats` reports them. An API seat is safe only
|
||
when its organisation holds nothing but the plan's monthly credits — no
|
||
payment method, no purchased credits, auto-reload off — so that "credit
|
||
balance is too low" is a hard stop. `--seats local|api|oauth|<list>`,
|
||
`--no-probe`, `--api-budget` override. OpenRouter seats and the opencode
|
||
harness are carried for the hours when every seat is out (`--seats
|
||
openrouter --harness opencode`); a lens whose environment has the browser
|
||
or the database cannot run there (no MCP server; the database check reads
|
||
the claude stream) — `--without` it, and the round is PARTIAL in spirit
|
||
even though the driver calls it full: say so in the manifest.
|
||
|
||
Exit 0 SEND-READY = **push-ready**; 1 FIX FIRST; 2 INCOMPLETE / PARTIAL /
|
||
CONFIRMING (gates clean, code units still owe clean reads — run the same line
|
||
again, see "Three clean reads"); 3 preflight. A whole pass gates its block
|
||
passes; coverage is fail-closed per inventory unit (`inventory commits`:
|
||
`pK:subject`, `pK:msg`, `pK:diffstat`, `pK:<file>#hdr`, `pK:<file>#H`,
|
||
`pK:outside`, `pK:absent`, `whole:order`, `whole:absent`,
|
||
`whole:behaviour`); per-unit reuse across hashes, salvage of finished
|
||
outputs after a kill, budgets, `--only` / `--units` test-only runs (PARTIAL,
|
||
never a pass), the dump at `gate-state/commits-<hash8>-run-<n>.json`.
|
||
|
||
**Who reads what.** The four code lenses read code: hunks, file headers and
|
||
(the static lenses) the per-commit slots `pK:outside` and `pK:absent`. The
|
||
commit-truth lens reads the prose: the text units — subjects, messages,
|
||
diffstats — and the units of documentation files (`*.md`), each
|
||
heading-delimited part of a hunk on its own, and it asks `whole:order` and
|
||
`whole:absent` in slot passes of its own; a documentation file's diff header
|
||
and a part the commit writes nothing in are a mechanical `n/a`. A lens the
|
||
config leaves off (every lens is off until `lenses.<name>.run = true`) reads
|
||
nothing: the units only it would have read (its whole slot —
|
||
`whole:behaviour` for the functional lens; the prose for the commit-truth
|
||
lens) are listed under `coverage.notReviewed`, never as holes, and the
|
||
summary says so. The functional lens plans no pass on a diff header, on a
|
||
unit of a file matching `functional.exclude`, or on the per-commit slots (its
|
||
whole pass judges adjacent flows and missing deliveries range-wide): each is
|
||
a mechanical `n/a` with its basis.
|
||
|
||
**What a re-freeze re-runs** (the rules of 2026-10-06, with one addition for
|
||
the functional lens):
|
||
|
||
- a hunk or a file header, for the static lenses (the approach lens, the
|
||
bug-prover, the security lens): its file and its own
|
||
text, with the blob ids of the `index` line, the line numbers of the `@@`
|
||
header and format-patch's tail taken out. Not in the key: the commit id,
|
||
the message, the unit's number, the rest of its commit. A reworded message,
|
||
a new set of commit ids, a hunk or a commit added before it re-run nothing;
|
||
a changed line in the hunk, context lines included, re-runs that hunk;
|
||
- a hunk, for the functional lens: the same, plus the code of the whole
|
||
range — what the application shows for one hunk depends on every other
|
||
hunk (a later commit changes the screen it renders), so any code change in
|
||
the range exercises every functional unit again. The static lenses' blocks
|
||
stay self-contained by the owner's ruling (2026-10-06: "blocks fully
|
||
own the change that results from their block even if its in other
|
||
blocks") — the browser cannot;
|
||
- `pK:outside` and `pK:absent` (static lenses) have no text and stand for
|
||
one commit in the repository: they are asked again when any code of the
|
||
range changed, or when a documentation file of their commit is added,
|
||
deleted or renamed (`absent` also when the commit gains or loses its body).
|
||
The commit's position is in the key;
|
||
- a message (`pK:subject`, `pK:msg`), for the commit-truth lens: its own
|
||
text, the base and the code of the range up to and including its commit —
|
||
a fix to the code a message describes asks the message again; a diffstat
|
||
its own text alone;
|
||
- a unit of a documentation file, for the commit-truth lens: its file and
|
||
its own text (normalised like a hunk), the base and the code of the whole
|
||
range — a sentence must hold for the code as its commit leaves it and as
|
||
the range leaves it;
|
||
- the commit-truth lens's slot passes (`whole:order`, `whole:absent`): never
|
||
stored, bought every run;
|
||
- both whole passes: the code of every commit of the range in order, which
|
||
commits have a body and which documentation files each touches. No message
|
||
text and no documentation text. A whole pass that returned a finding is
|
||
never stored, so it runs on the next hash whatever changed;
|
||
- every code unit and both whole passes: the range base (a rebase re-runs
|
||
all of them);
|
||
- as always: the lens charter, the prompt version, the model, the
|
||
dispositions in force. An edit to a `repo-*.md` charter re-runs that lens
|
||
on the next hash; a re-run on the SAME hash resumes and salvages by hash and
|
||
model alone (use `--no-reuse --no-salvage` there).
|
||
|
||
What is stored as reusable: a pass that itself returned no undischarged
|
||
finding, on a unit that is clean in the merged result; and where several
|
||
units share a key (the same file header in two commits), only when all of
|
||
them are. A reused whole pass gates its block passes like a fresh one and
|
||
hands them the summary it stored. `--no-reuse` buys the full round.
|
||
|
||
**Three clean reads (2026-10-06.5).** A static lens is not
|
||
deterministic: over one release's fifty rounds, of 58 defect verdicts on
|
||
unchanged code at most 14 were in the block when a first read called it clean. The owner's
|
||
rule: "per round each block gets run once, then in future rounds the blocks
|
||
from the previous rounds get run again until they reach x"; x is 3
|
||
(`--reads`).
|
||
|
||
- It applies to the code units of Gates 1 and 3: hunks, file headers and the
|
||
per-commit slots. Both whole passes need one read; the functional lens's
|
||
units need one read (their key already holds the whole range's code, so
|
||
every code change exercises them again; flip `reads` in `:Plan`'s lens table to make
|
||
them three — three browser reads per hunk per round).
|
||
- A round reads every such unit below three clean reads ONCE. A clean read
|
||
counts; a finding drops the count (and its fix changes the key anyway); a
|
||
changed key starts at zero. A unit at three is reused.
|
||
- A round is the runs on one hash up to the first that finished. The
|
||
relaunch of a killed or paused run continues its round; the same line run
|
||
again after a finished run is the next round.
|
||
- All gates clean with reads still owed is `CONFIRMING — NOT SEND-READY`
|
||
(exit 2); run the same line again, on the same hash, until it says
|
||
SEND-READY. Gate 5 is asked for a hash only then.
|
||
- Every finding in the dump carries `read`, the number of the read that
|
||
found it.
|
||
|
||
## Verdicts, findings, dispositions
|
||
|
||
A lens reports **findings** (defects) and **notes** (everything else worth
|
||
the record). It never grades a finding. What makes something a defect is the
|
||
lens's charter question — objective wrongness: a better mechanism that
|
||
exists, a demonstrated bug, a behaviour the application does not deliver, a
|
||
protection the code lacks. "No reviewer would notice" is no defence. Every finding blocks the push until it
|
||
is fixed or discharged by a disposition in force. Notes never affect the
|
||
verdict and are never waived. Note kinds: `reserve-answer`, `invariant`,
|
||
`pre-existing`, `residual-risk`, `hold`, `info`, `immaterial`,
|
||
`send-time-check`, `refutation-verified`, `unverified`.
|
||
|
||
**verdict** — FIX FIRST iff any finding is undischarged. The driver derives
|
||
it: SEND-READY declared over an undischarged finding fails closed; FIX FIRST
|
||
declared with everything discharged and no contest is a silent veto — the
|
||
derived pass stands and the violation is logged.
|
||
|
||
**Dispositions** discharge ONE named finding each and are never the lens's.
|
||
Two kinds: `waiver-owner` (the owner's ruling — every owner ask of a gate
|
||
stop batched into one message: finding, evidence, proposed grounds, cost of
|
||
fixing instead; the round blocks until they rule) and `refuted` (a FACT that
|
||
falsifies the finding's premise; a competing preference is an owner ask).
|
||
Each entry is dated, names what it `supersedes`, binds to a hash, records
|
||
grounds, and lists the artifact `anchors` its grounds depend on with the
|
||
count of occurrences at binding. The register is
|
||
`<out>/gate-state/dispositions.json` — the authority and the record; the
|
||
manifest's rounds table names each discharge with its grounds for Gate 5.
|
||
Carry across a hash change
|
||
is mechanical: an entry is in force when bound to the current hash, carried
|
||
when `carry` is true and every anchor still occurs its recorded count of
|
||
times, lapsed otherwise — a lapsed entry discharges nothing and the finding
|
||
re-blocks. The lens receives the entries in force, verifies the grounds of
|
||
any it discharges, still reports the finding marked `dischargedBy`, and may
|
||
contest one only with evidence the grounds do not survive; a contest is
|
||
itself a claim and is verified before it reaches the owner; a reaffirmed or
|
||
rejected contest is final for that binding. Every undischarged finding is
|
||
fixed (amend → `freeze --replace` → rerun), refuted, or an owner ask. Notes
|
||
never block: an `invariant` is recorded in the manifest, a `send-time-check` is a
|
||
push-time check, an `unverified` names what a manual check must cover before
|
||
the push, the rest are banked per kind.
|
||
|
||
A new disposition in force re-keys every unit, so a waiver costs every static
|
||
code unit its three reads again.
|
||
|
||
## Gate 5 — the owner, then the push
|
||
|
||
Present the frozen patches themselves (never summaries), the rounds table,
|
||
every discharged finding with its grounds, every contest, every `unverified`
|
||
note and every other note — then STOP. On their approval of the hash:
|
||
|
||
1. `flatpak run net.catcrafts.CrafterReview freeze --slug <slug> --verify` must
|
||
print MATCH (HEAD, a clean tree and a fresh export still equal
|
||
`freeze.json`); a MISMATCH means the range moved and the review is void.
|
||
2. `git push` (and open the pull request, where the repository's flow is a
|
||
PR into its integration branch).
|
||
3. `git worktree remove .review-gates/trees/<repo>-<hash8>` when one was
|
||
used; stop the application (`functional.stop`).
|
||
4. Record in `<out>/manifest.md` (`flatpak run net.catcrafts.CrafterReview doc manifest` prints the template): the
|
||
rounds, `approved: <date> @ <hash>`, `pushed: <date> <remote>/<branch> @
|
||
<sha>`. `.review-gates/push/` is gitignored; paste the rounds table into
|
||
the pull request when the repository keeps its record there.
|
||
|
||
## Driver changes and test-only runs
|
||
|
||
`--only` / `--units` runs are PARTIAL and never a pass. After any driver
|
||
change: its suite passes (`crafter-build test` in a checkout of
|
||
Crafter.Review), a `--dry-run` prompt comparison before and after on a frozen
|
||
fixture shows exactly the intended differences, and a run proves each gate
|
||
can still reject (a fixture repository with one planted defect per gate;
|
||
`AGENTS.md` of Crafter.Review says how).
|
||
|
||
## Feedback loop
|
||
|
||
A defect that reaches a pushed release, or a review comment on the
|
||
repository nobody's lens predicted, is a process bug: post-mortem it and
|
||
append a dated calibration entry to the `repo-*.md` lens that should have
|
||
caught it, or here. The entries dated before 2026-10-15 were carried from the
|
||
pipeline's earlier home because they are rules of the mechanics, not of one
|
||
repository.
|
||
|
||
## Calibration (append dated entries at the bottom)
|
||
|
||
- 2026-09-25 — the pipeline's first form (Jorijn: "a slim version with better solution,
|
||
bug prover and facts"). Decided: the bug-prover stays static; the author
|
||
reads the frozen patches at Gate 4, there is no author sign-off gate before
|
||
the lenses.
|
||
- 2026-09-29 (round 13 of a release) — **launch from the freeze's `run` line
|
||
verbatim, never retyped.** A round ran with a base hand-copied from a
|
||
display cut at 120 columns; it did not resolve and the lenses fell back to
|
||
the artifact's base-commit line. Copy from `run-line.txt` or build the
|
||
command from `freeze.json`.
|
||
- 2026-10-01 (round 15 of a release) — **a scripted rebase must never commit
|
||
at a stop it did not verify.** One stop per command; before any `--amend`
|
||
at a stop assert the subject of HEAD and `git status` free of `UU` entries.
|
||
A chain rebuild runs as a `sh -e` script file, never typed into the tool
|
||
shell (2026-10-14).
|
||
- 2026-10-06 — **a review pass is paid for again only when what it
|
||
judged changed; measure what a round re-runs before the tenth round, not
|
||
after the fiftieth.** At every gate stop read the dump's passes run /
|
||
reused per lens beside the findings, and say so when a round re-ran code
|
||
that had not changed; a fix pass that touches only messages should cost
|
||
nothing in Gates 1 and 3.
|
||
- 2026-10-06 — **a reuse rule is reviewed by someone who did not write
|
||
it, and its STORE side is tested as hard as its key side.** For every new
|
||
key ask "two units, one key, different verdicts" and "a FIX FIRST round,
|
||
then a text-only re-freeze". A prompt for a reviewer names the files and
|
||
what was already verified, never the conclusion.
|
||
- 2026-10-06 — **a fix pass is where the next round's findings come
|
||
from: delete, use the finding's own words, and sweep the fix's
|
||
neighbours.** A sentence added to pre-empt a finding is new surface; take
|
||
the finding's own minimal proposedAction.
|
||
- 2026-10-06 — **agree the exit with the owner early.** Once the code
|
||
gates are SEND-READY and what remains is sentence-level or taste, put the
|
||
stopping rule to the owner at THAT gate stop, with the trend; `waiver-owner`
|
||
is the designed exit. Before offering "a re-run on the same hash, minutes":
|
||
the driver RESUMES a finished lens result with its undischarged findings and
|
||
SALVAGES old outputs by job label — re-running only the waived lens needs
|
||
the checkpoint entry removed plus `--no-salvage`; the documented alternative
|
||
is `--no-reuse --no-salvage`, a full round.
|
||
- 2026-10-14 (a CI run of an image build) — **when a push depends on an external
|
||
mutable resource (a registry, a mirror, a tag, a hosted environment), the
|
||
send-time check names every reader of it in the run and verifies the
|
||
resource at the level those readers see; a precondition of step N is never
|
||
repaired by step N+k of the same run.**
|
||
- 2026-10-15 — created as the submodule pipeline for the owner's repositories
|
||
(Jorijn: "a repo that can be included as a git submodule and carries the
|
||
prepush review pipeline … remove the facts lens and add a
|
||
functional lens that is whole and block scoped that has a playwright mcp;
|
||
this should be gate 2 and the bug prover should be gate 3"). First
|
||
consumer: an application repository. Decided here, open to the owner's
|
||
ruling: the functional lens's blocks are keyed on the whole range's code
|
||
(above) and read once; its passes run four at a time by default on one
|
||
shared application and database; the text and documentation units are
|
||
listed as not reviewed rather than read by a lens the owner removed. The
|
||
consumer's `.env` already carries six OAuth seats; the driver reads it from
|
||
the consumer's root. Proofs at creation: the
|
||
tests, a `--dry-run` on the pipeline's fixture range and on a real
|
||
consumer range, and firing proofs.
|
||
- 2026-10-15 — **a lens that says it had no tools is not a pass; the driver
|
||
must know what a pass had, not only what it returned.** The first firing
|
||
run of the functional lens (the planted greeter range, `--only
|
||
repo-functional.md`) came back SEND-READY: all four hunks `n/a` with
|
||
`unverified` notes saying "no mcp__playwright__* tools in this process" and
|
||
`hold` notes saying "this SEND-READY is a formality". The charter's
|
||
fail-closed wording worked — the lens refused to call anything clear — but
|
||
the verdict rule (FIX FIRST iff a finding) still read it as a pass. Cause:
|
||
the CLI's `--safe-mode`, which the driver passed to every lens process, drops
|
||
every MCP server (`mcp_servers: []` with it, connected without; CLI
|
||
2.1.300). Fixed in the driver: a functional pass runs without
|
||
`--safe-mode`, and a pass whose init line shows no connected Playwright
|
||
server is an error (retried once, then NOT RETURNED). Rule for any lens
|
||
that depends on a capability the driver provides: assert the capability
|
||
from the process's own init line, never from the lens's report of it.
|
||
- 2026-10-22 — **the database under test: two environments, `supabase` and
|
||
`browser+supabase`** (Jorijn: "can you make a browser+supabase envoirement
|
||
and a supabase only envoirement? … can we give each agent its own
|
||
supabase, or make them share one correctly?"). Decided here: the passes
|
||
share the one stack the operator brings up. The database's sharing is
|
||
made correct by the helper rather than by instruction: every call is a
|
||
transaction it rolls back. A statement that would end the transaction is
|
||
refused before anything connects, every statement goes to the server as
|
||
its own Parse (two commands in one are refused), and a transaction found
|
||
closed after a statement stops the call. Not chosen: Supabase's own MCP
|
||
server (`/mcp` is served by Studio, which the throwaway bring-up leaves
|
||
out, and its `execute_sql` commits), and one stack per pass. A stack per
|
||
pass is possible on a machine that runs several side by side (a minimal
|
||
scratch stack started in 33 s), but every stack also needs its own build
|
||
of an application that bakes the API URL in, and its own seed. Also
|
||
decided: raw `psql` is denied, the database URL never reaches a prompt,
|
||
the preflight compares the applied migrations with HEAD's, and a defect
|
||
found in one environment drops the clean verdicts the other environments
|
||
hold for the same code. The firing proofs are in this pipeline's
|
||
`AGENTS.md`.
|
||
- 2026-10-22 — **a stack per pass; the limit from free memory, by
|
||
environment** (Jorijn: "ideally i would like to remove the cap … by not
|
||
making the database shared"; "make the concurrency limit from free
|
||
memory, instead of just guessing … the concurrency limit should be on
|
||
the environment"). `functional.concurrency` (4, never measured) and the
|
||
per-lens cap are gone. A pass with the browser holds a stack of its own;
|
||
every pass is admitted against free memory at its environment's
|
||
measured cost. Costs are measured from a pass's or a stack's own
|
||
processes, not from the machine's free memory, which other work moves —
|
||
that number is the budget only. Not reset between passes: a slot keeps
|
||
what earlier passes created, so Method 5's rule stays. The driver still
|
||
never starts a stack: `slots` sizes them, the skill runs the commands.
|
||
Numbers and proofs in this pipeline's `AGENTS.md`.
|
||
- 2026-10-22 — **the security lens, beside the bug-prover in Gate 3**
|
||
(Jorijn: "please write the security lens", and written so that no
|
||
process reads it as a request to do harm — a charter that reads so is
|
||
declined, and a declined pass is NOT RETURNED). Decided here: the
|
||
bug-prover's shape (static code blocks keyed on the code, three clean
|
||
reads, `none` by default, the database or the browser when the config
|
||
gives them), in the bug-prover's gate rather than one of its own — one
|
||
gate stop for both proofs, and the owner stays Gate 4. The charter is the
|
||
owner's own defensive review: the promise of the repository first, every
|
||
decision point the hunk touches, the classes settled one by one, a gap
|
||
shown in the smallest step on the throwaway local copy and never a step
|
||
past it, refutations by the control that closes the class. A charter
|
||
edit re-keys the lens alone. Its firing proof is in this pipeline's
|
||
`AGENTS.md`.
|
||
- 2026-10-31 — **the driver as a Flatpak** (Jorijn: "… make it an portable
|
||
flatpak executable like Crafter.Build"). The helpers are subcommands
|
||
(`sql`, `totp`). The submodule goes: `crafter-review init` sets a
|
||
repository up and ships the skill, `doc` the texts, `browser install` the
|
||
Chromium, `stop` ends a run from any sandbox. A driver takes its passes
|
||
with it however it ends.
|
||
- 2026-10-31 — **accounts without a 5-hour limit** (Jorijn: "antrophic is
|
||
gradually removing the 5h limits"). Anthropic removes the 5-hour limit in
|
||
stages, and an account without it reports the weekly window alone. Such
|
||
an account is ranked and admitted by its weekly window with the passes in
|
||
flight projected on it, never taken for an unprobed seat; every OAuth
|
||
account takes at most 128 passes at once (every run measured with 149 or
|
||
more in flight on one account drew usage-limit rejections); a 'concurrents' rejection backs the seat off for seconds,
|
||
not until a window's reset; `seats` shows `no limit` in such an account's
|
||
5-hour column. Then (Jorijn: "there is no need to do a global cap anymore,
|
||
the cap should be 128 per available account") the machine-wide width
|
||
went: `--concurrency` caps only when given.
|
||
- 2026-10-31 — **every lens off by default, opt-in only; the commit-truth
|
||
lens as Gate 4** (Jorijn: "make it so all lenses are off by default and
|
||
it's opt in only"; and of the commit-truth lens, "fix that in
|
||
Crafter.Review"). A config that
|
||
turns on no lens is a problem `config` names and `freeze` and `run` stop
|
||
on; `init` says to turn lenses on; the owner's gate is 5. The commit-truth
|
||
lens's firing proof in this pipeline is owed (the owner's go).
|