// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Review:Run; import std; import :Json; import :Plan; import :Pool; import :SeatPool; import :Memory; import :PgWire; import :State; import :Platform; // Run: the driver for a commit range about to be pushed // (process/prepush.md). One claude process per inventory unit per lens, // whole passes where a lens has one (each gating its block passes), // fail-closed coverage per unit, the matrix, per-unit reuse across hashes, a // seat pool. // // Gates: 1 repo-better-solution (approach) -> 2 repo-functional (the range // exercised in the running application) -> 3 repo-bug-prover and // repo-security -> 4 the owner. SEND-READY = push-ready. Exit codes: 0 // SEND-READY, 1 FIX FIRST, 2 INCOMPLETE / PARTIAL / CONFIRMING (gates clean, // code units still owe clean reads - run the same line again), 3 preflight. // // A Run is built empty and filled field by field, as the tests fill one, so // its attributes are public; Main builds and runs the real one. export namespace Crafter::Review::Run { using Log = std::function; // `crafter-review run`'s options: the dump records them under `args`, in // this order. struct Arguments { std::optional config; std::string mode = "commits"; std::string artifact; std::optional hash; std::optional dispositions; std::string base; std::string date; std::optional trees; std::string contextPaths; std::optional only; std::optional without; // --units, split on commas. std::optional> units; bool warm = false; // A cap on the passes in flight on this machine, every driver's // counted; none by default (2026-10-31, Jorijn: "there is no need to // do a global cap anymore, the cap should be 128 per available // account"): each account's own cap is the limit. std::optional concurrency; std::string effort; std::int64_t reads = Plan::Reads; bool noReuse = false; bool noSalvage = false; bool dryRun = false; std::optional claudeBin; std::string models; std::optional scratch; std::optional seats; std::optional envFile; bool noProbe = false; std::optional apiBudget; std::string harness = "claude"; std::optional variant; std::optional opencodeBin; }; // vars(a) without `cmd`, the units sorted. Json ArgsJson(const Arguments& a); // die(): "preflight: " on stderr, exit 3. class Preflight : public std::runtime_error { public: using std::runtime_error::runtime_error; }; // (ok, detail): does the application under test answer at ? // Below 500 is ok. std::pair AppReady(std::string_view url, std::string_view path = "/", double timeout = 10); // The machine's memory as SlotsPlan reads it (the tests give their own). struct Machine { std::function()> available; std::function reserve; }; // `slots`: how many stacks to bring up - as many as free memory holds, // and no more than the range's browser passes can use at once - and the // commands for the ones not up yet. (want, commands). std::pair> SlotsPlan(const Json& cfg, const std::filesystem::path& repo, const std::optional& artifact = std::nullopt, const std::optional& scratchRoot = std::nullopt, const Log& log = {}, const Machine& machine = {}); // ({version: path} of the migrations at the range HEAD, the range's added // or changed migrations, {path: fingerprint at HEAD} of those). using MigrationsHead = std::tuple, std::vector, std::map>; using QueryResult = std::pair, std::vector>>>; class Run { public: Run(); Run(Arguments a, Json cfg); ~Run(); Run(const Run&) = delete; Run& operator=(const Run&) = delete; // The whole run: the gates, the dump, the summary; the exit code. std::int32_t Execute(); // A signal or `crafter-review stop`: every running pass killed, the // pool aborted, and the stop stays in force for the gates after. void Stop(const std::string& why, std::int32_t signal = 0); // The file `crafter-review stop` writes, once the scratch dir is // known. std::optional StopFile() const; // The handler a signal reaches: false before gate 0 set it up (the // signal then ends the process). bool Ready() const; // ---- gate 0 and its preflights --------------------------------------- void Gate0(); std::map AppLenses(std::span lensFiles) const; std::map DbLenses(std::span lensFiles) const; void InstrumentPreflight(std::span lensFiles); static std::optional MigrationVersion(std::string_view file); MigrationsHead MigrationsAtHead(); // (problem, note): the migrations at the range HEAD against the ones // the database applied. std::pair, std::optional> MigrationsDrift(const std::string& url); std::unique_ptr LoadStacks(); std::string StacksLine() const; std::vector DbStacks(std::span lensFiles) const; void StackPreflight(std::span lensFiles); std::optional LoginMissing(const Json& stack) const; std::optional TreeDrift(); // ---- prompts --------------------------------------------------------------- // (appended system prompt file, stdin user message) of a pass. std::pair PromptFor(const Json& job); std::optional CacheKey(Json& job); bool WarmProbe(const Json& job) const; // ---- reuse ----------------------------------------------------------------- const Plan::CommitCode& RangeCode(); std::optional ReuseWhole(const std::string& name, Json& job); std::optional ReuseKey(const Json& job, const std::optional& environment = std::nullopt); std::vector OtherKeys(const Json& job, const std::optional& key); std::optional> Salvage(const Json& job); Json DirEnvironments(const std::filesystem::path& dir); static std::int64_t DirRound(const std::filesystem::path& dir); std::int64_t ReadsNeeded(const Json& job) const; // ---- one process, a gate ------------------------------------------------- std::pair ExecJob(const Json& job, std::int64_t attempt); Json PartFrom(const Json& job, const Json& rec, Json raw); static Json DryPart(const Json& job); std::vector RunGate(std::string_view title, std::vector lensFiles); std::unique_ptr Admission(std::span lensFiles); std::optional Resumable(const std::string& name, const std::string& lensFile, std::string_view title); void Checkpoint(std::string_view title); std::vector PrepareBlocks(const std::string& name, std::vector blocks, std::map>& parts); // ---- the end --------------------------------------------------------------- std::int32_t Finish(std::string overall, const Json& stoppedAt, Json incomplete); Json BuildDump(std::string_view overall, const Json& stoppedAt, const Json& incomplete); Json DryReport(); bool Storable(const Json& rec) const; Json StoreState(); void Message(const std::string& message); // ---- the run's state ----------------------------------------------------- Arguments a; Json cfg = Json::object(); std::vector off; // {lens file: environment}, in the config's lens order: the dump, the // resume file and the scratch dir's marker write it in that order. Json environments = Json::object(); std::filesystem::path repo; std::vector logs; double t0 = 0; std::optional logFile; std::vector records; std::vector results; std::map lensLayersBytes; std::vector jobsRun; std::map summaries; std::optional abort; Pool::Scheduler* sched = nullptr; std::optional commitCode; std::int64_t x = 1; std::int64_t round = 1; bool newRound = false; Json owed = Json::object(); std::map wholeParts; std::optional dbChecked; std::optional dbBypass; std::optional dbRole; std::unique_ptr broker; std::unique_ptr stacks; bool slotMode = false; std::vector slotProblems; std::unique_ptr board; std::unique_ptr runLock; std::unique_ptr lifetime; std::vector trees; std::vector contextPaths; std::string hash; std::string h8; std::string head; std::filesystem::path gateDir; Json units = Json::array(); std::vector lines; bool testMode = false; std::filesystem::path scratch; std::filesystem::path dumpPath; std::vector notReviewed; std::vector dispEntries; std::vector dispRows; std::vector inForce; std::filesystem::path statePath; Json state = State::EmptyState(); std::vector salvageDirs; std::filesystem::path resumePath; Json resume; std::map charters; std::map charterSha; std::string systemText; // {sha: path}, in the order first written. std::vector> systemPaths; // {lens: the shas of its system files}, lenses in the order first met. std::vector>> systemByLens; std::string runLayer; std::optional loginFixture; std::string appBlock; std::map schemas; std::unique_ptr calibration; std::unique_ptr sampler; std::unique_ptr runner; Json apiCfg = Json::object(); std::unique_ptr ledger; std::vector seats; std::map envMarkers; std::set> envSkips; std::map, MigrationsHead> migCache; // The log every line goes to (the tests put their own here). Log log; // ---- the seams a test replaces --------------------------------------------- // Each starts as the real one; the methods call through these. std::function(std::string_view url, std::string_view path)> appReady; std::function(std::string_view url)> probe; std::function query; std::function(std::string_view url)> bypassesRls; std::function migrationsAtHead; std::function, std::optional>(const std::string& url)> migrationsDrift; std::function()> treeDrift; std::function(const Json& job)> promptFor; std::function(const Json& job, const std::string& prompt, const std::filesystem::path& systemPath, std::string_view schema, std::int64_t maxTurns, std::int64_t timeoutMin, bool persist)> runPass; std::function(const std::string& name, const std::string& lensFile, std::string_view title)> resumable; std::function lensFiles)> instrumentPreflight; private: // What exec_job's threads share with the pool's: the system files, // the lens layers, the jobs run. mutable std::recursive_mutex shared; mutable std::mutex logLock; std::optional stopped; std::atomic ready = false; std::optional stopFile; }; // The `run` command: argv parsed by the CLI; the exit code. std::int32_t Main(Arguments a, const Json& cfg); // What SIGINT, SIGTERM and SIGHUP do (Process::InstallSignalThread): // stop the run once gate 0 is through; before that, and for every other // command, the process ends, its descendants with it. void OnSignal(std::int32_t signal); }