feat(run): the repository under review is the freeze record's, not the working directory's

A directory can review repositories other than itself: a workbench whose
config, env file and records serve several, freezing each with --repo and
--out. The run took the working directory for the repository under review:
the prompt named it, the calibration was kept under it, and --trees fell
back to it. The run now reads the repository from the artifact's
freeze.json - the prompt names it and the calibration is kept under it -
and --trees defaults to what the record says: the worktree, else the
repository when it is not the working directory, else `.`, the same rule
that builds the printed run line. The working directory keeps its roles:
the config, the env file, the slots, the lens processes' cwd and every
logged path. A damaged record stops the run at preflight.
This commit is contained in:
Jorijn van der Graaf 2026-10-31 21:31:48 +01:00
commit 4fc15fe1c2
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
9 changed files with 133 additions and 14 deletions

View file

@ -423,6 +423,30 @@ namespace Crafter::Review::Freeze {
Dyn::Str(Dyn::At(report, "base")), trees, Dyn::JoinPath(Dyn::JoinPath(outRel, "gate-state"), "dispositions.json"), Dyn::Str(Dyn::At(report, "date")));
}
std::string TreeOf(const Json& record, const fs::path& cwd) {
const Json& worktree = Dyn::Get(record, "worktree");
if (Dyn::Truthy(worktree)) return Dyn::Str(worktree);
const Json& repo = Dyn::Get(record, "repo");
if (!Dyn::Truthy(repo)) return ".";
std::error_code error;
return fs::weakly_canonical(fs::path(Dyn::Str(repo)), error) == fs::weakly_canonical(cwd, error) ? std::string(".") : Dyn::Str(repo);
}
Reviewed ReviewedBy(const fs::path& artifact, const fs::path& cwd) {
const fs::path file = artifact / "freeze.json";
std::error_code error;
if (!fs::exists(file, error)) return {cwd, "."};
Json record;
try {
record = Dyn::LoadJsonFile(file);
} catch (const std::exception& e) {
throw std::runtime_error(std::format("{}: not a freeze record ({})", file.string(), e.what()));
}
if (!record.is_object()) throw std::runtime_error(std::format("{}: not a freeze record", file.string()));
const Json& repo = Dyn::Get(record, "repo");
return {Dyn::Truthy(repo) ? fs::path(Dyn::AbsPath(Dyn::Str(repo))) : cwd, TreeOf(record, cwd)};
}
std::int32_t Main(const Arguments& arguments, const Json& config) {
const std::string cwd = fs::current_path().string();
const std::string repo = Dyn::AbsPath(arguments.repo && !arguments.repo->empty() ? *arguments.repo : cwd);
@ -467,14 +491,7 @@ namespace Crafter::Review::Freeze {
std::println("frozen: {} commit(s) {}..{} (base from {}) -> {}/ ({})", files.size(), Head12(report.at("base").get<std::string>()), Head12(report.at("head").get<std::string>()),
report.at("baseFrom").get<std::string>(), rel, Dyn::Join(files, ", "));
std::println("hash: {}", report.at("hash").get<std::string>());
std::string tree;
if (Dyn::Truthy(Dyn::Get(report, "worktree"))) {
tree = report.at("worktree").get<std::string>();
} else {
std::error_code error;
tree = fs::weakly_canonical(repo, error) == fs::weakly_canonical(cwd, error) ? std::string(".") : report.at("repo").get<std::string>();
}
const std::string line = RunLine(report, rel, tree);
const std::string line = RunLine(report, rel, TreeOf(report, cwd));
std::ofstream(fs::path(report.at("out").get<std::string>()) / "run-line.txt", std::ios::binary) << line << "\n";
std::println("run (from the repository root, in a detached background shell; also in {}):\n {}", Dyn::JoinPath(rel, "run-line.txt"), line);
return 0;

View file

@ -238,7 +238,7 @@ namespace Crafter::Review::Run {
return {0, {}};
}
const fs::path root = scratchRoot && !scratchRoot->empty() ? *scratchRoot : fs::path(ScratchRoot(std::nullopt));
const Memory::Calibration calib(root / "memory.json", repo, DefaultCosts());
const Memory::Calibration calib(root / "memory.json", artifact ? Freeze::ReviewedBy(*artifact, repo).repo : repo, DefaultCosts());
std::optional<Memory::Calibration::Cost> best;
for (const std::string& e : appEnvs) {
Memory::Calibration::Cost cost = calib.CostOf(e);
@ -307,7 +307,7 @@ namespace Crafter::Review::Run {
}
// ---- the Run ---------------------------------------------------------------------------
Run::Run() : repo(fs::current_path()), t0(Platform::Now()) {
Run::Run() : repo(fs::current_path()), reviewed(repo), t0(Platform::Now()) {
log = [this](const std::string& message) { Message(message); };
appReady = [](std::string_view url, std::string_view path) { return AppReady(url, path); };
probe = [](std::string_view url) { return PgWire::Probe(url); };
@ -483,6 +483,8 @@ namespace Crafter::Review::Run {
}
logFile = scratch / "log.txt";
log(std::format("crafter-review {} prompt v{}: commits {} @ {}", Version(), PromptVersion, Dyn::RelPath(a.artifact, repo.string()), hash));
std::error_code canonical;
if (fs::weakly_canonical(reviewed, canonical) != fs::weakly_canonical(repo, canonical)) log(std::format("repository under review: {} (read at {})", reviewed.string(), trees.front()));
log(std::format("config: {}", Dyn::Truthy(Dyn::Get(cfg, "_path")) ? Dyn::Str(cfg.at("_path")) : std::string("defaults (no .review-gates/config.json)")));
for (const Json& w : Dyn::Or(Dyn::Get(cfg, "_warnings"), Dyn::EmptyList())) log(std::format("config: warning: {}", Dyn::Str(w)));
std::vector<std::string> lensParts;
@ -560,7 +562,7 @@ namespace Crafter::Review::Run {
systemText = Prompt::SystemText();
systemPaths.clear();
systemByLens.clear();
runLayer = Prompt::RunLayer(Dyn::RelPath(a.artifact, repo.string()), a.base, a.date, trees, hash, lines, units, contextPaths, Basename(repo));
runLayer = Prompt::RunLayer(Dyn::RelPath(a.artifact, repo.string()), a.base, a.date, trees, hash, lines, units, contextPaths, Basename(reviewed));
// The application under test: the login fixture, the TOTP helper.
const Json& fcfg = cfg.at("functional");
loginFixture = Config::LoginFixture(cfg, repo);
@ -570,7 +572,7 @@ namespace Crafter::Review::Run {
schemas = {{"block", Schema::SchemaJson("block")}, {"whole", Schema::SchemaJson("whole")}};
// Memory: every pass's tree measured, its peak recorded per repository
// and environment; the budget per gate.
calibration = std::make_unique<Memory::Calibration>(scratchRoot / "memory.json", repo, DefaultCosts());
calibration = std::make_unique<Memory::Calibration>(scratchRoot / "memory.json", reviewed, DefaultCosts());
if (!a.dryRun) {
// A pass runs under the supervisor: its tree is claude's, the
// supervisor itself left out, so a peak measures claude's tree
@ -1685,7 +1687,14 @@ namespace Crafter::Review::Run {
// ---- the run ------------------------------------------------------------------------------
std::int32_t Run::Execute() {
trees = CommaList(a.trees.value_or("."));
Freeze::Reviewed frozen;
try {
frozen = Freeze::ReviewedBy(a.artifact, repo);
} catch (const std::runtime_error& e) {
Die(e.what());
}
reviewed = frozen.repo;
trees = CommaList(a.trees.value_or(frozen.tree));
contextPaths = CommaList(a.contextPaths);
for (const std::string& c : contextPaths) {
if (!fs::exists(c)) Die(std::format("--context-paths: {} does not exist", c));