feat(core): the data home, the lens texts, and --version naming both
process/ holds the procedure, the five lens texts, the templates and the skill under process/skill/prepush/. A lens text's sha256 is part of every reuse key and the texts are part of every prompt, so they carry no SPDX header; --version prints the charter hashes: 1728bd4c, 148d5c4c, c7ffef44 and 2b9a5f83 for the four charters. Home() finds them: CRAFTER_REVIEW_HOME, natively only (a host setting must not swap the Flatpak's texts for a checkout's), then <exe>/../share/crafter-review, then the checkout the binary was built in. Main() hands `__supervise` to the supervisor before anything else, then decides the sandbox once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f94d895701
commit
f2b25db50d
12 changed files with 1372 additions and 4 deletions
574
process/prepush.md
Normal file
574
process/prepush.md
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
# 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; four lenses,
|
||||
the functional one as Gate 2 (Jorijn, 2026-10-15), the security lens beside
|
||||
the bug-prover in Gate 3 (2026-10-22).
|
||||
|
||||
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 4.
|
||||
|
||||
## 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) — 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 and in an API key's slots; and `--concurrency` 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` — `false` leaves the lens 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–3 (the driver) and Gate 4 (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 | the owner | approves this exact hash, then the push happens |
|
||||
|
||||
Every lens runs on Opus 5.5 at effort `xhigh` (`--effort` overrides).
|
||||
`--concurrency` is 128 by default (2026-10-15), the width of the machine:
|
||||
two drivers running at once share it (2026-10-31, the board). 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`), 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 lenses read code: hunks, file headers and (the
|
||||
static lenses) the per-commit slots `pK:outside` and `pK:absent`. The
|
||||
text units — subjects, messages, diffstats — and the units of documentation
|
||||
files (`*.md`) are read by no lens of this pipeline (the facts lens was
|
||||
removed, 2026-10-15); the dump lists them under `coverage.notReviewed` and
|
||||
the summary says so. A lens the config turns off (`lenses.<name>.run =
|
||||
false`) reads nothing: the units only it would have read (its whole slot —
|
||||
`whole:behaviour` for the functional lens) are listed there too, never as
|
||||
holes. 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;
|
||||
- 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 4 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 4.
|
||||
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 4 — 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.
|
||||
Loading…
Reference in a new issue