docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
# Crafter Review
2026-10-31 20:09:07 +01:00
The pre-push review pipeline: adversarial lenses review a frozen commit range, one `claude --print` process per inventory unit, before the range is pushed. Five lenses in a fixed order: the approach, the range exercised in the running application through the Playwright MCP server, side by side the static bug proof and the security review, and last the commit truth of the messages and the documentation. Every lens is off until the repository's config turns it on, and the config says what each gets: the browser, the local Supabase database, both, or neither.
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
| Gate | Lens | Question |
|---|---|---|
| 1 | `repo-better-solution` (a whole pass gating code blocks) | the right mechanism, layer and commit shape? compatible with what users have? |
| 2 | `repo-functional` (a whole pass gating code blocks; the `browser` or `browser+supabase` environment) | 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 | `repo-bug-prover` (code blocks, static; with the database a proof may also run there) | a demonstrated defect in the range's code |
| 3 | `repo-security` (code blocks, static, beside the bug-prover) | a demonstrated gap in a protection the repository promises |
feat(lenses): the commit-truth lens, repo-facts, as Gate 4
Every claim in every commit message, and in the documentation the range
adds or changes, proven or gone. The lens's block kinds are text, doc and
slot: a message unit is keyed on its text, the base and the code up to its
commit (MessageDeps); a documentation unit on its file, its normalised
text, the base and the code of the whole range (DocKeyParts); a diffstat on
its text alone; whole:order and whole:absent are asked in slot passes of
its own, never stored. A documentation file's diff header and a part the
commit writes nothing in are a mechanical n/a. Budgets 60/40 for a text or
doc block, 50/35 for a slot; one read. Gate 4, after the proofs, so the
messages and documents are read once the code they describe has settled;
the owner's gate is 5.
The goldens that follow from the lens table are derived by
tests/Goldens/derive.py, which asserts that the recorded table still
reproduces the recorded entries before it writes.
Proven, with the opt-in commit after this one: the suite (26 passed); a dry
run of a scratch range (2 commits, a README with sections) by the previous
binary and this one with the three static lenses on and the rest off, 25
prompts and 4 system files byte for byte; with repo-facts on as well,
those files unchanged and 10 passes and 1 system file added.
The lens's firing proof in this pipeline is owed (the owner's go).
2026-10-31 20:08:26 +01:00
| 4 | `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 the exact frozen hash, then the push happens |
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
crafter-review's prompts, reuse keys, exit codes and shared files are pinned byte for byte by its goldens (`tests/Goldens` ): a review one driver started, another resumes and reuses, and every driver on a machine sees the others' passes on its seat board.
## Install
2026-11-01 02:36:07 +01:00
crafter-review ships as a Flatpak with its own git, Node (for the Playwright MCP server) and ripgrep, so every machine reviews with the same tools. Inside it the lenses see the host's filesystem, with the host's `/usr` at `/run/host/usr` , but not the user and system Flatpak installations (`~/.local/share/flatpak` is empty in the sandbox, `/var/lib/flatpak` absent, by Flatpak's rule): a repository whose dependencies live in another Flatpak points its lens sections at the host's copies.
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
```bash
flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo
flatpak install catcrafts net.catcrafts.CrafterReview
alias crafter-review='flatpak run net.catcrafts.CrafterReview'
```
2026-11-02 10:57:23 +01:00
Every push to `master` is a release, published as `stable` . The alias is for people: the `/prepush` skill spells every command as `flatpak run net.catcrafts.CrafterReview …` , because Claude's Bash tool sees no aliases.
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
What the machine provides:
- **The Claude Code CLI.** It is proprietary and not bundled. crafter-review looks for it in this order: `CLAUDE_BIN` or `REVIEW_GATES_CLAUDE_BIN` , the PATH, the editor extensions' bundled binary (newest first), then `~/.claude/local` and `~/.local/bin` . It runs inside the sandbox, so every pass is measured and every pass's process tree can be killed.
feat(seats): the env file's settings count, and a machine's own seats file
ApiConfig and PoolConfig read the seat order, the budgets and the
concurrency from the process environment alone, so a `.env` that said
REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver
reads those lines from the file under the process environment
(config_env, 2026-10-17: a setting lives beside the seats it orders).
SeatPool::SettingsOf does the same - the file's placement settings
under the process environment, in a map handed to the placement readers
alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the
process environment untouched, so no other reader sees a line of the
file: a path's or a binary's line reaches nothing, and two drivers
reading one .env keep one board and one ledger. The placement settings
are one list, SeatPool::PlacementSettings - the fifteen names those
readers read - for the map, the run's log and what makes a working
directory's .env a seats file; a REVIEW_GATES_* line of any other name
stays out of the map and is named, by the run's log and on `seats`'s
stderr. One dotenv grammar serves the tokens and the settings
(Seats::DotenvLines).
`run` and `seats` resolve the file from their own --env-file
(SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else
the working directory's .env - the operator's, as the Python driver and
`seats` read it, not the repository under review's - when it is a file
holding a seat or a placement setting, each with a value, or one that
cannot be read as UTF-8 text (its error is then the command's) - a
readable application .env, its Vite or Supabase lines, one of blank
placeholder lines or one that is no file is passed over, and the run
says so - else the machine's own
$XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env:
one file for every repository on a machine, a repository's seats .env
winning over it. Each file is read once and the settings and the tokens
are taken from that read (SeatsFile::Text), so a file that can be read
only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives
its settings and its seats alike, and a failed read raises its own error
where the tokens are laid over the environment: SeatPool::WithFileTokens,
for `run` and `seats` alike, and LoadSeats takes the merged environment
and no longer reads a file by path. An empty `seats --env-file` means no file
now, as it did for `run`, and the `seats` report names the side that
runs first (it said "api seats run first" whatever the order).
Runs, Commands and KernelModes, the tests that run `run` and `seats`,
point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a
path of their own that nothing creates, so a global file stays out of
them; the grammar, the settings and their precedence, the lookup and the
one read have their tests, a run with `--env` (a prefix) and with a
repeated --env-file, and a `seats` with `--env`, take tokens and
settings from the one file, an application's .env and a venv are passed
over, a FIFO is read once, no line of the file reaches `stop`, every
listed placement setting moves a reader, another REVIEW_GATES_* line is
named and a .env of blank lines passed over, and the
README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
- **Seats.** The lens processes run on the seats of a dotenv file (`CLAUDE_CODE_OAUTH_TOKEN1..6` , `ANTHROPIC_API_KEY1..6` ): `--env-file` , else `$SEATS_ENV_FILE` , else the working directory's `.env` if it is a file holding a seat or a placement setting, or one that cannot be read as UTF-8 text (its read error then stops `run` and `seats` ) - a readable application `.env` is passed over, and the run says so - else the machine's own `~/.config/crafter-review/env` (`$XDG_CONFIG_HOME/crafter-review/env` : one file for every repository, a repository's seats `.env` winning over it). Its seat lines go over the process environment's; with no seat in either, this machine's own login. The placement settings among its lines - the seat order, the budgets, the concurrency, the utilisation ceilings - are read with it, under the process environment; any other `REVIEW_GATES_*` line (a path, a binary) is not, and `run` and `seats` name it. `crafter-review seats` probes and reports them. It never prints a token. An account whose 5-hour limit Anthropic has removed shows `no limit` in that column and is placed by its weekly window. Every account takes at most 128 passes at once, every driver's counted, and there is no machine-wide cap beyond that and free memory (`run --concurrency N` sets one).
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
- **Chromium, for a lens with the browser.** Run `crafter-review browser install` once. It installs the Chromium build the pinned `@playwright/mcp@0.0.83` drives into `~/.cache/ms-playwright` , where the MCP server looks. It runs Playwright's own installer, which also removes the browser builds no installed Playwright on the machine refers to.
- **A local Supabase stack, for a lens with the database** (`supabase start` , or the repository's own bring-up). The `sql` helper speaks the Postgres protocol itself: no `psql` and no driver to install. Every call is one transaction, rolled back.
- **gpg, if your commits are signed.** `freeze` reads each commit's signature through gnupg's keyboxd socket, which is all the sandbox is given. The agent, which signs, stays out of reach.
## Setting a repository up
From the repository's toplevel:
```bash
crafter-review init
crafter-review config # edit .review-gates/config.json until it prints no problem
```
`init` does four things:
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.
Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.
The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-01 01:31:40 +01:00
- it writes `.review-gates/config.json` from the template when there is none - the three artifact kinds as modes with every lens of their kind: trim each to what this repository runs;
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
- it adds `.review-gates/push/` and `.review-gates/trees/` to `.gitignore` ;
fix(init): the skill it writes ignores itself, as a generated directory
A repository that ignores .claude wholesale, as Crafter.Build does,
never sees in git the prepush skill init writes, and the question was
whether it should: it should not. The skill is generated
from the shipped text with init's marker and refreshed by every init, a
tracked copy lags the installed binary whenever the shipped skill
changes, and every refresh would be a diff in the consumer's own ranges.
Jorijn's ruling: every consumer leaves it untracked, one init per
checkout.
When init owns the skill - wrote or refreshed it - it writes a
.gitignore of `*` beside it, as Python's venv and pytest's cache do for
theirs: the directory ignores itself, that file included, on every
checkout that runs init, whatever the repository's own rules or one
machine's say, and nothing of the repository's is edited for it. A copy
init wrote that the repository tracks gets the step that untracks it, in
the output and in the README's migration. A skill the repository wrote
itself is kept and not ignored - the ignore file an earlier init wrote
beside it goes - and an ignore file init did not write is kept unless
--force; a link there, dangling or not, is refused like the skill's
other links unless --force, which replaces it (the old setup's exemption
- a link that resolves into the review-gates submodule at the toplevel,
not one that merely names it - is the skill directory's and SKILL.md's
alone), and nothing is written through it: the links go before anything
is written, and one init cannot remove refuses. The procedure's log
records the ruling.
2026-11-01 17:24:03 +01:00
- it writes the `/prepush` skill to `.claude/skills/prepush/SKILL.md` , which a later `init` refreshes (the line after its frontmatter marks it as init's), with a `.gitignore` of `*` beside it: the skill is generated from the shipped text, so it stays out of git and every checkout runs `init` once (a tracked copy would lag the installed binary); a skill the repository wrote itself, without that line, is left alone and not ignored, unless `--force` ;
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
- it prints the next steps.
fix(init): the skill it writes ignores itself, as a generated directory
A repository that ignores .claude wholesale, as Crafter.Build does,
never sees in git the prepush skill init writes, and the question was
whether it should: it should not. The skill is generated
from the shipped text with init's marker and refreshed by every init, a
tracked copy lags the installed binary whenever the shipped skill
changes, and every refresh would be a diff in the consumer's own ranges.
Jorijn's ruling: every consumer leaves it untracked, one init per
checkout.
When init owns the skill - wrote or refreshed it - it writes a
.gitignore of `*` beside it, as Python's venv and pytest's cache do for
theirs: the directory ignores itself, that file included, on every
checkout that runs init, whatever the repository's own rules or one
machine's say, and nothing of the repository's is edited for it. A copy
init wrote that the repository tracks gets the step that untracks it, in
the output and in the README's migration. A skill the repository wrote
itself is kept and not ignored - the ignore file an earlier init wrote
beside it goes - and an ignore file init did not write is kept unless
--force; a link there, dangling or not, is refused like the skill's
other links unless --force, which replaces it (the old setup's exemption
- a link that resolves into the review-gates submodule at the toplevel,
not one that merely names it - is the skill directory's and SKILL.md's
alone), and nothing is written through it: the links go before anything
is written, and one init cannot remove refuses. The procedure's log
records the ruling.
2026-11-01 17:24:03 +01:00
It writes nothing in a subdirectory, in your home directory or over a `SKILL.md` , a `.gitignore` beside it or a link there that it did not make (the old link into the review-gates submodule aside, which it removes); `--force` allows all but the first.
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
`.review-gates/config.json` is committed. It says what the pipeline must know about the repository:
- who may author its commits, and which branch a range is measured against;
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.
Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.
The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-01 01:31:40 +01:00
- its modes (`modes.<name>` ): each an artifact kind (`reply` , `series` , `commits` ; a kind is a mode of its own name), the lenses it runs (every lens is off until a mode lists it; they run in their kind's gate order - the table above for commits, the kernel modes' gates below for a series or a reply) and the effort it runs at; `run --mode` names one;
- the environment each lens gets (`none` , `browser` , `supabase` or `browser+supabase` );
feat(config): lens sections - a repository's own text appended to a shipped charter
A lens has one charter, the one this pipeline ships; what a repository
knows that is not general to the lens - its platform and layers, the
record it keeps its decisions in, what its users already have, the
calibration entries that are its own - goes in a file the config names
for the lens (lenses.<name>.section), and Charter appends it after one
blank line. The charter sha in every reuse key covers the combined text.
The config refuses a path outside the repository - absolute, or
climbing out with `..` - by the check slots.dir had, now one both
settings share; a file missing or one the run could not use (a dangling
link, unreadable, not UTF-8) is a problem too, checked through the
reader Charter uses (Prompt::ReadSection), so a section is never
dropped in silence, and a name that is no lens is the unknown key it
always was. Nothing lists a directory, so what lies beside the files is
no concern of the config's: a directory listed would have to tolerate or
refuse every foreign entry. The run logs which charters got one.
The better-solution charter gains a consumer's general entry
(2026-10-31: a mechanism reworked a second time in one submission is the
signal to derive the fix at every layer the data crosses). The procedure
names the section wherever it names the charter: among the config's keys,
in the lens inputs, in the reuse rule (an edit to either re-runs the lens)
and in the feedback loop, which routes a repository's own lesson to its
section and a general one to the shipped charter.
2026-11-01 01:30:22 +01:00
- each lens's own section (`lenses.<name>.section` ), a file in the repository appended to that lens's shipped charter, for the calibration entries and conventions that are this repository's rather than the lens's (the reuse keys carry the combined text's hash); a lesson general to the lens goes into the shipped charter;
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
- how its application is started and reached: one stack, or several side by side (`slots` );
- where its local database answers;
- which paths carry no runtime behaviour.
`crafter-review doc config` prints the template; `crafter-review doc prepush` prints the procedure, `process/prepush.md` .
### Moving a repository off the review-gates submodule
1. Re-freeze every review in flight with crafter-review. The Flatpak's git may be another version than the one that froze it, and a different git version gives a different hash. The old `run-line.txt` also points into the submodule.
2. `git submodule deinit -f review-gates && git rm review-gates`
3. `crafter-review init` . If `.claude/skills/prepush` is the old link into `review-gates/` , it removes the link, never writing through it, and makes a real directory with the skill.
fix(init): the skill it writes ignores itself, as a generated directory
A repository that ignores .claude wholesale, as Crafter.Build does,
never sees in git the prepush skill init writes, and the question was
whether it should: it should not. The skill is generated
from the shipped text with init's marker and refreshed by every init, a
tracked copy lags the installed binary whenever the shipped skill
changes, and every refresh would be a diff in the consumer's own ranges.
Jorijn's ruling: every consumer leaves it untracked, one init per
checkout.
When init owns the skill - wrote or refreshed it - it writes a
.gitignore of `*` beside it, as Python's venv and pytest's cache do for
theirs: the directory ignores itself, that file included, on every
checkout that runs init, whatever the repository's own rules or one
machine's say, and nothing of the repository's is edited for it. A copy
init wrote that the repository tracks gets the step that untracks it, in
the output and in the README's migration. A skill the repository wrote
itself is kept and not ignored - the ignore file an earlier init wrote
beside it goes - and an ignore file init did not write is kept unless
--force; a link there, dangling or not, is refused like the skill's
other links unless --force, which replaces it (the old setup's exemption
- a link that resolves into the review-gates submodule at the toplevel,
not one that merely names it - is the skill directory's and SKILL.md's
alone), and nothing is written through it: the links go before anything
is written, and one init cannot remove refuses. The procedure's log
records the ruling.
2026-11-01 17:24:03 +01:00
4. If a copy of the skill that an earlier `init` wrote is tracked, `git rm --cached .claude/skills/prepush/SKILL.md` (init says so when it sees one): the skill is generated, and the `.gitignore` init writes beside it keeps it out of git from then on.
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.
Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.
The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-01 01:31:40 +01:00
An existing `config.json` needs one change since 2026-11-01: `lenses.<name>.run` is retired, so list the lenses in `modes.commits.lenses` (`config` names what is missing, and `freeze` , `run` and `slots` refuse until it is there). The reuse keys keep working: a range reviewed before the move is reused, not bought again.
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
## Running a review
```bash
crafter-review freeze --slug <branch> # Gate 0: checks, export, hash, run line
# start the application / the stack from the checkout at HEAD, when a lens has the browser / the database
crafter-review run --artifact .review-gates/push/<branch>/ --hash <sha256> \
--base <sha> --trees . --dispositions .review-gates/push/<branch>/gate-state/dispositions.json --date <date>
```
`freeze` prints the exact run line and writes it to `run-line.txt` ; launch from there in a detached shell (`setsid nohup … &` ). The exit codes:
- 0: SEND-READY (push-ready);
- 1: FIX FIRST;
- 2: INCOMPLETE, PARTIAL or CONFIRMING (run the same line again);
- 3: preflight.
The dump with every finding, note and coverage entry is `gate-state/commits-<hash8>-run-<n>.json` .
feat(run): the repository under review is the freeze record's, not the working directory's
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.
2026-10-31 21:31:48 +01:00
### Reviewing another repository
feat(seats): the env file's settings count, and a machine's own seats file
ApiConfig and PoolConfig read the seat order, the budgets and the
concurrency from the process environment alone, so a `.env` that said
REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver
reads those lines from the file under the process environment
(config_env, 2026-10-17: a setting lives beside the seats it orders).
SeatPool::SettingsOf does the same - the file's placement settings
under the process environment, in a map handed to the placement readers
alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the
process environment untouched, so no other reader sees a line of the
file: a path's or a binary's line reaches nothing, and two drivers
reading one .env keep one board and one ledger. The placement settings
are one list, SeatPool::PlacementSettings - the fifteen names those
readers read - for the map, the run's log and what makes a working
directory's .env a seats file; a REVIEW_GATES_* line of any other name
stays out of the map and is named, by the run's log and on `seats`'s
stderr. One dotenv grammar serves the tokens and the settings
(Seats::DotenvLines).
`run` and `seats` resolve the file from their own --env-file
(SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else
the working directory's .env - the operator's, as the Python driver and
`seats` read it, not the repository under review's - when it is a file
holding a seat or a placement setting, each with a value, or one that
cannot be read as UTF-8 text (its error is then the command's) - a
readable application .env, its Vite or Supabase lines, one of blank
placeholder lines or one that is no file is passed over, and the run
says so - else the machine's own
$XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env:
one file for every repository on a machine, a repository's seats .env
winning over it. Each file is read once and the settings and the tokens
are taken from that read (SeatsFile::Text), so a file that can be read
only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives
its settings and its seats alike, and a failed read raises its own error
where the tokens are laid over the environment: SeatPool::WithFileTokens,
for `run` and `seats` alike, and LoadSeats takes the merged environment
and no longer reads a file by path. An empty `seats --env-file` means no file
now, as it did for `run`, and the `seats` report names the side that
runs first (it said "api seats run first" whatever the order).
Runs, Commands and KernelModes, the tests that run `run` and `seats`,
point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a
path of their own that nothing creates, so a global file stays out of
them; the grammar, the settings and their precedence, the lookup and the
one read have their tests, a run with `--env` (a prefix) and with a
repeated --env-file, and a `seats` with `--env`, take tokens and
settings from the one file, an application's .env and a venv are passed
over, a FIFO is read once, no line of the file reaches `stop`, every
listed placement setting moves a reader, another REVIEW_GATES_* line is
named and a .env of blank lines passed over, and the
README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
A directory can review repositories other than itself: a workbench whose config, `.env` and records serve several. From it, `crafter-review freeze --repo <path> --out <dir> --worktree` , then the printed run line. The run reads the repository under review from the artifact's `freeze.json` : the prompt names it, the calibration is kept under it, and `--trees` defaults to the recorded worktree. The config and the slots are the working directory's, the seats file is looked up from it (Seats, above), the lenses run in it, and `--context-paths` are relative to it.
feat(run): the repository under review is the freeze record's, not the working directory's
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.
2026-10-31 21:31:48 +01:00
feat(modes): the kernel modes - a patch series and a mailing-list reply
Beside the push gates, crafter-review runs the pre-send review of a kernel
contributor's workbench: --mode series over a format-patch export with its
cover letter, --mode reply over a mail draft. The inventory partitions a
cover letter, a plain mail and the camera review's shape; ten kernel lenses
join the table with their gates (structure, code, artifact truth) and the
persona gate - one maintainer-persona instance per --reviewers entry, its
whole pass gating its blocks and pruning the units that are not theirs. A
series' code units are keyed on the code like the push gates'; every text
unit on its text, the references it cites and the anchor when it is dated.
The references an artifact cites are prefetched through utilities/lore.py
into the gate-state, with a digest in the run layer, and a persona's
calibration map is its lore listing. The kernel schema carries the persona
vocabulary, the kernel note kinds, the predicted review and the not-mine
list; the matrix the persona holes, the thin and the modelled units; gates
clean without a persona pass is GATES CLEAN. A reply's gate-state sits
beside the draft and is read once; a kernel mode needs --subsystem,
defaults the effort to max and the trees to work/linux,work/milos-linux,
and excuses no unit from the holes.
The kernel lenses and the common text ship verbatim; the table lists them
after the push gates' lenses and the config template holds them off, so a
repository that never turns them on changes nothing. Prompts, keys and plans
were checked byte for byte against the pipeline these modes replace, on its
own artifacts: two replies and a series, every system prompt, user prompt,
job and reuse key identical.
2026-10-31 22:56:12 +01:00
### The kernel modes: a patch series, a mailing-list reply
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.
Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.
The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-01 01:31:40 +01:00
Beside the push gates, crafter-review runs the pre-send review a kernel contributor's workbench keeps: `--mode series` over a `git format-patch` export with its cover letter, and `--mode reply` over a mail draft (a reply's gate-state sits beside the draft). Both take `--subsystem <name>` , whose journal - `journal/<name>/ledger.md` , `captures/` , `captures/threads/` - is the lenses' evidence, and the trees `--trees` names (default `work/linux,work/milos-linux` ; history only in the full clone). lore.kernel.org is reached through `python3 utilities/lore.py` in the working directory, which also prefetches every reference the artifact cites into `gate-state/refs/` before the gates run. The series gates are structure (better-solution), code (code-quality, minimalism, bug-prover) and artifact truth (facts, consistency, process-docs, etiquette, conciseness); a reply runs an artifact truth of its own alone (facts, etiquette, consistency, conciseness). `--reviewers "A B,C D"` adds the persona gate after them: one instance of the maintainer-persona lens per reviewer, calibrated on their own lore mail, its whole pass gating its blocks and pruning the units that are not theirs (`--persona-blocks all` keeps them). Gates clean without a persona pass is GATES CLEAN, not SEND-READY. These lenses are off until the config turns them on, like every other; the effort defaults to max, and a reply's units are read once.
feat(modes): the kernel modes - a patch series and a mailing-list reply
Beside the push gates, crafter-review runs the pre-send review of a kernel
contributor's workbench: --mode series over a format-patch export with its
cover letter, --mode reply over a mail draft. The inventory partitions a
cover letter, a plain mail and the camera review's shape; ten kernel lenses
join the table with their gates (structure, code, artifact truth) and the
persona gate - one maintainer-persona instance per --reviewers entry, its
whole pass gating its blocks and pruning the units that are not theirs. A
series' code units are keyed on the code like the push gates'; every text
unit on its text, the references it cites and the anchor when it is dated.
The references an artifact cites are prefetched through utilities/lore.py
into the gate-state, with a digest in the run layer, and a persona's
calibration map is its lore listing. The kernel schema carries the persona
vocabulary, the kernel note kinds, the predicted review and the not-mine
list; the matrix the persona holes, the thin and the modelled units; gates
clean without a persona pass is GATES CLEAN. A reply's gate-state sits
beside the draft and is read once; a kernel mode needs --subsystem,
defaults the effort to max and the trees to work/linux,work/milos-linux,
and excuses no unit from the holes.
The kernel lenses and the common text ship verbatim; the table lists them
after the push gates' lenses and the config template holds them off, so a
repository that never turns them on changes nothing. Prompts, keys and plans
were checked byte for byte against the pipeline these modes replace, on its
own artifacts: two replies and a series, every system prompt, user prompt,
job and reuse key identical.
2026-10-31 22:56:12 +01:00
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
To stop a run, run `crafter-review stop <its scratch dir>` , or `crafter-review stop --hash <hash8>` for every live run of a hash. Its running passes are killed, no later gate starts and the dump is written. A run takes its passes with it however it ends, SIGKILL included: each pass runs under a supervisor that kills its whole tree, the MCP server and Chromium included. `flatpak kill net.catcrafts.CrafterReview` is the hard stop.
| Command | What it does |
|---|---|
| `freeze` | Gate 0: the checks, the export, the hash, the run line; `--verify` at push time |
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.
Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.
The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-01 01:31:40 +01:00
| `run` | the gates of the mode (`--mode` : `commits` , `series` , `reply` or one of the repository's own, as the config defines it): the lenses, the dump; `--dry-run` launches nothing |
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
| `slots` | how many stacks fit in free memory, and the commands for the missing ones |
| `config` | the merged config and its problems |
| `seats` | every seat probed: both windows, their resets, the drivers running |
| `init` , `doc` , `browser install` , `stop` | the setup, the shipped texts, the Chromium, a stop |
feat(modes): the kernel modes - a patch series and a mailing-list reply
Beside the push gates, crafter-review runs the pre-send review of a kernel
contributor's workbench: --mode series over a format-patch export with its
cover letter, --mode reply over a mail draft. The inventory partitions a
cover letter, a plain mail and the camera review's shape; ten kernel lenses
join the table with their gates (structure, code, artifact truth) and the
persona gate - one maintainer-persona instance per --reviewers entry, its
whole pass gating its blocks and pruning the units that are not theirs. A
series' code units are keyed on the code like the push gates'; every text
unit on its text, the references it cites and the anchor when it is dated.
The references an artifact cites are prefetched through utilities/lore.py
into the gate-state, with a digest in the run layer, and a persona's
calibration map is its lore listing. The kernel schema carries the persona
vocabulary, the kernel note kinds, the predicted review and the not-mine
list; the matrix the persona holes, the thin and the modelled units; gates
clean without a persona pass is GATES CLEAN. A reply's gate-state sits
beside the draft and is read once; a kernel mode needs --subsystem,
defaults the effort to max and the trees to work/linux,work/milos-linux,
and excuses no unit from the holes.
The kernel lenses and the common text ship verbatim; the table lists them
after the push gates' lenses and the config template holds them off, so a
repository that never turns them on changes nothing. Prompts, keys and plans
were checked byte for byte against the pipeline these modes replace, on its
own artifacts: two replies and a series, every system prompt, user prompt,
job and reuse key identical.
2026-10-31 22:56:12 +01:00
| `sql` , `totp` , `inventory` | the helpers a lens runs, and the partition of an export, a series or a reply |
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
## Inside the Flatpak
The sandbox gets the network, the whole host filesystem (`--filesystem=host` ), the host's `/tmp` and gnupg's keyboxd socket. `--filesystem=host` is a convenience for a developer tool, **not an isolation boundary ** : a lens's tool allowlist is what keeps a pass to its job, as it was outside the Flatpak.
The driver hands its children the host's XDG directories. git then reads `~/.config/git/config` , Playwright finds `~/.cache/ms-playwright` , and claude and npx behave as on the host. A pass's shell is bash. A few things do differ from a native run:
2026-11-01 02:36:07 +01:00
- a lens reading `/usr` sees the Sdk's; the host's is at `/run/host/usr` , which the commit-range lenses' common text (`_common-repo.md` ) tells them;
docs: the README
Install, what the machine provides, setting a repository up with init,
moving a repository off the review-gates submodule, running and stopping a
review, the commands, what the sandbox gets (--filesystem=host is a
convenience, not an isolation boundary), the board several drivers share,
building from source, the licence and nlohmann/json's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:18:42 +01:00
- git hooks run with the Sdk's tools;
- `FLATPAK_ID` reaches every child;
- a runtime update can change git between `freeze` and `--verify` , which the MISMATCH message then names.
**Several drivers on one machine.** Every driver registers its passes on one board, `~/.cache/review-gates/seat-board.json` , and holds a lifetime lock beside it while it runs. A driver in the Flatpak publishes the host pid of its sandbox, so a driver outside it sees it alive exactly as long as it runs and sweeps it the moment it is gone.
## Building from source
```bash
crafter-build --local test # the suite; --local takes ../Crafter.Network instead of the pinned commit
crafter-build --local lint
crafter-build run flatpak # the Flatpak on the stable Crafter.Build base (flatpak-builder or org.flatpak.Builder)
crafter-build run install # install that build as net.catcrafts.CrafterReview//local
```
`run flatpak --base=<branch>` builds on another branch of the base app, and `--deps=local` takes Crafter.Network and Crafter.Thread from the checkouts next to this one, for commits not yet on forgejo. `AGENTS.md` is the guide for changing the pipeline.
## License
crafter-review is licensed under the GNU Affero General Public License v3.0 only (`LICENSE` ). It vendors [nlohmann/json ](https://github.com/nlohmann/json ) 3.12.0 (`lib/json.hpp` , MIT License, © 2013-2025 Niels Lohmann).
## Copyright
Copyright (C) 2026 Catcrafts®