Crafter.Review/tests/Runs/main.cpp

635 lines
42 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Full runs end to end, at no cost: this binary as crafter-review on a
// scratch repository, and a claude stand-in (/bin/sh) that answers every pass
// from what the driver hands it. A run SIGKILLed in its last gate, then the
// next one resuming its first gate from the checkpoint and salvaging the
// outputs of its last; a run with nothing left to buy; a FIX FIRST; a stop by
// `crafter-review stop`'s file and by SIGTERM; a stop between two gates. No
// pass outlives any of them.
#include <signal.h>
#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) {
std::ifstream in(path, std::ios::binary);
return {std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
}
std::vector<std::string> Lines(const fs::path& path) {
const std::string text = Read(path);
return text.empty() ? std::vector<std::string>() : Dyn::Split(Dyn::RStrip(text, "\n"), "\n");
}
// claude for the runs: the lens from its system file, the units from its
// prompt, every unit clear. Each pass adds a line to $FAKE_DIR/calls (its
// pid, its lens, its units). FAKE_FIX: a lens:unit that comes back a
// defect; FAKE_SLEEP: seconds a pass waits before it answers - every pass,
// or the one FAKE_SLEEP_ON names. It speaks first, so the warm-up lets
// the passes behind it go.
constexpr std::string_view FakeClaude = R"sh(#!/bin/sh
set -f
[ "$1" = --version ] && { echo '2.1.314 (Claude Code)'; exit 0; }
sys=
model=
sid=
prev=
for a in "$@"; do
case $prev in
--append-system-prompt-file) sys=$a ;;
--model) model=$a ;;
--session-id) sid=$a ;;
esac
prev=$a
done
lens=$(sed -n 's/^# Lens: //p' "$sys" | head -n 1)
ids=$(sed -n -e 's/^SCOPE - BLOCK PASS, unit "\([^"]*\)".*/\1/p' -e 's/^SCOPE - SLOT PASS, unit "\([^"]*\)".*/\1/p' -e 's/^- \(whole:[a-z]*\) \[.*/\1/p')
echo "$$ $lens" $ids >> "$FAKE_DIR/calls"
echo "{\"type\":\"system\",\"subtype\":\"init\",\"session_id\":\"$sid\",\"model\":\"$model\",\"mcp_servers\":[],\"tools\":[\"Read\"]}"
echo '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"reading"}]}}'
pause=$FAKE_SLEEP
if [ -n "$FAKE_SLEEP_ON" ]; then
pause=
for id in $ids; do [ "$lens:$id" = "$FAKE_SLEEP_ON" ] && pause=$FAKE_SLEEP; done
fi
if [ -n "$pause" ]; then
sleep "$pause" &
echo "$!" >> "$FAKE_DIR/sleeps"
wait "$!"
fi
verdict=SEND-READY
findings=
coverage=
summary=
for id in $ids; do
v=clear
if [ "$lens:$id" = "$FAKE_FIX" ]; then
v=defect
verdict='FIX FIRST'
findings="{\"finding\":\"planted\",\"evidence\":\"hello.py:2\",\"proposedAction\":\"fix it\",\"items\":[\"$id\"]}"
fi
coverage="$coverage${coverage:+,}{\"item\":\"$id\",\"verdict\":\"$v\",\"basis\":\"read at HEAD\"}"
case $id in whole:*) summary=',"summary":"the approach holds"' ;; esac
done
echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\":\"ok\",\"structured_output\":{\"verdict\":\"$verdict\",\"findings\":[$findings],\"notes\":[],\"coverage\":[$coverage]$summary},\"usage\":{\"input_tokens\":1,\"output_tokens\":1},\"total_cost_usd\":0.01}"
)sh";
// The environment of every child: git with no user config (the user's
// signs commits), this checkout as the home, a board of the test's own,
// and none of the review or seat variables of the shell the test runs in.
std::map<std::string, std::string> Env;
Process::Captured Git(const fs::path& repo, std::vector<std::string> args) {
args.insert(args.begin(), {"git", "-C", repo.string()});
return Process::Run(args, std::nullopt, std::chrono::seconds(60), {}, 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;
}
// The stand-in's processes and their sleeps that still run.
std::vector<std::string> Leftovers(const fs::path& fakeDir) {
std::vector<std::string> alive;
auto look = [&](const fs::path& list, std::string_view marker) {
for (const std::string& line : Lines(list)) {
const std::string pid = line.substr(0, line.find(' '));
const std::string cmdline = Read(std::format("/proc/{}/cmdline", pid));
if (cmdline.contains(marker)) alive.push_back(std::format("{}: {}", pid, Dyn::Replace(cmdline, std::string(1, '\0'), " ")));
}
};
look(fakeDir / "calls", (fakeDir / "claude").string());
look(fakeDir / "sleeps", "sleep");
return alive;
}
// Leftovers once the supervisors have had their moment.
std::vector<std::string> LeftoversAfter(const fs::path& fakeDir) {
for (std::int32_t i = 0; i < 100; ++i) {
if (Leftovers(fakeDir).empty()) break;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return Leftovers(fakeDir);
}
bool WaitFor(const std::function<bool()>& ready, std::chrono::seconds limit) {
const auto end = std::chrono::steady_clock::now() + limit;
while (std::chrono::steady_clock::now() < end) {
if (ready()) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return ready();
}
// The dump of the highest run number on this hash.
Json LatestDump(const fs::path& gateDir, std::string_view h8) {
const std::string prefix = std::format("commits-{}-run-", h8);
std::int64_t best = 0;
fs::path found;
for (const fs::directory_entry& entry : fs::directory_iterator(gateDir)) {
const std::string name = entry.path().filename().string();
if (!name.starts_with(prefix) || !name.ends_with(".json")) continue;
const std::string number = name.substr(prefix.size(), name.size() - prefix.size() - 5);
if (number.empty() || !std::ranges::all_of(number, [](char c) { return c >= '0' && c <= '9'; })) continue;
if (const std::int64_t n = std::stoll(number); n > best) {
best = n;
found = entry.path();
}
}
return found.empty() ? Json() : Dyn::LoadJsonFile(found);
}
// The run number in a dump's own name.
std::int64_t RunNumber(const Json& dump) {
if (!dump.is_object()) return 0;
const std::string path = Dyn::Str(dump.at("dumpPath"));
const std::size_t at = path.rfind("-run-");
return at == std::string::npos ? 0 : std::stoll(path.substr(at + 5));
}
std::int64_t Count(const Json& records, std::string_view key, const Json& value) {
return std::ranges::count_if(records, [&](const Json& r) { return Dyn::Get(r, key) == value; });
}
std::string Text(const Json& value) {
return value.is_string() ? value.get<std::string>() : value.dump();
}
std::int32_t Checks() {
std::println("runs: a full run on a claude stand-in - killed and salvaged, resumed, nothing left to buy, FIX FIRST, stopped by the stop file, by SIGTERM and between gates");
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-runs-{}", Platform::PublishedPid());
fs::remove_all(base);
const fs::path repo = base / "repo";
const fs::path fakeDir = base / "fake";
const fs::path fake = fakeDir / "claude";
const fs::path scratchRoot = base / "scratch";
Write(fake, FakeClaude);
fs::permissions(fake, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec);
for (const auto& [k, v] : Platform::Environment()) {
const bool review = k.starts_with("REVIEW_GATES_") || k.starts_with("CRAFTER_REVIEW_") || k == "SEATS_ENV_FILE";
const bool seat = k.starts_with("CLAUDE_CODE_OAUTH_TOKEN") || k.starts_with("ANTHROPIC_API_KEY") || k.starts_with("OPENROUTER_KEY") || k.starts_with("FAKE_");
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"}, {"GIT_AUTHOR_NAME", "Me"}, {"GIT_AUTHOR_EMAIL", "me@example.com"},
{"GIT_COMMITTER_NAME", "Me"}, {"GIT_COMMITTER_EMAIL", "me@example.com"}, {"CRAFTER_REVIEW_HOME", checkout.string()},
{"REVIEW_GATES_BOARD", (base / "board.json").string()}, {"FAKE_DIR", fakeDir.string()}});
// A repository with one commit to review, the three static code lenses
// on (every lens is off until a config turns it on; the functional
// lens needs a running application, the commit-truth lens gets the
// last scenario).
fs::create_directories(repo);
Git(repo, {"init", "-q", "-b", "main"});
Git(repo, {"config", "user.email", "me@example.com"});
Git(repo, {"config", "user.name", "Me"});
Write(repo / ".gitignore", ".review-gates/push/\n.review-gates/trees/\n");
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
Write(repo / ".review-gates" / "config.json", R"({"modes": {"commits": {"lenses": ["repo-better-solution", "repo-bug-prover", "repo-security"]}}})");
Write(repo / "README.md", "a greeter\n");
Git(repo, {"add", "-A"});
Git(repo, {"commit", "-q", "-m", "base"});
const std::string baseSha = Dyn::Strip(Git(repo, {"rev-parse", "HEAD"}).out);
Write(repo / "hello.py", "def hello(name):\n return f'hello {name}'\n");
Git(repo, {"add", "hello.py"});
Git(repo, {"commit", "-q", "-m", "add hello\n\nThe greeting the README promises."});
const Process::Captured frozen = Process::Run({Platform::SelfExe().string(), "freeze", "--slug", "x", "--base", baseSha}, repo, std::chrono::seconds(60), {}, Env);
const fs::path artifact = repo / ".review-gates" / "push" / "x";
const fs::path gateDir = artifact / "gate-state";
Json freezeJson;
try {
freezeJson = Dyn::LoadJsonFile(artifact / "freeze.json");
} catch (const std::exception&) {
Check("freeze: the scratch range freezes", false, frozen.out + frozen.err);
return 1;
}
const std::string hash = freezeJson.at("hash").get<std::string>();
const std::string h8 = hash.substr(0, 8);
auto runArgs = [&](std::vector<std::string> extra) {
std::vector<std::string> args = {Platform::SelfExe().string(), "run", "--artifact", ".review-gates/push/x", "--hash", hash, "--base", baseSha, "--trees", ".",
"--dispositions", ".review-gates/push/x/gate-state/dispositions.json", "--date", Text(freezeJson.at("date")),
"--claude-bin", fake.string(), "--scratch", scratchRoot.string(), "--seats", "local", "--no-probe", "--reads", "1"};
std::ranges::move(extra, std::back_inserter(args));
return args;
};
auto run = [&](std::vector<std::string> extra, const std::map<std::string, std::string>& env = {}) {
return Process::Run(runArgs(std::move(extra)), repo, std::chrono::seconds(60), {}, With(Env, env));
};
auto start = [&](std::vector<std::string> extra, const std::map<std::string, std::string>& env, std::string_view name) {
return Process::Start({.argv = runArgs(std::move(extra)),
.environment = With(Env, env),
.cwd = repo,
.input = {.kind = Process::Stream::Kind::Null},
.output = {.kind = Process::Stream::Kind::File, .file = base / std::format("{}.out", name)},
.error = {.kind = Process::Stream::Kind::File, .file = base / std::format("{}.err", name)}});
};
auto calls = [&] { return static_cast<std::int64_t>(Lines(fakeDir / "calls").size()); };
// The plan, from a dry run: what every full run below buys.
const Process::Captured dry = run({"--dry-run"});
Json plan;
try {
plan = Dyn::LoadJsonFile(scratchRoot / std::format("{}-run-1-dry", h8) / "dry-run.json");
} catch (const std::exception&) {
}
const Json& planned = plan.is_object() ? plan.at("processes") : Json::array();
const auto all = static_cast<std::int64_t>(planned.size());
const std::int64_t gate1 = Count(planned, "lens", "repo-better-solution");
std::string securityUnit;
std::string proverUnit;
for (const Json& p : planned) {
if (p.at("kind") != "block") continue;
if (securityUnit.empty() && p.at("lens") == "repo-security") securityUnit = Text(p.at("unit"));
if (proverUnit.empty() && p.at("lens") == "repo-bug-prover") proverUnit = Text(p.at("unit"));
}
Check("dry run: the range plans the approach lens's whole pass and blocks, then the bug-prover's and the security lens's blocks - and no pass reaches claude",
dry.returnCode == 2 && all > gate1 && gate1 > 1 && !securityUnit.empty() && !proverUnit.empty() && calls() == 0 && Count(planned, "lens", "repo-functional") == 0,
dry.out.substr(dry.out.size() > 2000 ? dry.out.size() - 2000 : 0) + dry.err);
// Dry runs with no full run between them share one dir: the next
// drops the last one's prompts.
const fs::path dryDir = scratchRoot / std::format("{}-run-1-dry", h8);
Write(dryDir / "prompts" / "stale.txt", "a prompt of another range");
const Process::Captured again = run({"--dry-run"});
std::int64_t prompts = 0;
for (const fs::directory_entry& entry : fs::directory_iterator(dryDir / "prompts")) prompts += entry.path().extension() == ".txt" ? 1 : 0;
Check("dry run: a second dry run on a hash counts its own prompts, the one before's gone", again.returnCode == 2 && !fs::exists(dryDir / "prompts" / "stale.txt") && prompts == all,
std::to_string(prompts));
// Killed in its last gate: one security pass never answers, and the
// driver is SIGKILLed once every other pass has.
const fs::path scratchA = scratchRoot / std::format("{}-run-1", h8);
const Process::Child a = start({}, {{"FAKE_SLEEP", "571"}, {"FAKE_SLEEP_ON", std::format("repo-security:{}", securityUnit)}}, "a");
auto answered = [&] {
std::int64_t n = 0;
std::error_code error;
for (fs::directory_iterator it(scratchA / "out", error), end; !error && it != end; it.increment(error)) {
if (it->path().extension() == ".jsonl" && Read(it->path()).contains(R"("type":"result")")) ++n;
}
return n;
};
const bool settled = WaitFor([&] { return calls() == all && answered() == all - 1; }, std::chrono::seconds(30));
Process::Signal(a.pid, SIGKILL);
const std::optional<std::int32_t> aCode = Process::Wait(a.pid, std::chrono::seconds(10));
const std::vector<std::string> aLeft = LeftoversAfter(fakeDir);
Json resumeA;
try {
resumeA = Dyn::LoadJsonFile(gateDir / std::format("resume-{}.json", h8));
} catch (const std::exception&) {
}
const Json dumpA = LatestDump(gateDir, h8);
Check("SIGKILL: a driver killed in its last gate takes its passes with it (each pass's supervisor ends its tree); its first gate is on record in the checkpoint and the partial dump",
settled && aCode == -9 && aLeft.empty() && dumpA.is_object() && Text(dumpA.at("overall")).starts_with("IN PROGRESS - after Gate 1 — Approach") && resumeA.is_object()
&& resumeA.at("results").size() == 1 && resumeA.at("results").at(0).at("lens") == "repo-better-solution",
std::format("settled {}, exit {}, left {}, {} of {} answered, {}", settled, aCode.value_or(0), Dyn::Join(aLeft, "; "), answered(), all, resumeA.dump().substr(0, 300)));
// The next run: the first gate resumed, the outputs of the killed run
// salvaged, the one pass that never answered bought again.
std::int64_t before = calls();
const Process::Captured b = run({});
const Json dumpB = LatestDump(gateDir, h8);
const Json& processesB = dumpB.is_object() ? dumpB.at("processes") : Json::array();
Check("the next run resumes the first gate from the checkpoint, salvages every output the killed run finished and buys only the pass that never answered - SEND-READY, exit 0",
b.returnCode == 0 && calls() - before == 1 && dumpB.is_object() && dumpB.at("overall") == "SEND-READY" && dumpB.at("resumedLenses") == Json::array({"repo-better-solution"})
&& std::ranges::count_if(processesB, [](const Json& r) { return Dyn::Truthy(Dyn::Get(r, "salvaged")); }) == all - gate1 - 1
&& Count(processesB, "status", "ok") == all - gate1,
std::format("exit {}, {} call(s), {}", b.returnCode, calls() - before, dumpB.is_object() ? Text(dumpB.at("overall")) : b.out + b.err));
const Json stored = State::LoadState(gateDir / "unit-state.json").at("entries");
Check("...and stores the clean block results of the range for reuse", static_cast<std::int64_t>(stored.size()) >= all - gate1, std::to_string(stored.size()));
// Nothing left to buy.
before = calls();
const Process::Captured c = run({});
const Json dumpC = LatestDump(gateDir, h8);
Check("a run on the same hash has nothing left to buy: every lens resumes, claude is never called, SEND-READY",
c.returnCode == 0 && calls() == before && dumpC.is_object() && dumpC.at("overall") == "SEND-READY" && dumpC.at("resumedLenses").size() == 3,
std::format("exit {}, {} call(s)", c.returnCode, calls() - before));
// A defect, on a run that reuses nothing; the scratch dir it claims
// holds a stop file from before, which is not this run's.
const std::string nextRun = std::format("{}-run-{}", h8, RunNumber(dumpC) + 1);
const fs::path stale = scratchRoot / nextRun / "stop";
Write(stale, "");
before = calls();
const Process::Captured d = run({"--no-reuse", "--no-salvage"},
{{"FAKE_FIX", std::format("repo-bug-prover:{}", proverUnit)}, {"FAKE_SLEEP", "3"}, {"FAKE_SLEEP_ON", "repo-better-solution:whole:order"}});
const Json dumpD = LatestDump(gateDir, h8);
Check("FIX FIRST: a defect in the last gate stops the run there, exit 1 - every pass bought again with --no-reuse --no-salvage",
d.returnCode == 1 && calls() - before == all && dumpD.is_object() && dumpD.at("overall") == "FIX FIRST" && dumpD.at("stoppedAt") == "Gate 3 — Proof",
std::format("exit {}, {} call(s), {}", d.returnCode, calls() - before, dumpD.is_object() ? Text(dumpD.at("stoppedAt")) : d.out + d.err));
Check("stop: a stop file left in the scratch dir a run claims is dropped - the run ran its course past the watcher's first look",
!fs::exists(stale) && dumpD.is_object() && Dyn::Str(dumpD.at("scratch")).ends_with(nextRun), nextRun);
// Stopped by `crafter-review stop`'s file, and by SIGTERM, while the
// first gate's whole pass is under way: the pass is killed, no later
// gate starts, nothing is left running.
std::vector<std::string> widths;
for (const bool bySignal : {false, true}) {
before = calls();
const std::string name = bySignal ? "term" : "file";
const Process::Child child = start({"--no-reuse", "--no-salvage"}, {{"FAKE_SLEEP", "573"}}, name);
const bool running = WaitFor([&] { return calls() > before; }, std::chrono::seconds(30));
// While it runs, the board shows its width: per account, there
// being no machine-wide cap.
std::string width;
try {
const Json drivers = Dyn::LoadJsonFile(base / "board.json").at("drivers");
for (auto it = drivers.begin(); it != drivers.end(); ++it) width = Text(Dyn::Get(Dyn::Get(it.value(), "settings"), "concurrency"));
} catch (const std::exception&) {
}
widths.push_back(width);
std::string scratch;
for (const std::string& line : Lines(base / std::format("{}.out", name))) {
if (const std::size_t at = line.rfind("; scratch "); at != std::string::npos) scratch = line.substr(at + 10);
}
if (bySignal) {
Process::Signal(child.pid, SIGTERM);
} else if (!scratch.empty()) {
Write(fs::path(scratch) / "stop", "");
}
const std::optional<std::int32_t> code = Process::Wait(child.pid, std::chrono::seconds(30));
if (!code) Process::Signal(child.pid, SIGKILL);
const std::vector<std::string> left = LeftoversAfter(fakeDir);
const Json dump = LatestDump(gateDir, h8);
const std::string why = bySignal ? "stopped by signal 15 (1 running processes killed)" : "stopped by crafter-review stop (1 running processes killed)";
Check(bySignal ? "SIGTERM: the run stops as the stop file stops it - the same abort, the same exit, nothing left running"
: "stop file: `crafter-review stop` stops the run - its running pass killed, no later gate started, INCOMPLETE, exit 2, nothing left running",
running && code == 2 && calls() - before == 1 && left.empty() && dump.is_object() && dump.at("stoppedAt") == std::format("Gate 1 — Approach - ABORTED ({})", why)
&& Text(dump.at("overall")).starts_with("INCOMPLETE - NOT SEND-READY"),
std::format("running {}, exit {}, {} call(s), left {}, {}", running, code.value_or(-1), calls() - before, Dyn::Join(left, "; "),
dump.is_object() ? Text(dump.at("stoppedAt")) : Read(base / std::format("{}.err", name))));
}
// No machine-wide width by default (2026-10-31): the dump's args record
// none, a live run's board entry shows the per-account one, and a
// width below 1 is refused.
const Process::Captured noWidth = run({"--concurrency", "0"});
Check("no global cap by default: the dump records none, a live run shows `128 per account` on the board; --concurrency below 1 is a usage error",
dumpB.is_object() && dumpB.at("args").at("concurrency").is_null() && widths == std::vector<std::string>{"128 per account", "128 per account"} && noWidth.returnCode == 3
&& noWidth.err.contains("argument --concurrency: a number from 1 up"),
std::format("{} | {}", Dyn::Join(widths, ", "), noWidth.err));
// An env file that is not UTF-8: the preflight stops the run.
Write(base / "bad.env", std::string("\xff\n", 2));
const Process::Captured badEnv = run({"--no-reuse", "--no-salvage", "--env-file", (base / "bad.env").string()});
Check("preflight: an env file that is not UTF-8 stops the run, exit 3, with the decoder's own words",
badEnv.returnCode == 3 && badEnv.err.contains("preflight: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"), badEnv.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
// One seats file for the settings and the tokens: the one --env-file
// names as the run's own parser reads it - a unique prefix, the last
// of a repeat - with the machine's own file as the decoy the usual
// lookup would find; the log names it with the settings put in.
const fs::path decoyXdg = base / "decoy-xdg";
Write(decoyXdg / "crafter-review" / "env", "REVIEW_GATES_API_BUDGET_USD=5\nCLAUDE_CODE_OAUTH_TOKEN1=tok-decoy\nANTHROPIC_API_KEY1=sk-ant-decoy\n");
Write(base / "other.env", "REVIEW_GATES_API_BUDGET_USD=7\nREVIEW_GATES_CLAUDE_BIN=/nowhere/claude\nCLAUDE_CODE_OAUTH_TOKEN2=tok-other\nANTHROPIC_API_KEY2=sk-ant-other\n");
const std::map<std::string, std::string> withDecoy = {{"XDG_CONFIG_HOME", decoyXdg.string()}, {"HOST_XDG_CONFIG_HOME", decoyXdg.string()}};
const std::string other = (base / "other.env").string();
const Process::Captured prefixed = run({"--env", other, "--seats", "auto", "--dry-run", "--scratch", (base / "env-prefix").string()}, withDecoy);
const Process::Captured repeated = run({"--env-file", (decoyXdg / "crafter-review" / "env").string(), "--env-file", other, "--seats", "auto", "--dry-run", "--scratch", (base / "env-repeat").string()}, withDecoy);
auto oneFile = [&](const Process::Captured& dry) {
return dry.returnCode == 2 && dry.out.contains(std::format("settings: REVIEW_GATES_API_BUDGET_USD from {} (under", other)) && dry.out.contains("api2 (api)") && dry.out.contains("2 (oauth)")
&& dry.out.contains(std::format("seats file: REVIEW_GATES_CLAUDE_BIN in {} - no placement setting, read from the process environment alone", other))
&& dry.out.contains(std::format("from {}; api-first, budget $7 per api seat per cycle", other)) && !dry.out.contains("api1") && !dry.out.contains("1 (oauth)");
};
Check("--env (a prefix) and the last of a repeated --env-file name the one file the settings are applied from and the tokens are loaded from, never the decoy",
oneFile(prefixed) && oneFile(repeated), std::format("{} {}\n{} {}", prefixed.returnCode, prefixed.out + prefixed.err, repeated.returnCode, repeated.out + repeated.err));
// A seats file that can be read only once - a FIFO, as `--env-file
// <(gpg -d seats.env.gpg)` gives one - is read once: the run's
// settings and its seats both come from that read (round 10).
const fs::path fifo = base / "seats.fifo";
std::vector<std::string> viaFifo = {
"sh", "-c",
std::format("mkfifo '{0}' && (printf 'REVIEW_GATES_API_BUDGET_USD=9\\nCLAUDE_CODE_OAUTH_TOKEN5=tok-five\\nANTHROPIC_API_KEY5=sk-ant-five\\n' > '{0}' &) && exec \"$@\"", fifo.string()), "sh"};
std::ranges::move(runArgs({"--env-file", fifo.string(), "--seats", "auto", "--dry-run", "--scratch", (base / "env-fifo").string()}), std::back_inserter(viaFifo));
const Process::Captured fromFifo = Process::Run(viaFifo, repo, std::chrono::seconds(60), {}, Env);
Check("a seats file that can be read only once (a FIFO) is read once: the run's settings and its seats both come from it",
fromFifo.returnCode == 2 && fromFifo.out.contains(std::format("settings: REVIEW_GATES_API_BUDGET_USD from {} (under", fifo.string())) && fromFifo.out.contains("api5 (api)")
&& fromFifo.out.contains("5 (oauth)") && fromFifo.out.contains(std::format("from {}; api-first, budget $9 per api seat per cycle", fifo.string())),
std::format("{} {}{}", fromFifo.returnCode, fromFifo.out, fromFifo.err));
// A filter that names nothing is refused: skipping its empty items
// must not leave no filter, which is a full run.
const Process::Captured noUnits = run({"--units", ","});
const Process::Captured noLenses = run({"--only", " , "});
Check("--units and --only that name nothing are usage errors (exit 3) - never a full run",
noUnits.returnCode == 3 && noUnits.err.contains("argument --units: names no unit id (',')") && noLenses.returnCode == 3
&& noLenses.err.contains("argument --only: names no lens file (' , ')"),
noUnits.err + noLenses.err);
// A patch that is not UTF-8 is no artifact: a preflight stop.
const fs::path badArtifact = repo / ".review-gates" / "push" / "bad";
fs::copy(artifact, badArtifact, fs::copy_options::recursive);
for (const fs::directory_entry& entry : fs::directory_iterator(badArtifact)) {
if (entry.path().extension() == ".patch") std::ofstream(entry.path(), std::ios::binary | std::ios::app) << std::string("\xff\n", 2);
}
const Process::Captured badPatch = Process::Run({Platform::SelfExe().string(), "run", "--artifact", ".review-gates/push/bad", "--base", baseSha, "--trees", ".", "--dispositions",
".review-gates/push/bad/gate-state/dispositions.json", "--claude-bin", fake.string(), "--scratch", scratchRoot.string(), "--dry-run"},
repo, std::chrono::seconds(60), {}, Env);
Check("preflight: a patch that is not UTF-8 stops the run, exit 3, saying so",
badPatch.returnCode == 3 && badPatch.err.contains("the artifact's patches are not UTF-8 ('utf-8' codec can't decode byte 0xff"), badPatch.err);
// An error that escapes a run ends it with its message, exit 1,
// through the run's cleanup.
const Process::Captured badReserve = run({"--no-reuse", "--no-salvage"}, {{"REVIEW_GATES_MEM_RESERVE_MB", "x"}});
Check("an error escaping a full run (a reserve that is no number) ends it with its ValueError, exit 1",
badReserve.returnCode == 1 && badReserve.err.contains("crafter-review: ValueError: could not convert string to float: 'x'"), badReserve.err);
// The commit-truth lens (2026-10-31), alone: a config that turns on
// only it (through --config: the tracked config file must stay as
// committed, or the tree is dirty) runs Gate 4 - its three text
// blocks and its two slot passes (the stand-in answers a SLOT PASS
// too) - push-ready, with the code units, the per-commit slots and
// the functional lens's slot listed as not reviewed.
{
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
Write(base / "facts.json", R"({"modes": {"commits": {"lenses": ["repo-facts"]}}})");
const std::int64_t before = calls();
std::vector<std::string> truthArgs = runArgs({"--no-reuse", "--no-salvage"});
truthArgs.insert(truthArgs.begin() + 1, {"--config", (base / "facts.json").string()});
const Process::Captured truth = Process::Run(truthArgs, repo, std::chrono::seconds(60), {}, Env);
const Json dumpT = LatestDump(gateDir, h8);
const Json& processesT = dumpT.is_object() ? dumpT.at("processes") : Json::array();
std::set<std::string> labels;
for (const Json& p : processesT) labels.insert(Text(p.at("label")));
const Json notReviewedT = dumpT.is_object() ? dumpT.at("coverage").at("notReviewed") : Json::array();
auto notReviewed = [&](std::string_view id) { return std::ranges::any_of(notReviewedT, [&](const Json& x) { return x == id; }); };
Check("the commit-truth lens alone: Gate 4 runs its three text blocks and its two slot passes (5 calls) - SEND-READY, exit 0; the code units, the per-commit slots and whole:behaviour are not reviewed",
truth.returnCode == 0 && calls() - before == 5 && dumpT.is_object() && dumpT.at("overall") == "SEND-READY" && dumpT.at("stoppedAt").is_null()
&& labels == std::set<std::string>{"repo-facts:block:p1:subject", "repo-facts:block:p1:msg", "repo-facts:block:p1:diffstat", "repo-facts:slot:whole:order", "repo-facts:slot:whole:absent"}
&& Count(processesT, "kind", "slot") == 2 && dumpT.at("lensesOff") == Json::array({"repo-better-solution", "repo-functional", "repo-bug-prover", "repo-security"})
&& notReviewed("p1:hello.py#1") && notReviewed("p1:absent") && notReviewed("whole:behaviour") && !notReviewed("whole:order") && !notReviewed("p1:msg"),
std::format("exit {}, {} call(s), {}", truth.returnCode, calls() - before, dumpT.is_object() ? Text(dumpT.at("overall")) : truth.out + truth.err));
}
// A workbench (2026-10-31): a directory that reviews another
// repository - its own config and env file, the artifact wherever
// --out says, the lenses' tree the frozen worktree. The run reads the
// repository under review from the freeze record: the prompt names
// it, not the working directory, and --trees defaults to the
// recorded worktree.
{
const fs::path bench = base / "bench";
fs::create_directories(bench / ".review-gates");
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
Write(bench / ".review-gates" / "config.json", R"({"modes": {"commits": {"lenses": ["repo-bug-prover"]}}})");
const fs::path out = bench / "journal" / "push" / "y";
const Process::Captured benchFreeze = Process::Run({Platform::SelfExe().string(), "freeze", "--repo", repo.string(), "--out", out.string(), "--worktree", "--base", baseSha},
bench, std::chrono::seconds(60), {}, Env);
Json benchRecord;
try {
benchRecord = Dyn::LoadJsonFile(out / "freeze.json");
} catch (const std::exception&) {
}
const std::string benchHash = benchRecord.is_object() ? Text(benchRecord.at("hash")) : std::string();
const std::string benchTree = benchRecord.is_object() ? Text(Dyn::Get(benchRecord, "worktree")) : std::string();
Check("a workbench freezes another repository: the record names it, and the worktree is under the workbench's .review-gates/trees/",
benchFreeze.returnCode == 0 && benchRecord.is_object() && Text(benchRecord.at("repo")) == Dyn::AbsPath(repo.string())
&& benchTree == std::format(".review-gates/trees/repo-{}", benchHash.substr(0, 8)) && fs::is_directory(bench / benchTree),
benchFreeze.out + benchFreeze.err);
const fs::path benchScratch = base / "bench-scratch";
const Process::Captured benchRun = Process::Run({Platform::SelfExe().string(), "run", "--artifact", "journal/push/y", "--hash", benchHash, "--base", baseSha, "--dispositions",
"journal/push/y/gate-state/dispositions.json", "--date", Text(freezeJson.at("date")), "--claude-bin", fake.string(), "--scratch",
benchScratch.string(), "--seats", "local", "--no-probe", "--dry-run"},
bench, std::chrono::seconds(60), {}, Env);
const fs::path benchDry = benchScratch / std::format("{}-run-1-dry", benchHash.substr(0, 8));
// The run layer is in the system prompts (system-<sha8>.md).
std::string systems;
std::error_code benchError;
for (fs::directory_iterator it(benchDry, benchError), end; !benchError && it != end; it.increment(benchError)) {
const std::string name = it->path().filename().string();
if (name.starts_with("system-") && name.ends_with(".md")) systems += Platform::ReadFile(it->path()).value_or("");
}
const std::string benchLog = Platform::ReadFile(benchDry / "log.txt").value_or("");
Check("...and a dry run from the workbench without --trees (exit 2, as every dry run) reads the recorded worktree, names the repository under review in the run layer, not the workbench, and logs it",
benchRun.returnCode == 2 && !systems.empty()
&& systems.contains(std::format("Repository under review (repo), checked out at the range HEAD (the last commit of the artifact): {} -", benchTree)) && !systems.contains("(bench)")
&& benchLog.contains(std::format("repository under review: {} (read at {})", Dyn::AbsPath(repo.string()), benchTree)),
std::format("exit {}, {} bytes of system prompt; {}{}", benchRun.returnCode, systems.size(), benchRun.out, benchRun.err));
}
// A stop between two gates: the next gate's pool starts aborted and
// runs nothing of it.
{
const fs::path fx = checkout / "tests" / "Goldens" / "fixture" / "commits";
Run::Run g;
g.log = [](const std::string&) {};
g.a.artifact = fx.string();
g.hash = Artifact::ArtifactHash("commits", fx);
g.testMode = true;
g.cfg = Config::Defaults();
g.environments = Config::Environments(g.cfg);
for (const Plan::Lens& lens : Plan::Lenses) {
const std::string lf(lens.file);
g.charters[lf] = Prompt::Charter(lf);
g.charterSha[lf] = Plan::Sha(g.charters[lf]);
}
const Inventory::Built built = Inventory::Build("commits", fx.string());
g.units = built.out.at("units");
g.lines = built.lines;
g.scratch = base / "between";
fs::create_directories(g.scratch);
g.calibration = std::make_unique<Memory::Calibration>(base / "between" / "memory.json", base, std::map<std::string, std::int64_t> {{"none", 210}});
std::int32_t ran = 0;
g.runPass = [&](const Json& job, const std::string&, const fs::path&, std::string_view, std::int64_t, std::int64_t, bool) {
++ran;
return std::pair<Json, Json>({{"label", job.at("label")}, {"status", "error"}, {"reason", "ran"}}, nullptr);
};
g.Stop("crafter-review stop");
const std::vector<Json> res = g.RunGate("Gate 3 — Proof", {"repo-bug-prover.md"});
Check("stop between gates: the next gate's pool starts aborted - none of its passes runs, and the abort is the run's",
ran == 0 && g.abort && g.abort->starts_with("stopped by crafter-review stop") && res.size() == 1 && !res[0].at("coverageMissing").empty(), g.abort.value_or("None"));
}
// The dry run's re-slice reads the artifact back from the run layer
// by stripping the line-number prefix - `0001| `, five digits from
// line 10000 on. A fixed six characters was wrong there: the first
// artifact past 9999 lines, the pipeline's own review of 2026-11-01,
// refused itself at preflight.
{
const fs::path big = base / "big";
fs::create_directories(big / ".review-gates");
Write(big / ".review-gates" / "config.json", R"({"modes": {"commits": {"lenses": ["repo-bug-prover"]}}})");
Git(big, {"init", "-q"});
Git(big, {"config", "user.email", "me@example.com"});
Git(big, {"config", "user.name", "Me"});
Write(big / "README.md", "# big\n");
Git(big, {"add", "."});
Git(big, {"commit", "-q", "-m", "base"});
const std::string bigBase = Dyn::Strip(Git(big, {"rev-parse", "HEAD"}).out);
std::string wide;
for (std::int32_t i = 0; i < 10'050; ++i) wide += std::format("line {}\n", i);
Write(big / "wide.txt", wide);
Git(big, {"add", "."});
Git(big, {"commit", "-q", "-m", "add a wide file\n\nTen thousand and fifty lines."});
const fs::path bigFake = base / "big-fake";
Write(bigFake, "#!/bin/sh\nexit 1\n");
fs::permissions(bigFake, fs::perms::owner_all);
const fs::path bigOut = base / "big-out";
const Process::Captured bigFreeze = Process::Run({Platform::SelfExe().string(), "freeze", "--out", bigOut.string(), "--base", bigBase}, big, std::chrono::seconds(60), {}, Env);
Json bigRecord;
try {
bigRecord = Dyn::LoadJsonFile(bigOut / "freeze.json");
} catch (const std::exception&) {
}
const std::string bigHash = bigRecord.is_object() ? Text(bigRecord.at("hash")) : std::string();
std::string patchText;
std::error_code bigError;
for (fs::directory_iterator it(bigOut, bigError), end; !bigError && it != end; it.increment(bigError)) {
if (it->path().extension() == ".patch") patchText = Platform::ReadFile(it->path()).value_or("");
}
const fs::path bigScratch = base / "big-scratch";
const Process::Captured bigRun = Process::Run({Platform::SelfExe().string(), "run", "--artifact", bigOut.string(), "--hash", bigHash, "--base", bigBase, "--date", "2026-11-01", "--claude-bin",
bigFake.string(), "--scratch", bigScratch.string(), "--seats", "local", "--no-probe", "--dry-run"},
big, std::chrono::seconds(120), {}, Env);
Json bigDry;
try {
bigDry = Dyn::LoadJsonFile(bigScratch / std::format("{}-run-1-dry", bigHash.substr(0, 8)) / "dry-run.json");
} catch (const std::exception&) {
}
Check("an artifact past 9999 lines: the dry run's re-slice of the run layer's artifact matches every unit (the prefix is read to its bar, not six characters)",
bigFreeze.returnCode == 0 && std::ranges::count(patchText, '\n') > 10'000 && bigRun.returnCode == 2 && bigDry.is_object() && Dyn::Truthy(bigDry.at("dryRun").at("resliceOk")),
std::format("freeze {} run {}: {}{}", bigFreeze.returnCode, bigRun.returnCode, Dyn::Slice(bigRun.out, -600), bigRun.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 - the driver, its
// supervisor, its helpers - 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, "runs: {}", error.what());
return 1;
}
}