refactor: simplify pass (trekexecute)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-24 22:22:18 +02:00
commit af90e82794
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
12 changed files with 303 additions and 186 deletions

View file

@ -464,9 +464,13 @@ function orders(xs) {
return xs.flatMap((x, i) => orders([...xs.slice(0, i), ...xs.slice(i + 1)]).map((rest) => [x, ...rest]));
}
/** A row's members, rebuilt from its survivor plus each "Also flagged by" line, as sorted `reviewer|title` strings. */
/**
* A row's members, rebuilt from its survivor plus each "Also flagged by" line,
* as sorted `reviewer|title` strings; an untitled member is named by its
* rule_key, as the line names it.
*/
function members(row) {
const out = [`${row.reviewer}|${row.title}`];
const out = [`${row.reviewer}|${row.title ?? row.rule_key}`];
for (const m of String(row.detail ?? '').matchAll(/^Also flagged by (.+?): (.+)\.$/gm)) out.push(`${m[1]}|${m[2]}`);
return out.sort();
}
@ -535,13 +539,6 @@ test('[ORD] a full tie on one row: two /code-review MAJORs titled T, one marked
assert.deepEqual(split(a), split(b), 'the kept/unverified split');
});
/** A row's member set, rebuilt from its survivor plus each "Also flagged by" line; an untitled member is named by its rule_key, as the line names it. */
function memberSet(row) {
const out = [`${row.reviewer}|${row.title ?? row.rule_key}`];
for (const m of String(row.detail ?? '').matchAll(/^Also flagged by (.+?): (.+)\.$/gm)) out.push(`${m[1]}|${m[2]}`);
return out.sort().join('+');
}
test('[ORD] the title classes are placed in alphabetical order: cA, fB, cB and an untitled correctness BLOCKER give {cA, cU} BLOCKER and {fB, cB} MAJOR and one verdict in all 24 orders', () => {
const K = 'NOT_IN_THE_CATALOGUE';
const at = { file: 'bin/slug.mjs', line: 46, rule_key: K, detail: 'd' };
@ -562,12 +559,12 @@ test('[ORD] the title classes are placed in alphabetical order: cA, fB, cB and a
for (const order of all) {
const name = order.map(([n]) => n).join(', ');
const rows = dedupByTriplet(order.map(([, f]) => structuredClone(f)));
const bySet = Object.fromEntries(rows.map((r) => [memberSet(r), r.severity]));
const bySet = Object.fromEntries(rows.map((r) => [members(r).join('+'), r.severity]));
assert.deepEqual(bySet, expected, `order ${name}`);
const j = judgeFilter(rows);
verdicts.add(JSON.stringify({
kept: j.kept.map(memberSet).sort(),
unverified: j.unverified.map(memberSet).sort(),
kept: j.kept.map((r) => members(r).join('+')).sort(),
unverified: j.unverified.map((r) => members(r).join('+')).sort(),
verdict: computeVerdict(j.kept, { unverified: j.unverified }).verdict,
}));
}

View file

@ -19,7 +19,7 @@
// `unverified`, not dropped (lib/review/coordinator-contract.mjs).
// Fix the SOURCE a pin guards, not the pin.
import { test } from 'node:test';
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
@ -33,10 +33,15 @@ import { childEnv } from '../helpers/child-env.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
/** A repo file's text; the file must exist. */
const readCache = new Map();
/** A repo file's text; the file must exist. Read from disk once per run. */
function read(rel) {
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
return readFileSync(join(ROOT, rel), 'utf8');
if (!readCache.has(rel)) {
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8'));
}
return readCache.get(rel);
}
/** Whitespace (line breaks too) collapsed to one space. */
@ -84,6 +89,16 @@ function bash(cmd, cwd) {
return spawnSync('bash', ['-c', cmd.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)], { cwd, encoding: 'utf8' });
}
/** The runtime ```bash block of `text` that holds `cmd`, or undefined. */
function bashBlockHolding(text, cmd) {
return text.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]).find((b) => b.includes(cmd));
}
/** A Phase 0 parser line run through bash() with `args` in place of $ARGUMENTS. */
function runParser(cmd, args) {
return bash(cmd.split('$ARGUMENTS').join(args), ROOT);
}
// --- #23 ---------------------------------------------------------------------------
test('[#23] no agent file refers to a MANDATORY OVERRIDE rule', () => {
@ -656,11 +671,16 @@ function gatesShim(cmd, value) {
return l.replace(' [value]', value ? ` ${value}` : '');
}
/** Run a shim line from the prose; its `next_state`. */
const shimCache = new Map();
/** Run a shim line from the prose; its `next_state`. Each distinct line runs once. */
function shimNext(cmdLine) {
const r = bash(cmdLine, ROOT);
assert.equal(r.status, 0, `shim exited ${r.status}: ${r.stdout}${r.stderr}`);
return JSON.parse(r.stdout).next_state;
if (!shimCache.has(cmdLine)) {
const r = bash(cmdLine, ROOT);
assert.equal(r.status, 0, `shim exited ${r.status}: ${r.stdout}${r.stderr}`);
shimCache.set(cmdLine, JSON.parse(r.stdout).next_state);
}
return shimCache.get(cmdLine);
}
for (const cmd of GATES_COMMANDS) {
@ -1087,6 +1107,15 @@ function staleRepo() {
return { repo, reviewed, head, review, cleanup: () => rmSync(base, { recursive: true, force: true }) };
}
let sharedStale = null;
after(() => sharedStale?.cleanup());
/** The file's one staleRepo, built on first use: its tests only rewrite review.md, outside the repo. */
function staleFixture() {
if (!sharedStale) sharedStale = staleRepo();
return sharedStale;
}
/** The lock line with its review path filled in, run in `repo` through bash(). */
function runLock(cmd, dir, repo) {
const filled = cmd.split('{review_path}').join(join(dir, 'review.md')).split('{project_dir}').join(dir);
@ -1104,17 +1133,15 @@ test('[R6] the Main-merge gate and Phase 7.2 step 4 each hold exactly one verdic
test('[R6] the extracted lock lines, run on an ALLOW review of an older commit with a later source commit, exit 2 with review-stale', () => {
const text = read(EXEC);
const lines = [line(section(text, MERGE_GATE), 'review/verdict-gate.mjs'), line(section(text, PHASE_72), 'review/verdict-gate.mjs')];
const fx = staleRepo();
try {
for (const cmd of lines) {
const ok = runLock(cmd, fx.review('ALLOW', fx.head), fx.repo);
assert.equal(ok.status, 0, `control: an ALLOW review of HEAD opens the lock: ${ok.stdout}${ok.stderr}`);
const r = runLock(cmd, fx.review('ALLOW', fx.reviewed), fx.repo);
assert.equal(r.status, 2, `${cmd}: an ALLOW review of an older commit must exit 2: ${r.stdout}${r.stderr}`);
const reason = JSON.parse(r.stdout).reason;
assert.ok(reason.startsWith('review-stale'), `the reason must start review-stale: ${reason}`);
}
} finally { fx.cleanup(); }
const fx = staleFixture();
for (const cmd of lines) {
const ok = runLock(cmd, fx.review('ALLOW', fx.head), fx.repo);
assert.equal(ok.status, 0, `control: an ALLOW review of HEAD opens the lock: ${ok.stdout}${ok.stderr}`);
const r = runLock(cmd, fx.review('ALLOW', fx.reviewed), fx.repo);
assert.equal(r.status, 2, `${cmd}: an ALLOW review of an older commit must exit 2: ${r.stdout}${r.stderr}`);
const reason = JSON.parse(r.stdout).reason;
assert.ok(reason.startsWith('review-stale'), `the reason must start review-stale: ${reason}`);
}
});
test('[R6] both decline lists name review-stale, the exit table says to re-run /trekreview on this commit, and --resume runs the same gate', () => {
@ -1133,12 +1160,10 @@ test('[R6] both decline lists name review-stale, the exit table says to re-run /
test('keep: Phase 7.2 step 4 run on a BLOCK review of an older commit still exits 1, so the fix loop opens', () => {
const step4 = line(section(read(EXEC), PHASE_72), 'review/verdict-gate.mjs');
const fx = staleRepo();
try {
const r = runLock(step4, fx.review('BLOCK', fx.reviewed), fx.repo);
assert.equal(r.status, 1, `a BLOCK review must exit 1 before any staleness check: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
} finally { fx.cleanup(); }
const fx = staleFixture();
const r = runLock(step4, fx.review('BLOCK', fx.reviewed), fx.repo);
assert.equal(r.status, 1, `a BLOCK review must exit 1 before any staleness check: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
});
test('keep: the other verdict-gate callers carry no --repo', () => {
@ -1202,11 +1227,6 @@ test('[D67] the End to end section says to record run_seen, also when no app was
const ENDSESSION = 'commands/trekendsession.md';
const ENDSESSION_PARSER_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession -- $ARGUMENTS';
/** The Phase 0 parser line run through bash() with `args` in place of $ARGUMENTS. */
function runEndsessionParser(cmd, args) {
return bash(cmd.split('$ARGUMENTS').join(args), ROOT);
}
test('[#15] /trekendsession Phase 0 has no "contains --help or -h" check, and its parser line run on fix-header gives no help', () => {
const phase0 = section(read(ENDSESSION), '## Phase 0 — `--help` handling');
assert.doesNotMatch(flat(phase0), /contains\s+`?--help`?\s+or\s+`?-h`?/i,
@ -1215,19 +1235,19 @@ test('[#15] /trekendsession Phase 0 has no "contains --help or -h" check, and it
assert.match(flat(phase0), /`positional\[0\] === '-h'`/, 'Phase 0 does not dispatch on a lone -h as the first positional');
const cmd = line(phase0, 'parsers/arg-parser.mjs');
assert.equal(cmd, ENDSESSION_PARSER_LINE, 'the Phase 0 parser line is not the trekendsession parser call on $ARGUMENTS');
const block = phase0.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]).find((b) => b.includes(cmd));
const block = bashBlockHolding(phase0, cmd);
assert.ok(block, 'the parser line is not in a runtime ```bash block');
assert.equal(block.split('\n')[0], 'setopt nonomatch 2>/dev/null || true', 'the block does not open with setopt nonomatch');
const plain = runEndsessionParser(cmd, 'fix-header');
const plain = runParser(cmd, 'fix-header');
assert.equal(plain.status, 0, `the parser line refused fix-header: ${plain.stdout}${plain.stderr}`);
const r = JSON.parse(plain.stdout);
assert.equal(r.flags['--help'], undefined, 'fix-header set the --help flag');
assert.deepEqual(r.positional, ['fix-header']);
assert.notEqual(r.positional[0], '-h');
const help = runEndsessionParser(cmd, '--help');
const help = runParser(cmd, '--help');
assert.equal(help.status, 0, help.stdout + help.stderr);
assert.equal(JSON.parse(help.stdout).flags['--help'], true, 'control: --help must set the help flag');
const two = runEndsessionParser(cmd, 'fix-header "Session 2"');
const two = runParser(cmd, 'fix-header "Session 2"');
assert.equal(two.status, 0, two.stdout + two.stderr);
assert.deepEqual(JSON.parse(two.stdout).positional, ['fix-header', 'Session 2']);
});
@ -1276,31 +1296,26 @@ const CONTINUE = 'commands/trekcontinue.md';
const CONTINUE_PARSER_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekcontinue -- $ARGUMENTS';
const CONSISTENCY_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/next-session-prompt-validator.mjs --json --consistency <RESOLVED-PATH-A> <RESOLVED-PATH-B> --state-file <RESOLVED-PATH-STATE>';
/** The Phase 0 parser line run through bash() with `args` in place of $ARGUMENTS. */
function runContinueParser(cmd, args) {
return bash(cmd.split('$ARGUMENTS').join(args), ROOT);
}
test('[#66] /trekcontinue Phase 0 has no parseArgs($ARGUMENTS call, and its parser line run with --help sets the help flag', () => {
const phase0 = section(read(CONTINUE), '## Phase 0 — `--help` handling');
assert.doesNotMatch(phase0, /parseArgs\(\$ARGUMENTS/, 'Phase 0 still calls parseArgs($ARGUMENTS, ...) as a function');
const cmd = line(phase0, 'parsers/arg-parser.mjs');
assert.equal(cmd, CONTINUE_PARSER_LINE, 'the Phase 0 parser line is not the trekcontinue parser call on $ARGUMENTS');
const block = phase0.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]).find((b) => b.includes(cmd));
const block = bashBlockHolding(phase0, cmd);
assert.ok(block, 'the parser line is not in a runtime ```bash block');
assert.match(flat(phase0), /Run first, exactly like this:/);
assert.match(flat(phase0), /Read `flags` and `positional` from the JSON it prints/);
assert.match(flat(phase0), /`flags\['--help'\] === true`/, 'Phase 0 does not dispatch on the parsed --help flag');
assert.match(flat(phase0), /`positional\[0\] === '-h'`/, 'Phase 0 does not dispatch on a lone -h as the first positional');
const help = runContinueParser(cmd, '--help');
const help = runParser(cmd, '--help');
assert.equal(help.status, 0, help.stdout + help.stderr);
assert.equal(JSON.parse(help.stdout).flags['--help'], true, 'the parser line run with --help does not set the help flag');
const dir = runContinueParser(cmd, '.claude/projects/demo');
const dir = runParser(cmd, '.claude/projects/demo');
assert.equal(dir.status, 0, dir.stdout + dir.stderr);
const r = JSON.parse(dir.stdout);
assert.equal(r.flags['--help'], undefined, 'a project dir set the --help flag');
assert.deepEqual(r.positional, ['.claude/projects/demo']);
const h = runContinueParser(cmd, '-h');
const h = runParser(cmd, '-h');
assert.equal(h.status, 0, h.stdout + h.stderr);
assert.equal(JSON.parse(h.stdout).positional[0], '-h');
const p05 = flat(section(read(CONTINUE), '## Phase 0.5 — Cleanup mode dispatch'));

View file

@ -3,9 +3,9 @@
// build of the re-planned step they are moved into the report dir, never
// deleted (voyage-lekerepo trial 5, finding 4).
import { test } from 'node:test';
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, symlinkSync } from 'node:fs';
import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, symlinkSync, cpSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
@ -20,15 +20,25 @@ function git(cwd, ...args) {
return r.stdout;
}
// The base repo (bin/slug.mjs committed), built once; repo() gives each test its own copy.
let seed = null;
after(() => {
if (seed) rmSync(seed, { recursive: true, force: true });
});
function repo() {
if (!seed) {
seed = mkdtempSync(join(tmpdir(), 'voyage-park-seed-'));
git(seed, 'init', '-q');
git(seed, 'config', 'user.email', 't@example.invalid');
git(seed, 'config', 'user.name', 't');
mkdirSync(join(seed, 'bin'));
writeFileSync(join(seed, 'bin/slug.mjs'), 'old\n');
git(seed, 'add', 'bin/slug.mjs');
git(seed, 'commit', '-q', '-m', 'base');
}
const dir = mkdtempSync(join(tmpdir(), 'voyage-park-'));
git(dir, 'init', '-q');
git(dir, 'config', 'user.email', 't@example.invalid');
git(dir, 'config', 'user.name', 't');
mkdirSync(join(dir, 'bin'));
writeFileSync(join(dir, 'bin/slug.mjs'), 'old\n');
git(dir, 'add', 'bin/slug.mjs');
git(dir, 'commit', '-q', '-m', 'base');
cpSync(seed, dir, { recursive: true });
return dir;
}
@ -57,9 +67,10 @@ const DIFF = [
'',
].join('\n');
function run(dir, diff = DIFF) {
/** The CLI run with `--cwd <cwd>` (the repo by default) on `diff` written into the repo's git dir. */
function run(dir, diff = DIFF, cwd = dir) {
writeFileSync(join(dir, '.git', 'upward.diff'), diff);
return spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', dir], { encoding: 'utf8' });
return spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', cwd], { encoding: 'utf8' });
}
// Parked outside the work tree, in the git dir, so a later `git ls-files --others` never lists them.
@ -207,12 +218,6 @@ test('trekexecute: a re-planned step parks the earlier attempt\'s new files befo
// [F4c] The root was the --cwd as given, so a subdirectory read every
// root-relative path as absent; a git call that failed read as "nothing tracked".
/** The CLI run with `--cwd <cwd>` on `diff` written into the repo's git dir. */
function runFrom(dir, cwd, diff = DIFF) {
writeFileSync(join(dir, '.git', 'upward.diff'), diff);
return spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', cwd], { encoding: 'utf8' });
}
/** The attempt's two new files, written on disk. */
function attemptFiles(dir) {
mkdirSync(join(dir, 'tests'));
@ -275,7 +280,7 @@ test('[F4c] --cwd <repo>/bin parks the root-relative path', () => {
const dir = repo();
try {
attemptFiles(dir);
const r = runFrom(dir, join(dir, 'bin'));
const r = run(dir, DIFF, join(dir, 'bin'));
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.ok(JSON.parse(r.stdout).parked.includes('tests/batch.sh'), `tests/batch.sh was not parked from a subdirectory: ${r.stdout}`);
assert.equal(existsSync(join(dir, 'tests/batch.sh')), false, 'the file is still in the tree');

View file

@ -53,16 +53,20 @@ function ev(name, over = {}) {
}
const passing = (name) => ev(name, { ok: true, code: null, cname: null, failureType: null });
/** The judge on a results file, the id list written into `dir`, with extra flags. */
function judgeFile(dir, ids, results, extra = []) {
writeFileSync(join(dir, 'ids'), ids.join('\n') + '\n');
const r = spawnSync(process.execPath, [JUDGE, '--ids', join(dir, 'ids'), '--results', results, ...extra],
{ encoding: 'utf8', env: childEnv() });
return { status: r.status, out: `${r.stdout}${r.stderr}` };
}
/** Run the judge on an id list and planted events, with extra flags. */
function judge(ids, events, extra = []) {
return inTmp((dir) => {
const idsFile = join(dir, 'ids');
const results = join(dir, 'results.jsonl');
writeFileSync(idsFile, ids.join('\n') + '\n');
writeFileSync(results, events.map((e) => JSON.stringify(e)).join('\n') + (events.length ? '\n' : ''));
const r = spawnSync(process.execPath, [JUDGE, '--ids', idsFile, '--results', results, ...extra],
{ encoding: 'utf8', env: childEnv() });
return { status: r.status, out: `${r.stdout}${r.stderr}` };
return judgeFile(dir, ids, results, extra);
});
}
@ -170,14 +174,6 @@ function report(dir, files) {
return out;
}
/** The judge on a results file the reporter wrote. */
function judgeFile(dir, ids, results) {
writeFileSync(join(dir, 'ids'), ids.join('\n') + '\n');
const r = spawnSync(process.execPath, [JUDGE, '--ids', join(dir, 'ids'), '--results', results],
{ encoding: 'utf8', env: childEnv() });
return { status: r.status, out: `${r.stdout}${r.stderr}` };
}
const HEAD = "import { test } from 'node:test';\nimport { strict as assert } from 'node:assert';\n";
test('red reporter + judge: a passing [X1] test and an [X2] test that throws a TypeError, run by node --test -> exit 1', () => {

View file

@ -9,10 +9,10 @@
// pointer block. Anything else is exit 2 `review-stale: ...`. Those tests run
// the CLI only, in throwaway git repos with no hooks.
import { test } from 'node:test';
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, symlinkSync } from 'node:fs';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, symlinkSync, appendFileSync, cpSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
@ -260,42 +260,63 @@ function g(cwd, ...args) {
return r.stdout.trim();
}
/**
* A throwaway repo with one reviewed commit (a.mjs, CLAUDE.md, the lessons
* file), no git hooks. `sub` puts the project in a subdirectory of the repo;
* `claudeLink` makes CLAUDE.md a symlink to AGENTS.md.
*/
function gateRepo({ sub = '', claude = '# Project\n', claudeLink = false } = {}) {
const base = mkdtempSync(join(tmpdir(), 'verdict-gate-r6-'));
/** Write `text` to `rel` under `dir`, making its directories. */
function putIn(dir, rel, text) {
mkdirSync(dirname(join(dir, rel)), { recursive: true });
writeFileSync(join(dir, rel), text);
}
/** A fixture variant's seed: `{ base, reviewed }`, built once (see gateRepo) and removed after the file's tests. */
const seeds = new Map();
after(() => {
for (const seed of seeds.values()) rmSync(seed.base, { recursive: true, force: true });
});
function seedRepo({ sub, claude, claudeLink }) {
const base = mkdtempSync(join(tmpdir(), 'verdict-gate-r6-seed-'));
const repo = join(base, 'repo');
const dir = sub ? join(repo, sub) : repo;
mkdirSync(join(dir, '.claude', 'rules'), { recursive: true });
mkdirSync(join(base, 'hooks-none'));
g(repo, 'init', '-q', '-b', 'main');
g(repo, 'config', 'user.name', 'Test');
g(repo, 'config', 'user.email', 'test@example.invalid');
g(repo, 'config', 'commit.gpgsign', 'false');
g(repo, 'config', 'core.hooksPath', join(base, 'hooks-none'));
const put = (rel, text) => {
mkdirSync(dirname(join(dir, rel)), { recursive: true });
writeFileSync(join(dir, rel), text);
};
put('a.mjs', 'export const a = 1;\n');
put(LESSONS, LESSONS_BEFORE);
appendFileSync(join(repo, '.git', 'config'), '[user]\n\tname = Test\n\temail = test@example.invalid\n'
+ `[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "${join(base, 'hooks-none')}"\n`);
putIn(dir, 'a.mjs', 'export const a = 1;\n');
putIn(dir, LESSONS, LESSONS_BEFORE);
if (claudeLink) {
put('AGENTS.md', claude);
putIn(dir, 'AGENTS.md', claude);
symlinkSync('AGENTS.md', join(dir, 'CLAUDE.md'));
} else {
put('CLAUDE.md', claude);
putIn(dir, 'CLAUDE.md', claude);
}
g(repo, 'add', '-A');
g(repo, 'commit', '-q', '-m', 'feat: the work');
const reviewed = g(repo, 'rev-parse', 'HEAD');
return { base, reviewed: g(repo, 'rev-parse', 'HEAD') };
}
/**
* A throwaway repo with one reviewed commit (a.mjs, CLAUDE.md, the lessons
* file), no git hooks. `sub` puts the project in a subdirectory of the repo;
* `claudeLink` makes CLAUDE.md a symlink to AGENTS.md. Each variant is built
* once; every call gets its own copy of it.
*/
function gateRepo({ sub = '', claude = '# Project\n', claudeLink = false } = {}) {
const key = JSON.stringify([sub, claude, claudeLink]);
if (!seeds.has(key)) seeds.set(key, seedRepo({ sub, claude, claudeLink }));
const seed = seeds.get(key);
const base = mkdtempSync(join(tmpdir(), 'verdict-gate-r6-'));
cpSync(seed.base, base, { recursive: true, verbatimSymlinks: true });
const repo = join(base, 'repo');
const dir = sub ? join(repo, sub) : repo;
// The copy's hooks path is its own empty hooks-none, not the seed's.
const config = join(repo, '.git', 'config');
writeFileSync(config, readFileSync(config, 'utf8').split(seed.base).join(base));
const { reviewed } = seed;
const put = (rel, text) => putIn(dir, rel, text);
const commit = (msg, files) => {
for (const [rel, text] of Object.entries(files)) put(rel, text);
g(repo, 'add', '-A');
g(repo, 'commit', '-q', '-m', msg);
return g(repo, 'rev-parse', 'HEAD');
};
const gate = ({ verdict = 'ALLOW', sha = reviewed } = {}) => {
const p = join(base, 'review.md');
@ -311,7 +332,7 @@ function gateRepo({ sub = '', claude = '# Project\n', claudeLink = false } = {})
/** The lessons commit /trekexecute Phase 7.2 makes after a review. */
function lessonsCommit(fx, claudeAfter = `# Project\n\n${POINTER}`) {
return fx.commit('docs(voyage): review lessons (round 1)', { [LESSONS]: LESSONS_AFTER, 'CLAUDE.md': claudeAfter });
fx.commit('docs(voyage): review lessons (round 1)', { [LESSONS]: LESSONS_AFTER, 'CLAUDE.md': claudeAfter });
}
function assertStale(r, re = /review-stale/) {
@ -380,7 +401,8 @@ test('[R6] --repo: the reviewed commit on an unmerged side branch → exit 2 "no
const fx = gateRepo();
try {
fx.git('checkout', '-q', '-b', 'side');
const side = fx.commit('fix: side work', { 'a.mjs': 'export const a = 3;\n' });
fx.commit('fix: side work', { 'a.mjs': 'export const a = 3;\n' });
const side = fx.git('rev-parse', 'HEAD');
fx.git('checkout', '-q', 'main');
lessonsCommit(fx);
assertStale(fx.gate({ sha: side }), /not an ancestor of HEAD/);