Crafter.Review/implementations/Crafter.Review-Driver.cpp

989 lines
58 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include <stdio.h>
module Crafter.Review:Driver_impl;
import :Driver;
import :Core;
import :Json;
import :Text;
import :Platform;
import :Process;
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
import :Inventory;
import :Config;
import :Freeze;
import :PgWire;
import :Totp;
import :Plan;
import :Mcp;
import :Seats;
import :SeatPool;
import :OpenCode;
import :Run;
import std;
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
namespace fs = std::filesystem;
namespace Crafter::Review {
namespace {
constexpr std::string_view Usage =
R"(usage: crafter-review [--config FILE] {run,freeze,slots,config} ...
crafter-review {seats,inventory,sql,totp,init,doc,browser,stop} ...
crafter-review --version
The pre-push review pipeline: adversarial lenses, one claude process per
inventory unit, against a frozen commit range.)";
std::int32_t PrintVersion() {
std::println("{}", DriverLine());
try {
std::println("home {}", Home().string());
for (const auto& [name, sha] : CharterHashes()) std::println("charter {} {}", name, sha);
} catch (const std::exception& e) {
std::println("home not found: {}", e.what());
}
return 0;
}
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
// A usage error: exit 3, the preflight's code, for every command that
// cannot start.
std::int32_t UsageError(std::string_view usage, std::string_view prog, std::string_view message) {
std::println(std::cerr, "{}\n{}: error: {}", usage, prog, message);
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
return 3;
}
// die(): the preflight's message and exit 3.
std::int32_t Die(std::string_view message) {
std::println(std::cerr, "preflight: {}", message);
return 3;
}
// ---- the option parser these commands use ---------------------------------
enum class Kind {
String,
Int,
Float,
};
struct OptionSpec {
std::string_view name;
// A value follows (`--x V` or `--x=V`); else a store_true flag.
bool takesValue;
std::string_view metavar;
std::string_view help;
// An integer or a float option: the value must parse as
// Dyn::ParseInt / Dyn::ParseFloat takes it.
Kind kind = Kind::String;
std::span<const std::string_view> choices = {};
bool required = false;
// Left out of --help.
bool hidden = false;
};
struct Parsed {
std::map<std::string, std::string, std::less<>> values;
std::set<std::string, std::less<>> flags;
std::vector<std::string> positionals;
bool help = false;
std::string error;
std::optional<std::string> Value(std::string_view name) const {
const auto found = values.find(name);
if (found == values.end()) return std::nullopt;
return found->second;
}
bool Flag(std::string_view name) const {
return flags.contains(name);
}
};
// A positional as the usage line names it: its choices in braces, else
// its name.
std::string PositionalName(const OptionSpec& positional) {
if (positional.choices.empty()) return std::string(positional.name);
const std::vector<std::string> names(positional.choices.begin(), positional.choices.end());
return std::format("{{{}}}", Dyn::Join(names, ","));
}
std::string UsageLine(std::string_view prog, std::span<const OptionSpec> specs, std::span<const OptionSpec> positionals = {}) {
std::string line = std::format("usage: {} [-h]", prog);
for (const OptionSpec& spec : specs) {
if (spec.hidden) continue;
const std::string text = spec.takesValue ? std::format("{} {}", spec.name, spec.metavar) : std::string(spec.name);
line += spec.required ? std::format(" {}", text) : std::format(" [{}]", text);
}
for (const OptionSpec& positional : positionals) {
line += positional.required ? std::format(" {}", PositionalName(positional)) : std::format(" [{}]", PositionalName(positional));
}
return line;
}
std::string HelpRow(std::string_view left, std::string_view help) {
return left.size() < 20 ? std::format(" {:<20} {}\n", left, help) : std::format(" {}\n {}\n", left, help);
}
std::string Help(std::string_view prog, std::string_view description, std::span<const OptionSpec> specs, std::span<const OptionSpec> positionals = {}) {
std::string text = std::format("{}\n\n{}\n\n", UsageLine(prog, specs, positionals), description);
if (!positionals.empty()) {
text += "positional arguments:\n";
for (const OptionSpec& positional : positionals) text += HelpRow(PositionalName(positional), positional.help);
text += "\n";
}
text += "options:\n -h, --help show this help message and exit\n";
for (const OptionSpec& spec : specs) {
if (spec.hidden) continue;
text += HelpRow(spec.takesValue ? std::format("{} {}", spec.name, spec.metavar) : std::string(spec.name), spec.help);
}
return text;
}
// `--opt value`, `--opt=value`, any unique prefix of a long option, the
// last of a repeated option, every unrecognized argument named at the
// end; positionals in their order, each one word, optional unless
// required.
Parsed ParseOptions(std::span<const std::string_view> args, std::span<const OptionSpec> specs, std::span<const OptionSpec> positionals = {}) {
Parsed parsed;
std::vector<std::string> unrecognized;
for (std::size_t i = 0; i < args.size(); ++i) {
const std::string_view arg = args[i];
if (arg == "-h") {
parsed.help = true;
continue;
}
if (!arg.starts_with("--") || arg == "--") {
if (arg != "--" && !(arg.starts_with('-') && arg.size() > 1) && parsed.positionals.size() < positionals.size()) {
const OptionSpec& positional = positionals[parsed.positionals.size()];
if (!positional.choices.empty() && !std::ranges::contains(positional.choices, arg)) {
std::vector<std::string> quoted;
for (const std::string_view c : positional.choices) quoted.push_back(Dyn::Repr(c));
parsed.error = std::format("argument {}: invalid choice: {} (choose from {})", positional.name, Dyn::Repr(arg), Dyn::Join(quoted, ", "));
return parsed;
}
parsed.positionals.emplace_back(arg);
} else {
unrecognized.emplace_back(arg);
}
continue;
}
const std::size_t equals = arg.find('=');
const std::string_view name = arg.substr(0, equals);
if (name.size() > 2 && std::string_view("--help").starts_with(name) && std::ranges::none_of(specs, [&](const OptionSpec& s) { return s.name.starts_with(name); })) {
parsed.help = true;
continue;
}
std::vector<const OptionSpec*> matches;
for (const OptionSpec& spec : specs) {
if (spec.name == name) {
matches = {&spec};
break;
}
if (spec.name.starts_with(name)) matches.push_back(&spec);
}
if (matches.empty()) {
unrecognized.emplace_back(arg);
continue;
}
if (matches.size() > 1) {
std::vector<std::string> names;
for (const OptionSpec* spec : matches) names.emplace_back(spec->name);
parsed.error = std::format("ambiguous option: {} could match {}", name, Dyn::Join(names, ", "));
return parsed;
}
const OptionSpec& spec = *matches.front();
const std::string key(spec.name.substr(2));
if (!spec.takesValue) {
if (equals != std::string_view::npos) {
parsed.error = std::format("argument {}: ignored explicit argument {}", spec.name, Dyn::Repr(arg.substr(equals + 1)));
return parsed;
}
parsed.flags.insert(key);
continue;
}
std::string value;
if (equals != std::string_view::npos) {
value = std::string(arg.substr(equals + 1));
} else if (i + 1 < args.size() && !(args[i + 1].starts_with('-') && args[i + 1].size() > 1)) {
value = std::string(args[++i]);
} else {
parsed.error = std::format("argument {}: expected one argument", spec.name);
return parsed;
}
if (spec.kind == Kind::Int && !Dyn::ParseInt(value)) {
parsed.error = std::format("argument {}: invalid int value: {}", spec.name, Dyn::Repr(value));
return parsed;
}
if (spec.kind == Kind::Float && !Dyn::ParseFloat(value)) {
parsed.error = std::format("argument {}: invalid float value: {}", spec.name, Dyn::Repr(value));
return parsed;
}
if (!spec.choices.empty() && !std::ranges::contains(spec.choices, value)) {
std::vector<std::string> quoted;
for (const std::string_view c : spec.choices) quoted.push_back(Dyn::Repr(c));
parsed.error = std::format("argument {}: invalid choice: {} (choose from {})", spec.name, Dyn::Repr(value), Dyn::Join(quoted, ", "));
return parsed;
}
parsed.values[key] = std::move(value);
}
if (!unrecognized.empty()) {
parsed.error = std::format("unrecognized arguments: {}", Dyn::Join(unrecognized, " "));
return parsed;
}
std::vector<std::string> missing;
for (const OptionSpec& spec : specs) {
if (spec.required && !parsed.values.contains(spec.name.substr(2))) missing.emplace_back(spec.name);
}
for (std::size_t i = parsed.positionals.size(); i < positionals.size(); ++i) {
if (positionals[i].required) missing.emplace_back(positionals[i].name);
}
if (!missing.empty()) parsed.error = std::format("the following arguments are required: {}", Dyn::Join(missing, ", "));
return parsed;
}
// The consumer config, loaded before every command but `config`:
// warnings on stderr (`run` logs them with its other lines), a problem
// stops it.
std::optional<Json> LoadConfig(const std::optional<std::string>& configPath, bool warn = true) {
Config::Loaded loaded = Config::Load(fs::current_path(), configPath ? fs::path(*configPath) : fs::path());
if (warn) {
for (const Json& warning : Dyn::Or(Dyn::Get(loaded.config, "_warnings"), Dyn::EmptyList())) std::println(std::cerr, "config: warning: {}", warning.get<std::string>());
}
if (!loaded.problems.empty()) {
Die(std::format("config: {}", Dyn::Join(loaded.problems, "; ")));
return std::nullopt;
}
return std::move(loaded.config);
}
constexpr std::array<OptionSpec, 9> FreezeOptions = {{
{"--repo", true, "REPO", "the repository (toplevel of a checkout; default: the working directory)"},
{"--slug", true, "SLUG", "the artifact dir name: .review-gates/push/<slug>/"},
{"--out", true, "OUT", "the artifact dir (overrides --slug)"},
{"--base", true, "BASE", "the range base; default @{upstream}, else merge-base with the configured base branch"},
{"--replace", false, "", "re-freeze into an out dir that already holds patches (gate-state/ is kept) and supersede this repo's worktrees"},
{"--worktree", false, "", "check out a detached worktree of HEAD under .review-gates/trees/ for the lenses (printed as --trees)"},
{"--require-signed", false, "", "an unsigned commit fails instead of warning"},
{"--date", true, "DATE", "the run date (default today)"},
{"--verify", false, "", "push-time check: HEAD, a clean tree and a fresh export still match freeze.json"},
}};
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
constexpr std::array<std::string_view, 3> Modes = {"reply", "series", "commits"};
constexpr std::array<std::string_view, 2> Harnesses = {"claude", "opencode"};
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
constexpr std::array<std::string_view, 2> PersonaBlocks = {"pruned", "all"};
const std::vector<OptionSpec>& RunOptions() {
static const std::string effort = std::format("reasoning effort of every lens process (default {})", Plan::Effort);
static const std::string reads =
std::format("clean reads a code unit of the static code lenses needs before its verdict is reused, one per round (default {})", Plan::Reads);
static const std::string models = [] {
std::vector<std::string> keys;
for (const Plan::ModelTier& m : Plan::Models()) keys.push_back(m.tier);
return std::format("override the lens model table, e.g. fable=claude-fable-5-1 (keys: {})", Dyn::Join(keys, ","));
}();
static const std::string variant = std::format("opencode harness: the model's reasoning variant (default {})", OpenCode::DefaultVariant);
static const std::vector<OptionSpec> specs = {
{"--mode", true, "MODE", "", Kind::String, Modes, false, true},
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
{"--subsystem", true, "SUBSYSTEM", "journal subsystem: required for reply and series; optional in commits mode (names the ledger and captures the lenses may read)"},
{"--artifact", true, "ARTIFACT", "the frozen artifact: a dir of *.patch (.review-gates/push/<slug>/, a series), or the reply draft file"},
{"--hash", true, "HASH", ""},
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
{"--reviewers", true, "REVIEWERS", "reply and series: the reviewers a persona reads as, comma-separated (the persona gate runs after the others)"},
{"--dispositions", true, "DISPOSITIONS", ""},
{"--base", true, "BASE", ""},
{"--date", true, "DATE", ""},
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
{"--trees", true, "TREES", "the checkout the lenses read, at the range HEAD: the repository itself (`.`, default) or the frozen worktree; reply and series default work/linux,work/milos-linux"},
{"--context-paths", true, "CONTEXT_PATHS", "comma-separated paths of material the artifact discusses (captures, evidence) - mechanical pointers, no narrative"},
{"--only", true, "ONLY", "comma-separated lens files (test only)"},
{"--without", true, "WITHOUT", "comma-separated lens files to leave out of this run by owner ruling (logged; the run stays a full run)"},
{"--units", true, "UNITS", "comma-separated unit ids (test only)"},
{"--warm", false, "", "test runs only: let one pass per prompt prefix go first, as a full run does"},
{"--concurrency", true, "CONCURRENCY",
"a cap on the lens processes in flight on this machine, every driver's counted (default none: each OAuth account takes at most 128 - "
"REVIEW_GATES_OAUTH_CONCURRENCY - and an API seat REVIEW_GATES_API_CONCURRENCY; the pool still places each pass by seat headroom, launches it only when it "
"fits in free memory at its environment's measured cost, and gives a pass with the browser a stack of its own)",
Kind::Int},
{"--effort", true, "EFFORT", effort},
{"--reads", true, "READS", reads, Kind::Int},
{"--no-reuse", false, "", ""},
{"--no-salvage", false, "", "do not reuse finished process outputs of an earlier run on this hash"},
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
{"--persona-blocks", true, "{pruned,all}", "reply and series: pruned (default) skips the block passes of the units a persona's whole pass marked not-mine", Kind::String, PersonaBlocks},
{"--dry-run", false, "", ""},
{"--claude-bin", true, "CLAUDE_BIN", ""},
{"--models", true, "MODELS", models},
{"--scratch", true, "SCRATCH", ""},
{"--seats", true, "SEATS", "the seats the lenses run on: auto (every Anthropic seat in the env file, else local), local, api, oauth, openrouter (never in auto), or a list like api1,1,2,local"},
{"--env-file", true, "ENV_FILE", "dotenv file with CLAUDE_CODE_OAUTH_TOKEN1..6 / ANTHROPIC_API_KEY1..6 (default $SEATS_ENV_FILE, else the repository .env)"},
{"--no-probe", false, "", "skip the one-request probe of every seat at the start"},
{"--api-budget", true, "API_BUDGET", "USD an API-credit seat may spend per cycle (default REVIEW_GATES_API_BUDGET_USD or 200)", Kind::Float},
{"--harness", true, "{claude,opencode}",
"the agent harness of every lens process: claude (default) or opencode (OpenRouter seats only; no lens whose environment needs an MCP server)", Kind::String, Harnesses},
{"--variant", true, "VARIANT", variant},
{"--opencode-bin", true, "OPENCODE_BIN", ""},
};
return specs;
}
std::int32_t RunCommand(std::span<const std::string_view> args, const std::optional<std::string>& configPath) {
constexpr std::string_view Prog = "crafter-review run";
const std::vector<OptionSpec>& specs = RunOptions();
const Parsed parsed = ParseOptions(args, specs);
if (parsed.help) {
std::print("{}", Help(Prog, "one claude process per inventory unit per lens, the gates, the dump (process/prepush.md)", specs));
return 0;
}
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, specs), Prog, parsed.error);
const std::optional<Json> config = LoadConfig(configPath, false);
if (!config) return 3;
Run::Arguments a;
a.config = configPath;
a.mode = parsed.Value("mode").value_or("commits");
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
a.subsystem = parsed.Value("subsystem");
a.artifact = parsed.Value("artifact").value_or("");
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
if (a.artifact.empty()) return UsageError(UsageLine(Prog, specs), Prog, "the following arguments are required: --artifact");
a.hash = parsed.Value("hash");
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
a.reviewers = parsed.Value("reviewers").value_or("");
a.personaBlocks = parsed.Value("persona-blocks").value_or("pruned");
a.dispositions = parsed.Value("dispositions");
a.base = parsed.Value("base").value_or("");
a.date = parsed.Value("date").value_or(Platform::TodayIso());
a.trees = parsed.Value("trees");
a.contextPaths = parsed.Value("context-paths").value_or("");
a.only = parsed.Value("only");
a.without = parsed.Value("without");
if (const std::optional<std::string> units = parsed.Value("units"); units && !units->empty()) {
// A trailing comma names no unit.
a.units = std::set<std::string>();
for (const std::string& part : Dyn::Split(*units, ",")) {
if (std::string id = Dyn::Strip(part); !id.empty()) a.units->insert(std::move(id));
}
}
// Empty items are skipped, so a typo such as `--units ,` must not
// leave no filter at all: that is a full run.
auto namesNothing = [](const std::optional<std::string>& list) {
return list && !list->empty() && std::ranges::all_of(Dyn::Split(*list, ","), [](const std::string& part) { return Dyn::Strip(part).empty(); });
};
if (namesNothing(a.only)) return UsageError(UsageLine(Prog, specs), Prog, std::format("argument --only: names no lens file ({})", Dyn::Repr(*a.only)));
if (namesNothing(parsed.Value("units"))) return UsageError(UsageLine(Prog, specs), Prog, std::format("argument --units: names no unit id ({})", Dyn::Repr(*parsed.Value("units"))));
a.warm = parsed.Flag("warm");
if (const std::optional<std::string> n = parsed.Value("concurrency")) {
a.concurrency = *Dyn::ParseInt(*n);
if (*a.concurrency < 1) return UsageError(UsageLine(Prog, specs), Prog, "argument --concurrency: a number from 1 up (leave it out for no cap)");
}
a.effort = parsed.Value("effort").value_or("");
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
if (a.effort.empty()) a.effort = std::string(Plan::DefaultEffort(a.mode));
if (a.mode == "commits") {
if (!a.reviewers.empty()) return Die("commits mode has no persona gate (the owner is the last gate): drop --reviewers");
} else {
if (!a.subsystem || a.subsystem->empty()) return Die(std::format("{} mode: --subsystem is required", a.mode));
if (!a.trees || a.trees->empty()) a.trees = "work/linux,work/milos-linux";
}
a.noReuse = parsed.Flag("no-reuse");
a.noSalvage = parsed.Flag("no-salvage");
a.dryRun = parsed.Flag("dry-run");
a.claudeBin = parsed.Value("claude-bin");
a.models = parsed.Value("models").value_or("");
a.scratch = parsed.Value("scratch");
a.seats = parsed.Value("seats");
if (!a.seats) a.seats = Platform::GetEnv("REVIEW_GATES_SEATS");
a.envFile = parsed.Value("env-file");
a.noProbe = parsed.Flag("no-probe");
if (const std::optional<std::string> budget = parsed.Value("api-budget")) a.apiBudget = *Dyn::ParseFloat(*budget);
a.harness = parsed.Value("harness").value_or("claude");
a.variant = parsed.Value("variant");
if (!a.variant || a.variant->empty()) a.variant = a.harness == "opencode" ? std::optional<std::string>(std::string(OpenCode::DefaultVariant)) : std::nullopt;
a.opencodeBin = parsed.Value("opencode-bin");
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
a.reads = Plan::ReadsFor(a.mode);
if (const std::optional<std::string> reads = parsed.Value("reads")) {
const std::int64_t n = *Dyn::ParseInt(*reads);
feat(modes): the kernel modes - a patch series and a mailing-list reply Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
2026-10-31 22:56:12 +01:00
if (n < 1 || (a.mode == "reply" && n != 1)) return Die("--reads: a number from 1 up; a reply has no code units and is read once");
a.reads = n;
}
for (const std::string& part : Dyn::Split(a.models, ",")) {
const std::string kv = Dyn::Strip(part);
if (kv.empty()) continue;
const std::size_t equals = kv.find('=');
if (equals == std::string::npos) throw Dyn::Error("ValueError", "not enough values to unpack (expected 2, got 1)");
const std::string key = kv.substr(0, equals);
std::vector<Plan::ModelTier>& tiers = Plan::Models();
const auto tier = std::ranges::find_if(tiers, [&](const Plan::ModelTier& m) { return m.tier == key; });
if (tier == tiers.end()) {
std::vector<std::string> known;
for (const Plan::ModelTier& m : tiers) known.push_back(m.tier);
std::println(std::cerr, "--models: unknown key {} (known: {})", Dyn::Repr(key), Dyn::Join(known, ", "));
return 1;
}
tier->id = kv.substr(equals + 1);
}
return Run::Main(std::move(a), *config);
}
constexpr std::array<OptionSpec, 1> SlotsOptions = {{
{"--artifact", true, "ARTIFACT", "a frozen artifact dir: no more slots than its browser passes can use at once"},
}};
std::int32_t SlotsCommand(std::span<const std::string_view> args, const std::optional<std::string>& configPath) {
constexpr std::string_view Prog = "crafter-review slots";
const Parsed parsed = ParseOptions(args, SlotsOptions);
if (parsed.help) {
std::print("{}", Help(Prog, "how many stacks (slots.*) fit in free memory, and the commands that bring up the missing ones", SlotsOptions));
return 0;
}
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, SlotsOptions), Prog, parsed.error);
const std::optional<Json> config = LoadConfig(configPath);
if (!config) return 3;
const std::optional<std::string> artifact = parsed.Value("artifact");
try {
Run::SlotsPlan(*config, fs::current_path(), artifact ? std::optional<fs::path>(*artifact) : std::nullopt);
} catch (const Inventory::InvariantError& e) {
return Die(e.what());
}
return 0;
}
std::int32_t FreezeCommand(std::span<const std::string_view> args, const std::optional<std::string>& configPath) {
constexpr std::string_view Prog = "crafter-review freeze";
const Parsed parsed = ParseOptions(args, FreezeOptions);
if (parsed.help) {
std::print("{}", Help(Prog, "cut the frozen commit-range artifact for the push gates (process/prepush.md Gate 0)", FreezeOptions));
return 0;
}
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, FreezeOptions), Prog, parsed.error);
const std::optional<Json> config = LoadConfig(configPath);
if (!config) return 3;
Freeze::Arguments arguments;
arguments.repo = parsed.Value("repo");
arguments.slug = parsed.Value("slug");
arguments.out = parsed.Value("out");
arguments.base = parsed.Value("base");
arguments.replace = parsed.Flag("replace");
arguments.worktree = parsed.Flag("worktree");
arguments.requireSigned = parsed.Flag("require-signed");
arguments.date = parsed.Value("date").value_or(Platform::TodayIso());
arguments.verify = parsed.Flag("verify");
// `%G?` starts gpg, which can leave a daemon behind: as a subreaper
// this process inherits it, and takes it down on the way out.
Process::BecomeSubreaper();
struct Sweep {
~Sweep() {
Process::KillDescendants();
}
} const sweep;
return Freeze::Main(arguments, *config);
}
// ---- seats: the report, the ledger and the board beside it ------------------
constexpr std::array<OptionSpec, 5> SeatsOptions = {{
{"--env-file", true, "ENV_FILE", "dotenv file holding the seat tokens (default: $SEATS_ENV_FILE, else ./.env when it exists)"},
{"--json", false, "", "the probe results as JSON instead of the report"},
{"--no-fable", false, "", "probe on Haiku instead of Fable (cheaper; the Fable weekly window stays unread)"},
{"--local", false, "", "also probe the CLI's own login, shown as the seat `local`"},
{"--timeout", true, "TIMEOUT", "seconds per probe (default 30)", Kind::Float},
}};
// datetime.fromtimestamp(t, timezone.utc).isoformat(timespec='milliseconds')
// with Z for +00:00.
std::string IsoUtcMillis(double epoch) {
double whole = std::floor(epoch);
auto micros = static_cast<std::int64_t>(std::nearbyint((epoch - whole) * 1e6));
if (micros >= 1'000'000) {
whole += 1;
micros -= 1'000'000;
}
const std::chrono::sys_seconds at {std::chrono::seconds(static_cast<std::int64_t>(whole))};
return std::format("{:%Y-%m-%dT%H:%M:%S}.{:03d}Z", at, micros / 1000);
}
struct SeatJob {
std::int32_t seat = 0;
std::string token;
bool local = false;
};
// Every seat probed, eight at once at most; a probe that throws ends
// the command.
std::vector<Json> ProbeAll(const std::vector<SeatJob>& todo, const Seats::ProbeOptions& base) {
std::vector<Json> out(todo.size());
std::atomic<std::size_t> next = 0;
std::mutex lock;
std::exception_ptr failed;
std::vector<std::thread> workers;
for (std::size_t w = 0; w < std::min<std::size_t>(8, todo.size()); ++w) {
workers.emplace_back([&] {
for (std::size_t i = next++; i < todo.size(); i = next++) {
try {
Seats::ProbeOptions options = base;
options.local = todo[i].local;
out[i] = Seats::ProbeSeat(todo[i].seat, todo[i].token, options);
} catch (const std::exception&) {
const std::lock_guard guard(lock);
if (!failed) failed = std::current_exception();
}
}
});
}
for (std::thread& worker : workers) worker.join();
if (failed) std::rethrow_exception(failed);
return out;
}
std::int32_t SeatsCommand(std::span<const std::string_view> args) {
constexpr std::string_view Prog = "crafter-review seats";
const Parsed parsed = ParseOptions(args, SeatsOptions);
if (parsed.help) {
std::print("{}\nSeats: CLAUDE_CODE_OAUTH_TOKEN1..6 from the environment and/or the dotenv file. Never prints a token.\n",
Help(Prog, "How much usage each Claude seat has left: every seat probed, both windows and their resets.", SeatsOptions));
return 0;
}
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, SeatsOptions), Prog, parsed.error);
const std::optional<std::string> timeoutText = parsed.Value("timeout");
const double timeout = timeoutText ? Dyn::ParseFloat(*timeoutText).value_or(Seats::ProbeTimeoutS) : Seats::ProbeTimeoutS;
const std::map<std::string, std::string> process = Platform::Environment();
const Json cfg = Seats::ConfigFromEnv(process);
std::string envFile = parsed.Value("env-file").value_or("");
if (envFile.empty()) envFile = Platform::GetEnv("SEATS_ENV_FILE").value_or("");
std::error_code error;
if (envFile.empty() && fs::exists(".env", error)) envFile = ".env";
std::map<std::string, std::string> env = process;
if (!envFile.empty()) {
// The env file, read as text: one that cannot be read, or is not
// UTF-8, ends the command.
for (const auto& [name, value] : Seats::SeatTokensFromEnvFile(Dyn::ReadText(envFile))) env[name] = value;
}
std::vector<SeatJob> jobs;
for (const auto& [seat, token] : Seats::SeatTokensFromEnv(env)) jobs.push_back({seat, token, false});
if (parsed.Flag("local")) jobs.insert(jobs.begin(), SeatJob {0, "", true});
const std::vector<std::pair<std::int32_t, std::string>> apiJobs = Seats::ApiKeysFromEnv(env);
if (jobs.empty() && apiJobs.empty()) {
std::println("no seat configured (CLAUDE_CODE_OAUTH_TOKEN1..6 / ANTHROPIC_API_KEY1..6{}; --local probes your own login)", envFile.empty() ? std::string() : std::format(" in {}", envFile));
return 1;
}
const std::string claudeBin = Seats::FindClaudeBin();
const double now = Platform::Now();
Seats::ProbeOptions haiku;
haiku.timeoutS = timeout;
haiku.claudeBin = claudeBin;
std::vector<Json> seats;
if (!jobs.empty() && parsed.Flag("no-fable")) {
seats = ProbeAll(jobs, haiku);
} else if (!jobs.empty()) {
// One Fable probe per seat carries all three windows and is the
// seat; only a seat whose Fable probe could not be read, or was
// refused for the Fable window alone, is probed again on Haiku
// for its own state.
Seats::ProbeOptions fable = haiku;
fable.model = std::string(Seats::FableProbeModel);
const std::vector<Json> first = ProbeAll(jobs, fable);
std::vector<SeatJob> again;
for (std::size_t i = 0; i < jobs.size(); ++i) {
if (Seats::NeedsHaiku(first[i])) again.push_back(jobs[i]);
}
std::map<std::int64_t, Json> standIns;
const std::vector<Json> got = again.empty() ? std::vector<Json>() : ProbeAll(again, haiku);
for (std::size_t i = 0; i < again.size(); ++i) standIns[again[i].seat] = got[i];
for (const Json& s : first) {
const auto found = standIns.find(s.at("seat").get<std::int64_t>());
seats.push_back(Seats::MergeFableProbe(found != standIns.end() ? found->second : s, s));
}
}
const Json apiConfig = SeatPool::ApiConfig(process);
std::vector<Json> apiSeats;
if (!apiJobs.empty()) {
std::vector<SeatJob> todo;
for (const auto& [n, key] : apiJobs) todo.push_back({n, key, false});
Seats::ProbeOptions api = haiku;
api.api = true;
apiSeats = ProbeAll(todo, api);
// The driver's ledger: what it spent on each key this cycle.
SeatPool::Ledger ledger = SeatPool::Ledger::Load(SeatPool::LedgerPath(), apiConfig);
for (Json& s : apiSeats) s["ledger"] = ledger.Entry(std::format("api{}", s.at("seat").get<std::int64_t>()));
}
// The board: the drivers running now and what they hold on each
// account - read, never written.
const Json board = SeatPool::Board(SeatPool::BoardPath()).View(now);
std::map<std::int32_t, std::string> keys;
for (const SeatJob& j : jobs) keys[j.seat] = SeatPool::SeatKey(j.local ? "local" : "oauth", j.token);
std::map<std::int32_t, std::string> apiKeys;
for (const auto& [n, key] : apiJobs) apiKeys[n] = SeatPool::SeatKey("api", key);
const Seats::Zone zone = Seats::LocalZone();
if (parsed.Flag("json")) {
Json eligible = Json::array();
for (const Json& s : seats) {
if (Seats::IsEligible(s, cfg)) eligible.push_back(s.at("seat"));
}
const Json out = {{"probedAt", IsoUtcMillis(now)}, {"config", cfg}, {"seats", seats}, {"apiSeats", apiSeats}, {"eligible", eligible},
{"next", seats.empty() ? Json() : Seats::PickNext(seats, cfg)}, {"board", board}};
std::println("{}", out.dump(2, ' ', false, Json::error_handler_t::replace));
return 0;
}
const std::string source = std::format("{}{}", envFile.empty() ? std::string("the environment") : envFile, parsed.Flag("local") ? " and your own login" : "");
std::vector<std::string> ids;
for (const Json& s : seats) ids.push_back(s.at("seat") == 0 ? std::string("local") : std::to_string(s.at("seat").get<std::int64_t>()));
for (const Json& s : apiSeats) ids.push_back(std::format("api{}", s.at("seat").get<std::int64_t>()));
std::println("seat pool: {} seat(s) ({}) from {}, probed {} {}, claude: {}\n", ids.size(), Dyn::Join(ids, ", "), source, Seats::Clock(now, zone), zone.label, claudeBin);
if (!seats.empty()) std::println("{}", Seats::RenderStatusReport(seats, cfg, now, zone, board, keys));
if (!apiSeats.empty()) std::println("{}{}", seats.empty() ? "" : "\n", Seats::RenderApiReport(apiSeats, apiConfig, board, apiKeys));
std::println("\n{}", Seats::RenderDrivers(board, now, zone));
// The pids above are no use from inside the sandbox: the way to
// stop a run instead.
if (Platform::InSandbox() && !Dyn::Get(board, "drivers").empty()) {
std::println("stop a run with `{} stop --hash <hash8>` or `{} stop <its scratch dir>`", Invocation(), Invocation());
}
return 0;
}
// ---- stop: the file a running driver looks for every 2 s ------------------------
constexpr std::array<OptionSpec, 1> StopOptions = {{
{"--hash", true, "HASH8", "every live run of this artifact hash on the board (its first 8 characters)"},
}};
constexpr std::array<OptionSpec, 1> StopPositionals = {{
{"scratch", false, "", "the run's scratch dir (the last word of its gate-0 log)"},
}};
std::int32_t StopCommand(std::span<const std::string_view> args) {
constexpr std::string_view Prog = "crafter-review stop";
const Parsed parsed = ParseOptions(args, StopOptions, StopPositionals);
if (parsed.help) {
std::print("{}", Help(Prog, "stop a running review as SIGTERM would - its running passes killed, no later gate started, the dump written - from any sandbox", StopOptions,
StopPositionals));
return 0;
}
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, StopOptions, StopPositionals), Prog, parsed.error);
const std::optional<std::string> hash = parsed.Value("hash");
if (hash.has_value() == !parsed.positionals.empty()) return UsageError(UsageLine(Prog, StopOptions, StopPositionals), Prog, "a scratch dir or --hash, one of the two");
const fs::path boardPath = SeatPool::BoardPath();
std::vector<std::pair<std::string, fs::path>> targets;
if (!hash) {
const fs::path dir = fs::absolute(parsed.positionals.front());
const std::string key = Dyn::Strip(Platform::ReadFile(dir / ".driver").value_or(""));
if (key.empty()) {
std::println(std::cerr, "stop: {} is no run's scratch dir (it has no .driver marker)", dir.string());
return 1;
}
if (!SeatPool::DriverAlive(key, boardPath.parent_path(), false)) {
std::println(std::cerr, "stop: the driver of {} ({}) is no longer running", dir.string(), key);
return 1;
}
targets.emplace_back(key, dir);
} else {
const std::string h8 = hash->substr(0, 8);
const Json board = SeatPool::Board(boardPath).View(Platform::Now());
const Json& drivers = Dyn::Get(board, "drivers");
for (auto it = drivers.begin(); it != drivers.end(); ++it) {
const Json& d = it.value();
if (Dyn::StrOr(Dyn::Get(d, "run"), "").ends_with(std::format("@ {}", h8)) && Dyn::Truthy(Dyn::Get(d, "scratch"))) targets.emplace_back(it.key(), Dyn::Str(d.at("scratch")));
}
if (targets.empty()) {
std::println(std::cerr, "stop: no live run of {} on the board ({})", h8, boardPath.string());
return 1;
}
}
for (const auto& [key, dir] : targets) {
std::ofstream file(dir / "stop", std::ios::binary);
file << std::format("asked by `{} stop` at {}\n", Invocation(), Platform::LocalTime("%Y-%m-%dT%H:%M:%S"));
if (!file) {
std::println(std::cerr, "stop: cannot write {}", (dir / "stop").string());
return 1;
}
std::println("stop: driver {} ({}) - asked to stop; a run stops within 2 s, else SIGTERM its pid", key, dir.string());
}
return 0;
}
// ---- doc: the texts the Flatpak ships -------------------------------------------------
constexpr std::array<std::string_view, 3> DocNames = {"prepush", "manifest", "config"};
constexpr std::array<OptionSpec, 1> DocOptions = {{
{"--path", false, "", "where the file is, as the host sees it, instead of the file (the procedure is 35 KB: the skill reads it from there)"},
}};
constexpr std::array<OptionSpec, 1> DocPositionals = {{
{"name", false, "", "prepush: the procedure; manifest: the manifest template; config: the config template", Kind::String, DocNames, true},
}};
// Each text under process/.
std::string_view DocFile(std::string_view name) {
if (name == "manifest") return "templates/manifest.md";
if (name == "config") return "templates/config.json";
return "prepush.md";
}
// A file under the home as the host sees it. In the Flatpak that is
// under /.flatpak-info's app-path, with the deployment's `active` link
// in place of its commit, so the path outlives an update.
fs::path HostPath(const fs::path& path) {
if (!Platform::InSandbox()) return path;
const std::string info = Platform::ReadFile(Platform::Paths().flatpakInfo).value_or("");
for (const std::string& line : Dyn::Split(info, "\n")) {
if (!line.starts_with("app-path=")) continue;
const fs::path files(line.substr(9));
std::error_code error;
const fs::path inside = fs::relative(path, "/app", error);
if (error || inside.empty() || inside.string().starts_with("..")) return path;
return files.parent_path().parent_path() / "active" / files.filename() / inside;
}
return path;
}
std::int32_t DocCommand(std::span<const std::string_view> args) {
constexpr std::string_view Prog = "crafter-review doc";
const Parsed parsed = ParseOptions(args, DocOptions, DocPositionals);
if (parsed.help) {
std::print("{}", Help(Prog, "print a text the pipeline ships, or with --path where it is", DocOptions, DocPositionals));
return 0;
}
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, DocOptions, DocPositionals), Prog, parsed.error);
const fs::path file = Home() / "process" / DocFile(parsed.positionals.front());
if (parsed.Flag("path")) {
std::println("{}", HostPath(file).string());
return 0;
}
const std::optional<std::string> text = Platform::ReadFile(file);
if (!text) {
std::println(std::cerr, "doc: cannot read {}", file.string());
return 1;
}
std::print("{}", *text);
return 0;
}
// ---- browser install: the Chromium the MCP server drives ----------------------------
constexpr std::array<std::string_view, 1> BrowserActions = {"install"};
constexpr std::array<OptionSpec, 1> BrowserPositionals = {{
{"action", false, "", "install: the Chromium build the pinned Playwright MCP server drives, where it looks for it", Kind::String, BrowserActions, true},
}};
// Where Playwright keeps its browsers on Linux: PLAYWRIGHT_BROWSERS_PATH,
// else $XDG_CACHE_HOME/ms-playwright, else ~/.cache/ms-playwright. In
// the Flatpak XDG_CACHE_HOME is the host's again (Platform::Init).
fs::path PlaywrightBrowsers() {
const std::string set = Platform::GetEnv("PLAYWRIGHT_BROWSERS_PATH").value_or("");
if (!set.empty() && set != "0") return set;
const std::string cache = Platform::GetEnv("XDG_CACHE_HOME").value_or("");
return fs::path(cache.empty() ? std::format("{}/.cache", Platform::Home()) : cache) / "ms-playwright";
}
std::int32_t BrowserCommand(std::span<const std::string_view> args) {
constexpr std::string_view Prog = "crafter-review browser";
const Parsed parsed = ParseOptions(args, {}, BrowserPositionals);
if (parsed.help) {
std::print("{}", Help(Prog, "the browser a lens with the browser environment drives", {}, BrowserPositionals));
return 0;
}
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, {}, BrowserPositionals), Prog, parsed.error);
const std::vector<std::string> argv = {"npx", "-y", std::format("playwright@{}", Mcp::PlaywrightVersion), "install", "chromium"};
std::println("browser: {} (the Playwright of @playwright/mcp@{})", Dyn::Join(argv, " "), Mcp::PlaywrightMcpVersion);
std::fflush(stdout);
std::optional<std::int32_t> code;
try {
code = Process::Wait(Process::Start({.argv = argv}).pid);
} catch (const std::exception& e) {
std::println(std::cerr, "browser: {}", e.what());
return 1;
}
if (code.value_or(1) != 0) {
std::println(std::cerr, "browser: the install failed (exit {})", code.value_or(1));
return 1;
}
const fs::path dir = PlaywrightBrowsers() / std::format("chromium-{}", Mcp::ChromiumRevision);
std::error_code error;
if (!fs::is_directory(dir, error)) {
std::println(std::cerr, "browser: installed, but {} is not there - the pinned Playwright and Chromium revision disagree", dir.string());
return 1;
}
std::println("browser: chromium-{} in {}", Mcp::ChromiumRevision, dir.string());
return 0;
}
// ---- init: a repository set up for the gates ---------------------------------------------
constexpr std::array<OptionSpec, 1> InitOptions = {{
{"--force", false, "", "replace a SKILL.md or a link init did not make, and run from your home directory"},
}};
// After the skill's frontmatter: what tells a later `init` the file is
// its own to refresh.
constexpr std::string_view InitMarker = "<!-- written by crafter-review init";
std::string WithMarker(const std::string& skill) {
const std::string marker = std::format("{} {}: `init` refreshes this file while this line is in it -->\n", InitMarker, Version());
if (skill.starts_with("---\n")) {
const std::size_t end = skill.find("\n---\n", 3);
if (end != std::string::npos) return std::format("{}{}{}", skill.substr(0, end + 5), marker, skill.substr(end + 5));
}
return std::format("{}{}", marker, skill);
}
std::int32_t InitCommand(std::span<const std::string_view> args) {
constexpr std::string_view Prog = "crafter-review init";
const Parsed parsed = ParseOptions(args, InitOptions);
if (parsed.help) {
std::print("{}", Help(Prog, "set this repository up for the pre-push gates: .review-gates/config.json from the template, the artifact dirs ignored, the prepush skill", InitOptions));
return 0;
}
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, InitOptions), Prog, parsed.error);
const bool force = parsed.Flag("force");
auto refuse = [](const std::string& message) {
std::println(std::cerr, "init: {}", message);
return 3;
};
// A directory the Flatpak cannot see becomes $HOME inside it, PWD
// included: the toplevel check and the home check catch that.
const fs::path cwd = fs::current_path();
const Process::Captured top = Process::Run({"git", "rev-parse", "--show-toplevel"});
if (top.returnCode != 0) return refuse(std::format("{} is not in a git checkout: run init from the repository's toplevel", cwd.string()));
std::error_code error;
const fs::path toplevel = Dyn::Strip(top.out);
if (!fs::equivalent(toplevel, cwd, error)) return refuse(std::format("run it from the repository's toplevel, {}, not {}", toplevel.string(), cwd.string()));
if (!force && fs::equivalent(cwd, Platform::Home(), error)) {
return refuse(std::format("{} is your home directory, which is where the Flatpak lands in a directory it cannot see: cd into the repository (--force if it is the repository)", cwd.string()));
}
// The skill's place. The old setup linked .claude/skills/prepush
// into the review-gates submodule: the link goes, never written
// through, which would change the submodule's tracked file and
// fail the next freeze on a dirty tree. Any other link stays
// unless --force - checked before anything is written.
const fs::path skillDir = cwd / ".claude" / "skills" / "prepush";
const fs::path skill = skillDir / "SKILL.md";
std::vector<std::pair<fs::path, fs::path>> links;
for (const fs::path& link : {skillDir, skill}) {
if (!fs::is_symlink(fs::symlink_status(link, error))) continue;
const fs::path target = fs::read_symlink(link, error);
if (!force && !target.string().contains("review-gates")) {
return refuse(std::format("{} is a link to {}, not into the review-gates submodule: --force replaces it", link.lexically_relative(cwd).string(), target.string()));
}
links.emplace_back(link, target);
}
std::println("init: {}", cwd.string());
// The config, from the template, only when there is none. Written
// fresh, not copied: the Flatpak's files are read-only.
const fs::path config = cwd / ".review-gates" / "config.json";
if (fs::exists(config, error)) {
std::println(" kept .review-gates/config.json");
} else {
fs::create_directories(config.parent_path());
std::ofstream(config, std::ios::binary) << Platform::ReadFile(Home() / "process" / "templates" / "config.json").value_or("{}\n");
std::println(" wrote .review-gates/config.json (the template: every lens off - turn on the ones this repository runs, set base, then what their environments need)");
}
// The artifacts and the worktrees stay out of git.
const fs::path ignore = cwd / ".gitignore";
const std::string ignored = Platform::ReadFile(ignore).value_or("");
std::vector<std::string> present;
for (const std::string& line : Dyn::Split(ignored, "\n")) present.push_back(Dyn::Strip(Dyn::Strip(line), "/"));
std::vector<std::string> added;
for (const std::string_view entry : {std::string_view(".review-gates/push/"), std::string_view(".review-gates/trees/")}) {
if (!std::ranges::contains(present, Dyn::Strip(entry, "/")) && !std::ranges::contains(present, std::string(".review-gates"))) added.emplace_back(entry);
}
if (added.empty()) {
std::println(" kept .gitignore");
} else {
std::string text = ignored;
if (!text.empty() && !text.ends_with('\n')) text += '\n';
for (const std::string& entry : added) text += std::format("{}\n", entry);
std::ofstream(ignore, std::ios::binary) << text;
std::println(" added {} to .gitignore", Dyn::Join(added, " and "));
}
// The skill.
for (const auto& [link, target] : links) {
fs::remove(link, error);
std::println(" removed the link {} -> {}", link.lexically_relative(cwd).string(), target.string());
}
fs::create_directories(skillDir);
const std::optional<std::string> shipped = Platform::ReadFile(Home() / "process" / "skill" / "prepush" / "SKILL.md");
if (!shipped) return refuse(std::format("cannot read the skill under {}", Home().string()));
const std::optional<std::string> existing = Platform::ReadFile(skill);
if (existing && !existing->contains(InitMarker) && !force) {
std::println(" kept .claude/skills/prepush/SKILL.md (init did not write it: --force replaces it)");
} else {
std::ofstream(skill, std::ios::binary) << WithMarker(*shipped);
std::println(" {} .claude/skills/prepush/SKILL.md", existing ? "refreshed" : "wrote ");
}
// What comes next.
const Config::Loaded loaded = Config::Load(cwd);
const std::vector<std::string> off = Config::LensesOff(loaded.config);
const Json environments = Config::Environments(loaded.config);
bool browser = false;
for (auto it = environments.begin(); it != environments.end(); ++it) {
const Plan::Environment* environment = Plan::FindEnvironment(Dyn::Str(it.value()));
if (!std::ranges::contains(off, it.key()) && environment && !environment->mcp.empty()) browser = true;
}
std::println("\nnext:\n {} config - until it reports no problem", Invocation());
if (off.size() == Plan::Lenses.size()) std::println(" turn lenses on in .review-gates/config.json (lenses.<name>.run = true) - every lens is off until the repository says which run");
if (browser) std::println(" {} browser install - a lens drives the application in Chromium", Invocation());
return 0;
}
}
std::int32_t Main(std::span<const std::string_view> args) {
const std::span<const std::string_view> rest = args.empty() ? args : args.subspan(1);
// The supervisor first and alone: it is single-threaded and has to
// stay so, and it runs between the driver and claude, not as a
// command anyone types.
if (!rest.empty() && rest.front() == Process::SuperviseVerb) return Process::SuperviseMain(rest.subspan(1));
Platform::Init();
// Before any other thread: SIGINT, SIGTERM and SIGHUP go to a thread of
// their own, which stops a run once it is under way and otherwise ends
// the process.
Process::InstallSignalThread(Run::OnSignal);
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
// The options before the command: --config FILE or --config=FILE, and
// any unique prefix of a long option.
std::optional<std::string> configPath;
std::size_t at = 0;
while (at < rest.size() && rest[at].starts_with("--") && rest[at] != "--") {
const std::string_view arg = rest[at];
const std::size_t equals = arg.find('=');
const std::string_view name = arg.substr(0, equals);
if (name.size() > 2 && std::string_view("--config").starts_with(name)) {
if (equals != std::string_view::npos) {
configPath = std::string(arg.substr(equals + 1));
} else if (at + 1 < rest.size()) {
configPath = std::string(rest[++at]);
} else {
return UsageError(Usage, "crafter-review", "argument --config: expected one argument");
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
}
} else if (name.size() > 2 && std::string_view("--version").starts_with(name)) {
return PrintVersion();
} else if (name.size() > 2 && std::string_view("--help").starts_with(name)) {
std::println("{}", Usage);
return 0;
} else {
return UsageError(Usage, "crafter-review", std::format("unrecognized arguments: {}", arg));
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
}
++at;
}
if (at < rest.size() && rest[at] == "-h") {
std::println("{}", Usage);
return 0;
}
if (at >= rest.size()) return UsageError(Usage, "crafter-review", "a command is required");
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
const std::string_view command = rest[at];
const std::span<const std::string_view> after = rest.subspan(at + 1);
try {
// The helpers a lens runs: they read neither the home nor the
// config, only their arguments, stdin and REVIEW_GATES_DB.
if (command == "sql") return PgWire::SqlMain(after);
if (command == "totp") return Totp::Main(after);
if (command == "inventory" && !configPath) return Inventory::Main(after);
if (command == "config") {
const Parsed parsed = ParseOptions(after, {});
if (parsed.help) {
std::print("{}", Help("crafter-review config", "print the merged consumer config and its problems", {}));
return 0;
}
if (!parsed.error.empty()) return UsageError(Usage, "crafter-review", parsed.error);
return Config::Main(fs::current_path(), configPath ? fs::path(*configPath) : fs::path());
}
if (command == "freeze") return FreezeCommand(after, configPath);
if (command == "run") return RunCommand(after, configPath);
if (command == "slots") return SlotsCommand(after, configPath);
if (command == "seats") return SeatsCommand(after);
if (command == "stop") return StopCommand(after);
if (command == "doc") return DocCommand(after);
if (command == "browser") return BrowserCommand(after);
if (command == "init") return InitCommand(after);
} catch (const Dyn::Error& error) {
// An error no command catches: its message, exit 1.
std::println(std::cerr, "crafter-review: {}: {}", error.Kind(), error.what());
return 1;
} catch (const std::exception& error) {
// Anything else that escapes a command, as a traceback would.
std::println(std::cerr, "crafter-review: {}", error.what());
return 1;
feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
}
return UsageError(Usage, "crafter-review", std::format("argument cmd: invalid choice: '{}'", command));
}
}