voyage/tests/lib/fix-round-4-5a-prose.test.mjs
2026-10-22 15:50:13 +02:00

700 lines
40 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 { existsSync, readFileSync, readdirSync } from 'node:fs';
import { dirname, join } 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';
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]);
});