feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The commands around a run, this binary as crafter-review: init (writes,
// refreshes, keeps what it did not write, replaces the old link into the
// submodule, refuses a foreign link, a subdirectory and the home directory
// before it writes anything), doc, stop (a scratch dir, --hash on the
// board), seats on a claude stand-in, and browser's usage.
# include <unistd.h>
import std ;
import Crafter . Review ;
namespace fs = std : : filesystem ;
using namespace Crafter : : Review ;
namespace {
std : : int32_t Failures = 0 ;
void Check ( std : : string_view name , bool cond , std : : string_view detail = { } ) {
std : : println ( " {}{}{} " , cond ? " ok " : " FAIL " , name , ! cond & & ! detail . empty ( ) ? std : : format ( " ({}) " , detail ) : std : : string ( ) ) ;
if ( ! cond ) + + Failures ;
}
void Write ( const fs : : path & path , std : : string_view text ) {
fs : : create_directories ( path . parent_path ( ) ) ;
std : : ofstream ( path , std : : ios : : binary ) < < text ;
}
std : : string Read ( const fs : : path & path ) {
return Platform : : ReadFile ( path ) . value_or ( " " ) ;
}
// A claude that answers a seat probe as the CLI does: a rate_limit_event
// with both windows, and on a Fable request the Fable window too.
constexpr std : : string_view FakeClaude = R " sh(#!/bin/sh
model =
prev =
for a in " $@ " ; do [ " $prev " = - - model ] & & model = $ a ; prev = $ a ; done
cat > / dev / null
now = $ ( date + % s )
fable =
[ " $model " = claude - fable - 5 - 1 ] & & fable = " , \" seven_day_overage_included \" :{ \" utilization \" :0.3, \" resetsAt \" :$((now+172800))} "
echo ' { " type " : " system " , " subtype " : " init " , " session_id " : " 0b6f3c56-1111-4222-8333-944455556666 " } '
echo " { \" type \" : \" rate_limit_event \" , \" rate_limit_info \" :{ \" status \" : \" allowed \" , \" rateLimitType \" : \" five_hour \" , \" resetsAt \" :$((now+3600)), \" unifiedWindows \" :{ \" five_hour \" :{ \" utilization \" :0.42, \" resetsAt \" :$((now+3600))}, \" seven_day \" :{ \" utilization \" :0.13, \" resetsAt \" :$((now+259200))}$fable}}} "
echo ' { " type " : " result " , " subtype " : " success " , " is_error " : false , " result " : " ok " , " total_cost_usd " : 0.0001 } '
) sh " ;
// The environment of every child: git with no user config, this
// checkout as the home, nothing of the shell's review or seat setup.
std : : map < std : : string , std : : string > Env ;
std : : map < std : : string , std : : string > With ( std : : map < std : : string , std : : string > env , const std : : map < std : : string , std : : string > & changes ) {
for ( const auto & [ k , v ] : changes ) env [ k ] = v ;
return env ;
}
Process : : Captured Self ( std : : vector < std : : string > args , const fs : : path & cwd , const std : : map < std : : string , std : : string > & env = { } ) {
args . insert ( args . begin ( ) , Platform : : SelfExe ( ) . string ( ) ) ;
return Process : : Run ( args , cwd , std : : chrono : : seconds ( 60 ) , { } , With ( Env , env ) ) ;
}
std : : int32_t Checks ( ) {
std : : println ( " commands: init, doc, stop, seats on a claude stand-in, browser's usage " ) ;
Platform : : SetMode ( Platform : : Mode : : Native ) ;
const fs : : path checkout = fs : : current_path ( ) ;
SetHome ( checkout ) ;
const fs : : path base = fs : : temp_directory_path ( ) / std : : format ( " crafter-review-commands-{} " , Platform : : PublishedPid ( ) ) ;
fs : : remove_all ( base ) ;
for ( const auto & [ k , v ] : Platform : : Environment ( ) ) {
const bool review = k . starts_with ( " REVIEW_GATES_ " ) | | k . starts_with ( " CRAFTER_REVIEW_ " ) | | k = = " SEATS_ENV_FILE " | | k = = " CLAUDE_BIN " ;
const bool seat = k . starts_with ( " CLAUDE_CODE_OAUTH_TOKEN " ) | | k . starts_with ( " ANTHROPIC_API_KEY " ) | | k . starts_with ( " OPENROUTER_KEY " ) ;
if ( ! review & & ! seat ) Env [ k ] = v ;
}
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
// No seats file of the machine's own: XDG_CONFIG_HOME natively, and
// HOST_XDG_CONFIG_HOME, which the Flatpak's Platform::Init copies
// over it - a directory of this run's own, which nothing creates.
const std : : string noConfig = ( base / " no-config " ) . string ( ) ;
Env = With ( Env , { { " XDG_CONFIG_HOME " , noConfig } , { " HOST_XDG_CONFIG_HOME " , noConfig } , { " GIT_CONFIG_GLOBAL " , " /dev/null " } , { " GIT_CONFIG_SYSTEM " , " /dev/null " } , { " CRAFTER_REVIEW_HOME " , checkout . string ( ) } ,
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
{ " REVIEW_GATES_BOARD " , ( base / " board.json " ) . string ( ) } , { " REVIEW_GATES_SCRATCH " , ( base / " scratch " ) . string ( ) } , { " TZ " , " UTC " } } ) ;
auto repo = [ & ] ( std : : string_view name ) {
const fs : : path dir = base / name ;
fs : : create_directories ( dir ) ;
Process : : Run ( { " git " , " init " , " -q " , " -b " , " main " , dir . string ( ) } , std : : nullopt , std : : chrono : : seconds ( 60 ) , { } , Env ) ;
return dir ;
} ;
const std : : string shipped = Read ( checkout / " process " / " skill " / " prepush " / " SKILL.md " ) ;
const std : : string marker = " <!-- written by crafter-review init " ;
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
const std : : string ignoreMarker = " # written by crafter-review init " ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
// init on a repository that has nothing yet, then again.
const fs : : path r = repo ( " r " ) ;
const Process : : Captured first = Self ( { " init " } , r ) ;
const std : : string skill = Read ( r / " .claude " / " skills " / " prepush " / " SKILL.md " ) ;
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
const std : : string skillIgnore = Read ( r / " .claude " / " skills " / " prepush " / " .gitignore " ) ;
const Process : : Captured firstStatus = Process : : Run ( { " git " , " -C " , r . string ( ) , " status " , " --porcelain " , " --untracked-files=all " } , std : : nullopt , std : : chrono : : seconds ( 60 ) , { } , Env ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
const std : : size_t frontmatter = shipped . find ( " \n --- \n " , 3 ) + 5 ;
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
Check ( " init: the config from the template (the three kinds as modes), the artifact and worktree dirs ignored, the skill with init's marker after its frontmatter and a .gitignore of `*` beside it - git sees nothing of the skill, by no rule of the repository's - the next steps (trim each mode's lenses; the browser line, since the template lists the functional lens) " ,
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
first . returnCode = = 0 & & Read ( r / " .review-gates " / " config.json " ) = = Read ( checkout / " process " / " templates " / " config.json " )
& & Read ( r / " .gitignore " ) = = " .review-gates/push/ \n .review-gates/trees/ \n " & & skill . starts_with ( shipped . substr ( 0 , frontmatter ) )
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
& & skill . substr ( frontmatter ) . starts_with ( marker ) & & skill . ends_with ( shipped . substr ( frontmatter ) ) & & skillIgnore . starts_with ( ignoreMarker ) & & skillIgnore . ends_with ( " \n * \n " )
& & firstStatus . returnCode = = 0 & & firstStatus . out . contains ( " .review-gates/config.json " ) & & ! firstStatus . out . contains ( " .claude " )
& & first . out . contains ( " wrote .claude/skills/prepush/.gitignore " ) & & first . out . contains ( " config - until it reports no problem " )
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
& & first . out . contains ( " trim each to what this repository runs " ) & & first . out . contains ( " browser install - " ) ,
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
first . out + first . err + firstStatus . out ) ;
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
// A kept config has no trim step (that line is the template's own,
// printed as init writes it); one that lists no lens is told to list
// them.
Write ( r / " .review-gates " / " config.json " , R " ({ " modes " : { " commits " : { " lenses " : [ " repo - facts " ]}}}) " ) ;
const Process : : Captured listed = Self ( { " init " } , r ) ;
Write ( r / " .review-gates " / " config.json " , R " ({ " base " : " origin / dev " }) " ) ;
const Process : : Captured unlisted = Self ( { " init " } , r ) ;
Check ( " init on a kept config: one whose modes list lenses gets no lens line; one that lists none is told to list them " ,
listed . returnCode = = 0 & & listed . out . contains ( " kept .review-gates/config.json " ) & & ! listed . out . contains ( " trim " ) & & ! listed . out . contains ( " list the lenses " )
& & unlisted . returnCode = = 0 & & unlisted . out . contains ( " list the lenses each mode runs in .review-gates/config.json (modes.<mode>.lenses) - every lens is off until a mode lists it " ) ,
listed . out + unlisted . out ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
Write ( r / " .review-gates " / " config.json " , R " ({ " base " : " origin / dev " }) " ) ;
Write ( r / " .gitignore " , " node_modules/ \n /.review-gates/push \n " ) ;
Write ( r / " .claude " / " skills " / " prepush " / " SKILL.md " , std : : format ( " --- \n name: prepush \n --- \n {} 0.0.1: old --> \n old text \n " , marker ) ) ;
const Process : : Captured again = Self ( { " init " } , r ) ;
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
Check ( " init again: the config and an entry already ignored are kept, the missing one added once, the skill it wrote refreshed, and the .gitignore beside it " ,
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
again . returnCode = = 0 & & Read ( r / " .review-gates " / " config.json " ) = = R " ({ " base " : " origin / dev " }) "
& & Read ( r / " .gitignore " ) = = " node_modules/ \n /.review-gates/push \n .review-gates/trees/ \n " & & Read ( r / " .claude " / " skills " / " prepush " / " SKILL.md " ) = = 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
& & again . out . contains ( " refreshed .claude/skills/prepush/SKILL.md " ) & & again . out . contains ( " refreshed .claude/skills/prepush/.gitignore " )
& & Read ( r / " .claude " / " skills " / " prepush " / " .gitignore " ) = = skillIgnore ,
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
again . out + again . err ) ;
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
// The repository's own .gitignore is never edited for the skill: one
// that ignores .claude wholesale (Crafter.Build) is kept as it is.
Write ( r / " .gitignore " , " node_modules/ \n /.review-gates/push \n .review-gates/trees/ \n .claude \n " ) ;
const Process : : Captured wholesale = Self ( { " init " } , r ) ;
Check ( " init: a repository that ignores .claude wholesale keeps its .gitignore as it is, and the skill its own .gitignore " ,
wholesale . returnCode = = 0 & & Read ( r / " .gitignore " ) = = " node_modules/ \n /.review-gates/push \n .review-gates/trees/ \n .claude \n " & & wholesale . out . contains ( " kept .gitignore " )
& & Read ( r / " .claude " / " skills " / " prepush " / " .gitignore " ) = = skillIgnore ,
wholesale . out + wholesale . err ) ;
// A copy init wrote that the repository tracks (a base-era init
// ignored nothing): the note says how to untrack it, on every init.
// A hand-written tracked skill is kept and not ignored - the ignore
// file the earlier init wrote beside it goes - with no note.
const fs : : path tracked = repo ( " tracked " ) ;
Self ( { " init " } , tracked ) ;
Process : : Run ( { " git " , " -C " , tracked . string ( ) , " add " , " -f " , " .claude/skills/prepush/SKILL.md " , " .gitignore " } , std : : nullopt , std : : chrono : : seconds ( 60 ) , { } , Env ) ;
Process : : Run ( { " git " , " -C " , tracked . string ( ) , " -c " , " user.email=me@example.com " , " -c " , " user.name=Me " , " commit " , " -q " , " -m " , " track the skill " } , std : : nullopt , std : : chrono : : seconds ( 60 ) , { } , Env ) ;
const Process : : Captured trackedInit = Self ( { " init " } , tracked ) ;
const Process : : Captured trackedAgain = Self ( { " init " } , tracked ) ;
Write ( tracked / " .claude " / " skills " / " prepush " / " SKILL.md " , " my own skill \n " ) ;
Process : : Run ( { " git " , " -C " , tracked . string ( ) , " -c " , " user.email=me@example.com " , " -c " , " user.name=Me " , " commit " , " -q " , " -am " , " my own skill " } , std : : nullopt , std : : chrono : : seconds ( 60 ) , { } , Env ) ;
const Process : : Captured handWritten = Self ( { " init " } , tracked ) ;
const bool stillIgnored =
Process : : Run ( { " git " , " -C " , tracked . string ( ) , " check-ignore " , " --no-index " , " -q " , " .claude/skills/prepush/SKILL.md " } , std : : nullopt , std : : chrono : : seconds ( 60 ) , { } , Env ) . returnCode = = 0 ;
Check ( " init on a tracked skill: one init wrote gets the note with `git rm --cached` on every init; one the repository wrote is kept and no rule ignores it any more - the earlier init's .gitignore beside it removed - and no note " ,
trackedInit . returnCode = = 0 & & trackedInit . out . contains ( " git rm --cached .claude/skills/prepush/SKILL.md " ) & & trackedAgain . returnCode = = 0 & & trackedAgain . out . contains ( " git rm --cached " )
& & handWritten . returnCode = = 0 & & handWritten . out . contains ( " kept .claude/skills/prepush/SKILL.md " ) & & handWritten . out . contains ( " removed .claude/skills/prepush/.gitignore " )
& & ! fs : : exists ( tracked / " .claude " / " skills " / " prepush " / " .gitignore " ) & & ! stillIgnored & & ! handWritten . out . contains ( " git rm --cached " ) ,
std : : format ( " exits {} {} {}, ignored {}; {}{}{}{} " , trackedInit . returnCode , trackedAgain . returnCode , handWritten . returnCode , stillIgnored , trackedInit . out , trackedAgain . out , handWritten . out ,
handWritten . err ) ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
Write ( r / " .claude " / " skills " / " prepush " / " SKILL.md " , " my own skill \n " ) ;
const Process : : Captured kept = Self ( { " init " } , r ) ;
const std : : string mine = Read ( r / " .claude " / " skills " / " prepush " / " SKILL.md " ) ;
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
const bool keptIgnoreFile = fs : : exists ( r / " .claude " / " skills " / " prepush " / " .gitignore " ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
const Process : : Captured forced = Self ( { " init " , " --force " } , r ) ;
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
Check ( " init: a SKILL.md init did not write is kept, and the .gitignore init wrote beside it goes; --force replaces the skill and writes the .gitignore again " ,
kept . returnCode = = 0 & & mine = = " my own skill \n " & & kept . out . contains ( " --force replaces it " ) & & ! keptIgnoreFile & & forced . returnCode = = 0
& & Read ( r / " .claude " / " skills " / " prepush " / " SKILL.md " ) = = skill & & Read ( r / " .claude " / " skills " / " prepush " / " .gitignore " ) = = skillIgnore ,
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
kept . out + forced . out ) ;
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
// A .gitignore beside the skill that init did not write is kept;
// --force replaces it.
Write ( r / " .claude " / " skills " / " prepush " / " .gitignore " , " my own rules \n " ) ;
const Process : : Captured foreignIgnore = Self ( { " init " } , r ) ;
const std : : string ownRules = Read ( r / " .claude " / " skills " / " prepush " / " .gitignore " ) ;
const Process : : Captured forcedIgnore = Self ( { " init " , " --force " } , r ) ;
Check ( " init: a .gitignore beside the skill that init did not write is kept, saying so; --force replaces it " ,
foreignIgnore . returnCode = = 0 & & ownRules = = " my own rules \n " & & foreignIgnore . out . contains ( " kept .claude/skills/prepush/.gitignore (init did not write it: --force replaces it) " )
& & forcedIgnore . returnCode = = 0 & & Read ( r / " .claude " / " skills " / " prepush " / " .gitignore " ) = = skillIgnore ,
foreignIgnore . out + forcedIgnore . out ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
// The old setup: the skill dir a link into the submodule.
const fs : : path old = repo ( " old " ) ;
Write ( old / " review-gates " / " process " / " skill " / " prepush " / " SKILL.md " , " the submodule's file \n " ) ;
fs : : create_directories ( old / " .claude " / " skills " ) ;
fs : : create_directory_symlink ( " ../../review-gates/process/skill/prepush " , old / " .claude " / " skills " / " prepush " ) ;
const Process : : Captured relinked = Self ( { " init " } , old ) ;
std : : error_code error ;
Check ( " init: the old link into the submodule goes - a real directory with the skill in its place, the submodule's file untouched " ,
relinked . returnCode = = 0 & & ! fs : : is_symlink ( fs : : symlink_status ( old / " .claude " / " skills " / " prepush " , error ) )
& & Read ( old / " .claude " / " skills " / " prepush " / " SKILL.md " ) = = skill & & Read ( old / " review-gates " / " process " / " skill " / " prepush " / " SKILL.md " ) = = " the submodule's file \n "
& & relinked . out . contains ( " removed the link .claude/skills/prepush -> ../../review-gates/process/skill/prepush " ) ,
relinked . out + relinked . err ) ;
// What init refuses, before it writes anything.
const fs : : path foreign = repo ( " foreign " ) ;
fs : : create_directories ( base / " elsewhere " ) ;
fs : : create_directories ( foreign / " .claude " / " skills " ) ;
fs : : create_directory_symlink ( " ../../../elsewhere " , foreign / " .claude " / " skills " / " prepush " ) ;
const Process : : Captured refused = Self ( { " init " } , foreign ) ;
fs : : create_directories ( r / " sub " ) ;
const Process : : Captured sub = Self ( { " init " } , r / " sub " ) ;
const fs : : path home = repo ( " home " ) ;
const Process : : Captured atHome = Self ( { " init " } , home , { { " HOME " , home . string ( ) } } ) ;
const bool homeUntouched = ! fs : : exists ( home / " .review-gates " ) ;
const Process : : Captured atHomeForced = Self ( { " init " , " --force " } , home , { { " HOME " , home . string ( ) } } ) ;
fs : : create_directories ( base / " plain " ) ;
const Process : : Captured plain = Self ( { " init " } , base / " plain " ) ;
Check ( " init refuses, exit 3, writing nothing: a link that is not into the submodule (--force replaces it), a subdirectory, the home directory (--force goes on), no checkout " ,
refused . returnCode = = 3 & & refused . err . contains ( " .claude/skills/prepush is a link to ../../../elsewhere " ) & & ! fs : : exists ( foreign / " .review-gates " ) & & ! fs : : exists ( foreign / " .gitignore " )
& & sub . returnCode = = 3 & & sub . err . contains ( " from the repository's toplevel " ) & & ! fs : : exists ( r / " sub " / " .review-gates " ) & & atHome . returnCode = = 3
& & atHome . err . contains ( " is your home directory " ) & & homeUntouched & & atHomeForced . returnCode = = 0 & & plain . returnCode = = 3
& & plain . err . contains ( " is not in a git checkout " ) ,
refused . err + sub . err + atHome . err + plain . err ) ;
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
// A .gitignore beside the skill that is a link - to a file outside
// the skill's directory, or dangling - is a link like the skill's
// others: refused before anything is written, never written through;
// --force replaces the link with init's file and leaves its target as
// it was.
const fs : : path linked = repo ( " linked " ) ;
Write ( linked / " outside.ignore " , " keep me \n " ) ;
fs : : create_directories ( linked / " .claude " / " skills " / " prepush " ) ;
fs : : create_symlink ( " ../../../outside.ignore " , linked / " .claude " / " skills " / " prepush " / " .gitignore " ) ;
const Process : : Captured linkRefused = Self ( { " init " } , linked ) ;
const bool linkUntouched = ! fs : : exists ( linked / " .review-gates " ) & & Read ( linked / " outside.ignore " ) = = " keep me \n " ;
const Process : : Captured linkForced = Self ( { " init " , " --force " } , linked ) ;
std : : error_code linkError ;
const bool replaced = ! fs : : is_symlink ( fs : : symlink_status ( linked / " .claude " / " skills " / " prepush " / " .gitignore " , linkError ) )
& & Read ( linked / " .claude " / " skills " / " prepush " / " .gitignore " ) = = skillIgnore & & Read ( linked / " outside.ignore " ) = = " keep me \n " ;
const fs : : path dangling = repo ( " dangling " ) ;
fs : : create_directories ( dangling / " shared " ) ;
fs : : create_directories ( dangling / " .claude " / " skills " / " prepush " ) ;
fs : : create_symlink ( " ../../../shared/skill.ignore " , dangling / " .claude " / " skills " / " prepush " / " .gitignore " ) ;
const Process : : Captured danglingRefused = Self ( { " init " } , dangling ) ;
Check ( " init: a .gitignore beside the skill that is a link is refused before anything is written and never written through, dangling or not; --force replaces the link with init's file, "
" its target untouched " ,
linkRefused . returnCode = = 3 & & linkRefused . err . contains ( " .claude/skills/prepush/.gitignore is a link to ../../../outside.ignore " ) & & linkUntouched & & linkForced . returnCode = = 0
& & replaced & & danglingRefused . returnCode = = 3 & & ! fs : : exists ( dangling / " shared " / " skill.ignore " ) & & ! fs : : exists ( dangling / " .review-gates " ) ,
linkRefused . err + linkForced . out + linkForced . err + danglingRefused . err ) ;
// A .gitignore link whose target names review-gates is the
// repository's too - the old setup linked the skill's directory, never
// its .gitignore - so it is refused like any other. A link init cannot
// remove (its directory not writable, where permissions bind) refuses
// under --force before anything is written, nothing written through it.
const fs : : path named = repo ( " named " ) ;
Write ( named / " .review-gates " / " skill.gitignore " , " mine \n " ) ;
fs : : create_directories ( named / " .claude " / " skills " / " prepush " ) ;
fs : : create_symlink ( " ../../../.review-gates/skill.gitignore " , named / " .claude " / " skills " / " prepush " / " .gitignore " ) ;
const Process : : Captured namedRefused = Self ( { " init " } , named ) ;
// A SKILL.md link into the pipeline's own .review-gates/ names the
// submodule but does not resolve into it: refused and kept too.
const fs : : path namedSkill = repo ( " named-skill " ) ;
Write ( namedSkill / " .review-gates " / " skill.md " , " mine \n " ) ;
fs : : create_directories ( namedSkill / " .claude " / " skills " / " prepush " ) ;
fs : : create_symlink ( " ../../../.review-gates/skill.md " , namedSkill / " .claude " / " skills " / " prepush " / " SKILL.md " ) ;
const Process : : Captured namedSkillRefused = Self ( { " init " } , namedSkill ) ;
const bool namedSkillKept = fs : : is_symlink ( fs : : symlink_status ( namedSkill / " .claude " / " skills " / " prepush " / " SKILL.md " , linkError ) )
& & Read ( namedSkill / " .review-gates " / " skill.md " ) = = " mine \n " & & ! fs : : exists ( namedSkill / " .review-gates " / " config.json " ) ;
const bool namedKept = fs : : is_symlink ( fs : : symlink_status ( named / " .claude " / " skills " / " prepush " / " .gitignore " , linkError ) )
& & Read ( named / " .review-gates " / " skill.gitignore " ) = = " mine \n " & & ! fs : : exists ( named / " .review-gates " / " config.json " ) ;
const fs : : path stuck = repo ( " stuck " ) ;
Write ( stuck / " outside.ignore " , " keep me \n " ) ;
fs : : create_directories ( stuck / " .claude " / " skills " / " prepush " ) ;
fs : : create_symlink ( " ../../../outside.ignore " , stuck / " .claude " / " skills " / " prepush " / " .gitignore " ) ;
fs : : permissions ( stuck / " .claude " / " skills " / " prepush " , fs : : perms : : owner_read | fs : : perms : : owner_exec ) ;
const bool writable = static_cast < bool > ( std : : ofstream ( stuck / " .claude " / " skills " / " prepush " / " .probe " ) ) ;
fs : : remove ( stuck / " .claude " / " skills " / " prepush " / " .probe " , linkError ) ;
const Process : : Captured stuckForced = Self ( { " init " , " --force " } , stuck ) ;
fs : : permissions ( stuck / " .claude " / " skills " / " prepush " , fs : : perms : : owner_all ) ;
Check ( " init: a .gitignore or SKILL.md link into a review-gates path that is not the submodule is refused and kept; a link init cannot remove refuses under --force, writing nothing (where "
" permissions bind) " ,
namedRefused . returnCode = = 3 & & namedRefused . err . contains ( " .claude/skills/prepush/.gitignore is a link to ../../../.review-gates/skill.gitignore " ) & & namedKept
& & namedSkillRefused . returnCode = = 3 & & namedSkillRefused . err . contains ( " .claude/skills/prepush/SKILL.md is a link to ../../../.review-gates/skill.md " ) & & namedSkillKept
& & ( writable
| | ( stuckForced . returnCode = = 3 & & stuckForced . err . contains ( " cannot remove the link .claude/skills/prepush/.gitignore " ) & & Read ( stuck / " outside.ignore " ) = = " keep me \n "
& & ! fs : : exists ( stuck / " .review-gates " ) ) ) ,
namedRefused . err + namedSkillRefused . err + stuckForced . out + stuckForced . err ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
// doc: the texts, and where they are.
const Process : : Captured prepush = Self ( { " doc " , " prepush " } , base ) ;
const Process : : Captured where = Self ( { " doc " , " --path " , " config " } , base ) ;
const Process : : Captured missing = Self ( { " doc " } , base ) ;
const Process : : Captured wrong = Self ( { " doc " , " nope " } , base ) ;
Check ( " doc: the procedure byte for byte, a template's path, usage errors for no name and an unknown one (exit 3) " ,
prepush . returnCode = = 0 & & prepush . out = = Read ( checkout / " process " / " prepush.md " ) & & where . returnCode = = 0
& & where . out = = std : : format ( " {} \n " , ( checkout / " process " / " templates " / " config.json " ) . string ( ) ) & & missing . returnCode = = 3
& & missing . err . contains ( " the following arguments are required: name " ) & & wrong . returnCode = = 3
& & wrong . err . contains ( " argument name: invalid choice: 'nope' (choose from 'prepush', 'manifest', 'config') " ) ,
prepush . err + where . out + missing . err + wrong . err ) ;
// stop: a live driver is this process; one that is gone, a sleep that
// was reaped.
const std : : string me = Platform : : Self ( ) - > Key ( ) ;
const fs : : path live = base / " scratch " / " abcd1234-run-1 " ;
Write ( live / " .driver " , me ) ;
const std : : int64_t gone = Process : : Start ( { . argv = { " true " } } ) . pid ;
Process : : Wait ( gone ) ;
const fs : : path dead = base / " scratch " / " abcd1234-run-2 " ;
Write ( dead / " .driver " , std : : format ( " {}:{} " , gone , 1 ) ) ;
const Process : : Captured byDir = Self ( { " stop " , live . string ( ) } , base ) ;
const bool wroteDir = fs : : exists ( live / " stop " ) ;
fs : : remove ( live / " stop " ) ;
const Process : : Captured byDead = Self ( { " stop " , dead . string ( ) } , base ) ;
const Process : : Captured byNone = Self ( { " stop " , ( base / " plain " ) . string ( ) } , base ) ;
const Process : : Captured neither = Self ( { " stop " } , base ) ;
Check ( " stop <dir>: the stop file for the live driver its .driver names (exit 0); a driver that is gone, a dir no run holds, no target: nothing written " ,
byDir . returnCode = = 0 & & wroteDir & & byDir . out . contains ( std : : format ( " stop: driver {} " , me ) ) & & byDead . returnCode = = 1 & & byDead . err . contains ( " is no longer running " )
& & ! fs : : exists ( dead / " stop " ) & & byNone . returnCode = = 1 & & byNone . err . contains ( " no .driver marker " ) & & neither . returnCode = = 3 ,
byDir . out + byDir . err + byDead . err + byNone . err ) ;
Json drivers = Json : : object ( ) ;
drivers [ me ] = { { " run " , " commits x @ abcd1234 " } , { " scratch " , live . string ( ) } , { " since " , Platform : : Now ( ) } , { " boot " , Platform : : BootId ( ) } } ;
Write ( base / " board.json " , DumpFile ( { { " version " , 1 } , { " drivers " , drivers } , { " seats " , Json : : object ( ) } } ) ) ;
const std : : string boardBefore = Read ( base / " board.json " ) ;
const Process : : Captured byHash = Self ( { " stop " , " --hash " , " abcd1234ffff " } , base ) ;
const bool wroteHash = fs : : exists ( live / " stop " ) ;
const Process : : Captured otherHash = Self ( { " stop " , " --hash " , " 00000000 " } , base ) ;
Check ( " stop --hash: every live run of the hash on the board gets its stop file (the first 8 characters decide); another hash finds none (exit 1); the board is read, never written " ,
byHash . returnCode = = 0 & & wroteHash & & otherHash . returnCode = = 1 & & otherHash . err . contains ( " no live run of 00000000 on the board " ) & & Read ( base / " board.json " ) = = boardBefore ,
byHash . out + byHash . err + otherHash . err ) ;
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
// Only `run` and `seats` read the seats file, and its lines reach the
// placement readers alone (rounds 8 and 11 of the self-review): a
// `stop` with the board path only in the working directory's .env
// looks at the machine's default board, as every driver does - under
// a HOME of the test's own here.
std : : map < std : : string , std : : string > envNoPaths = Env ;
envNoPaths . erase ( " REVIEW_GATES_BOARD " ) ;
envNoPaths . erase ( " REVIEW_GATES_SCRATCH " ) ;
envNoPaths [ " HOME " ] = ( base / " home " ) . string ( ) ;
const fs : : path fileRepo = base / " file-repo " ;
fs : : create_directories ( fileRepo ) ;
Write ( fileRepo / " .env " , std : : format ( " REVIEW_GATES_BOARD={} \n REVIEW_GATES_SCRATCH={} \n " , ( base / " file-board.json " ) . string ( ) , ( base / " file-scratch " ) . string ( ) ) ) ;
const Process : : Captured fromFile = Process : : Run ( { Platform : : SelfExe ( ) . string ( ) , " stop " , " --hash " , " 00000000 " } , fileRepo , std : : chrono : : seconds ( 60 ) , { } , envNoPaths ) ;
Check ( " no line of the seats file reaches `stop`: with the board's and the scratch root's paths only in the working directory's .env it reads the machine's default board " , fromFile . returnCode = = 1
& & fromFile . err . contains ( std : : format ( " on the board ({}) " , ( base / " home " / " .cache " / " review-gates " / " seat-board.json " ) . string ( ) ) ) & & ! fromFile . err . contains ( " file-board " ) ,
fromFile . err ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
// seats on the stand-in: a token from the env file, an API key, the
// CLI's own login.
const fs : : path fake = base / " claude " ;
Write ( fake , FakeClaude ) ;
fs : : permissions ( fake , fs : : perms : : owner_all ) ;
Write ( base / " seats " / " .env " , " CLAUDE_CODE_OAUTH_TOKEN1= \" tok-one \" \n ANTHROPIC_API_KEY1=sk-ant-api-one \n " ) ;
fs : : remove ( base / " board.json " ) ;
const std : : map < std : : string , std : : string > withFake = { { " CLAUDE_BIN " , fake . string ( ) } } ;
const Process : : Captured report = Self ( { " seats " , " --local " , " --env-file " , " .env " } , base / " seats " , withFake ) ;
const Process : : Captured json = Self ( { " seats " , " --json " , " --no-fable " } , base / " seats " , withFake ) ;
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
// --env (a prefix of --env-file) names the file for the tokens and
// the settings alike, with ./.env in the way (the API seat's section
// is where the order shows).
Write ( base / " seats " / " other.env " , " CLAUDE_CODE_OAUTH_TOKEN2=tok-two \n ANTHROPIC_API_KEY2=sk-ant-api-two \n REVIEW_GATES_SEAT_ORDER=oauth-first \n " ) ;
const Process : : Captured other = Self ( { " seats " , " --no-fable " , " --env " , " other.env " } , base / " seats " , withFake ) ;
// An application's .env (no seat line, no setting) is passed over for
// the machine's own seats file.
Write ( base / " app " / " .env " , " VITE_API_URL=http://localhost:54321 \n " ) ;
Write ( base / " app-xdg " / " crafter-review " / " env " , " CLAUDE_CODE_OAUTH_TOKEN3=tok-three \n " ) ;
const Process : : Captured appSeats =
Self ( { " seats " , " --no-fable " } , base / " app " , { { " CLAUDE_BIN " , fake . string ( ) } , { " XDG_CONFIG_HOME " , ( base / " app-xdg " ) . string ( ) } , { " HOST_XDG_CONFIG_HOME " , ( base / " app-xdg " ) . string ( ) } } ) ;
// A seats file that can be read only once - a FIFO, as `--env-file
// <(gpg -d seats.env.gpg)` gives one - is read once: its settings and
// its tokens come from that one read (round 10 of the self-review).
fs : : create_directories ( base / " fifo " ) ;
const Process : : Captured fromFifo = Process : : Run (
{ " sh " , " -c " ,
std : : format ( " mkfifo seats.fifo && (printf 'CLAUDE_CODE_OAUTH_TOKEN4=tok-four \\ nANTHROPIC_API_KEY4=sk-ant-api-four \\ nREVIEW_GATES_SEAT_ORDER=oauth-first \\ n' > seats.fifo &) && exec '{}' seats --no-fable --env-file seats.fifo " ,
Platform : : SelfExe ( ) . string ( ) ) } ,
base / " fifo " , std : : chrono : : seconds ( 60 ) , { } , With ( Env , withFake ) ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
Json parsed ;
try {
parsed = Json : : parse ( json . out ) ;
} catch ( const std : : exception & ) {
}
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
Check ( " seats: every seat probed - the CLI's own login, the env file's token, the API key - its windows read, the API seat beside the driver's ledger, the drivers on the board; an application's .env "
" is passed over for the machine's own file; a FIFO is read once, its settings and its seats alike " ,
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
report . returnCode = = 0 & & report . out . starts_with ( " seat pool: 3 seat(s) (local, 1, api1) from .env and your own login, probed " )
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
& & report . out . contains ( " | local | 42% | " ) & & report . out . contains ( " | 1 | 42% | " ) & & report . out . contains ( " | api1 | ok | $0.00 of $200 " ) & & report . out . contains ( " drivers: none running " )
& & other . returnCode = = 0 & & other . out . starts_with ( " seat pool: 2 seat(s) (2, api2) from other.env, probed " ) & & other . out . contains ( " | api2 | ok | $0.00 of $200 " )
& & other . out . contains ( " oauth seats run first (REVIEW_GATES_SEAT_ORDER=oauth-first) " ) & & ! other . out . contains ( " tok-two " ) & & ! other . out . contains ( " sk-ant-api-two " )
& & appSeats . returnCode = = 0 & & appSeats . out . starts_with ( std : : format ( " seat pool: 1 seat(s) (3) from {}, probed " , ( base / " app-xdg " / " crafter-review " / " env " ) . string ( ) ) )
& & fromFifo . returnCode = = 0 & & fromFifo . out . starts_with ( " seat pool: 2 seat(s) (4, api4) from seats.fifo, probed " )
& & fromFifo . out . contains ( " oauth seats run first (REVIEW_GATES_SEAT_ORDER=oauth-first) " ) ,
report . out + report . err + other . out + other . err + appSeats . out + appSeats . err + fromFifo . out + fromFifo . err ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
Check ( " seats --json: the probe results as JSON - the seats, the API seats, the eligible ones and the next placement; ./.env read by default; no token, no key printed " ,
json . returnCode = = 0 & & parsed . is_object ( ) & & parsed . at ( " seats " ) . size ( ) = = 1 & & parsed . at ( " seats " ) . at ( 0 ) . at ( " probedWith " ) = = " haiku " & & parsed . at ( " apiSeats " ) . size ( ) = = 1
& & parsed . at ( " eligible " ) = = Json : : array ( { 1 } ) & & parsed . at ( " next " ) . at ( " seat " ) = = 1 & & Dyn : : Str ( parsed . at ( " probedAt " ) ) . ends_with ( " Z " )
& & ! ( report . out + json . out ) . contains ( " tok-one " ) & & ! ( report . out + json . out ) . contains ( " sk-ant-api-one " ) ,
json . out . substr ( 0 , 600 ) + json . err ) ;
const Process : : Captured none = Self ( { " seats " } , base / " plain " , withFake ) ;
const Process : : Captured noFile = Self ( { " seats " , " --env-file " , " missing.env " } , base / " plain " , withFake ) ;
2026-10-31 16:35:26 +01:00
Write ( base / " plain " / " bad.env " , std : : string ( " \xff \n " , 2 ) ) ;
const Process : : Captured badFile = Self ( { " seats " , " --env-file " , " bad.env " } , base / " plain " , withFake ) ;
Check ( " seats: no seat configured is exit 1 with what it looked for; a missing env file and one that is not UTF-8 are exit 1 with the read error " ,
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
none . returnCode = = 1 & & none . out . starts_with ( " no seat configured (CLAUDE_CODE_OAUTH_TOKEN1..6 / ANTHROPIC_API_KEY1..6; --local probes your own login) " )
2026-10-31 16:35:26 +01:00
& & noFile . returnCode = = 1 & & noFile . err . contains ( " FileNotFoundError: [Errno 2] No such file or directory: 'missing.env' " ) & & badFile . returnCode = = 1
& & badFile . err . contains ( " UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte " ) ,
none . out + noFile . err + badFile . err ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
2026-10-31 16:45:53 +01:00
// What escapes a command ends it with exit 1, never an abort; config's
// help.
const Process : : Captured noHome = Self ( { " doc " , " prepush " } , base , { { " CRAFTER_REVIEW_HOME " , ( base / " nowhere " ) . string ( ) } } ) ;
const Process : : Captured configHelp = Self ( { " config " , " -h " } , base ) ;
Check ( " an error no command catches ends it with exit 1 and its message, never an abort; config -h prints its usage, exit 0 " ,
noHome . returnCode = = 1 & & noHome . err . starts_with ( " crafter-review: " ) & & noHome . err . contains ( " holds no process/lenses " ) & & configHelp . returnCode = = 0
& & configHelp . out . starts_with ( " usage: crafter-review config [-h] " ) ,
noHome . err + configHelp . out + configHelp . err ) ;
feat(cli): seats, init, doc, browser install and stop
- `seats` reports every seat probed (Fable first, Haiku for a seat that
needs it), the API seats beside the driver's ledger, the drivers on the
board, or all of it as JSON. In the sandbox a last line names `stop`,
since the pids above cannot be signalled from there.
- `init` sets a repository up: the config from the template, the artifact
and worktree dirs ignored, the prepush skill with a marker that lets a
later init refresh it. The old link into the review-gates submodule is
replaced by a real directory, never written through. A foreign link, a
subdirectory, the home directory and a dir outside git are refused before
anything is written.
- `doc [--path] prepush|manifest|config` prints a shipped text, or where
the host sees it (the deployment's `active` link in the Flatpak).
- `browser install` runs the installer of the Playwright the pinned MCP
server depends on, and checks the Chromium revision landed.
- `stop <scratch>` and `stop --hash <hash8>` write the stop file a running
driver checks every 2 s, for live drivers only; the board is read, never
written.
The option parser takes positionals, in the usage line, the help and the
errors. The Commands test runs all of them through this binary, seats on a
claude stand-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:45:36 +01:00
// browser: its usage (an install fetches from npm).
const Process : : Captured noAction = Self ( { " browser " } , base ) ;
const Process : : Captured badAction = Self ( { " browser " , " remove " } , base ) ;
Check ( " browser: an action is required, and install is the one there is (exit 3) " ,
noAction . returnCode = = 3 & & noAction . err . contains ( " the following arguments are required: action " ) & & badAction . returnCode = = 3
& & badAction . err . contains ( " argument action: invalid choice: 'remove' (choose from 'install') " ) ,
noAction . err + badAction . err ) ;
fs : : remove_all ( base ) ;
return Failures ? 1 : 0 ;
}
}
int main ( int argc , char * * argv ) {
const std : : vector < std : : string_view > args ( argv , argv + argc ) ;
// With arguments this binary is crafter-review itself, in native mode
// whatever builds it.
if ( args . size ( ) > 1 ) {
Platform : : SystemPaths paths = Platform : : Paths ( ) ;
paths . flatpakInfo = " /nonexistent/.flatpak-info " ;
Platform : : SetPaths ( paths ) ;
return Crafter : : Review : : Main ( args ) ;
}
try {
return Checks ( ) ;
} catch ( const std : : exception & error ) {
std : : println ( std : : cerr , " commands: {} " , error . what ( ) ) ;
return 1 ;
}
}