voyage/tests/lib/fix-round-4-5a-prose.test.mjs

1114 lines
65 KiB
JavaScript

// tests/lib/fix-round-4-5a-prose.test.mjs
// Prose pins for fix round 4-5a (the serious findings, plan
// .claude/projects/2026-10-20-fix-round-serious-findings): the fixes that are
// text a session or a helper reads. The name is the brief's;
// lib/util/test-census.mjs counts it as a behaviour test (it does not match
// PIN_FILE_RE). Every test carries its id tag in brackets, and every tagged
// test fails on an assertion at ab6b32c (the round's start point).
// #23 code-correctness-reviewer cites a "MANDATORY OVERRIDE rule above"
// that is no longer there; the sentence says the rule itself.
// #24 research-scout says "Try Tavily first" but its tools held no Tavily
// tool; its tools line now holds both, in docs-researcher's order.
// #111 A reviewer drops a finding that feels off-tier only when it can say
// why it is wrong, and then as one Declined to Judge line with that
// reason; the skill's line agrees; an unknown rule_key is routed to
// `unverified`, not dropped (lib/review/coordinator-contract.mjs).
// Fix the SOURCE a pin guards, not the pin.
import { 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';
import { dirname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { findSection } from '../../lib/parsers/plan-schema.mjs';
import { readFrontmatterFields } from '../../lib/skills/skill-frontmatter.mjs';
import { childEnv } from '../helpers/child-env.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
/** A repo file's text; the file must exist. */
function read(rel) {
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
return readFileSync(join(ROOT, rel), 'utf8');
}
/** Whitespace (line breaks too) collapsed to one space. */
const flat = (s) => s.replace(/\s+/g, ' ').trim();
/**
* The text of a heading's section, up to the next heading of the same or a
* higher level outside a fence. The heading must exist.
*/
function section(text, heading) {
const level = heading.match(/^#+/)[0].length;
const s = findSection(text, heading.replace(/^#+ /, ''), level);
assert.ok(s, `missing ${heading}`);
const lines = s.text.split('\n');
let fenced = false;
for (let i = 1; i < lines.length; i++) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
const m = !fenced && lines[i].match(/^(#+) /);
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
}
return s.text;
}
/** From the line holding `marker` to the first line where `stop` is true, flattened. */
function partAt(rel, marker, stop) {
const lines = read(rel).split('\n');
const start = lines.findIndex((l) => l.includes(marker));
assert.ok(start !== -1, `${rel} must hold "${marker}"`);
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) if (stop(lines[i])) { end = i; break; }
return flat(lines.slice(start, end).join(' '));
}
/** The single line in `text` that runs `lib/<mod>` (and holds `verb`, when given). */
function line(text, mod, verb = '') {
const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
const re = new RegExp(`^\\s*node \\$\\{CLAUDE_PLUGIN_ROOT\\}/lib/${esc}\\b`);
const hits = text.split('\n').filter((l) => re.test(l) && l.includes(verb));
assert.equal(hits.length, 1, `expected exactly one \`${mod}${verb ? ` ${verb}` : ''}\` line; found ${hits.length}`);
return hits[0].trim();
}
/** Run a command line from the prose with the plugin root filled in. */
function bash(cmd, cwd) {
return spawnSync('bash', ['-c', cmd.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)], { cwd, encoding: 'utf8' });
}
// --- #23 ---------------------------------------------------------------------------
test('[#23] no agent file refers to a MANDATORY OVERRIDE rule', () => {
const hits = readdirSync(join(ROOT, 'agents'))
.filter((f) => f.endsWith('.md'))
.filter((f) => read(`agents/${f}`).includes('MANDATORY OVERRIDE'));
assert.deepEqual(hits, [], 'an agent still points at a MANDATORY OVERRIDE rule that is not there');
const d5 = flat(section(read('agents/code-correctness-reviewer.md'), '### 5. Placeholder code — `PLACEHOLDER_IN_CODE` (MAJOR)'));
assert.match(d5, /Never accept "not implemented" placeholders as fine "for now"/);
});
// --- #24 ---------------------------------------------------------------------------
test('[#24] research-scout holds both Tavily tools, in docs-researcher\'s order, and still tries Tavily first', () => {
const text = read('agents/research-scout.md');
const fm = readFrontmatterFields(text);
assert.ok(fm.ok, fm.error);
assert.deepEqual(fm.fields.tools.value,
['WebSearch', 'WebFetch', 'Read', 'mcp__tavily__tavily_search', 'mcp__tavily__tavily_research']);
assert.match(text, /\*\*Try Tavily first\*\*/);
});
// --- #111 --------------------------------------------------------------------------
/** No bare "drop it" choice; a drop needs a reason, written as one Declined to Judge line. */
function dropsOnlyWithTrace(part, where) {
assert.doesNotMatch(part, /either drop (it|the finding)|or drop (it|the finding)\s*\.|drop (it|the finding) \(it was wrong\)/i,
`${where}: an off-tier finding may still be dropped with no trace`);
assert.match(part, /drop[^.]{0,60}only when you can say why it is wrong[^.]{0,120}Declined to Judge[^.]{0,60}with that reason/i,
`${where}: a drop must carry its reason on a Declined to Judge line`);
assert.match(part, /catalogue('s)? (tier|severity)/i, `${where}: the catalogue tier stays the default`);
}
const toBlank = (l) => l.trim() === '';
test('[#111] code-correctness-reviewer dimension 4: a nice-to-have test gap is dropped only with a Declined to Judge line', () => {
dropsOnlyWithTrace(partAt('agents/code-correctness-reviewer.md', 'When the brief explicitly asks for tests', toBlank),
'code-correctness-reviewer dimension 4');
});
test('[#111] code-correctness-reviewer severity rules: an off-tier finding is dropped only with a Declined to Judge line', () => {
dropsOnlyWithTrace(partAt('agents/code-correctness-reviewer.md', 'If a finding feels off-tier', toBlank),
'code-correctness-reviewer ## Severity rules');
});
test('[#111] code-correctness-reviewer rules: "No silent severity downgrades" drops only with a Declined to Judge line', () => {
dropsOnlyWithTrace(partAt('agents/code-correctness-reviewer.md', '**No silent severity downgrades.**', (l) => /^- /.test(l) || toBlank(l)),
'code-correctness-reviewer ## Rules');
});
test('[#111] brief-conformance-reviewer severity rules: a finding is dropped only with a Declined to Judge line', () => {
dropsOnlyWithTrace(partAt('agents/brief-conformance-reviewer.md', 'If a finding feels less severe than its catalogue tier', toBlank),
'brief-conformance-reviewer ## Severity rules');
});
test('[#111] reviewing-adversarially: keep the tier, or drop as wrong with its reason on a Declined to Judge line', () => {
const s5 = flat(section(read('skills/reviewing-adversarially/SKILL.md'), '### 5. No downgrade without a reason'));
assert.match(s5, /keep the tier, or drop the finding as wrong with its reason on a Declined to Judge line; never soften it in between/);
assert.doesNotMatch(s5, /keep the tier or drop the finding as wrong;/);
});
/** The stale "unknown rule keys are dropped" line now says they are routed to `unverified`. */
function routedNotDropped(part, where) {
assert.doesNotMatch(part, /dropped by (the coordinator's reasonableness filter|Pass 3)/i, `${where}: still says unknown rule keys are dropped`);
assert.match(part, /routed to `unverified`/, `${where}: must say an unknown rule key is routed to \`unverified\``);
}
const toBullet = (l) => /^- /.test(l) || toBlank(l) || /^#/.test(l);
test('[#111] code-correctness-reviewer: an unknown rule_key is routed to `unverified`, not dropped', () => {
routedNotDropped(partAt('agents/code-correctness-reviewer.md', '`rule_key` MUST be in the catalogue', toBullet),
'code-correctness-reviewer ## JSON output rules');
});
test('[#111] brief-conformance-reviewer: an unknown rule_key is routed to `unverified`, not dropped', () => {
routedNotDropped(partAt('agents/brief-conformance-reviewer.md', '`rule_key` MUST be in the catalogue', toBullet),
'brief-conformance-reviewer ## JSON output rules');
});
test('[#111] review-coordinator input: a rule_key outside the catalogue is routed to `unverified` by Pass 3', () => {
routedNotDropped(partAt('agents/review-coordinator.md', '**Rule catalogue** — `lib/review/rule-catalogue.mjs`', toBullet),
'review-coordinator ## Input');
});
// Steps 2-24 use these helpers; referenced here so the file states its interface.
test('helpers: line and bash are the shared line-runner pair', () => {
assert.equal(typeof line, 'function');
assert.equal(typeof bash, 'function');
assert.equal(line('x\n node ${CLAUDE_PLUGIN_ROOT}/lib/util/a.mjs go\n', 'util/a.mjs', 'go'), 'node ${CLAUDE_PLUGIN_ROOT}/lib/util/a.mjs go');
const r = bash('printf %s "${CLAUDE_PLUGIN_ROOT}"', ROOT);
assert.equal(r.status, 0);
assert.equal(r.stdout, ROOT);
});
// --- #2 ----------------------------------------------------------------------------
// planning-orchestrator.md is a reference document that nothing runs; the text
// that sent a reader to it ("re-run", "must emit", "should have prevented",
// Consumer/Producer) now names /trekplan.
test('[#2] /trekexecute never sends anyone to re-run the planning reference document', () => {
const text = flat(read('commands/trekexecute.md'));
assert.doesNotMatch(text, /re-run planning-orchestrator/i, 'still says to re-run planning-orchestrator');
assert.doesNotMatch(text, /planning-orchestrator must/i, 'still says planning-orchestrator must emit or include');
assert.doesNotMatch(text, /planning-orchestrator should have prevented/i, 'still says planning-orchestrator should have prevented this');
assert.match(text, /Re-run \/trekplan — the plan is not executable\./);
assert.match(text, /Fix: re-run \/trekplan — the plan must use v1\.7 format/);
assert.match(text, /Fix: re-run \/trekplan — every step needs a Manifest YAML block/);
assert.match(text, /\/trekplan's validator should have prevented this/);
});
test('[#2] sibling: commands/trekbrief.md names /trekplan as what finishes', () => {
const text = flat(read('commands/trekbrief.md'));
assert.doesNotMatch(text, /When the planning-orchestrator finishes/);
assert.match(text, /When \/trekplan finishes/);
});
test('[#2] sibling: agents/brief-reviewer.md names /trekplan Phase 4b as its reader', () => {
const text = flat(read('agents/brief-reviewer.md'));
assert.doesNotMatch(text, /`planning-orchestrator` Phase 1b/);
assert.match(text, /\/trekplan Phase 4b/);
});
test('[#2] sibling: docs/HANDOVER-CONTRACTS.md has /trekplan, not planning-orchestrator, as Consumer, Producer and the one that must run', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
assert.doesNotMatch(text, /^\*\*(Consumer|Producer):\*\*[^\n]*planning-orchestrator/m, 'planning-orchestrator is still a Consumer or Producer');
assert.doesNotMatch(text, /planning-orchestrator\)? \*\*must\*\* run/, 'planning-orchestrator is still the one that must run the validator');
const lines = text.split('\n');
for (const want of [
'**Consumer:** `/trekplan` Phase 1 (project-dir auto-discovery) + `/trekplan` Phase 3 and Phase 7 (consumes findings as context).',
'**Consumer:** `/trekplan` Phase 1 (architecture-discovery) + `/trekplan` Phase 7 (cross-reference architecture-note as priors during synthesis).',
'**Producer:** `/trekplan` Phase 8 (plan synthesis, then the schema self-check via `plan-validator --strict`).',
]) assert.ok(lines.includes(want), `missing line: ${want}`);
assert.match(text, /`\/trekplan` Phase 8 \*\*must\*\* run `plan-validator --strict` before handing the plan to plan-critic\./);
});
test('[#2] sibling: docs/architecture.md names /trekplan Phase 8 as the plan-validator caller', () => {
const text = read('docs/architecture.md');
assert.doesNotMatch(text, /`planning-orchestrator` Phase 5\.5/);
assert.match(text, /^- `\/trekplan` Phase 8 → `plan-validator --strict` \(replaces 3 `grep -cE` calls\)$/m);
});
// --- #26 ---------------------------------------------------------------------------
test('[#26] session-decomposer says to re-run /trekplan, not the planning reference document', () => {
const text = flat(read('agents/session-decomposer.md'));
assert.doesNotMatch(text, /Re-run planning-orchestrator/i);
assert.match(text, /Re-run \/trekplan\./);
});
test('[#26] the three reference documents hold read-only tools', () => {
for (const name of ['planning-orchestrator', 'research-orchestrator', 'review-orchestrator']) {
const fm = readFrontmatterFields(read(`agents/${name}.md`));
assert.ok(fm.ok, fm.error);
assert.deepEqual(fm.fields.tools.value, ['Read', 'Glob', 'Grep'], `${name}: tools must be Read, Glob, Grep`);
assert.equal(fm.fields.model.value, 'opus', `${name}: model stays opus`);
}
});
// --- #52 ---------------------------------------------------------------------------
// /trekplan's "execute" answer began implementing the plan in the planning
// session, past /trekexecute's approval check (K2), test lock and step helpers.
// It now prints the /trekexecute line and stops.
/** The `/trekexecute` line /trekplan prints, in both forms. */
function printsTrekexecuteLine(part, where) {
assert.match(part, /`\/trekexecute --project \{project_dir\}`/, `${where}: must print the /trekexecute --project line`);
assert.match(part, /`\/trekexecute \{plan_path\}`/, `${where}: must print the /trekexecute {plan_path} line`);
}
/**
* No text that has /trekplan build the plan itself: no "Begin implementing",
* and no "in this session" other than the one prohibition sentence.
*/
function neverImplementsHere(part, where) {
const rest = part.split('Do not implement in this session.').join('');
assert.doesNotMatch(rest, /Begin implementing/i, `${where}: still begins implementing`);
assert.doesNotMatch(rest, /in this session/i, `${where}: still implements in this session`);
}
test('[#52] Phase 11 "execute": prints the /trekexecute line and stops, never implements in this session', () => {
const block = flat(section(read('commands/trekplan.md'), '### "execute" / "go" / "start"'));
neverImplementsHere(block, 'Phase 11 "execute"');
assert.match(block, /^### "execute" \/ "go" \/ "start" Do not implement in this session\./);
printsTrekexecuteLine(block, 'Phase 11 "execute"');
assert.match(block, /without a project directory — and stop\./);
assert.match(block, /`\/trekexecute` is what checks the plan's approval \(K2\), locks the tests, runs one step helper per step and the review\./);
});
test('[#52] Phase 11 "team": no team path, the same /trekexecute line, then stop', () => {
const block = flat(section(read('commands/trekplan.md'), '### "execute with team" / "team"'));
neverImplementsHere(block, 'Phase 11 "team"');
assert.doesNotMatch(block, /proceed exactly as for "execute"/, 'Phase 11 "team": still proceeds as "execute" did');
assert.match(block, /There is no team path\. Print the same `\/trekexecute` line and stop; parallel work is `--decompose` followed by `\/trekexecute`'s session orders\./);
});
test('[#52] Phase 10 menu: "execute" is for the /trekexecute line, and a stopped gate withholds it', () => {
const p10 = flat(section(read('commands/trekplan.md'), '## Phase 10 — Present and refine'));
assert.doesNotMatch(p10, /start implementing/i, 'Phase 10 still offers or withholds "start implementing"');
assert.match(p10, /- Say \*\*"execute"\*\* for the `\/trekexecute` line to run ← only when the gate exited 0/);
assert.match(p10, /`Execution is blocked: \{the gate's reasons\}` and do not print the `\/trekexecute` line, whatever is said next, until the gate exits 0\./);
});
test('[#52] sibling: plan mode step 3 prints the /trekexecute line and stops', () => {
const pm = flat(section(read('commands/trekplan.md'), '### In plan mode'));
assert.doesNotMatch(pm, /Then continue with the operator's choice \(Phase 11\)\./, 'plan mode still continues with the operator\'s choice');
assert.match(pm, /Then print the `\/trekexecute` line \(Phase 11\) and stop — leaving plan mode is not an order to implement\./);
});
test('[#52] hard rule: "No premature execution" holds, and nothing in Phases 10-11 implements the plan', () => {
const text = read('commands/trekplan.md');
const rules = flat(section(text, '## Hard rules'));
assert.match(rules, /\*\*No premature execution\*\*: Do not modify any project files until the user explicitly approves the plan, and never while `plan-gate\.mjs` exits non-zero on it\./);
for (const h of ['## Phase 10 — Present and refine', '## Phase 11 — Handoff']) {
const part = flat(section(text, h));
neverImplementsHere(part, h);
assert.doesNotMatch(part, /start implementing|Follow the plan exactly|Mark each step complete/i, `${h}: still has /trekplan build the plan`);
}
});
test('[#52] sibling: agents/planning-orchestrator.md says "execute" gives the /trekexecute line', () => {
const text = read('agents/planning-orchestrator.md');
assert.doesNotMatch(text, /Say "execute" to implement/, 'planning-orchestrator still says "execute" implements');
assert.ok(text.split('\n').includes('- Say "execute" for the /trekexecute line'), 'missing line: - Say "execute" for the /trekexecute line');
});
// --- #48 / #12 / #57 ---------------------------------------------------------------
// The planning and research spawn sites pinned `model: "opus"`, past the
// resolved model (brief signal > profile > agent frontmatter); every spawn now
// takes `phase_signal_result.model`. No `maxTurns` budget is promised: the
// Agent tool takes none, so "scale turns by size" was a promise nothing kept.
/**
* From the line holding `marker` (it must be the only one) to the end of the
* blank-line-separated paragraph after the marker's own paragraph, flattened.
*/
function paraAfter(rel, marker) {
const lines = read(rel).split('\n');
const hits = lines.filter((l) => l.includes(marker)).length;
assert.equal(hits, 1, `${rel}: expected exactly one line holding "${marker}"; found ${hits}`);
const start = lines.findIndex((l) => l.includes(marker));
let i = start;
while (i < lines.length && lines[i].trim() !== '') i++;
while (i < lines.length && lines[i].trim() === '') i++;
while (i < lines.length && lines[i].trim() !== '') i++;
return flat(lines.slice(start, i).join('\n'));
}
const PASS_MODEL = /Pass `model: \{phase_signal_result\.model\}` at this spawn; without it \(the `--brief` branch, where the resolver does not run, or a resolver that printed nothing\) the agent's own `model:` applies\./;
test('[#48] commands/trekplan.md pins no spawn to `model: "opus"`', () => {
assert.doesNotMatch(read('commands/trekplan.md'), /model: "opus"/);
});
for (const [where, marker] of [
['session-decomposer (Phase 1.5)', 'Launch the **session-decomposer** agent:'],
['brief-reviewer (Phase 4b)', 'Launch the **brief-reviewer** agent.'],
['### Always launch (Phase 5)', '### Always launch (all codebase sizes):'],
['research-scout (Phase 5)', 'launch **research-scout** in'],
['plan-critic and scope-guardian (Phase 9)', 'it traces into `## Verification`.'],
]) {
test(`[#48] ${where}: the spawn takes phase_signal_result.model, the agent's own model: as the fallback`, () => {
const part = paraAfter('commands/trekplan.md', marker);
assert.match(part, PASS_MODEL, `${where}: missing the resolved-model paragraph after "${marker}"`);
assert.doesNotMatch(part, /model: "opus"/);
});
}
test('[#48] convention-scanner (Phase 5): the spawn takes phase_signal_result.model, the frontmatter as the fallback', () => {
const part = paraAfter('commands/trekplan.md', 'use the `convention-scanner` plugin agent');
assert.match(part, /\(model: phase_signal_result\.model, the agent's frontmatter as the fallback\)/);
assert.doesNotMatch(part, /model: "opus"/);
});
test('[#48] deep-dives (Phase 6): the spawn takes phase_signal_result.model, else opus', () => {
const part = paraAfter('commands/trekplan.md', 'spawn a targeted deep-dive agent');
assert.match(part, /\(model: phase_signal_result\.model, else opus — Explore is Claude Code's built-in agent and has no Voyage frontmatter to fall back to, subagent_type: "Explore"\)/);
assert.doesNotMatch(part, /model: "opus"/);
});
test('[#48] sibling: agents/planning-orchestrator.md:137 takes phase_signal_result.model', () => {
const text = read('agents/planning-orchestrator.md');
assert.doesNotMatch(text, /model: "opus"/);
const part = partAt('agents/planning-orchestrator.md', '**Convention Scanner** — use the `convention-scanner` plugin agent', toBlank);
assert.match(part, /\(model: phase_signal_result\.model, the agent's frontmatter as the fallback\)/);
});
test('[#12] trekresearch Phase 5 follow-ups take phase_signal_result.model, else opus', () => {
const text = read('commands/trekresearch.md');
const p5 = flat(section(text, '## Phase 5 — Targeted follow-ups'));
assert.doesNotMatch(p5, /model: "opus"/);
const part = partAt('commands/trekresearch.md', 'launch a targeted follow-up agent', toBlank);
assert.match(part, /launch a targeted follow-up agent \(model: phase_signal_result\.model, else opus — a follow-up may be one of Claude Code's built-in agents, which have no Voyage frontmatter to fall back to; see Launch rules\)/);
assert.match(flat(text), /captured as `phase_signal_result` and used at Agent-spawn sites in Phase 4 and Phase 5 to inject the resolved model/);
});
test('[#57] commands/trekresearch.md promises no maxTurns budget', () => {
const text = read('commands/trekresearch.md');
assert.doesNotMatch(text, /maxTurns/);
assert.match(text, /- convention-scanner: medium\+ codebases only \(50\+ files\)/);
});
test('[#57] sibling: commands/trekplan.md promises no maxTurns budget, and Phase 4 calibrates no turns', () => {
const text = read('commands/trekplan.md');
assert.doesNotMatch(text, /maxTurns/);
const p4 = flat(section(text, '## Phase 4 — Codebase sizing'));
assert.doesNotMatch(p4, /calibrate agent turns/);
assert.match(p4, /Determine codebase scale to decide whether convention-scanner runs\./);
assert.match(text, /^\*\*All agents run for all codebase sizes\.\*\*$/m);
});
test('[#57] sibling: agents/planning-orchestrator.md promises no maxTurns budget and scales no turns', () => {
const text = read('agents/planning-orchestrator.md');
assert.doesNotMatch(text, /maxTurns/);
assert.doesNotMatch(text, /Scale turns/i);
assert.match(text, /^\*\*All agents run for all codebase sizes\.\*\*$/m);
assert.match(text, /^- \*\*Adaptive:\*\* All agents run for all sizes\.$/m);
});
test('keep: /trekexecute\'s controller and its model note stay pinned to opus', () => {
const text = read('commands/trekexecute.md');
assert.match(text, /`subagent_type: "general-purpose"`, `model: "opus"`, no `name`/);
assert.match(flat(text), /controller's `model: "opus"` — so `phase_signal_result\.model` still has no spawn site here/);
});
// --- #46 ---------------------------------------------------------------------------
// Six commands said "Stats records emit `profile`, `profile_source` ..." under
// a stats heredoc that writes none of those keys. A paragraph outside a code
// fence that mentions stats may name a profile key only when the command's own
// `stats-append.mjs <kind> <<'JSON'` record writes it.
const STATS_PROFILE_KEYS = ['profile', 'phase_models', 'parallel_agents', 'external_research_enabled',
'profile_source', 'model_used', 'phase_models_resolved'];
/** The keys of every `stats-append.mjs <kind> <<'JSON'` … `JSON` heredoc in `text`. */
function statsHeredocs(text) {
const out = [];
for (const m of text.matchAll(/stats-append\.mjs (\S+) <<'JSON'\n([\s\S]*?)\nJSON\s*$/gm)) {
out.push({ kind: m[1], keys: new Set([...m[2].matchAll(/"([A-Za-z_][A-Za-z0-9_]*)"\s*:/g)].map((k) => k[1])) });
}
return out;
}
/**
* Every backticked profile key named in a paragraph (outside code fences,
* split on blank lines) that mentions stats, and not in `written`; sorted.
*/
function unwrittenStatsKeys(text, written) {
const paras = [];
let cur = [];
let fenced = false;
for (const l of text.split('\n')) {
if (/^\s{0,3}(`{3,}|~{3,})/.test(l)) { fenced = !fenced; paras.push(cur); cur = []; continue; }
if (fenced) continue;
if (l.trim() === '') { paras.push(cur); cur = []; } else cur.push(l);
}
paras.push(cur);
const bad = new Set();
for (const p of paras.map((x) => x.join('\n')).filter((p) => /stats/i.test(p))) {
for (const m of p.matchAll(/`([^`\n]+)`/g)) {
if (STATS_PROFILE_KEYS.includes(m[1]) && !written.has(m[1])) bad.add(m[1]);
}
}
return [...bad].sort();
}
for (const cmd of ['trekbrief', 'trekresearch', 'trekplan', 'trekexecute', 'trekreview', 'trekcontinue']) {
test(`[#46] commands/${cmd}.md: every profile key its stats prose names is one its stats record writes`, () => {
const text = read(`commands/${cmd}.md`);
const docs = statsHeredocs(text);
assert.equal(docs.length, 1, `${cmd}: expected exactly one stats-append.mjs heredoc; found ${docs.length}`);
assert.equal(docs[0].kind, cmd, `${cmd}: the stats heredoc writes kind ${docs[0].kind}`);
assert.ok(docs[0].keys.has('ts'), `${cmd}: the stats heredoc's keys were not read`);
assert.deepEqual(unwrittenStatsKeys(text, docs[0].keys), [],
`${cmd}: stats prose promises keys its stats record does not write`);
});
}
test('control: the stats-key checker reports a planted paragraph that promises `profile`', () => {
const written = statsHeredocs(read('commands/trekplan.md'))[0].keys;
assert.deepEqual(unwrittenStatsKeys('Intro.\n\nStats rows carry `profile`.\n', written), ['profile']);
assert.deepEqual(unwrittenStatsKeys('Intro.\n\n```\nStats rows carry `profile`.\n```\n', written), []);
assert.deepEqual(unwrittenStatsKeys('Rows carry `profile`.\n', written), []);
});
test('[#46] profile_source appears in no file under commands/', () => {
const hits = readdirSync(join(ROOT, 'commands'))
.filter((f) => read(`commands/${f}`).includes('profile_source'));
assert.deepEqual(hits, [], 'a command still names profile_source, a key no stats record writes');
});
test('[#46] sibling: docs/profiles.md claims no profile field in the stats records', () => {
const text = read('docs/profiles.md');
const f = flat(text);
assert.doesNotMatch(f, /recorded in two places/);
assert.doesNotMatch(f, /recorded in one place/);
assert.ok(text.split('\n').includes('The plan-file fields for the resolved value:'),
'missing line: The plan-file fields for the resolved value:');
assert.doesNotMatch(f, /Stats stream/);
assert.doesNotMatch(f, /`profile_source` distinguishes/);
assert.doesNotMatch(f, /Per-profile actuals are emitted/);
assert.match(f, /The stats records carry no profile field, so they cannot split cost by profile\./);
});
/** The one blank-line paragraph of `rel` that opens with `opening`, flattened. */
function paraOpening(rel, opening) {
const hits = read(rel).split(/\n[ \t]*\n/).filter((p) => p.trimStart().startsWith(opening));
assert.equal(hits.length, 1, `${rel}: expected exactly one paragraph opening "${opening}"; found ${hits.length}`);
return flat(hits[0]);
}
for (const rel of ['README.md', 'docs/operations.md']) {
test(`[#46] sibling: ${rel} profile paragraph says nothing is emitted to stats`, () => {
const p = paraOpening(rel, 'Four built-in model profiles');
assert.doesNotMatch(p, /emitted to/, `${rel}: the profile paragraph still says the profile is emitted`);
assert.doesNotMatch(p, /stats/i, `${rel}: the profile paragraph still names stats`);
});
}
// --- #47 ---------------------------------------------------------------------------
// /trekbrief Phase 5 said "Skip this phase if research_topics = 0. Proceed
// directly to Phase 6.", so a brief with no research topics walked into the
// auto-only research dispatch nobody opted into. It now prints the manual
// next steps without the /trekresearch lines, runs Phase 7 and stops.
test('[#47] Phase 5: a brief with no research topics prints /trekplan --project and stops, never proceeds to Phase 6', () => {
const text = read('commands/trekbrief.md');
const p5 = flat(section(text, '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)'));
assert.doesNotMatch(p5, /Skip this phase if research_topics = 0/, 'Phase 5 still skips straight on with zero topics');
const opening = partAt('commands/trekbrief.md', '## Phase 5 — Auto-orchestration opt-in', (l) => l.startsWith('Ask the user'));
assert.doesNotMatch(opening, /proceed directly to Phase 6/i, 'a zero-topic brief still proceeds directly to Phase 6');
assert.match(opening, /\*\*With research_topics = 0\*\*, print the manual-path output below without the `\/trekresearch` lines \(Brief, then `\/trekplan --project \{PROJECT_DIR\}`, then `\/trekexecute --project \{PROJECT_DIR\}`\), then run Phase 7 and stop\./);
assert.match(opening, /Phase 6 is never entered without the operator's opt-in\./);
const manual = flat(section(text, '### Manual path (default)'));
assert.match(manual, /Then: \/trekplan --project \{PROJECT_DIR\} Then: \/trekexecute --project \{PROJECT_DIR\}/);
assert.match(manual, /Stop\. Do not continue to Phase 6\./);
});
test('[#47] Phase 6 runs only after the operator opted into auto mode and Phase 4h approved the intent', () => {
const p6 = flat(section(read('commands/trekbrief.md'), '## Phase 6 — Auto research dispatch (auto path only)'));
assert.match(p6, /^## Phase 6 — Auto research dispatch \(auto path only\) \*\*Runs only when user opted into auto mode,? and only after Phase 4h approved the intent\.\*\*/);
});
test('keep: /trekbrief\'s auto path still requires an approved intent', () => {
assert.equal(partAt('commands/trekbrief.md', '**Auto requires an approved intent.**', toBlank),
'**Auto requires an approved intent.** If Phase 4h ended "Leave unapproved", the auto chain would halt at `/trekplan` anyway — print the manual-path output instead and stop.');
});
// --- #1 ----------------------------------------------------------------------------
// `--gates` meant one thing in the shim (a boolean), another in the docs (an
// open/closed/adaptive strictness policy), and /trekexecute promised pauses it
// never makes. Each of the four commands now holds one rule: the next token is
// the flag's value only when it is true, false, closed or open; the shim maps
// it, run once; a broken shim falls back to the same mapping in prose and the
// run goes on. /trekexecute records gates_mode and pauses on nothing.
const GATES_COMMANDS = ['trekbrief', 'trekresearch', 'trekplan', 'trekexecute'];
const GATES_RULE = [
'`--gates` takes the next token as its value only when it is `true`, `false`, `closed` or `open` (strip both); any other next token is not its value (strip only the flag).',
'Run the shim once to map the value, bare when there is no value:',
'`next_state: gates_on` sets `gates_mode = true`, `auto_running` sets it false, and nothing else reads the state.',
'On exit 1 or no JSON (a broken shim; the four values cannot cause it), print the error in one line and map in prose instead: bare, `true`, `closed` → true; `false`, `open` → false. The run goes on either way.',
];
/** The command's one shim line, `[value]` filled in with `value` (left out when ''). */
function gatesShim(cmd, value) {
const l = line(read(`commands/${cmd}.md`), 'util/autonomy-gate.mjs', '--event start');
assert.match(l, / --state idle --event start --gates \[value\]$/, `${cmd}: the shim line must end "--gates [value]"`);
return l.replace(' [value]', value ? ` ${value}` : '');
}
/** Run a shim line from the prose; its `next_state`. */
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;
}
for (const cmd of GATES_COMMANDS) {
test(`[#1] commands/${cmd}.md holds the shared --gates rule and a shim line that maps closed to gates_on`, () => {
const text = read(`commands/${cmd}.md`);
const f = flat(text);
for (const s of GATES_RULE) assert.ok(f.includes(s), `${cmd}: missing the shared --gates sentence: ${s}`);
assert.doesNotMatch(text, /--state X --event Y/, `${cmd}: the placeholder shim line is still there`);
assert.doesNotMatch(f, /consumed by the autonomy-gate/, `${cmd}: still says the flag is consumed by the state machine`);
assert.equal(shimNext(gatesShim(cmd, 'closed')), 'gates_on');
});
}
// Review focus: `/trekplan --gates --project d` and `/trekexecute --gates plan.md`
// are a bare --gates (the next token is not a value), so the shim runs bare.
test('keep: every command\'s shim line maps a bare --gates to gates_on and open to auto_running', () => {
for (const cmd of GATES_COMMANDS) {
assert.equal(shimNext(gatesShim(cmd, '')), 'gates_on', `${cmd}: bare --gates`);
assert.equal(shimNext(gatesShim(cmd, 'open')), 'auto_running', `${cmd}: --gates open`);
}
});
test('[#1] sibling: commands/trekbrief.md promises no execute pause and no pause at every wave in the executor', () => {
const f = flat(read('commands/trekbrief.md'));
assert.doesNotMatch(f, /every wave in the executor/, 'trekbrief still promises a pause at every wave in the executor');
assert.doesNotMatch(f, /\(research, plan, execute\)/, 'trekbrief still promises an execute pause');
assert.match(f, /approval pauses at every phase boundary in the downstream pipeline \(research, plan\)\./);
});
test('[#1] sibling: commands/trekexecute.md records gates_mode and pauses only at the merge into main', () => {
const f = flat(read('commands/trekexecute.md'));
assert.doesNotMatch(f, /consumed by the autonomy-gate state machine/);
assert.match(f, /`\/trekexecute` records `gates_mode`; no phase of it pauses on it\. The one stop is the merge into main, which always asks \(Phase 8\)\./);
});
test('[#1] sibling: docs/operations.md gives the four values and the bare form, and no executor strictness table', () => {
const text = read('docs/operations.md');
const sec = flat(section(text, '## Autonomy mode (`--gates`, v3.4.0)'));
assert.doesNotMatch(text, /\{open\\?\|closed\\?\|adaptive\}/);
assert.doesNotMatch(sec, /\| `gates_mode` \| Derived from/, 'the executor strictness table is still there');
assert.doesNotMatch(sec, /\| `adaptive` \|/, 'the executor strictness table is still there');
assert.doesNotMatch(sec, /how strict/);
assert.doesNotMatch(sec, /gates_mode_flag_override/, 'still names an override record the executor does not write');
assert.match(sec, /accept `--gates \[true\|false\|closed\|open\]`/);
assert.match(sec, /A bare `--gates`, `--gates true` or `--gates closed` turns gating \*\*on\*\*/);
for (const s of ['gates_on', 'auto_running', 'paused_for_gate']) assert.ok(sec.includes(s), `the state ${s} stays named`);
});
test('[#1] sibling: docs/architecture.md does not call --gates open the executor\'s low effort', () => {
const text = read('docs/architecture.md');
assert.doesNotMatch(text, /--gates open/);
assert.match(text, /\(`\/trekexecute` low effort = one session at a time\)/);
});
test('[#1] sibling: docs/command-modes.md gives every --gates row the four values and no strictness policy', () => {
const text = read('docs/command-modes.md');
assert.doesNotMatch(text, /--gates \{true\\\|false\}/);
assert.doesNotMatch(text, /\{open\\?\|closed\\?\|adaptive\}/);
assert.doesNotMatch(text, /Policy \(`gates_mode`\)/, 'a row still points at a gates_mode strictness policy');
const rows = text.split('\n').filter((l) => l.startsWith('| `--gates'));
assert.equal(rows.length, 4, 'one --gates row per command');
for (const r of rows) {
assert.ok(r.startsWith('| `--gates [true\\|false\\|closed\\|open]` |'), `row does not give the four values: ${r}`);
assert.match(r, /`## Autonomy mode` in `docs\/operations\.md`/);
}
});
test('[#1] sibling: README.md gives the four values, and no strictness table', () => {
const text = read('README.md');
const sec = flat(section(text, '### Autonomy mode (`--gates`, v3.4.0)'));
assert.doesNotMatch(text, /\{open\|closed\|adaptive\}/);
assert.doesNotMatch(sec, /\| Value \| Behavior \|/, 'the strictness table is still there');
assert.doesNotMatch(sec, /`adaptive` \(default\)/);
assert.match(sec, /accept `--gates \[true\|false\|closed\|open\]`/);
});
// --- #30 ---------------------------------------------------------------------------
const EXEC = 'commands/trekexecute.md';
const SAME_ON_OPUS = 'the step helper and the controller stay on opus';
/** Phase 1's top-level numbered rules, each as [number, text of its first line]. */
function phase1Rules() {
const sec = section(read(EXEC), '## Phase 1 — Parse mode and validate input').split('\n');
const stop = sec.findIndex((l) => l.startsWith('If no path is provided'));
assert.ok(stop !== -1, 'Phase 1 must still say what happens when no path is provided');
return sec.slice(0, stop)
.map((l) => l.match(/^(\d+)\. (.*)$/))
.filter(Boolean)
.map((m) => [Number(m[1]), m[2]]);
}
test('[#30] the frontmatter hint names --profile <name> and --gates', () => {
const m = read(EXEC).match(/^argument-hint: (.*)$/m);
assert.ok(m, 'trekexecute has no argument-hint');
assert.ok(m[1].includes('[--profile <name>]'), `the hint does not name --profile <name>: ${m[1]}`);
assert.ok(m[1].includes('[--gates [true|false|closed|open]]'), `the hint does not name --gates: ${m[1]}`);
assert.equal(m[1],
'"[--project <dir>] [--profile <name>] [--gates [true|false|closed|open]] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md]"');
});
test('[#30] Phase 1 strips --profile <name>, and its last rule reads what is left after the flags are stripped', () => {
const f = flat(section(read(EXEC), '## Phase 1 — Parse mode and validate input'));
assert.doesNotMatch(f, /the entire argument string is the file path/i,
'Phase 1 still reads the whole argument string as the file path');
const rules = phase1Rules();
assert.deepEqual(rules.map(([n]) => n), rules.map((_, i) => i + 1), 'Phase 1 rules are not numbered 1..n');
const [last, profile] = [rules[rules.length - 1][1], rules[rules.length - 2][1]];
assert.ok(last.startsWith('Otherwise: the arguments left after every flag above is stripped are the file path.'),
`the last rule does not read what is left after the flags: ${last}`);
assert.ok(profile.includes('`--profile <name>`: set `profile = {name}`; strip both tokens'),
`the rule before the last one is not the --profile strip rule: ${profile}`);
});
test('[#30] the usage block names --profile and --gates, and its --profile line keeps the helper and the controller on opus', () => {
const usage = partAt(EXEC, 'Usage: /trekexecute <plan.md>', (l) => l.startsWith('```'));
assert.ok(usage.includes('/trekexecute --profile <name> <plan.md>'), 'the usage block has no --profile line');
assert.ok(usage.includes(`--profile Profile for this run's resolver; ${SAME_ON_OPUS}`),
'the Modes list has no --profile line, or it does not keep the helper and the controller on opus');
assert.ok(usage.includes('--gates Record gates_mode: bare, true or closed = on; false or open = off'),
'the Modes list has no --gates line');
});
test('[#30] sibling: Profile section says the profile changes no model here', () => {
const f = flat(section(read(EXEC), '## Profile (v4.1)'));
assert.doesNotMatch(f, /drives `phase_models\.execute`/, 'the Profile section still says the profile drives the execute model');
assert.ok(f.includes('The selected profile goes to the resolver. It changes no model here: the step helper and the '
+ 'controller run opus (Composition rule below), and nothing in this command reads `parallel_agents`.'),
'the Profile section does not say the profile changes no model here');
});
test('[#30] sibling: docs/command-modes.md /trekexecute --profile row keeps the helper and the controller on opus', () => {
const sec = section(read('docs/command-modes.md'), '## /trekexecute modes');
const rows = sec.split('\n').filter((l) => l.startsWith('| `--profile <name>` |'));
assert.equal(rows.length, 1, 'expected one --profile row under /trekexecute modes');
assert.doesNotMatch(rows[0], /Model profile for the execute phase/);
assert.ok(rows[0].includes(`| (v4.1.0) Profile for this run's resolver; ${SAME_ON_OPUS}.`), rows[0]);
});
test('[#30] sibling: README.md /trekexecute Profile row keeps the helper and the controller on opus', () => {
const rows = read('README.md').split('\n').filter((l) => l.startsWith('| **Profile** | `/trekexecute '));
assert.equal(rows.length, 1, 'expected one /trekexecute Profile row in README.md');
assert.doesNotMatch(rows[0], /Pin model profile for execution/);
assert.ok(rows[0].includes(`| (v4.1.0) Profile for this run's resolver; ${SAME_ON_OPUS}.`), rows[0]);
});
// --- #33 ---------------------------------------------------------------------------
// /trekexecute promised a brief check and an effort-driven gates mode that no
// line ran. Phase 1's --project branch now runs the two lines /trekplan runs,
// for the execute phase, and halts only on BRIEF_V51_MISSING_SIGNALS (a brief
// edited since planning does not stop a --resume on any other error). Low
// effort means one session, step after step; high effort changes nothing.
/** Phase 1's rule 1 (the --project branch), as lines. */
function projectRule() {
const sec = section(read(EXEC), '## Phase 1 — Parse mode and validate input').split('\n');
const start = sec.findIndex((l) => l.startsWith('1. If arguments contain `--project <dir>`'));
assert.ok(start !== -1, 'Phase 1 must still open with the --project rule');
const end = sec.findIndex((l, i) => i > start && /^2\. /.test(l));
assert.ok(end !== -1, 'Phase 1 must still have a rule 2');
return sec.slice(start, end).join('\n');
}
test('[#33] Phase 1 --project runs brief-validator --soft --json and the execute resolver, and halts only on BRIEF_V51_MISSING_SIGNALS', () => {
const rule = projectRule();
const validator = line(rule, 'validators/brief-validator.mjs', '--soft --json');
const resolver = line(rule, 'profiles/resolver.mjs', '--resolve-phase-model --phase execute');
assert.equal(validator, 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"');
assert.equal(resolver,
'node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json');
const f = flat(rule);
for (const s of [
'The validator exits 1 when its `errors` are not empty; read its JSON either way.',
'Halt only when the validator\'s `errors` hold `BRIEF_V51_MISSING_SIGNALS`, with the message from "Sequencing gate surface" below.',
'Any other error is printed in one line and the run continues.',
'A validator or resolver that cannot run (no JSON) is "not measured" in one line, and the run continues.',
'Capture the resolver\'s JSON as `phase_signal_result` (Composition rule below).',
'No `{dir}/brief.md`: skip both lines.',
]) assert.ok(f.includes(s), `the --project rule is missing: ${s}`);
// The two lines run as printed: the validator names the one halting code for a
// 2.1 brief without signals, and the resolver hands Phase 2.5 its low effort.
const dir = mkdtempSync(join(tmpdir(), 'fix-33-'));
try {
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-v21-no-signals.md'), join(dir, 'brief.md'));
const v = bash(validator.split('{dir}').join(dir), ROOT);
assert.equal(v.status, 1, `validator: ${v.stdout}${v.stderr}`);
assert.ok(JSON.parse(v.stdout).errors.some((e) => e.code === 'BRIEF_V51_MISSING_SIGNALS'), v.stdout);
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-effort-low.md'), join(dir, 'brief.md'));
const r = bash(resolver.split('{dir}').join(dir).replace(' [--profile {profile}]', ''), ROOT);
assert.equal(r.status, 0, `resolver: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).effort, 'low');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('[#33] the Composition rule says the resolver runs in Phase 1, and no line says Phase 2.4 runs it', () => {
assert.doesNotMatch(flat(read(EXEC)), /invoked in Phase 2\.4/);
assert.ok(flat(section(read(EXEC), '## Composition rule (v5.1)')).includes('invoked in Phase 1 alongside the sequencing-gate brief-validator call'),
'the Composition rule does not say the resolver is invoked in Phase 1');
});
test('[#33] sibling: Composition rule has no "uniformity and stats" and no gates auto-escalation', () => {
const f = flat(section(read(EXEC), '## Composition rule (v5.1)'));
assert.doesNotMatch(f, /uniformity and stats/, 'the Composition rule still says the model is returned for stats');
assert.ok(f.includes('it is returned for cross-command uniformity; no stats record carries it.'),
'the Composition rule does not say no stats record carries the model');
assert.doesNotMatch(f, /gates auto-escalation/);
});
test('[#33] Phase 2.5 names low effort as one session, steps in order, never while a wave is in flight', () => {
const sec = section(read(EXEC), '## Phase 2.5 — Execution strategy decision').split('\n');
const start = sec.findIndex((l) => l.startsWith('**Run as single session (foreground)** when ANY of these are true:'));
assert.ok(start !== -1, 'Phase 2.5 must still list when it runs as a single session');
const end = sec.findIndex((l, i) => i > start && l.trim() === '');
const bullets = sec.slice(start + 1, end);
assert.ok(bullets.includes("- `phase_signal_result.effort == 'low'` → one session, steps in order — never while a wave is in flight (`WAVE_IN_FLIGHT`)"),
`the single-session list does not name low effort: ${bullets.join(' | ')}`);
});
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left, and high effort says it changes nothing', () => {
const text = read(EXEC);
const f = flat(text);
assert.doesNotMatch(f, /gates_mode = 'closed'/);
assert.doesNotMatch(f, /strictest/);
assert.doesNotMatch(f, /halts immediately/);
assert.doesNotMatch(text, /--event gates_mode_flag_override/);
assert.doesNotMatch(f, /gates_mode = '(adaptive|open)'/);
assert.equal(flat(section(text, '### High-effort behavior (v5.1.1)')),
'### High-effort behavior (v5.1.1) High effort changes nothing in `/trekexecute`: the step helper and the controller '
+ 'already run opus, and every gate already runs. No `gates_mode` is set from the effort.');
});
// --- #28 ---------------------------------------------------------------------------
/** The completion gate's text with its blockquote markers removed, flattened. */
function completionGate() {
return flat(section(read(EXEC), '### Machine-verifiable completion gate (stop-signal contract)').replace(/^> ?/gm, ''));
}
test('[#28] the completion gate calls nothing deterministic that is executor judgement, and cites no line number', () => {
const gate = completionGate();
assert.ok(!gate.includes('Counter liveness (deterministic'), 'clause 3 still calls the counter check deterministic');
assert.ok(!gate.includes('deterministic audit'), 'the gate still says termination rests on a deterministic audit');
assert.ok(!gate.includes('`:1221`'), 'the gate still cites `:1221`');
assert.doesNotMatch(gate, /`:\d+`/, 'the gate cites a line number; line numbers rot');
assert.ok(gate.includes('Counter liveness (checked by the executor against the audit\'s counts, defeats "never decremented")'),
'clause 3 does not say the executor checks the counter against the audit\'s counts');
assert.ok(gate.includes('rests on the audit and an objective stop-signal, never on the model\'s narrative'),
'the termination sentence does not rest on the audit and an objective stop-signal');
});
test('[#28] the completion gate names its commands and its executor judgement, and each command is in Phase 7.5', () => {
const gate = completionGate();
const c = gate.indexOf('Commands:');
const j = gate.indexOf('Executor judgement:');
assert.ok(c !== -1, 'the gate does not say which checks are commands');
assert.ok(j > c, 'the gate does not say which checks are executor judgement, after its commands');
const commands = gate.slice(c, j);
const judgement = gate.slice(j);
// Phase 7.5's own steps only: the gate subsection after them holds the Commands part under test.
const phase75 = section(read(EXEC), '## Phase 7.5 — Manifest audit (independent)');
const gateAt = phase75.indexOf('\n### Machine-verifiable completion gate');
assert.ok(gateAt !== -1, 'Phase 7.5 no longer holds the completion gate subsection');
const audit = flat(phase75.slice(0, gateAt));
assert.ok(!audit.includes('Commands:'), 'the audit slice still holds the gate\'s Commands part');
for (const cmd of ['git rev-list --count', 'bash -n', 'git diff --name-only', 'done-line.mjs --check']) {
assert.ok(commands.includes(cmd), `the Commands part does not name ${cmd}`);
assert.ok(audit.includes(cmd), `Phase 7.5 does not run ${cmd}, so the gate cannot call it a command`);
}
for (const s of ['criteria runner', 'arch-rules.mjs', 'plan-scope-sweep.mjs']) {
assert.ok(commands.includes(s), `the Commands part does not name ${s}`);
}
for (const s of ['steps 1, 2 and 4', 'the expected count in step 3', 'clause 3\'s formula']) {
assert.ok(judgement.includes(s), `the Executor judgement part does not name ${s}`);
}
});
// ---- Architecture rule (plan, Step 12): lib/ never imports from scripts/ ----
/**
* The import specifiers in `text` (a file at `file`) that resolve into the
* repo's scripts/ directory: static and side-effect imports, re-exports and
* dynamic imports with a string literal.
*/
function importsIntoScripts(file, text) {
const re = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)|\b(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g;
const scripts = join(ROOT, 'scripts') + sep;
const hits = [];
for (const m of text.matchAll(re)) {
const spec = m[1] ?? m[2];
if (!spec.startsWith('.') && !spec.startsWith('/')) continue;
if (resolve(dirname(file), spec).startsWith(scripts)) hits.push(spec);
}
return hits;
}
/** Every .mjs file under `dir`, recursively. */
function mjsFiles(dir) {
const out = [];
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) out.push(...mjsFiles(p));
else if (e.name.endsWith('.mjs')) out.push(p);
}
return out;
}
test('arch: lib/ never imports from scripts/, and the scan reports a planted import (control)', () => {
const planted = importsIntoScripts(join(ROOT, 'lib', 'review', 'x.mjs'), "import x from '../../scripts/release-check.mjs';\n");
assert.deepEqual(planted, ['../../scripts/release-check.mjs'], 'control: the scan does not report a planted import from scripts/');
const files = mjsFiles(join(ROOT, 'lib'));
assert.ok(files.length > 10, `the scan found only ${files.length} files under lib/`);
const bad = files.flatMap((f) => importsIntoScripts(f, readFileSync(f, 'utf8')).map((s) => `${f.slice(ROOT.length + 1)}: ${s}`));
assert.deepEqual(bad, [], 'lib/ imports from scripts/');
});
// --- R6 ----------------------------------------------------------------------------
// The merge lock (Phase 7.2 step 4 and the Phase 8 Main-merge gate) runs the
// verdict gate with `--repo .`, so a review of an older commit than the one
// being merged is refused (`review-stale`), and only the closing run's own
// review opens the merge.
const MERGE_GATE = '### Main-merge gate (MAIN_MERGE_GATE — always pauses)';
const PHASE_72 = '## Phase 7.2 — Simplify, review, verdict (P11)';
const LOCK_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{review_path}" --repo .';
const STEP4_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/review/verdict-gate.mjs --json "{project_dir}/review.md" --repo .';
/** A review_version 1.1 review.md of `sha`: ALLOW with no findings, or BLOCK over one BLOCKER. */
function r6Review(verdict, sha) {
const id = 'd'.repeat(40);
const findings = verdict === 'BLOCK'
? [{ id, severity: 'BLOCKER', rule_key: 'MISSING_TEST', file: 'a.mjs', line: 1, title: 't', detail: 'd', recommended_action: 'Add a test.' }]
: [];
const counts = { BLOCKER: findings.length, MAJOR: 0, MINOR: 0, SUGGESTION: 0 };
const block = JSON.stringify({ verdict, counts, allow_blocked_by: [], findings }, null, 2);
return [
'---', 'type: trekreview', 'review_version: "1.1"', 'task: "t"', 'slug: s',
'project_dir: .claude/projects/2026-10-23-s/', 'brief_path: .claude/projects/2026-10-23-s/brief.md',
`scope_sha_end: ${sha}`, 'reviewed_files_count: 1', `verdict: ${verdict}`,
findings.length ? `findings:\n - ${id}` : 'findings: []', '---', '', '# Review: t', '',
'## Executive Summary', '', 'x', '', '## Coverage', '', '| File | Treatment | Reason |', '|---|---|---|',
'| a.mjs | summary-only | default |', '', '## Remediation Summary', '', 'x', '', '```json', block, '```', '',
].join('\n');
}
/**
* A throwaway repo (no git hooks): the reviewed commit, then a later commit
* that changes source. `review(verdict, sha)` writes review.md outside the
* repo and returns its folder.
*/
function staleRepo() {
const base = mkdtempSync(join(tmpdir(), 'fix-round-r6-'));
const repo = join(base, 'repo');
const out = join(base, 'project');
mkdirSync(repo);
mkdirSync(out);
mkdirSync(join(base, 'hooks-none'));
const g = (...args) => {
const r = spawnSync('git', args, { cwd: repo, encoding: 'utf8', env: childEnv() });
assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
};
g('init', '-q', '-b', 'main');
g('config', 'user.name', 'Test');
g('config', 'user.email', 'test@example.invalid');
g('config', 'commit.gpgsign', 'false');
g('config', 'core.hooksPath', join(base, 'hooks-none'));
writeFileSync(join(repo, 'a.mjs'), 'export const a = 1;\n');
g('add', 'a.mjs');
g('commit', '-q', '-m', 'feat: the reviewed work');
const reviewed = g('rev-parse', 'HEAD');
writeFileSync(join(repo, 'a.mjs'), 'export const a = 2;\n');
g('add', 'a.mjs');
g('commit', '-q', '-m', 'feat: a later source change');
const head = g('rev-parse', 'HEAD');
const review = (verdict, sha) => {
writeFileSync(join(out, 'review.md'), r6Review(verdict, sha));
return out;
};
return { repo, reviewed, head, review, cleanup: () => rmSync(base, { recursive: true, force: true }) };
}
/** 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);
return bash(filled, repo);
}
test('[R6] the Main-merge gate and Phase 7.2 step 4 each hold exactly one verdict-gate.mjs line, with --repo .', () => {
const text = read(EXEC);
assert.equal(line(section(text, MERGE_GATE), 'review/verdict-gate.mjs'), LOCK_LINE,
'the Phase 8 lock does not run the verdict gate against the repo being merged');
assert.equal(line(section(text, PHASE_72), 'review/verdict-gate.mjs'), STEP4_LINE,
'Phase 7.2 step 4 does not run the verdict gate against the repo being merged');
});
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(); }
});
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', () => {
const gate = flat(section(read(EXEC), MERGE_GATE));
const closed = gate.match(/\*\*Closed:\*\* emit `main-merge-declined` with the reason ([^.]*)\./);
assert.ok(closed, 'the gate no longer says which reasons a closed lock emits');
assert.ok(closed[1].includes('`review-stale`'), `the closed-lock reasons do not name review-stale: ${closed[1]}`);
const literal = gate.match(/The reason is one of the literal words above \(([^)]*)\)/);
assert.ok(literal, 'the gate no longer lists the literal decline reasons');
assert.ok(literal[1].includes('`review-stale`'), `the literal decline reasons do not name review-stale: ${literal[1]}`);
assert.match(gate, /a reason that starts `review-stale`[^.]*decline reason `review-stale`[^.]*re-run \/trekreview on this commit/,
'the exit table does not map a review-stale reason to the decline reason and its "re-run /trekreview on this commit" line');
assert.match(gate, /`--resume re-enters at the gate`[^.]*runs the same gate[^.]*review of an older commit stays refused/,
'the --resume paragraph does not say the re-read runs the same gate');
});
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(); }
});
test('keep: the other verdict-gate callers carry no --repo', () => {
for (const rel of ['commands/trekreview.md', 'agents/review-orchestrator.md', 'README.md', 'docs/release-runbook.md', 'tests/e2e-files-trial4.sh']) {
const hits = read(rel).split('\n').filter((l) => /^\s*(?:out="\$\()?node \S*lib\/review\/verdict-gate\.mjs\b/.test(l));
assert.ok(hits.length > 0, `${rel} no longer runs verdict-gate.mjs; the check would be empty`);
for (const h of hits) assert.ok(!h.includes('--repo'), `${rel} gained --repo: ${h.trim()}`);
}
});
// --- D67: the final report says whether the app was seen with `run` ---------
// Phase 7's "run the thing" says to use Claude Code's built-in `run`, but the
// final report never said whether that happened. The line is said for every
// run, also one with no app to launch (the "no — {reason}" form).
const E2E = '### End to end — "run the thing" (P9 (6))';
/** Phase 8 of /trekexecute, read up to Phase 9 (its report fence holds its own `## ` heading). */
function phase8(text) {
const start = text.indexOf('## Phase 8 — Final report');
const end = text.indexOf('## Phase 9');
assert.ok(start !== -1 && end > start, 'Phase 8 — Final report and Phase 9 must both exist, in that order');
return text.slice(start, end);
}
test('[D67] the Phase 8 report template says, right after End to end, whether the app was seen with `run`', () => {
const lines = phase8(read(EXEC)).split('\n');
const e2e = lines.findIndex((l) => l.startsWith('**End to end:**'));
assert.ok(e2e !== -1, 'the report template has no **End to end:** line');
assert.equal(lines[e2e + 1], '**Seen with `run`:** {yes | no — {reason}}',
'the line after **End to end:** is not the "Seen with `run`" line in its yes / no — {reason} form');
});
test('[D67] the summary JSON carries "run_seen" right after "end_to_end"', () => {
const text = read(EXEC);
const sumIdx = text.indexOf('"trekexecute_summary": {');
assert.ok(sumIdx !== -1, 'summary JSON block missing');
const lines = text.slice(sumIdx, text.indexOf('\n```', sumIdx)).split('\n').map((l) => l.trim());
const e2e = lines.findIndex((l) => l.startsWith('"end_to_end":'));
assert.ok(e2e !== -1, 'the summary JSON has no "end_to_end" key');
assert.equal(lines[e2e + 1], '"run_seen": "{yes | no — reason}",',
'the key after "end_to_end" is not "run_seen" with its yes / no — reason value');
});
test('[D67] the End to end section says to record run_seen, also when no app was seen, and keeps the End-to-end: command as the verdict', () => {
const s = flat(section(read(EXEC), E2E));
assert.match(s, /Record `run_seen`: `yes` when the app was seen and driven with Claude Code's built-in `run` in this run, else `no — \{reason\}`/,
'the End to end section does not say to record run_seen as yes, or no with a reason');
assert.match(s, /no app to launch/, 'the no-form does not name the run with no app to launch');
assert.match(s, /The verdict is still the `End-to-end:` command\./,
'the End to end section no longer says the End-to-end: command is the verdict');
});
// --- #15 / #67 -----------------------------------------------------------------------
// #15: /trekendsession's Phase 0 printed its usage when $ARGUMENTS merely
// contained "--help" or "-h", so a slug such as "fix-header" asked for help.
// It now runs the arg parser and dispatches only on its exact result.
// #67: "NEVER auto-invoke this command" was a sentence only; the frontmatter
// now carries disable-model-invocation, so the model cannot start it.
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,
'Phase 0 still prints help when $ARGUMENTS merely contains --help or -h');
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 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));
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');
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');
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"');
assert.equal(two.status, 0, two.stdout + two.stderr);
assert.deepEqual(JSON.parse(two.stdout).positional, ['fix-header', 'Session 2']);
});
test('[#15] /trekendsession Phase 2 reads the brief path and the label from the parser\'s positional', () => {
const phase2 = flat(section(read(ENDSESSION), '## Phase 2 — Required args check (headless-safe)'));
assert.match(phase2, /`positional\[0\]`/, 'Phase 2 does not read <next-brief-path> from positional[0]');
assert.match(phase2, /`positional\[1\]`/, 'Phase 2 does not read <next-label> from positional[1]');
});
test('[#67] /trekendsession sets disable-model-invocation, and CLAUDE.md names it as the one command that does', () => {
const text = read(ENDSESSION);
const fm = readFrontmatterFields(text);
assert.ok(fm.ok, fm.error);
assert.equal(fm.fields['disable-model-invocation']?.value, 'true',
'commands/trekendsession.md does not set disable-model-invocation: true');
assert.match(flat(text),
/\*\*NEVER auto-invoke this command\.\*\* It is operator-typed only at session-end \(the frontmatter field keeps the model from starting it; the operator types it\)/,
'the hard rule does not say the frontmatter field keeps the model from starting it');
const withField = readdirSync(join(ROOT, 'commands')).filter((f) => f.endsWith('.md'))
.filter((f) => readFrontmatterFields(read(`commands/${f}`)).fields['disable-model-invocation'] !== undefined);
assert.deepEqual(withField, ['trekendsession.md'], 'trekendsession must be the one command with the field');
const trunk = read('CLAUDE.md').split('\n').find((l) => l.startsWith('> **Commands are the trunk**'));
assert.ok(trunk, 'CLAUDE.md has no "Commands are the trunk" note');
assert.doesNotMatch(trunk, /none sets `disable-model-invocation`/, 'CLAUDE.md still says no command or skill sets the field');
assert.ok(trunk.includes('no skill sets `disable-model-invocation`; the one command that does is `/trekendsession`, operator-typed only (Claude Code still lets the operator type it; the model cannot start it)'),
'CLAUDE.md does not name /trekendsession as the one command that sets the field');
});
test('keep: commands/trekreview.md carries no disable-model-invocation field (/trekexecute starts it through the Skill tool)', () => {
const fm = readFrontmatterFields(read('commands/trekreview.md'));
assert.ok(fm.ok, fm.error);
assert.equal(fm.fields['disable-model-invocation'], undefined);
});