Crafter.Review/AGENTS.md
Jorijn van der Graaf ea4d362a10
feat(seats): a pass's share of the windows by the run's effort, the weekly cutoff at 99 %
The 5-hour share of a running pass was a flat 0.25 points; it follows the
run's effort now, as the weekly share already did: 0.5 at max and any other
effort, 0.125 at xhigh - the pooled means of 5438 Opus 5.5 passes on 141
seat-runs (2026-10-12..15), rounded up. The weekly cutoff moves from 95 %
to 99 %: the weekly window is the budget the lenses run out of, and with
the running passes projected against the cutoff the last percent is spent
a pass at a time, not in a burst. REVIEW_GATES_LENS_COST_PCT and
REVIEW_GATES_LENS_COST_7D_PCT still override every effort.
2026-10-31 21:13:55 +01:00

36 KiB
Raw Blame History

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; Gate 4 is the commit-truth lens, the messages and the documentation of the range (2026-10-31). Every lens is off until the consumer's config turns it on.
  • Each lens is one claude --print process 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. :Normalize derives 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 the sql helper (:PgWire); or browser+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::RunLock allows 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 test passes before and after; add a check for what you changed. It needs no network and no seat. Run test before lint: the four AST rules of Crafter.LintRules read the PCMs a test build leaves. format and lint are clean.
  • A prompt change is proven with a --dry-run on a frozen range before and after (tests/Goldens/fixture/commits, or a scratch repository frozen with freeze). 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 bumps PromptVersion (: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.md carry no header. A charter's sha256 is part of every reuse key and both texts are part of every prompt; --version prints 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 .env seats. 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 and config.
    • INPUTS prints the login fixture's path, never its content.
  • 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>.json and the label-to-filename encoding in out/. 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; popen and system are not used. A claude pass and a seat probe run under crafter-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/null and GIT_CONFIG_SYSTEM=/dev/null; the owner's global config signs commits. Tests work under their own temp dirs, with REVIEW_GATES_BOARD and --scratch pointing 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/sh script, 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) and tests/Text/fixture/vectors.json. The entries that follow from the lens table and the config defaults - the config cases through config, the commit-truth lens's plans, the not-reviewed lists, the estimates and the gates - are re-derived by python3 -I tests/Goldens/derive.py <checkout> <crafter-review binary>, which asserts that the recorded table still reproduces the recorded entries before it writes; review its diff.
  • The suite runs inside the Flatpak build too (crafter-build test is a build command of the manifest), where /.flatpak-info exists: a test pins Platform::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.json and 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 run and freeze;
  • 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> sql and <exe> totp <fixture>. <exe> is the running binary's absolute path, /app/bin/crafter-review in 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.CrafterReview in 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>)", and seats adds a line naming stop; natively it says "wait for it or kill it". The pid inside a sandbox ignores SIGTERM.

  • The dump's driver field and the first log line read crafter-review <version> (prompt v1). Nothing reads them.

  • freeze.json records gitVersion. When the hashes differ, freeze --verify's MISMATCH names a git version change as the likely cause (Freeze).

  • config reports an absolute functional.mcp.command that is no program here, such as /usr/bin/npx inside the Flatpak, whose /usr is the Sdk's. config then exits 1, and a run stops at preflight (Config).

  • JSON edge cases.

    • NaN and Infinity are refused when read: a claude stream, opencode or seat-probe line that holds one is skipped. They are written as null.
    • 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. ClientHTTP1::Fetch ignores *_proxy variables. The OpenRouter requests send User-Agent: crafter-review; app_ready sends review-gates. Reason phrases are the standard ones, not the server's.

  • Seat placement (SeatPool, Seats, Drivers):

    • An account whose 5-hour limit Anthropic has removed reports the weekly window alone. Its burst window (Seats::BurstWindow) is the weekly one: it is ranked and admitted by the weekly window with the passes in flight projected on it (0.04 points per pass at xhigh, 0.125 otherwise), under the 99 % cutoff.
    • A pass's projected share of the windows follows the run's effort: 0.5 / 0.125 points of the 5-hour window and 0.125 / 0.04 of the weekly one at max / xhigh (SeatPool::LensCostPct, LensCost7dPct), the pooled means of 5438 passes on 141 seat-runs (2026-10-12..15) rounded up. The weekly cutoff is 99 %: the running passes are projected against it, so the last percent is spent a pass at a time.
    • Every OAuth account and the local login takes at most 128 passes at once, every driver's counted.
    • There is no machine-wide width (2026-10-31, Jorijn: "there is no need to do a global cap anymore, the cap should be 128 per available account"). --concurrency caps only when given, and the dump's args.concurrency is then null. The board's driver settings read 128 per account, which every driver's seats shows as that driver's width (Scheduler, Runs).
    • A 'concurrents' rejection pauses the seat for 20 s or its retry-after, not until a window's reset.
    • Board entries and the folded foreign record carry cost7d, and so does the dump's seat summary.
    • The seats report and the log table say no limit in such an account's 5-hour column.
  • Board identity and liveness, process lifetime, stop: see "Inside the Flatpak".

  • Edge cases:

    • Usage errors of run, freeze, slots, config and the other commands exit 3, the preflight's code, and so does an INVARIANT failure of run and slots. The helpers exit 64.
    • --only and --units skip 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 run at 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.json is 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.json is written atomically, through a temporary file and a rename.
    • The seats API report says the ledger counts what "the review drivers" spent: every driver writes it.
    • In the Flatpak, a pass's SHELL is /bin/bash (Platform).
  • 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.

  • Every lens is off until the consumer's config turns it on (2026-10-31, Jorijn: "all lenses off by default and opt in only"). Config::Defaults() gives every row run: false; a config that turns on none - no lenses key included - has the one problem that names the lenses, so config, freeze and run all say so, the template ships every lens off, and init tells the consumer to turn lenses on (Config, Commands, ReviewFixes, the Goldens' config cases). A consumer config without a lenses key needs one.

  • The commit-truth lens (repo-facts.md, 2026-10-31): Gate 4, after the proofs, so the messages and documents are read once the code they describe has settled. Its block kinds are text, doc and slot: the message units keyed on their text, the base and the code up to their 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 in slot passes of its own (a lens with no whole pass asks the slots named in its row; never stored). A documentation file's diff header and a part the commit writes nothing in are mechanical n/a. Budgets 60/40 for a text or doc block, 50/35 for a slot; one read. The owner's gate is 5. The other lenses' prompts, keys and plans are untouched (CommitsMode, CodeKeys, Runs, the Goldens' series and gates).

Known quirks, kept:

  • the label-to-filename collision in out/;
  • code_norm strips 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}_HOME at ~/.var/app/<id>. The binary restores the host's (HOST_XDG_*, else the defaults under $HOME) for itself and every child, and a child's SHELL becomes /bin/bash where flatpak set /bin/sh (RestoreShell). CRAFTER_REVIEW_HOME is 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 what flatpak ps shows as child-pid, so a driver outside that checks the pid sees the sandbox alive exactly as long as it runs. Without bwrapinfo.json (as in flatpak build) a full run refuses to start. Dry runs and every other command never join the board.
  • Lifetime lock. Every full run holds LOCK_EX on <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 try LOCK_SH|LOCK_NB, and whoever finds it free unlinks it under the board lock; Board::View never 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 /proc is never consulted for other keys: its pids count up from 1 and can equal a live host pid.
  • Stop. A SIGTERM to the flatpak run pid kills only the outer bwrap, and the published pid ignores SIGTERM. So a watcher thread checks <scratch>/stop every 2 s, and crafter-review stop writes 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 setsid launch reaches the driver; flatpak kill is the hard stop.
  • gpg. Only xdg-run/gnupg/S.keyboxd is 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 __supervise is 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 run and freeze kill 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.5 at max and any other effort, 0.125 at xhigh; seats: 10 As MAX_UTIL. PoolConfig puts in the effort's figure 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_OAUTH_CONCURRENCY :SeatPool PoolConfig 128 As MAX_UTIL, truncated toward zero. Passes at once on one OAuth account or the local login, every driver's counted
REVIEW_GATES_LENS_COST_7D_PCT :SeatPool PoolConfig 0.04 at xhigh, 0.125 otherwise As MAX_UTIL. Percentage points of the weekly window a running pass is projected to add: the placement of an account without a 5-hour limit, and cost7d on the board
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 tag X.Y.Z publishes stable and creates the release. The tag must equal the metainfo's newest <release>.
  • The runner. It runs on the crafter-flatpak runner, 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 shared catcrafts.flatpakrepo.

Pins, moved together:

  • the Flatpak's base, net.catcrafts.CrafterBuild stable (Crafter.Build 3.0.0 today), whose crafter-build and LLVM compile the project, with the manifest's runtime-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=local builds 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.