Run: gate 0 (the artifact, the trees, the inventory, the run lock, the scratch dir, dispositions, reuse state, resume, reads per round, seats, the board), the stack and migration preflights, the prompts, reuse of whole and block verdicts, salvage, a gate on the pool, checkpoints, the dump, the dry-run report and the unit-state store; slots_plan and app_ready beside it. The CLI parses run's 28 options, types and choices included. A full run takes its lifetime lock beside the board before it joins it. A signal - or the stop file `crafter-review stop` writes, looked at every 2 s - stops the run once gate 0 is through, and the stop stays in force for the gates after; before that it ends the process with its descendants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
249 lines
12 KiB
C++
249 lines
12 KiB
C++
// 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<void(const std::string&)>;
|
|
|
|
// `crafter-review run`'s options: the dump records them under `args`, in
|
|
// this order.
|
|
struct Arguments {
|
|
std::optional<std::string> config;
|
|
std::string mode = "commits";
|
|
std::string artifact;
|
|
std::optional<std::string> hash;
|
|
std::optional<std::string> dispositions;
|
|
std::string base;
|
|
std::string date;
|
|
std::optional<std::string> trees;
|
|
std::string contextPaths;
|
|
std::optional<std::string> only;
|
|
std::optional<std::string> without;
|
|
// --units, split on commas.
|
|
std::optional<std::set<std::string>> units;
|
|
bool warm = false;
|
|
std::int64_t concurrency = 128;
|
|
std::string effort;
|
|
std::int64_t reads = Plan::Reads;
|
|
bool noReuse = false;
|
|
bool noSalvage = false;
|
|
bool dryRun = false;
|
|
std::optional<std::string> claudeBin;
|
|
std::string models;
|
|
std::optional<std::string> scratch;
|
|
std::optional<std::string> seats;
|
|
std::optional<std::string> envFile;
|
|
bool noProbe = false;
|
|
std::optional<double> apiBudget;
|
|
std::string harness = "claude";
|
|
std::optional<std::string> variant;
|
|
std::optional<std::string> opencodeBin;
|
|
};
|
|
// vars(a) without `cmd`, the units sorted.
|
|
Json ArgsJson(const Arguments& a);
|
|
|
|
// die(): "preflight: <message>" 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 <url><path>?
|
|
// Below 500 is ok.
|
|
std::pair<bool, std::string> AppReady(std::string_view url, std::string_view path = "/", double timeout = 10);
|
|
|
|
// `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<std::int64_t, std::vector<std::string>> SlotsPlan(const Json& cfg, const std::filesystem::path& repo, const std::optional<std::filesystem::path>& artifact = std::nullopt,
|
|
const std::optional<std::filesystem::path>& scratchRoot = std::nullopt, const Log& log = {});
|
|
|
|
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<std::filesystem::path> 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<std::string, std::string> AppLenses(std::span<const std::string> lensFiles) const;
|
|
std::map<std::string, std::string> DbLenses(std::span<const std::string> lensFiles) const;
|
|
void InstrumentPreflight(std::span<const std::string> lensFiles);
|
|
static std::optional<std::string> MigrationVersion(std::string_view file);
|
|
// (problem, note): the migrations at the range HEAD against the ones
|
|
// the database applied.
|
|
std::pair<std::optional<std::string>, std::optional<std::string>> MigrationsDrift(const std::string& url);
|
|
std::unique_ptr<Pool::Stacks> LoadStacks();
|
|
std::string StacksLine() const;
|
|
std::vector<Json> DbStacks(std::span<const std::string> lensFiles) const;
|
|
void StackPreflight(std::span<const std::string> lensFiles);
|
|
std::optional<std::string> LoginMissing(const Json& stack) const;
|
|
std::optional<std::string> TreeDrift();
|
|
|
|
// ---- prompts ---------------------------------------------------------------
|
|
// (appended system prompt file, stdin user message) of a pass.
|
|
std::pair<std::filesystem::path, std::string> PromptFor(const Json& job);
|
|
std::optional<std::string> CacheKey(Json& job);
|
|
bool WarmProbe(const Json& job) const;
|
|
|
|
// ---- reuse -----------------------------------------------------------------
|
|
const Plan::CommitCode& RangeCode();
|
|
std::optional<Json> ReuseWhole(const std::string& name, Json& job);
|
|
std::optional<std::string> ReuseKey(const Json& job, const std::optional<std::string>& environment = std::nullopt);
|
|
std::vector<std::string> OtherKeys(const Json& job, const std::optional<std::string>& key);
|
|
std::optional<std::pair<Json, Json>> 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<Json, Json> 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<Json> RunGate(std::string_view title, std::vector<std::string> lensFiles);
|
|
std::unique_ptr<Pool::Admission> Admission(std::span<const std::string> lensFiles);
|
|
std::optional<Json> Resumable(const std::string& name, const std::string& lensFile, std::string_view title);
|
|
void Checkpoint(std::string_view title);
|
|
std::vector<Json> PrepareBlocks(const std::string& name, std::vector<Json> blocks, std::map<std::string, std::vector<Json>>& 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<std::string> 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<std::string> logs;
|
|
double t0 = 0;
|
|
std::optional<std::filesystem::path> logFile;
|
|
std::vector<Json> records;
|
|
std::vector<Json> results;
|
|
std::map<std::string, std::int64_t> lensLayersBytes;
|
|
std::vector<Json> jobsRun;
|
|
std::map<std::string, std::string> summaries;
|
|
std::optional<std::string> abort;
|
|
Pool::Scheduler* sched = nullptr;
|
|
std::optional<Plan::CommitCode> commitCode;
|
|
std::int64_t x = 1;
|
|
std::int64_t round = 1;
|
|
bool newRound = false;
|
|
Json owed = Json::object();
|
|
std::map<std::string, Json> wholeParts;
|
|
std::optional<std::string> dbChecked;
|
|
std::optional<bool> dbBypass;
|
|
std::optional<std::string> dbRole;
|
|
std::unique_ptr<PgWire::Broker> broker;
|
|
std::unique_ptr<Pool::Stacks> stacks;
|
|
bool slotMode = false;
|
|
std::vector<std::string> slotProblems;
|
|
std::unique_ptr<SeatPool::Board> board;
|
|
std::unique_ptr<State::RunLock> runLock;
|
|
std::unique_ptr<Platform::LifetimeLock> lifetime;
|
|
std::vector<std::string> trees;
|
|
std::vector<std::string> contextPaths;
|
|
std::string hash;
|
|
std::string h8;
|
|
std::string head;
|
|
std::filesystem::path gateDir;
|
|
Json units = Json::array();
|
|
std::vector<std::string> lines;
|
|
bool testMode = false;
|
|
std::filesystem::path scratch;
|
|
std::filesystem::path dumpPath;
|
|
std::vector<std::string> notReviewed;
|
|
std::vector<Json> dispEntries;
|
|
std::vector<Json> dispRows;
|
|
std::vector<Json> inForce;
|
|
std::filesystem::path statePath;
|
|
Json state = State::EmptyState();
|
|
std::vector<std::filesystem::path> salvageDirs;
|
|
std::filesystem::path resumePath;
|
|
Json resume;
|
|
std::map<std::string, std::string> charters;
|
|
std::map<std::string, std::string> charterSha;
|
|
std::string systemText;
|
|
// {sha: path}, in the order first written.
|
|
std::vector<std::pair<std::string, std::filesystem::path>> systemPaths;
|
|
// {lens: the shas of its system files}, lenses in the order first met.
|
|
std::vector<std::pair<std::string, std::set<std::string>>> systemByLens;
|
|
std::string runLayer;
|
|
std::optional<std::string> loginFixture;
|
|
std::string appBlock;
|
|
std::map<std::string, std::string> schemas;
|
|
std::unique_ptr<Memory::Calibration> calibration;
|
|
std::unique_ptr<Memory::Sampler> sampler;
|
|
std::unique_ptr<Pool::Runner> runner;
|
|
Json apiCfg = Json::object();
|
|
std::unique_ptr<SeatPool::Ledger> ledger;
|
|
std::vector<Json> seats;
|
|
std::map<std::filesystem::path, Json> envMarkers;
|
|
std::set<std::pair<std::string, std::string>> envSkips;
|
|
std::map<std::pair<std::string, std::string>, std::tuple<std::map<std::string, std::string>, std::vector<std::string>, std::map<std::string, std::string>>> migCache;
|
|
// The log every line goes to (the tests put their own here).
|
|
Log log;
|
|
|
|
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<std::string> stopped;
|
|
std::atomic<bool> ready = false;
|
|
std::optional<std::filesystem::path> 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);
|
|
}
|