The rule it lives by (prompts, reuse keys, shared files and exit codes pinned byte for byte by the goldens), the layout and the partitions, the rules for a change, the tests and their conventions, the behaviour the checks hold and the quirks kept, the sandbox and process lifetime, every environment variable the binary reads and how it is parsed, CI, the pins and the publish lock, and the licence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
33 KiB
Crafter.Review — developer guide
This file is for changing crafter-review itself. README.md says what it is and how a repository uses it; process/prepush.md is the procedure the /prepush skill drives, and process/lenses/ holds the lens texts.
What it is
crafter-review freeze exports base..HEAD with git format-patch into .review-gates/push/<slug>/ and hashes the export. It refuses on identity, trailer, dirty-tree, merge and fast-forward failures. crafter-review run partitions the export mechanically (:Inventory: every non-blank line in exactly one unit, fixed slots for what is absent), then runs the gates the config enables, in order.
- Each gate is a lens; Gate 3 has two, the bug-prover and the security lens.
- Each lens is one
claude --printprocess per unit (block passes). Where the lens has one, a whole pass gates the blocks and hands them its summary. - A pass returns structured output: a verdict, findings, notes, and one coverage entry per unit.
:Normalizederives the verdict from the findings and fails closed on a missing or blocking entry. - Clean results are stored per unit under a content key and reused across hashes (
:Plan). The static lenses need three clean reads of a unit before it is reused; the functional lens needs one. - Processes run on the seats of the consumer's
.env(:SeatPool). A lens's processes get the environment the config names:none;browser, a Playwright MCP server per pass (:Mcp,:Pool);supabase, the local stack's database through thesqlhelper (:PgWire); orbrowser+supabase. - A browser pass holds a stack of its own (
Pool::Stacks), and a pass launches only when it fits in free memory at its environment's measured cost (Pool::Admission,:Memory). - Drivers on one machine share one board,
~/.cache/review-gates/seat-board.json(SeatPool::Board).State::RunLockallows one full run per hash.
The rule it lives by
Anything that reaches a prompt, a reuse key, a shared file or an exit code is pinned byte for byte by the goldens (tests/Goldens) and the checks. A change to any of it is deliberate: it edits the goldens in the same commit and, where it reaches a prompt, comes with the dry-run proof below. Stored results outlive a release and every driver on the machine shares the files, so a byte that moves by accident is a bug.
Layout
project.cpp crafter.review-lib, crafter-review, the tests, Crafter.Network (pinned; --local
takes ../Crafter.Network), Crafter.LintRules (pinned pragma), scripts flatpak/install
lib/json.hpp nlohmann/json 3.12.0, vendored, never edited
interfaces/ Crafter.Review.cppm (the umbrella) and one partition per concern
implementations/ the partitions; main.cpp hands argv to Crafter::Review::Main
process/ prepush.md, lenses/*.md, templates/{config.json,manifest.md}, skill/prepush/SKILL.md
tests/<Name>/main.cpp one test each; tests/Common/FakePg.hpp is the Postgres stand-in two of them share
packaging/flatpak/ the manifest, the prune of the base, metainfo, flatpakref
.forgejo/workflows/ ci.yaml
The partitions, in import order:
| Partition | What it holds | Notes |
|---|---|---|
:Unicode, :Text, :Json |
Unicode 16.0.0's character tables; the text, number, URL and path semantics the rest relies on (Dyn::); JSON |
the tables are checked-in data; Json is nlohmann::ordered_json, so key order and unknown fields survive a rewrite |
:Crypto, :Platform, :Process, :Http |
hashes, HMAC, PBKDF2 and the encodings; files, locks and signals; child processes; HTTP | spawn, reaper, supervisor, signal and stop threads; flock and locked read-modify-write; the sandbox |
:Core |
the version, PromptVersion, the home, the invocation |
|
:Schema, :Tools, :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize |
the structured-output schema, the tool lists, planning and the reuse keys, the frozen artifact, the inventory, the disposition register, run state, normalisation and merge | :Tools is a leaf, so :Config and :Pool both read it without importing each other |
:PgWire, :Totp |
the Postgres client, the sql helper and its broker, the totp helper |
the client, the splitter, the refusals, the broker |
:Stream, :Prompt, :Config, :Memory, :Mcp |
claude's stream, the prompts, the consumer config, memory admission, the Playwright MCP config | a prompt literal changes only with a dry-run proof |
:Seats, :SeatPool, :OpenCode |
seat usage and its report, the seat pool and the board, the OpenCode harness | :Seats is the library; the report's glue is in :Driver |
:Pool, :Freeze, :Run, :Driver |
the pass pool, freeze, a run's gates, the commands | :Run holds Run, slots and app_ready; :Driver the command line |
Every seam a test needs is a member that starts as the real one: Run has appReady, probe, query, bypassesRls, migrationsAtHead, migrationsDrift, treeDrift, promptFor, runPass, resumable and instrumentPreflight. SlotsPlan takes a Machine. A regex on unbounded input is a hand-written scanner, never std::regex.
Rules for a change
crafter-build --local testpasses before and after; add a check for what you changed. It needs no network and no seat. Runtestbeforelint: the four AST rules of Crafter.LintRules read the PCMs a test build leaves.formatandlintare clean.- A prompt change is proven with a
--dry-runon a frozen range before and after (tests/Goldens/fixture/commits, or a scratch repository frozen withfreeze). The prompts under<scratch>/<hash8>-run-<n>-dry/prompts/and the system files must differ exactly where intended, and the goldens change with them. A text change a lens reads bumpsPromptVersion(:Core) only when earlier clean results must NOT carry over (2026-09-27: a layout change that left the text a lens reads the same did not). - The lens texts and
_common-repo.mdcarry no header. A charter's sha256 is part of every reuse key and both texts are part of every prompt;--versionprints the sha256s. Editing one is a prompt change. - A reuse-key change ships with an attack on its STORE side: "two units, one key, different verdicts" and "a FIX FIRST round, then a text-only re-freeze" (2026-10-06; the StoreRules and CodeKeys tests).
- A gate change ships with a firing proof: a scratch repository with one planted defect per gate, each lens run with
--only <lens> --units <unit>(PARTIAL by design) on the consumer's.envseats. It must give FIX FIRST on the planted range and SEND-READY on the corrected one. Record it in the lens's calibration section with the date, the units, the turns and the cost. It costs money: only with the owner's go. - The driver never runs a consumer's build, tests, stack or package manager; the skill does that, by hand, from the config's commands. The functional lens observes a running application it did not start.
- Secrets never reach a log, a dump or a prompt.
- Seat tokens go into a seat's own config dir, and API keys into that process's environment alone (
:SeatPool). - The database URL stays in the driver: a db pass gets the address of the driver's SQL broker and its stack's token (
PgWire::Broker,Runner::ProcessEnv). The URL is masked in the dump, the log andconfig. - INPUTS prints the login fixture's path, never its content.
- Seat tokens go into a seat's own config dir, and API keys into that process's environment alone (
- A shared file keeps its path, shape and locking. These are the board, the ledger, the memory record,
unit-state.json, the run lock, the scratch markers,progress.json,log.txt,resume-<h8>.jsonand the label-to-filename encoding inout/. Every driver on the machine reads and writes them, so a field another driver wrote survives a rewrite. - Every child process goes through
Process::Start;popenandsystemare not used. A claude pass and a seat probe run undercrafter-review __supervise, which ends the pass's whole tree.
Tests
One directory per test under tests/, each an ordinary main() linking the library: exit 0 passes, nonzero fails, 77 skips.
- the pipeline's pure parts: Stream, Normalize, Merge, Dispositions, Config, CommitsMode, CodeKeys, StoreRules, PgWire, StacksMemory, ReviewFixes and Freeze;
- the pool and the seats: Scheduler, SeatPool, Runner, Drivers, OpenCode and Seats;
- the foundations: Crypto, Text, Platform and Process;
- Goldens: the pure pipeline against the recorded answers in
tests/Goldens/fixture; - Runs: full runs end to end on a claude stand-in;
- Commands: init, doc, stop, seats, browser;
- LensTools: every allowlisted program on PATH, strict inside the Flatpak build.
Conventions:
- Git, temp dirs and timeouts. Every git call runs with
GIT_CONFIG_GLOBAL=/dev/nullandGIT_CONFIG_SYSTEM=/dev/null; the owner's global config signs commits. Tests work under their own temp dirs, withREVIEW_GATES_BOARDand--scratchpointing there, and each stays under crafter-build's 60 s. - Subprocesses. A test that needs one re-executes its own binary. The test binaries of Runs and Commands are crafter-review itself when given arguments, in native mode whatever builds them.
- Stand-ins. The claude stand-in is a
/bin/shscript, and the Postgres stand-in an in-process thread (tests/Common/FakePg.hpp). - Goldens.
tests/Goldens/fixture/holds the recorded answers the suite compares against: the inventory, the plans, the keys, the prompts, the normalised results, the stream, the config, the SQL helper's output. A deliberate change edits them in the same commit, by hand; so do:Unicode's tables (Unicode 16.0.0) andtests/Text/fixture/vectors.json. - The suite runs inside the Flatpak build too (
crafter-build testis a build command of the manifest), where/.flatpak-infoexists: a test pinsPlatform::SetMode(Native).
Behaviour the checks hold
Pinned byte for byte, by the goldens or a check:
- the inventory: its ids, lines, excerpts and
inventory-<h8>.json; - freeze's checks, messages,
freeze.jsonand the worktree layout. The artifact hash depends on the git version, because format-patch ends each patch with it; the Sdk's git is 2.56.0 today, the host's too; - the schema bytes;
- the block and whole reuse keys, which drop the git signature, so a re-freeze under another git keeps every key;
- the system files and user prompts, apart from the helper command strings, which are in no key;
- the SQL and TOTP helpers' output, refusals and exit codes, and the broker protocol;
- the stream classification, normalize and merge, the dump's keys and their order, the stdout summary, and the exit codes of
runandfreeze; - every shared file (Rules, above). The opencode meta line is one line with
", "and": "separators (DumpSpaced), because salvage matches on that prefix.
Worth knowing, each held by a test:
-
The helpers are subcommands. Prompts and the
Bash(…:*)allowlist name<exe> sqland<exe> totp <fixture>.<exe>is the running binary's absolute path,/app/bin/crafter-reviewin the Flatpak. These strings are in no reuse key. The stream's SQL-call detection matches the helper with any path before it (Stream, Runner). -
Printed command lines name the invocation:
flatpak run net.catcrafts.CrafterReviewin the Flatpak, the binary's absolute path natively (Freeze, StacksMemory, Goldens). -
Messages that point at a pid. In the sandbox, the run-lock message says "wait for it or stop it (
<invocation> stop <its scratch dir>)", andseatsadds a line namingstop; natively it says "wait for it or kill it". The pid inside a sandbox ignores SIGTERM. -
The dump's
driverfield and the first log line readcrafter-review <version> (prompt v1). Nothing reads them. -
freeze.jsonrecordsgitVersion. When the hashes differ,freeze --verify's MISMATCH names a git version change as the likely cause (Freeze). -
configreports an absolutefunctional.mcp.commandthat is no program here, such as/usr/bin/npxinside the Flatpak, whose/usris the Sdk's.configthen exits 1, and a run stops at preflight (Config). -
JSON edge cases.
NaNandInfinityare refused when read: a claude stream, opencode or seat-probe line that holds one is skipped. They are written asnull.- A lone surrogate is replaced.
- A float from 1e15 up to 1e16 prints with an exponent.
- An integer beyond 64 bits becomes a float.
No file the drivers write holds these in practice, and a file that fails to parse is treated as a damaged one.
-
HTTP.
FetchHTTP1ignores*_proxyvariables. The OpenRouter requests sendUser-Agent: crafter-review;app_readysendsreview-gates. Reason phrases are the standard ones, not the server's. -
Board identity and liveness, process lifetime, stop: see "Inside the Flatpak".
-
Edge cases:
- Usage errors of
run,freeze,slots,configand the other commands exit 3, the preflight's code, and so does an INVARIANT failure ofrunandslots. The helpers exit 64. --onlyand--unitsskip empty items, and one that names nothing is a usage error rather than no filter (Runs).- Salvage tries a hash's earlier scratch dirs newest first by run number.
- A dry run clears the prompts a dry run before it left in the directory they share (Runs).
- A run never takes the number of a dump on record, so it cannot write its dump over another run's (Runs).
- A stop or a SIGTERM between gates, or while a gate's pool is set up, stays in force: the next gate does not run (Runs).
- The stop file
<scratch>/stop, checked every 2 s and handled as SIGTERM; a claimed scratch dir drops a stale one (Runs). - A patch that is not UTF-8 stops
runat preflight, exit 3 (Runs). An exception no command catches ends it with its message and exit 1, never an abort (Commands). - The SQL helper's connect retry does not sleep after its last attempt, and TOTP reads the clock once per code.
- A broker call whose token is not a string gets NO DATABASE and the exit mark (PgWire).
- An API seat's
.claude.jsonis created whole or not at all, never over another writer's. - claude's stream-json is split on newlines only, after universal newlines:
\v,\f,\x1c–\x1e,\x85, U+2028 and U+2029 do not end a line inside one (Stream). progress.jsonis written atomically, through a temporary file and a rename.- The
seatsAPI report says the ledger counts what "the review drivers" spent: every driver writes it. - In the Flatpak, a pass's
SHELLis/bin/bash(Platform).
- Usage errors of
-
Commands beyond the gates:
init,doc,browser install,stop, and--version, which prints the home and the charters' sha256s (Commands). -
Order, not value:
estimate.cacheWrite's last digit depends on the order the passes ran, because it is a float sum.
Known quirks, kept:
- the label-to-filename collision in
out/; code_normstrips the patch tail only when the signature is present;- the two different rules for "has a body";
- resume ignores contests (this fails closed);
- a round is marked complete even on FIX FIRST;
- the SQL helper's "statements 4-4" wording and its edge case at exactly 40000 characters.
Inside the Flatpak
Platform::Init decides once whether the binary runs sandboxed: /.flatpak-info exists. Tests pin the mode, the identity's /proc root and the paths.
- Environment. Flatpak points
XDG_{CONFIG,CACHE,DATA,STATE}_HOMEat~/.var/app/<id>. The binary restores the host's (HOST_XDG_*, else the defaults under$HOME) for itself and every child, and a child'sSHELLbecomes/bin/bashwhere flatpak set/bin/sh(RestoreShell).CRAFTER_REVIEW_HOMEis honoured natively only, so a host setting cannot swap the Flatpak's lens texts for a checkout's. - Board identity. A full run publishes
<bwrapinfo child-pid>:<starttime of /proc/1>: the host pid of the sandbox's init and its host start time. That is whatflatpak psshows aschild-pid, so a driver outside that checks the pid sees the sandbox alive exactly as long as it runs. Withoutbwrapinfo.json(as inflatpak build) a full run refuses to start. Dry runs and every other command never join the board. - Lifetime lock. Every full run holds
LOCK_EXon<board dir>/drivers/<pid>-<start>.lock. It is created under a temporary name, locked, then renamed into place, so a prober can never lock a fresh file. Probers tryLOCK_SH|LOCK_NB, and whoever finds it free unlinks it under the board lock;Board::Viewnever does. - Liveness.
- A lock file exists: the lock decides.
- No lock file, natively: the pid and its start time decide.
- No lock file, in the sandbox: always alive. The sandbox's
/procis never consulted for other keys: its pids count up from 1 and can equal a live host pid.
- Stop. A SIGTERM to the
flatpak runpid kills only the outer bwrap, and the published pid ignores SIGTERM. So a watcher thread checks<scratch>/stopevery 2 s, andcrafter-review stopwrites it.- A stop is handled exactly like SIGTERM, and it stays in force across gates.
- Claiming a scratch dir drops a stale
stop. - A process-group SIGTERM to the
setsidlaunch reaches the driver;flatpak killis the hard stop.
- gpg. Only
xdg-run/gnupg/S.keyboxdis exposed, so%G?verifies against the public keys and the agent stays out of reach.
Process lifetime.
- Spawning. Every child is spawned from one long-lived spawner thread, because PR_SET_PDEATHSIG fires when the forking thread ends. The child gets PDEATHSIG, a
getppid()check, fds 3 and above closed, the signal mask cleared, and SIGPIPE and SIGXFSZ back at their defaults. - The driver. It ignores SIGPIPE, is a child subreaper, has one reaper thread owning
waitpid(-1), and handles signals on a thread of their own. - The supervisor.
crafter-review __superviseis single-threaded, a subreaper, and holds PDEATHSIG SIGTERM. It forks claude with PDEATHSIG SIGKILL, waits for it, and then kills and reaps everything left in its subtree: the MCP server and Chromium go with it, even though Playwright starts Chromium detached. On SIGTERM it SIGSTOPs its subtree, rescans until the set is stable, SIGKILLs it and exits 128+9, so a timed-out pass reports -9, a SIGKILLed child's code. That SIGTERM comes from a timeout, an abort, or the driver's death, SIGKILL included. - At exit
runandfreezekill their own remaining descendants, such as a gpg daemon, and never a process they did not start.
Environment variables
What the binary reads at run time, and how. A variable the driver only sets or strips for its children (CLAUDE_CONFIG_DIR, the bare ANTHROPIC_API_KEY, CLAUDE_CODE_ENTRYPOINT, MAX_THINKING_TOKENS, the opencode settings) is not read and not listed.
| Name | Read by | Default | Parsing rule | What it does |
|---|---|---|---|---|
CLAUDE_CODE_OAUTH_TOKEN1..6 (+ legacy CLAUDE_CODE_OAUTH_TOKEN) |
:Seats SeatTokensFromEnv, via :SeatPool LoadSeats (run) and seats |
none | The fixed names 1..6 only; TOKEN7 does nothing. Stripped; an empty value is no seat. The bare name is seat 1 when TOKEN1 is empty. The env file's lines override the process environment. Stripped from every child's environment. |
OAuth seats 1..6; a token reaches claude only as its seat's CLAUDE_CONFIG_DIR/.credentials.json |
ANTHROPIC_API_KEY1..6 |
:Seats ApiKeysFromEnv (run, seats) |
none | The fixed names 1..6, stripped; empty is no seat. The bare ANTHROPIC_API_KEY is never a seat (the env-file parser wants the digit too) and is stripped from every child. |
API-credit seats api1..api6, each key in its own process's ANTHROPIC_API_KEY only |
OPENROUTER_KEY, OPENROUTER_KEY1..6 |
:Seats OpenRouterKeysFromEnv, via LoadSeats (run; seats ignores them) |
none | As the OAuth tokens; the bare name is or1 when KEY1 is empty. Never in auto: --seats openrouter or orN ids; mixed with other seats it is a preflight error (exit 3). |
OpenRouter seats or1..or6 |
SEATS_ENV_FILE |
:Run Gate0 (run), seats |
run: <repo>/.env if it exists; seats: ./.env if it exists |
--env-file wins (in run even --env-file "", meaning no file); empty counts as unset. Only seat lines are taken ([export ]NAME=value, one pair of matching quotes stripped), overriding the process environment; no knob below is ever read from the file. A file that cannot be read, or is not UTF-8: run stops at preflight (exit 3), seats exits 1 with the read error. |
The dotenv file the seat secrets come from |
REVIEW_GATES_SEATS |
:Driver RunCommand → LoadSeats |
auto |
Only without --seats. Stripped, case-sensitive; unset or empty is auto (every api and oauth seat, else local); local, api, oauth, openrouter select that kind; anything else is a comma list of ids (apiN, N, orN, local; blank items skipped). An id not configured, or OpenRouter mixed with others, is a preflight error (exit 3). |
The seats a run places lens processes on |
REVIEW_GATES_SEAT_MAX_UTIL |
:Seats ConfigFromEnv (seats; run through :SeatPool PoolConfig) |
85 |
A finite number >= 0, in decimal or exponent notation; surrounding whitespace, _ between digits and Unicode digits are taken. Empty, garbage, negative, inf, nan keep the default; a whole value is stored as an int. Process environment only. |
Ceiling (%) of an OAuth seat's projected 5-hour utilisation |
REVIEW_GATES_LENS_COST_PCT |
:Seats ConfigFromEnv; :SeatPool PoolConfig |
run: 0.25; seats: 10 |
As MAX_UTIL. PoolConfig puts in 0.25 only when it is unset or blank, so a set but invalid value gives 10, in run too. | Percentage points of the 5-hour window a running process is projected to add (its cost on the board too) |
REVIEW_GATES_SEAT_POLICY |
:Seats ConfigFromEnv |
spread |
Exactly pack (case-sensitive, untrimmed) is pack; anything else spread. |
pack: the first eligible OAuth seat by id; spread: the lowest projected one |
REVIEW_GATES_SEAT_WAIT_MIN |
:Seats ConfigFromEnv |
15 |
As MAX_UTIL | Shown by seats --json as waitMaxMin; nothing else reads it |
REVIEW_GATES_SEAT_HANDOVER_PCT |
:Seats ConfigFromEnv |
90 |
As MAX_UTIL | Shown in the seats report and --json only |
REVIEW_GATES_SEAT_ORDER |
:SeatPool ApiConfig |
api-first |
Exactly oauth-first is oauth-first; anything else api-first. |
Whether the paid seats (API, OpenRouter) or the OAuth/local ones rank first |
REVIEW_GATES_API_BUDGET_USD |
:SeatPool ApiConfig (run, seats) |
200.0 |
MAX_UTIL's syntax, kept if >= 0 (inf means no cap); empty, garbage, negative, nan give the default. --api-budget overrides it in run. |
USD one API key may spend per cycle; at it the key is out |
REVIEW_GATES_API_LENS_COST_USD |
:SeatPool ApiConfig |
1.0 |
As API_BUDGET_USD | USD reserved per running process: a pass fits while spent + (running+1) × this + other drivers' cost ≤ budget |
REVIEW_GATES_API_CONCURRENCY |
:SeatPool ApiConfig |
8 |
As API_BUDGET_USD, truncated toward zero; 0 keeps every pass off the API seats; inf exits 1 with an OverflowError. |
Processes at once on one API seat, every driver's counted |
REVIEW_GATES_API_CYCLE_DAY |
:SeatPool ApiConfig |
1 |
As API_CONCURRENCY, then clamped to 1..28. | The day of the month the ledger's spend resets |
REVIEW_GATES_OPENROUTER_BUDGET_USD |
:SeatPool ApiConfig |
100.0 |
As API_BUDGET_USD | USD per cycle per OpenRouter key |
REVIEW_GATES_OPENROUTER_LENS_COST_USD |
:SeatPool ApiConfig |
0.1 |
As API_BUDGET_USD | Reserve per process on an OpenRouter seat |
REVIEW_GATES_OPENROUTER_CONCURRENCY |
:SeatPool ApiConfig |
16 |
As API_CONCURRENCY | Processes at once on one OpenRouter seat |
REVIEW_GATES_SCRATCH |
:Run ScratchRoot (run, slots); :SeatPool LedgerPath (run, seats) |
~/.cache/review-gates |
--scratch wins; empty counts as unset; used verbatim. |
Where the <hash8>-run-<n> dirs, memory.json and seat-ledger.json live; not the board |
REVIEW_GATES_BOARD |
:SeatPool BoardPath (run, seats, stop) |
~/.cache/review-gates/seat-board.json |
Non-empty: the board file, verbatim; its directory also holds drivers/, the lifetime locks. |
The one board per machine every driver shares; set it for tests and hand-made boards |
REVIEW_GATES_MEM_RESERVE_MB |
:Memory Reserve (run's admission, slots) |
max(2048, MemTotal / 10) MB |
Unset or empty: the default. Otherwise a number in MAX_UTIL's syntax, truncated toward zero; a negative value enlarges the budget. Garbage and nan exit 1 with a ValueError, inf with an OverflowError; a value past int64 saturates. |
MB of available memory never given to passes or slots |
CRAFTER_REVIEW_HOME |
:Core Home |
<exe>/../share/crafter-review (the Flatpak's /app/share/crafter-review), else the checkout when the exe runs from <checkout>/bin/crafter.review-exe-*/ |
Natively only; ignored in the Flatpak. Empty counts as unset. A set value without process/lenses is an error, with no fallback. |
Where process/ (the lenses, the templates, the skill) is read from |
REVIEW_GATES_CLAUDE_BIN |
:Pool Runner (lens processes, run's probes); :Seats FindClaudeBin (second) |
FindClaudeBin's search | Non-empty: used verbatim, unchecked. In run: --claude-bin, then this, then FindClaudeBin. |
The claude binary |
CLAUDE_BIN |
:Seats FindClaudeBin (seats, run's last fallback) |
after it: claude on PATH, the editor-extension bundles (~/.vscode-oss, .vscode, .vscode-insiders, .cursor; newest first), ~/.claude/local/claude, ~/.local/bin/claude, then the bare claude |
Non-empty: verbatim. FindClaudeBin checks it before REVIEW_GATES_CLAUDE_BIN, so it wins in seats and loses in run. |
The claude binary |
REVIEW_GATES_OPENCODE_BIN |
:OpenCode DefaultBin (--harness opencode) |
opencode on PATH |
--opencode-bin wins; empty counts as unset. |
The opencode binary |
REVIEW_GATES_DB |
:PgWire SqlMain → CallBroker (crafter-review sql, in a pass) |
none: NO DATABASE | <host>:<port>#<token>, split at the last #, then at the last :. Unset, empty or no #: NO DATABASE, exit 2; a port that is no integer: NO DATABASE with the parse error, exit 2; a port outside 0..65535: exit 1 with an OverflowError. The driver sets it per pass ("" without a database) and strips an inherited one. |
The driver's SQL broker and this pass's database token |
REVIEW_GATES_STRUCTURED_OUT |
the opencode tool :OpenCode ToolSource generates (reads process.env) |
none | A file path; unset or empty: the tool writes nothing and still reports success. Set per process by :OpenCode ChildEnv. |
Where an opencode pass's structured result goes |
REVIEW_GATES_SIMULATE_FAIL |
:Pool Runner (--dry-run only) |
none | Split on commas, items untrimmed, empty ones dropped: a unit id fails that unit's block pass, whole every whole pass. Ignored without --dry-run. |
A test hook: "simulated failure (REVIEW_GATES_SIMULATE_FAIL)" |
PATH |
:Process spawn and the supervisor; :Seats FindClaudeBin, ProbeSeat |
unset: /usr/local/bin:/usr/bin:/bin (spawn), /usr/bin:/bin (supervisor); the Flatpak sets /app/bin:/usr/bin |
Colon-separated; an empty entry is the cwd; a name with / is used as is. |
Where child programs (claude, git, npx …) are found |
HOME |
:Platform Home (every ~ path); :Seats ProbeEnv |
unset or empty: the password database's, else / |
Verbatim. | ~/.cache/review-gates, the XDG defaults, claude's install locations, ~/.cache/ms-playwright, init's home-directory check |
TZ |
:Seats LocalZone (the seats report); the C library behind :Platform LocalTime |
the zone /etc/localtime points to, else the C library's %Z |
A leading : stripped. UTC, or a name with a file under /usr/share/zoneinfo, is the zone (the C library converts with TZ set to it: libc++ has no tzdb); any other name only labels the report. |
The zone of the seats report and of every local timestamp (the default --date, the ledger's cycle) |
TMPDIR |
libc++'s temp_directory_path, from :Seats ProbeSeat and :Freeze |
/tmp |
The library's lookup: TMPDIR, TMP, TEMP, TEMPDIR. | Where the probes' config dirs (rg-seat-*, removed after) and freeze's temp dirs go |
HOST_XDG_{CONFIG,CACHE,DATA,STATE}_HOME |
:Platform RestoreHostXdg (Init, the sandbox only) |
$HOME/.config, .cache, .local/share, .local/state |
Non-empty: copied verbatim into XDG_<KIND>_HOME, which is always overwritten. Never read natively. |
The host's XDG dirs for git, claude, npx and Playwright, instead of ~/.var/app/… |
SHELL |
:Platform RestoreShell (Init, the sandbox only) |
— | Kept when it ends in /bash or /zsh and is executable; else /bin/bash when that is executable. Never read natively. |
The shell claude's Bash tool runs commands with |
XDG_RUNTIME_DIR |
:Platform Paths (at Init, the sandbox only) |
/run/user/<uid> |
Empty counts as unset; verbatim. | Where .flatpak/<instance>/bwrapinfo.json gives the driver its host-pid identity |
XDG_CACHE_HOME |
:Driver PlaywrightBrowsers (browser install) |
~/.cache |
Empty counts as unset; in the Flatpak it already holds the host's. | The parent of ms-playwright when PLAYWRIGHT_BROWSERS_PATH is unset |
PLAYWRIGHT_BROWSERS_PATH |
:Driver PlaywrightBrowsers (browser install) |
$XDG_CACHE_HOME/ms-playwright, else ~/.cache/ms-playwright |
Non-empty and not 0: verbatim; 0 or empty: the default. |
Where browser install checks that chromium-<rev> landed |
At build time:
| Name | Read by | Default | Parsing rule | What it does |
|---|---|---|---|---|
CRAFTER_REVIEW_VERSION |
project.cpp Version(), when crafter-build configures |
git describe --tags --always --dirty --exclude=latest, else 0.0.0-dev |
Non-empty: verbatim. It becomes the library's CRAFTER_REVIEW_VERSION define, so each value is a library variant of its own. |
The version --version and the driver line print; the manifest sets 0.0.0-dev, CI the tag's |
CRAFTER_BUILD_MARCH, CRAFTER_BUILD_MTUNE |
Crafter.Build's ApplyStandardArgs | native |
Non-empty: -march / -mtune; a --march= / --mtune= argument wins. |
The manifest sets x86-64-v2 and generic, so the shipped binary runs on any x86_64 of the last decade |
In the tests only:
| Name | Read by | Default | Parsing rule | What it does |
|---|---|---|---|---|
PATH |
LensTools | "" |
Colon-separated | Every program a lens's allowlist names, bash, node and npx must be on it: a skip natively, a FAIL inside the Flatpak |
FAKE_* |
the claude stand-ins of Runs and Scheduler | set by the test | — | Plumbing between a test and its stand-in |
Runs and Commands drop every REVIEW_GATES_*, CRAFTER_REVIEW_*, SEATS_ENV_FILE, seat secret and FAKE_* (Commands also CLAUDE_BIN) from the environment they run the binary with, then set their own CRAFTER_REVIEW_HOME and REVIEW_GATES_BOARD (Commands also REVIEW_GATES_SCRATCH and TZ=UTC): a developer's own settings never reach them.
CI and publishing
.forgejo/workflows/ci.yaml is Crafter.Build's workflow for this app.
- Builds. A pull request builds the Flatpak, runs the suite inside it and smoke-tests the app as it ships: the version and home, every library resolved and no RUNPATH outside
/app, the bundled tools, and a freeze and a dry run on a scratch repository. - Publishing. A push to master also publishes
beta, and a tagX.Y.Zpublishesstableand creates the release. The tag must equal the metainfo's newest<release>. - The runner. It runs on the
crafter-flatpakrunner, registered to Crafter.Build alone today: until this repository has such a runner, the workflow does not run, and it is unverified on the forge. Its smoke test passes when run by hand against a local build. - The shared repo. Both apps publish into one repo. The publish step holds
/srv/flatpak/.publish.lock, which protects anything only once Crafter.Build's workflow takes the same lock. This one writes its own flatpakref and never the sharedcatcrafts.flatpakrepo.
Pins, moved together:
- the Flatpak's base,
net.catcrafts.CrafterBuildstable(Crafter.Build 3.0.0 today), whose crafter-build and LLVM compile the project, with the manifest'sruntime-version; - the Crafter.LintRules pragma in
project.cpp, a commit that builds with the base's crafter-build; - the Crafter.Network commit in
project.cpp. While Crafter.Network commits are not on forgejo,crafter-build --local run flatpak --deps=localbuilds against the checkouts next to this one.
run flatpak and run install mirror Crafter.Build's scripts. --base=<branch> and --deps=local write a gitignored copy of the manifest, and flatpak-builder keys its cache on the manifest's path. The cleanup is a keep-list prune (packaging/flatpak/prune-base.sh), because top-level cleanup does not reach the base's files.
Licence
crafter-review is AGPL-3.0-only, Copyright (C) 2026 Catcrafts®. nlohmann/json is MIT, vendored unmodified.