voyage/tests/lib/doc-consistency.test.mjs
Kjell Tore Guttormsen 5cb2918baa
feat(review): the critic's own security key SECURITY_RISK (decision 167)
lib/review/rule-catalogue.mjs gains SECURITY_RISK (BLOCKER, category
security, memory route rule): Voyage's own security key for a finding by
its own reviewers that SECURITY_INJECTION does not cover.
SECURITY_REVIEW_VULNERABILITY is described as the engines' key only.
plan-critic and scope-guardian name SECURITY_INJECTION or SECURITY_RISK
and no longer the engines' key.

The count moves from 17 to 18 in every pin: rule-catalogue.test,
claude-md-scope [CAT] (which now also refuses a stale 17), doc-consistency,
review-prose-pins, docs/architecture.md and the two reviewer agents.
critic-severity-rules pins three security keys; its "every security key
named" check leaves out the engines' key, and a new pin says the critic
never names it.

Existing tests changed for the planned rule: the four count pins above
(17 -> 18), and critic-severity-rules' two-key test (now three keys).
tests/lib/engine-findings.test.mjs: a comment naming the "(17 keys)" pin
now says 18.

Part 2, step 2.3 of the E plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:55:34 +01:00

1220 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tests/lib/doc-consistency.test.mjs
// Pin invariants between prose (CLAUDE.md, README.md) and source files
// (agents/*.md, commands/*.md, templates/).
//
// When this test fails, fix the source-of-truth — do NOT rewrite the test to
// hide drift. Borrowed pattern from llm-security commit 97c5c9d.
//
// Two kinds of pin live here (devil's-advocate audit §Top changes #8, S19):
// • STRUCTURAL cross-file invariants — derive one side from a source-of-truth
// (file counts, dimension counts from `### N.` headers, version-sync) and
// survive harmless rewording. These are the
// load-bearing ones; keep and grow them.
// • PROSE / EXISTENCE pins — assert a literal string or file fact. Most still
// guard a real wiring (CLI shims, annotate.mjs, anti-stale CHANGELOG,
// anti-false-claim), but they break on reword. Prefer a structural form
// when one exists; do not add a prose-pin that a structural test subsumes.
// All tests in this file are documentation pins, not behavior coverage.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
function read(rel) { return readFileSync(join(ROOT, rel), 'utf-8'); }
function listMd(rel) { return readdirSync(join(ROOT, rel)).filter(f => f.endsWith('.md')); }
function listMjs(rel) { return readdirSync(join(ROOT, rel)).filter(f => f.endsWith('.mjs')); }
// Normalize a "<N|word> dimensions" descriptor to an integer (four..ten + digits).
const NUMWORDS = { four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 };
function dimCounts(text, anchor) {
// Every dimension count stated on a line mentioning `anchor`.
return text.split('\n')
.filter(l => l.includes(anchor) && /dimensions/.test(l))
.map(l => l.match(/(\d+|four|five|six|seven|eight|nine|ten)\s+(?:numbered\s+)?dimensions/i))
.filter(Boolean)
.map(m => /^\d+$/.test(m[1]) ? Number(m[1]) : NUMWORDS[m[1].toLowerCase()]);
}
test('CLAUDE.md agents table row count == agents/*.md file count', () => {
const md = read('CLAUDE.md');
const agentFiles = listMd('agents');
const agentTable = md.split('## Agents')[1] || '';
const tableSection = agentTable.split('\n## ')[0];
const dataRows = tableSection
.split('\n')
.filter(l => l.startsWith('|') && !l.match(/^\|[\s-]+\|/) && !l.match(/^\|\s*Agent\s*\|/));
assert.equal(
dataRows.length,
agentFiles.length,
`Drift: ${agentFiles.length} agent files vs ${dataRows.length} CLAUDE.md table rows. ` +
`Sync agents/ ↔ CLAUDE.md.`,
);
});
test('README architecture block agent-count == agents/*.md file count', () => {
const md = read('README.md');
const m = md.match(/agents\/\s+(\d+)\s+specialized agents/);
assert.ok(m, 'README architecture block must state "N specialized agents"');
assert.equal(
Number(m[1]),
listMd('agents').length,
`Drift: README says ${m[1]} specialized agents vs ${listMd('agents').length} agents/*.md files.`,
);
});
test('README architecture block hook-count == hooks/scripts/*.mjs file count', () => {
const md = read('README.md');
const m = md.match(/hooks\/\s+(\d+)\s+hooks\s*\(/);
assert.ok(m, 'README architecture block must state "N hooks (...)"');
assert.equal(
Number(m[1]),
listMjs('hooks/scripts').length,
`Drift: README says ${m[1]} hooks vs ${listMjs('hooks/scripts').length} hooks/scripts/*.mjs files.`,
);
});
test('plan-critic dimension count: README + CLAUDE.md match the agent definition', () => {
const dims = (read('agents/plan-critic.md').match(/^### \d+\./gm) || []).length;
assert.ok(dims > 0, 'agents/plan-critic.md must define numbered "### N." dimensions');
const stated = [...dimCounts(read('README.md'), 'plan-critic'), ...dimCounts(read('CLAUDE.md'), 'plan-critic')];
assert.ok(stated.length >= 2, 'README + CLAUDE.md must cite plan-critic (N dimensions)');
for (const n of stated) {
assert.equal(n, dims, `Drift: a doc cites plan-critic (${n} dimensions) vs ${dims} numbered dims in agents/plan-critic.md.`);
}
});
test('brief-reviewer dimension count: README matches the agent definition', () => {
const agentDims = dimCounts(read('agents/brief-reviewer.md'), 'dimensions');
assert.ok(agentDims.length >= 1, 'agents/brief-reviewer.md must state "N dimensions"');
const expected = agentDims[0];
// Only the line stating brief-reviewer's OWN count — exclude agent-list lines
// that co-mention `plan-critic (N dimensions)` (those carry plan-critic's count).
const readmeDims = read('README.md').split('\n')
.filter(l => l.includes('brief-reviewer') && /dimensions/.test(l) && !l.includes('plan-critic'))
.map(l => l.match(/(\d+|four|five|six|seven|eight|nine|ten)\s+dimensions/i))
.filter(Boolean)
.map(m => /^\d+$/.test(m[1]) ? Number(m[1]) : NUMWORDS[m[1].toLowerCase()]);
assert.ok(readmeDims.length >= 1, 'README must cite brief-reviewer (N dimensions)');
for (const n of readmeDims) {
assert.equal(n, expected, `Drift: README cites brief-reviewer (${n} dimensions) vs ${expected} in agents/brief-reviewer.md.`);
}
});
test('CLAUDE.md commands table mentions every commands/*.md file', () => {
const md = read('CLAUDE.md');
const commandFiles = listMd('commands');
for (const f of commandFiles) {
const cmdName = `/${f.replace(/\.md$/, '')}`;
assert.ok(
md.includes(cmdName),
`commands/${f} not mentioned in CLAUDE.md (looked for ${cmdName})`,
);
}
});
test('every command frontmatter name matches its filename', () => {
for (const f of listMd('commands')) {
const text = read(`commands/${f}`);
const doc = parseDocument(text);
if (!doc.valid) continue;
const expected = f.replace(/\.md$/, '');
if (doc.parsed.frontmatter && doc.parsed.frontmatter.name !== undefined) {
assert.equal(
doc.parsed.frontmatter.name,
expected,
`commands/${f} frontmatter.name="${doc.parsed.frontmatter.name}" should be "${expected}"`,
);
}
}
});
test('templates/plan-template.md declares plan_version: 1.9', () => {
const tpl = read('templates/plan-template.md');
assert.match(tpl, /plan_version:\s*['"]?1\.9['"]?/);
});
test('HANDOVER-CONTRACTS.md locates plan_version in the plan header line, not frontmatter (S28)', () => {
// The plan template emits plan_version only in the prose "Generated by"
// header line; brief-generated plans carry no frontmatter at all (the sole
// frontmatter block, `source_findings`, is trekreview-only and additive).
// The contract must therefore not label plan_version a frontmatter field.
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.doesNotMatch(t, /`plan_version`\s*\(frontmatter\)/,
'HANDOVER-CONTRACTS.md must not call plan_version a (frontmatter) field — it lives in the plan header line');
assert.doesNotMatch(t, /Mirrors plan's frontmatter/,
"progress.json's plan_version mirrors the plan header line, not frontmatter");
assert.match(t, /`plan_version`\s*\(plan header line\)/,
'HANDOVER-CONTRACTS.md versioning table must locate plan_version in the plan header line');
});
// S19 prune (audit §Top changes #8): removed a tautological prose-pin —
// "commands/trekexecute.md still parses v1.7 plan schema" was an OR-chain
// (`includes('plan_version') || includes('Step N:') || includes('### Step ')`)
// that is ~always true and asserts almost nothing. The real v1.7 coverage lives
// in the structural pin "templates/plan-template.md declares plan_version: 1.9"
// (above; 1.8 and 1.7 plans stay valid) + the plan-validator / plan-schema behavior tests.
// S19 prune (audit §Top changes #8): removed "CLAUDE.md mentions all six
// pipeline commands" — a hardcoded six-string list fully subsumed by the
// structural pin "CLAUDE.md commands table mentions every commands/*.md file"
// above, which derives the command set from the filesystem and auto-covers any
// new command. The hardcoded list was the weaker, reword-fragile form.
test('HANDOVER-CONTRACTS.md contains Handover 6 section', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(
text.includes('## Handover 6'),
'docs/HANDOVER-CONTRACTS.md should document Handover 6 (review → plan)',
);
});
test('HANDOVER-CONTRACTS.md contains Handover 7 section (session-state)', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(
text.includes('## Handover 7'),
'docs/HANDOVER-CONTRACTS.md should document Handover 7 (.session-state.local.json) ' +
'consumed by /trekcontinue',
);
assert.ok(
text.includes('.session-state.local.json'),
'Handover 7 section should name the artifact path',
);
});
test('review-validator has CLI shim', () => {
// Ordre 5: the realpath form (no-op-free under a symlink or a space), shared
// since 6.3.1 (lib/util/is-main.mjs); the run through a symlink is proven in
// tests/validators/review-validator.test.mjs.
const text = read('lib/validators/review-validator.mjs');
assert.ok(
text.includes('if (isMain(import.meta.url))'),
'lib/validators/review-validator.mjs should expose the realpath CLI shim so commands can call it from Bash',
);
});
test('session-state-validator has CLI shim', () => {
const text = read('lib/validators/session-state-validator.mjs');
assert.ok(
text.includes('if (isMain(import.meta.url))'),
'lib/validators/session-state-validator.mjs should expose the standard CLI shim ' +
'(if (isMain(import.meta.url))) so /trekcontinue can call it from Bash',
);
});
test('HANDOVER-CONTRACTS.md Handover 7 documents § Lifecycle subsection', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
const h7Start = text.indexOf('## Handover 7');
assert.ok(h7Start >= 0, 'Handover 7 heading missing');
const h7End = text.indexOf('## Stability summary', h7Start);
assert.ok(h7End > h7Start, 'Stability summary heading missing — could not bound Handover 7');
const h7 = text.slice(h7Start, h7End);
assert.ok(
h7.includes('Lifecycle'),
'Handover 7 section should include a § Lifecycle subsection (SC-5 stale-file principle)',
);
});
test('HANDOVER-CONTRACTS.md Handover 7 § Lifecycle names --cleanup and produced_by contract', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
const h7Start = text.indexOf('## Handover 7');
const h7End = text.indexOf('## Stability summary', h7Start);
const h7 = text.slice(h7Start, h7End);
assert.ok(
h7.includes('--cleanup'),
'Handover 7 § Lifecycle should mention --cleanup as the operator-invoked stale-file remover',
);
assert.ok(
h7.includes('produced_by'),
'Handover 7 § Lifecycle should document the produced_by frontmatter contract for NEXT-SESSION-PROMPT.local.md',
);
});
// S19 prune (audit §Top changes #8): removed "CLAUDE.md mentions /trekcontinue
// command" — subsumed by "CLAUDE.md commands table mentions every commands/*.md
// file" (commands/trekcontinue.md exists, so that loop already asserts the
// /trekcontinue reference). Single-command existence pin → redundant.
test('rule-catalogue has exactly 18 entries', async () => {
const mod = await import('../../lib/review/rule-catalogue.mjs');
assert.strictEqual(
mod.RULE_CATALOGUE.length,
18,
'lib/review/rule-catalogue.mjs RULE_CATALOGUE size invariant: must be 18 (12 v1.0 baseline + 2 engine keys, ordre 5, + CODE_REVIEW_IMPROVEMENT, lekerepo trial 2, + CLAUDE_MD_DEVIATION and CLAUDE_MD_STALE, decision 98, + SECURITY_RISK, E 2.3)',
);
});
test('Phase 2.6 hands waves over as session orders; no launch template ships', () => {
// The coupling pin for the PM way: the prose must call the order writer and
// the status reader (tests/lib/session-orders.test.mjs proves they work), keep
// push-before-cleanup, and carry the never-start rule.
assert.ok(!existsSync(join(ROOT, 'templates', 'headless-launch-template.md')),
'templates/headless-launch-template.md must stay removed');
const exec = read('commands/trekexecute.md');
const p26 = exec.slice(exec.indexOf('## Phase 2.6'), exec.indexOf('## Phase 3 '));
for (const needle of [
'lib/util/session-orders.mjs" write',
'lib/util/session-orders.mjs" status',
'GIT_OPTIONAL_LOCKS',
'git push origin',
'Voyage never starts a Claude Code',
]) {
assert.ok(p26.includes(needle), `trekexecute.md Phase 2.6 must include "${needle}"`);
}
assert.match(exec, /21\. \*\*Never start Claude Code\.\*\*/, 'trekexecute.md Hard rule 21 missing');
});
test('Phase 9 prose mandates parallel single-message dispatch + inline dedup', () => {
const cmd = read('commands/trekplan.md');
assert.ok(
cmd.includes('single assistant message turn'),
'commands/trekplan.md Phase 9 should reinforce single-message parallel dispatch',
);
assert.ok(
cmd.includes('plan-review-dedup.mjs'),
'commands/trekplan.md Phase 9 should call lib/review/plan-review-dedup.mjs after both reviewers complete',
);
});
// --- S23 — Phase-9 dedup hand-off is executable (defect #1 fix) ---
//
// The read-only reviewers (plan-critic, scope-guardian: Read/Glob/Grep, no Write)
// cannot write the temp files the old prose told them to. Pin the A′ fix so the
// broken /tmp-write instruction cannot creep back:
// 1. The Phase-9 prose of commands/trekplan.md must use --stdin, not the dead
// /tmp/*-out.json file path the reviewers can't produce.
// 2. BOTH reviewer agents must declare a machine-readable json findings block
// with the keys the dedup helper consumes (agent + findings + rule_key).
test('S23: Phase-9 dedup hand-off uses --stdin and the reviewers emit a json findings block', () => {
const cmd = read('commands/trekplan.md');
assert.ok(
cmd.includes('--stdin'),
'commands/trekplan.md Phase 9 must pipe reviewer findings into plan-review-dedup.mjs via --stdin (reviewers are read-only)',
);
assert.ok(
!cmd.includes('/tmp/plan-critic-out.json') && !cmd.includes('/tmp/scope-guardian-out.json'),
'commands/trekplan.md must NOT instruct the read-only reviewers to Write /tmp/*-out.json — they have no Write tool (defect #1)',
);
for (const agentFile of ['agents/plan-critic.md', 'agents/scope-guardian.md']) {
const a = read(agentFile);
assert.ok(
a.includes('"findings"') && a.includes('"rule_key"') && a.includes('"agent"'),
`${agentFile} must declare a machine-readable json findings block (agent + findings + rule_key) for the Phase-9 dedup helper`,
);
}
});
// --- S24 — anti-false-claim: operations.md + command-modes.md autonomy-gate truth-pass ---
//
// autonomy-gate.mjs is the --gates flag parser: --gates [true|false|closed|open]
// → {"ok":true,"gates":true|false}. It has no state machine (R9).
// The old prose (operations.md:15 / command-modes.md gates rows) (1) fabricated a
// state machine `idle → approved → executing → merge-pending → main-merged`, and
// later named the removed one (gates_on, auto_running, paused_for_gate); (3) advertised `--gates {open|closed|adaptive}` —
// there is no strictness policy and no `adaptive` value: the flag is boolean, and
// the CLI shim + all 4 command docs take `--gates [true|false|closed|open]`
// (bare, true, closed = on; false, open = off; fix round 4.5a, #1).
test('operations.md + command-modes.md describe the real autonomy-gate, not a fabricated one [S24]', () => {
const ops = read('docs/operations.md');
const modes = read('docs/command-modes.md');
// F1 — state names: fabricated (merge-pending/main-merged) or removed with the state machine (R9)
for (const bad of ['merge-pending', 'main-merged', 'gates_on', 'auto_running', 'paused_for_gate']) {
assert.ok(
!ops.includes(bad),
`docs/operations.md: fabricated autonomy state "${bad}" — autonomy-gate.mjs has no such state; fix the prose, not this test`,
);
}
// F1 — the module is documented as what it is: the flag parser
assert.ok(
ops.includes('`lib/util/autonomy-gate.mjs` is the `--gates` flag parser'),
'docs/operations.md must describe lib/util/autonomy-gate.mjs as the --gates flag parser',
);
// F3 — --gates is boolean; there is no open/closed/adaptive strictness enum
const GATES_ENUM_FORMS = ['{open|closed|adaptive}', '{open\\|closed\\|adaptive}'];
for (const [label, txt] of [['operations.md', ops], ['command-modes.md', modes]]) {
for (const bad of GATES_ENUM_FORMS) {
assert.ok(
!txt.includes(bad),
`docs/${label}: --gates flag enum "${bad}" is false — the flag is boolean: bare, true or closed = on, false or open = off, and there is no adaptive; fix the prose, not this test`,
);
}
}
});
test('CHANGELOG.md has v4.1.0 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(
cl,
/## v4\.1\.0\b/,
'CHANGELOG.md must include "## v4.1.0" entry per Keep-a-Changelog 1.1.0',
);
});
test('commands/trekplan.md Phase 8 seals Opus-4.7 schema-drift defense', () => {
const cmd = read('commands/trekplan.md');
// Locate Phase 8 section
const phase8Start = cmd.indexOf('## Phase 8');
assert.ok(phase8Start >= 0, 'Phase 8 heading missing');
const phase8End = cmd.indexOf('## Phase 9', phase8Start);
assert.ok(phase8End > phase8Start, 'Phase 9 heading missing — could not bound Phase 8');
const phase8 = cmd.slice(phase8Start, phase8End);
// Required regex source-of-truth references
assert.ok(
phase8.includes('STEP_HEADING_REGEX'),
'Phase 8 should inline STEP_HEADING_REGEX so format contract survives without orchestrator-doc loading',
);
assert.ok(
phase8.includes('FORBIDDEN_HEADING_REGEX'),
'Phase 8 should inline FORBIDDEN_HEADING_REGEX (Step 7 — schema-drift seal)',
);
// Required validator self-check
assert.ok(
phase8.includes('plan-validator.mjs --strict'),
'Phase 8 should mandate post-write `plan-validator.mjs --strict` self-check',
);
// Forbidden-headings list (literal "FORBIDDEN" appears more than once: in regex const + in human-readable list)
assert.ok(
/FORBIDDEN/.test(phase8),
'Phase 8 should explicitly enumerate FORBIDDEN headings',
);
});
// --- v5.0.0 / v5.0.1 — bespoke playground removed; /playground invocation explicit ---
//
// v5.0.0 removed the bespoke playground SPA, /trekrevise, and Handover 8.
// v5.0.1 dropped the v5.0.0 stop-gap (scripts/render-artifact.mjs) and made
// the producing commands print a literal, copy-paste-ready /playground
// document-critique invocation instead. These pins lock both removals in
// AND pin the new copy-paste invocation as the operator-facing contract.
import { existsSync } from 'node:fs';
test('playground/ directory no longer exists (removed in v5.0.0)', () => {
assert.ok(
!existsSync(join(ROOT, 'playground')),
'plugins/voyage/playground/ should be deleted — the bespoke playground was retired in v5.0.0',
);
});
test('commands/trekrevise.md no longer exists (removed in v5.0.0)', () => {
assert.ok(
!existsSync(join(ROOT, 'commands/trekrevise.md')),
'/trekrevise was removed in v5.0.0 — its command file should be gone',
);
});
test('Handover 8 deleted from HANDOVER-CONTRACTS.md (back to seven handovers)', () => {
const text = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(!text.includes('## Handover 8'), 'Handover 8 section should be removed in v5.0.0');
assert.ok(text.includes('## Handover 7'), 'Handover 7 must remain');
});
test('scripts/render-artifact.mjs is still removed (v5.0.1 + v5.0.2)', () => {
assert.ok(
!existsSync(join(ROOT, 'scripts/render-artifact.mjs')),
'scripts/render-artifact.mjs should be deleted — v5.0.1 dropped the standalone HTML render; v5.0.2 kept it removed (annotate.mjs is the replacement)',
);
});
test('scripts/annotate.mjs exists (v5.0.2 operator-annotation HTML generator)', () => {
assert.ok(
existsSync(join(ROOT, 'scripts/annotate.mjs')),
'scripts/annotate.mjs is required — producing commands call it to build the operator-annotation HTML',
);
});
test('producing commands reference scripts/annotate.mjs (v5.0.2 render-and-link step)', () => {
// v5.0.0 → v5.0.1 → v5.0.2 chain: v5.0.0 added an HTML render that didn't
// afford annotation; v5.0.1 pointed at /playground document-critique (which
// pre-generates Claude's suggestions, not operator-driven annotation); v5.0.2
// ships scripts/annotate.mjs — an operator-driven annotation surface where
// the OPERATOR clicks lines and writes their own notes. Pin the wiring.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
assert.ok(
read(`commands/${f}`).includes('scripts/annotate.mjs'),
`commands/${f} must invoke scripts/annotate.mjs to build the operator-annotation HTML (v5.0.2)`,
);
}
});
test('producing commands no longer print the v5.0.1 /playground document-critique line', () => {
// v5.0.1 told operators to copy-paste "/playground build a document-critique
// playground for X" — but that flow pre-generates Claude's suggestions. The
// operator asked for their own annotations, not a critique of Claude's.
// v5.0.2 removes that line from the producing commands' final report.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
assert.ok(
!read(`commands/${f}`).includes('/playground build a document-critique'),
`commands/${f} must not print the v5.0.1 /playground document-critique invocation — v5.0.2 replaces it with annotate.mjs`,
);
}
});
test('producing commands tell the operator the flow is THEIR own annotations', () => {
// Pin language: every producing command's prose must mention that the
// OPERATOR drives annotation, not Claude. Phrase variants are allowed
// ("YOUR OWN note", "operator drives", etc.) — we look for the operator-
// ownership signal.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
const text = read(`commands/${f}`);
assert.ok(
/YOUR OWN|operator drives|your own/i.test(text),
`commands/${f} must signal that the operator drives annotation (v5.0.2 contract)`,
);
}
});
test('producing commands emit file:// link in final report (operator-UX contract, 2026-05-13)', () => {
// Operator runs Ghostty / iTerm2 / modern Terminal.app — all support cmd+click
// on file:// URLs. Producing commands MUST emit both forms: (a) plain file://
// line in the report block, (b) `open file://...` copy-pasteable command.
// Both must reference $ANNOT_HTML (absolute path from scripts/annotate.mjs).
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
const text = read(`commands/${f}`);
assert.ok(
/file:\/\/\{\$ANNOT_HTML\}/.test(text),
`commands/${f} must include "file://{$ANNOT_HTML}" plain URL in the final report block`,
);
assert.ok(
/open file:\/\/\{\$ANNOT_HTML\}/.test(text),
`commands/${f} must include "open file://{$ANNOT_HTML}" copy-pasteable command in the final report block`,
);
}
});
test('package.json is marked private — the npm name `voyage` belongs to a third party', () => {
// D-census 2026-08-20/21: registry.npmjs.org/voyage answers 200, but that is
// NOT this package — it is "Advanced HTTP Routing System for Node.js"
// (NEURS/voyage, v0.0.1, 2022, maintainer kevin.martin). This manifest is test
// tooling for a Claude Code plugin distributed through the marketplace catalogue
// (`ref: vX.Y.Z`), never through npm. Without `private`, nothing in the manifest
// stops an accidental `npm publish` against a name someone else owns — a one-way
// action. Sibling non-package manifests (okr, repo-mailbox, repo-standard) all
// carry the flag; voyage was the exception. Operator decision (S93): option A.
const pkg = JSON.parse(read('package.json'));
assert.equal(
pkg.private,
true,
'package.json must declare "private": true — the npm name `voyage` is owned by a third party',
);
});
test('package.json still has no "npm run render" script (removed in v5.0.1)', () => {
const pkg = JSON.parse(read('package.json'));
assert.equal(
pkg.scripts && pkg.scripts.render,
undefined,
'package.json scripts.render should remain gone',
);
});
test('CHANGELOG.md has v5.0.0 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.0\.0\b/, 'CHANGELOG.md must include "## v5.0.0" entry');
});
test('CHANGELOG.md has v5.0.1 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.0\.1\b/, 'CHANGELOG.md must include "## v5.0.1" entry');
});
test('CHANGELOG.md has v5.0.2 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.0\.2\b/, 'CHANGELOG.md must include "## v5.0.2" entry');
});
test('CHANGELOG.md retains v4.2.0 entry (history is not rewritten)', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v4\.2\.0\b/, 'CHANGELOG.md must keep the historical "## v4.2.0" entry');
});
test('CHANGELOG.md has v5.5.0 entry (S13 coordinated release)', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.5\.0\b/, 'CHANGELOG.md must include "## v5.5.0" entry — the coordinated release that lands brief_version 2.2 + NW1–NW3');
});
test('version is consistent across plugin.json, package.json, README badge, and CHANGELOG top entry', () => {
// versjonssync invariant (CLAUDE.md rule): every file that names the plugin
// version must agree. Bumping one without the others is a release defect.
const pluginVersion = JSON.parse(read('.claude-plugin/plugin.json')).version;
const pkgVersion = JSON.parse(read('package.json')).version;
const badgeVersion = (read('README.md').match(/badge\/version-([\d.]+)-/) || [])[1];
const changelogTop = (read('CHANGELOG.md').match(/## v(\d+\.\d+\.\d+)\b/) || [])[1];
assert.ok(pluginVersion, 'plugin.json missing version');
assert.equal(pkgVersion, pluginVersion, `package.json version ${pkgVersion} != plugin.json ${pluginVersion}`);
assert.equal(badgeVersion, pluginVersion, `README badge ${badgeVersion} != plugin.json ${pluginVersion}`);
assert.equal(changelogTop, pluginVersion, `CHANGELOG top entry v${changelogTop} != plugin.json ${pluginVersion}`);
});
test('operational files no longer reference trekrevise (v5.0.0 removal)', () => {
// Templates, the touched command files, and the
// handover-contracts doc must be fully scrubbed. CLAUDE.md / README.md are
// intentionally allowed to mention /trekrevise in their "removed in v5.0.0"
// prose — those are historical notes, not live references.
const targets = [
'docs/HANDOVER-CONTRACTS.md',
'templates/plan-template.md', 'templates/trekbrief-template.md', 'templates/trekreview-template.md',
'commands/trekplan.md', 'commands/trekbrief.md', 'commands/trekreview.md',
];
for (const t of targets) {
assert.ok(
!read(t).includes('trekrevise'),
`${t} still references trekrevise — it was removed in v5.0.0`,
);
}
});
// --- v5.5 — brief_version 2.2 ---
test('v5.5 — templates/trekbrief-template.md declares brief_version: "2.2" (quoted)', () => {
const t = read('templates/trekbrief-template.md');
assert.match(t, /^brief_version: "2\.2"$/m,
'trekbrief-template.md must declare brief_version: "2.2" (quoted) — unquoted parses as Number and bypasses the version gate; v5.5 bumped 2.1→2.2 for framing enforcement');
});
// --- v5.4 — brief.md formalized as PUBLIC CONTRACT (Handover 1) ---
// S5: the brief schema is the only public producer↔Voyage integration boundary
// (Trinity asymmetry invariant). These pin the formalization as enforced doc-truth,
// not prose — same pattern as the v5.1 Handover-1 pins above. Freeze, not promote:
// brief_version 2.1 is the frozen baseline (S3 decision).
test('v5.4 — HANDOVER-CONTRACTS.md Handover 1 heading is labeled (PUBLIC CONTRACT)', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.match(t, /^## Handover 1 —.*\(PUBLIC CONTRACT\)\s*$/m,
'Handover 1 heading must carry the (PUBLIC CONTRACT) label — symmetric with Handover 3 (EXTERNAL CONTRACT)');
});
test('v5.4 — Handover 1 declares the PUBLIC CONTRACT callout + breaking-for-downstream guarantee', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('Stability tier: PUBLIC CONTRACT'),
'Handover 1 must open with a "Stability tier: PUBLIC CONTRACT" callout');
assert.ok(t.includes('breaking change for every downstream consumer'),
'the callout must state that a brief-schema change is a breaking change for every downstream consumer');
});
test('v5.5 — brief_version 2.2 is current in the public-contract schema table (no stale 2.0/2.1)', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(!t.includes('`"2.0"` (current)'),
'Handover 1 schema table must not still mark brief_version "2.0" as current (stale)');
assert.ok(!t.includes('`"2.1"` (current)'),
'Handover 1 schema table must not still mark brief_version "2.1" as current (stale — v5.5 bumped to 2.2)');
assert.ok(t.includes('`"2.2"` (current)'),
'Handover 1 schema table must mark brief_version "2.2" as current');
});
test('v5.5 — the Versioning note cites no phantom v5.4 and records the 2.1 baseline', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(!/v5\.4/.test(t),
'the contract doc must not cite the phantom v5.4 release (v5.2–5.4 never shipped; S16 → v5.5.0)');
assert.ok(t.includes('established `2.1` as the public-contract baseline'),
'the Versioning note must record the v5.5.0 contract formalization establishing 2.1 as the baseline');
});
// --- v5.5 — framing enforcement: brief_version 2.2 (Handover 1 contract evolution) ---
// S6: the framing-alignment invariant (CLAUDE.md) ships as a controlled brief_version
// bump 2.1→2.2 adding two required-at-2.2 elements (framing field + ## TL;DR section),
// version-gated so 2.0/2.1 briefs stay valid. These pin the contract-doc evolution.
test('v5.5 — Handover 1 schema table adds the framing field row', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('| `framing` |'),
'HANDOVER-CONTRACTS must add a framing row to the Handover 1 schema table');
assert.ok(t.includes('preserve \\| refine \\| replace \\| new-direction'),
'the framing row must list the four canonical enum values');
});
test('v5.5 — Handover 1 documents the framing gate + required-at-2.2 TL;DR section', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('v5.5 framing gate'),
'validation-strategy table must add a v5.5 framing gate row');
assert.ok(/##\s*TL;DR.*required at `brief_version ≥ 2\.2`/.test(t) || t.includes('`## TL;DR` — **required at `brief_version ≥ 2.2`**'),
'body-invariants must mark ## TL;DR as required at brief_version ≥ 2.2');
});
test('v5.5 — Versioning note records the 2.2 framing-enforcement evolution', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('v5.5 → `2.2` (framing enforcement)'),
'the Versioning note must document the v5.5 → 2.2 framing-enforcement evolution');
});
test('v5.5 — voyage README.md mentions framing enforcement / brief_version 2.2', () => {
const t = read('README.md');
assert.ok(t.includes('framing') && t.includes('brief_version 2.2'),
'voyage README.md must carry a "What\'s new" note for framing enforcement (brief_version 2.2)');
});
test('S15: README cost prose does not resurrect the false Sonnet-swarm claim', () => {
// All 24 sub-agents are model: opus (operator-pinned, commit 40d8742) and the
// model is uniform per phase — there is no "Opus orchestrates / Sonnet runs
// the swarms" split. These phrases asserted the false split and must stay out.
const md = read('README.md');
const BANNED = [
'Sonnet exploration',
'Sonnet runs the exploration',
'front-loads cheap Sonnet',
'exploration agents stay on Sonnet',
];
for (const phrase of BANNED) {
assert.ok(
!md.includes(phrase),
`README.md must not claim "${phrase}" — sub-agents are opus-pinned`,
);
}
});
// ── S18: framing-hardening cross-file invariants ───────────────────────────
test('S18: --min-brief-version is documented at the trekplan + trekresearch boundary', () => {
// The opt-in version floor must be discoverable wherever flags live: both
// consuming commands and the central flag reference.
for (const f of ['commands/trekplan.md', 'commands/trekresearch.md', 'docs/command-modes.md']) {
assert.ok(
read(f).includes('--min-brief-version'),
`${f} must document the --min-brief-version flag (S18 framing-hardening)`,
);
}
});
test('README research-dimension prose stays at the existing 3–8 ceiling', () => {
// /trekresearch Phase 2 determines 3-8 research dimensions; the README must not raise it.
assert.ok(
read('README.md').includes('3–8 research dimensions'),
'README.md must keep the "3–8 research dimensions" prose — it matches the Phase 2 ceiling, it does not raise it',
);
});
test('no banned Sonnet-swarm phrase on the reference surfaces', () => {
const BANNED = [
'Sonnet exploration',
'Sonnet runs the exploration',
'front-loads cheap Sonnet',
'exploration agents stay on Sonnet',
];
for (const f of ['docs/command-modes.md', 'CLAUDE.md', 'README.md', 'docs/architecture.md']) {
const t = read(f);
for (const phrase of BANNED) {
assert.ok(!t.includes(phrase), `${f} must not claim "${phrase}" — sub-agents are opus-pinned`);
}
}
});
test('S18: HANDOVER-CONTRACTS documents the pre-2.2 zero-framing-enforcement hole', () => {
// The framing defense is producer-elective: a brief declaring ≤ 2.1 sidesteps
// it entirely. Handover 1 (PUBLIC CONTRACT) must disclose this and name the remedy.
const h = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(
/BRIEF_VERSION_BELOW_MINIMUM/.test(h),
'HANDOVER-CONTRACTS.md must name the BRIEF_VERSION_BELOW_MINIMUM gate as the pre-2.2 remedy',
);
assert.ok(
/zero framing enforcement|no framing enforcement|sidestep/i.test(h),
'HANDOVER-CONTRACTS.md must state that pre-2.2 briefs receive zero framing enforcement',
);
});
// ── S32 — "delegate the engine, keep the policy": native-delegation hygiene ──
//
// Balance-analysis V01 / V07 / V08 / V11 / V24 audit (2026-06-20): the brief
// interview and the research / exploration / reviewer swarms must ride NATIVE
// Claude Code primitives — parallel `Agent` spawn in a single message +
// `AskUserQuestion` for interviews — and re-implement no scheduler /
// concurrency-loop / menu engine of their own. The audit found all five ALREADY
// native (no code change), so these pins are the standing guard against an
// engine creeping back in, plus the documented principle in docs/architecture.md.
// Fix the SOURCE (command prose / the architecture.md note), not the test.
const SWARM_COMMANDS = ['commands/trekresearch.md', 'commands/trekplan.md', 'commands/trekreview.md'];
test('S32: docs/architecture.md records the "delegate the engine, keep the policy" principle', () => {
const t = read('docs/architecture.md');
assert.ok(
/delegate the engine, keep the policy/i.test(t),
'docs/architecture.md must name the "delegate the engine, keep the policy" principle (V01/V07/V08/V11/V24 audit, S32)',
);
// The note must name the native primitives it delegates to (so the principle is
// anchored to real CC tools, not abstract prose).
assert.ok(
/AskUserQuestion/.test(t) && /\bAgent\b/.test(t),
'the principle note must name the native primitives delegated to (Agent + AskUserQuestion)',
);
});
test('S32: each swarm command lists the Agent tool and mandates native parallel single-message spawn', () => {
for (const f of SWARM_COMMANDS) {
const text = read(f);
assert.ok(
/^allowed-tools:.*\bAgent\b/m.test(text),
`${f}: allowed-tools frontmatter must list Agent (the native swarm-spawn engine)`,
);
assert.ok(
/in parallel/i.test(text),
`${f}: must instruct parallel swarm spawn (native delegation, not a hand-rolled scheduler)`,
);
assert.ok(
/single (assistant )?message|one message|multiple tool calls/i.test(text),
`${f}: must mandate single-message multi-tool-call dispatch (the native parallel-Agent pattern)`,
);
}
});
test('S32: trekbrief Phase 3 delegates Q&A turn-taking to AskUserQuestion (V01 — selection rule is policy, asking is native)', () => {
const text = read('commands/trekbrief.md');
assert.ok(
/^allowed-tools:.*AskUserQuestion/m.test(text),
'commands/trekbrief.md: allowed-tools must list AskUserQuestion (the native Q&A engine)',
);
const p3Start = text.indexOf('## Phase 3 — Completeness loop');
assert.ok(p3Start >= 0, 'trekbrief.md must have a "## Phase 3 — Completeness loop" section');
const p3End = text.indexOf('## Phase 4', p3Start);
const p3 = text.slice(p3Start, p3End > p3Start ? p3End : undefined);
assert.ok(
/Use `AskUserQuestion` for every question/.test(p3),
'Phase 3 must delegate Q&A turn-taking to AskUserQuestion — the selection rule picks WHICH ' +
'question to ask (policy); AskUserQuestion does the asking (engine). No hand-rolled menu loop.',
);
});
// ── S38 — fan-out hedge harmonization + relief-assertion forward-guard (SC d) ──
//
// The brief's SC(d) premise — an unhedged "fan-out relieves main context" string
// present in ~23 files awaiting a removal diff — is factually false: plan-time
// enumeration shows the unhedged set is ∅. Every live relief mention is already
// hedged (the sole one is CLAUDE.md:5, hedged since S18), and the
// T2 bake-off record's "frees the main context" lines were a DIFFERENT, true
// background-execution claim (must NOT be touched). SC(d) therefore ships as:
// (1) one surgical harmonization at CLAUDE.md:5 making the replacement phrase
// "parallel wall-clock" grep-positive (the Δ≈0 / "not yet demonstrated"
// qualifier kept intact), and
// (2) this durable forward-guard — a PRECISE relief-assertion matcher (verb-of-
// relief + main-context, NOT a bare "main context" grep that would false-fail
// benign locatives like "runs inline in main context") scoped to product-
// facing docs, requiring an adjacent hedge token on any match.
// Fix the SOURCE prose (add the hedge / drop the claim), never this test.
test('S38: CLAUDE.md carries the harmonized fan-out phrase "parallel wall-clock" (SC d)', () => {
assert.ok(
read('CLAUDE.md').includes('parallel wall-clock'),
'CLAUDE.md:5 must state the replacement phrase "parallel wall-clock + structured artifact handoffs" (SC d harmonization)',
);
});
test('S38: forward-guard — no product-facing doc asserts main-context relief unhedged (SC d)', () => {
// Banned form: a verb of relief adjacent to "main context" (or "main-context …
// relief"), NOT a plain locative ("runs in main context"). Decision/research
// records (docs/*) are append-only history and out of scope by design.
const RELIEF = /(relieve|relieves|relieving|free|frees|offload|offloads)\b[^.\n]{0,40}\bmain[-\s]context\b|\bmain[-\s]context\b[^.\n]{0,20}\brelief\b|swarm\s+relieves\s+context/i;
const HEDGE = /Δ|not measured|asserted-by-design|not yet demonstrated|not demonstrated/i;
const productFacing = ['README.md', 'CLAUDE.md', ...listMd('commands').map((f) => `commands/${f}`)];
const offenders = [];
for (const rel of productFacing) {
read(rel).split('\n').forEach((ln, i) => {
if (RELIEF.test(ln) && !HEDGE.test(ln)) offenders.push(`${rel}:${i + 1} ${ln.trim().slice(0, 100)}`);
});
}
assert.equal(
offenders.length,
0,
'Unhedged main-context-relief assertion(s) in product-facing docs — add an adjacent hedge ' +
'(Δ / "not measured" / "asserted-by-design" / "not yet demonstrated") or drop the claim:\n' +
offenders.join('\n'),
);
});
// ── v5.9 (fable-tier step 7) — orchestrator model-pin ABSENCE pin ───────────
// Command frontmatter deliberately omits `model:` — omission is the only
// session-inheritance spelling documented by BOTH official surfaces (AP2-2:
// the `inherit` literal is disputed between skills.md and the plugin-dev
// command-frontmatter reference). Re-adding a pin locks the orchestrator to a
// fixed model in every session; if deterministic pinning is ever wanted again,
// do it consciously and update this pin's rationale.
// ── S85 — every spawn site must forbid the Agent tool's `name` parameter ────
// MEASURED 2026-08-17 (this repo, session c37b89f7), after akashic-intelligence
// lost a whole Phase 9 to it. Passing `name` does not rename a subagent — it
// changes its kind: the meta record flips from a real subagent
// (`spawnDepth: 1`) to `taskKind: "in_process_teammate"` (`spawnDepth: 0`).
// A teammate's final assistant text is NOT a return value; it reaches the
// parent only if the teammate itself calls SendMessage(to: "main").
// voyage's reviewers declare `tools: ["Read","Glob","Grep"]` — no SendMessage —
// so as teammates they are STRUCTURALLY incapable of returning, whatever the
// prompt says. Denominators: named 0/5 returned, named-with-explicit-
// SendMessage 1/1, unnamed 3/3 (plan-critic + scope-guardian both returned
// full findings + JSON block in ~110s). Full measurement:
// docs/agent-return-channel-defect.md.
//
// The derived set is asserted exactly, so the pin cannot go vacuous if a
// command stops spawning or a new spawning command is added.
// Wave 1 (P8): /trekexecute joined the set - one fresh step helper per plan step
// plus one controller - so the no-name rule now covers five commands.
const SPAWNING_COMMANDS = ['trekbrief', 'trekexecute', 'trekplan', 'trekresearch', 'trekreview'];
test('S85: the set of agent-spawning commands is exactly the five that carry the no-name rule', () => {
const spawns = listMd('commands')
.filter((f) => /^Launch\b/m.test(read(`commands/${f}`)))
.map((f) => f.replace(/\.md$/, ''))
.sort();
assert.deepEqual(spawns, [...SPAWNING_COMMANDS].sort(),
'a command started or stopped spawning agents — add/remove it from SPAWNING_COMMANDS and give it the no-name rule');
});
for (const cmd of SPAWNING_COMMANDS) {
test(`S85: commands/${cmd}.md forbids the Agent tool's name parameter at its spawn sites`, () => {
const text = read(`commands/${cmd}.md`);
assert.ok(
text.includes('in_process_teammate'),
`commands/${cmd}.md must name the mechanism (in_process_teammate) so the rule is not mistaken for style`,
);
assert.ok(
/never pass .*`name`/i.test(text),
`commands/${cmd}.md must state that the Agent tool's \`name\` parameter is never passed — a named agent completes its work but its result never reaches the orchestrator`,
);
});
}
test('v5.9: no commands/*.md frontmatter carries a model: key (session inheritance by omission)', () => {
const offenders = [];
for (const f of listMd('commands')) {
const doc = parseDocument(read(`commands/${f}`));
const fm = doc.parsed && doc.parsed.frontmatter;
if (fm && 'model' in fm) offenders.push(f);
}
assert.deepEqual(offenders, [],
`command frontmatter must omit model: (orchestrator follows the session model); offenders: ${offenders.join(', ')}`);
});
// --- S95 (③): gemini-bridge is DROPPED from the pipeline (operator directive) ---
//
// Operator directive, three repetitions: gemini-mcp and voyage:gemini-bridge are
// never to be used. The drop is permanent, not a wait-for-upgrade — so the pin is
// on the LIVE pipeline surfaces only. Historical records (CHANGELOG entries,
// measurement/decision docs) must KEEP their references: they say what a past
// version did or what was measured then, and rewriting them is the same defect
// class as bumping a version string inside a measurement doc.
//
// BLIND SPOT, stated rather than hidden: THIS file cannot be in the live-surface
// list, because the S95 block below is necessarily full of the word.
// Live = anything that can still steer a run. Explicit, so a new file forces a decision.
const GEMINI_LIVE_SURFACES = [
'README.md',
'CLAUDE.md',
'docs/architecture.md',
'docs/command-modes.md',
'templates/research-brief-template.md',
'templates/trekbrief-template.md',
...listMd('commands').map((f) => `commands/${f}`),
...listMd('agents').map((f) => `agents/${f}`),
];
// Historical/measurement records that MUST keep their gemini references.
const GEMINI_HISTORICAL = [
'CHANGELOG.md',
'examples/02-real-cli/REGENERATED.md',
];
test('S95: agents/gemini-bridge.md is removed from the agent inventory', () => {
assert.ok(
!listMd('agents').includes('gemini-bridge.md'),
'gemini-bridge is dropped by operator directive — agents/gemini-bridge.md must not exist',
);
});
test('S95: no live pipeline surface still names the gemini-bridge agent', () => {
const offenders = [];
for (const rel of GEMINI_LIVE_SURFACES) {
const hits = read(rel).match(/gemini[\w-]*/gi) || [];
if (hits.length) offenders.push(`${rel} (${[...new Set(hits)].join(', ')})`);
}
assert.deepEqual(
offenders, [],
`gemini-bridge is dropped — these live surfaces still reference it: ${offenders.join(' · ')}`,
);
});
test('S95: known-positive control — the historical records still DO carry gemini references', () => {
// Verifiseringsloven ansikt 4: a scanner that finds nothing anywhere proves
// nothing. This pins that the same query CAN find, so the empty result above
// is a measurement and not a broken query.
const silent = GEMINI_HISTORICAL.filter((rel) => !/gemini/i.test(read(rel)));
assert.deepEqual(
silent, [],
`these historical records lost their gemini references — history must not be rewritten: ${silent.join(', ')}`,
);
});
// End-state defect D-07: since v5.9.0 no command pins `model:`, so the orchestrator follows
// the session model; a Voyage agent's spawn passes no model (its own `model: opus` applies)
// and a built-in agent's spawn passes `model: "opus"`. No user-facing doc may claim
// otherwise. Fix the SOURCE.
test('D-07: no doc claims an opus orchestrator or ships a recipe for model: lines no command has', () => {
const readme = read('README.md');
assert.ok(
!/\^model: opus\$/.test(readme),
'README.md must not ship a sed recipe for `model: opus` frontmatter lines — 0 of 7 command files have one',
);
assert.ok(
!/default for `\/trekbrief`[\s\S]{0,80}?is `opus`/.test(readme),
'README.md must not claim the command orchestrators default to opus — they follow the session model',
);
for (const f of ['README.md', 'CLAUDE.md', 'commands/trekresearch.md']) {
assert.ok(!/orchestrator runs on Opus/i.test(read(f)), `${f}: the orchestrator follows the session model`);
}
});
test('D-07: trekresearch launch rules leave a Voyage agent on its own pin, with no blanket opus rule', () => {
const t = read('commands/trekresearch.md');
const rules = (t.split('\n### Launch rules\n')[1] || '').split('\n## ')[0];
assert.ok(rules.length > 0, 'trekresearch.md must still carry the Launch rules sub-section');
assert.ok(
!/model: "opus" for all sub-agents/i.test(rules),
'a blanket opus rule contradicts the file\'s own rule: a Voyage agent keeps its own pin, only a built-in agent gets opus',
);
const flatRules = rules.replace(/\s+/g, ' ');
assert.ok(flatRules.includes("Pass no model at a Voyage agent's spawn: the agent's own frontmatter `model: opus` applies."),
'the launch rules must say a Voyage agent\'s spawn passes no model');
assert.ok(t.replace(/\s+/g, ' ').includes('Spawn it with `model: "opus"`: a built-in agent has no Voyage pin.'),
'a built-in follow-up agent is spawned with model opus');
});
// End-state defect D-03: a trekplan's `## Verification` is where the brief's success
// criteria land. Phase 7 used to say "Skip for trekplans", so on the single-session path
// the criteria were never run and `completed` meant "the executor believed it". The
// behaviour lives in lib/verification/criteria-runner.mjs; this pin guards the WIRING —
// a capability no phase calls is the same defect wearing a lib/ file. Fix the SOURCE.
test('D-03: trekexecute Phase 7 runs the plan Verification on the single-session path', () => {
const t = read('commands/trekexecute.md');
const phase7 = (t.split('\n## Phase 7 — ')[1] || '').split('\n## ')[0];
assert.ok(phase7.length > 0, 'trekexecute.md must still carry a Phase 7 section');
assert.ok(
!/^\*\*Skip for trekplans\.\*\*/m.test(phase7),
'Phase 7 may no longer skip trekplans — that skip IS defect D-03',
);
assert.match(
phase7, /criteria-runner\.mjs" --plan/,
'Phase 7 must invoke the criteria runner over the plan file, not describe the check in prose',
);
assert.match(
phase7, /MUST NOT emit `result: completed`/,
'a failing criterion must fell the run, not be noted in the final report',
);
});
// End-state defect D-04: the rubric required brief-conformance-reviewer to decide whether a
// Success Criterion's verification command "exists and passes" — with Read/Glob/Grep. The
// command now runs the commands and hands over the result; the reviewer stays read-only.
// Fix the SOURCE.
// Re-measured 2026-09-18, minor: /trekreview passed --cwd and Phase 7 did not,
// so the same criterion could resolve two ways in the two phases. It is no longer
// cosmetic - --cwd is where every allowlisted command is executed and where a
// `bash tests/<script>.sh` criterion resolves, so an unset one silently moves
// the ground the criteria are measured on to the process cwd.
test('both phases pin the criteria runner to the working tree with --cwd', () => {
const phase7 = (read('commands/trekexecute.md').split('\n## Phase 7 — ')[1] || '').split('\n## ')[0];
const phase45 = (read('commands/trekreview.md').split('\n## Phase 4.5 — ')[1] || '').split('\n## ')[0];
for (const [label, phase] of [['trekexecute Phase 7', phase7], ['trekreview Phase 4.5', phase45]]) {
assert.ok(phase.length > 0, `${label} must still exist`);
assert.match(phase, /--cwd "\$CRITERIA_CWD"/, `${label} must pass --cwd, and the same resolved value`);
assert.match(
phase, /CRITERIA_CWD="\$\(git rev-parse --show-toplevel 2>\/dev\/null \|\| pwd\)"/,
`${label} must resolve the working tree with a fallback — an empty --cwd is a moved boundary, not an error`,
);
}
});
test('D-04: the conformance reviewer judges a supplied result, and never runs a command', () => {
const a = read('agents/brief-conformance-reviewer.md');
assert.ok(
!/exists and passes/.test(a),
'the rubric may no longer ask a Read/Glob/Grep agent whether a command passes — that ask IS defect D-04',
);
assert.match(a, /^tools: \["Read", "Glob", "Grep"\]$/m, 'the reviewer stays read-only: no Bash');
assert.match(a, /You never run a command/, 'the reviewer must be told in-band that it does not execute');
assert.match(a, /Success-criteria check results/, 'the reviewer must document the supplied evidence as an input');
});
// Follow-up to D-04 (re-measured 2026-09-18): the runner now reports a criterion
// whose sentence named a flag or a path as NOT RUN with reason `not-a-command`,
// instead of running it and reporting exit 2 / exit 126. The rubric must not turn
// that absent measurement into a finding of its own - it judges delivered code.
test('D-04b: a NOT RUN check result is never, on its own, a defect in the rubric', () => {
const a = read('agents/brief-conformance-reviewer.md');
assert.ok(
!/NOT RUN` because the criterion declares no command/.test(a),
'a criterion that declares no runnable command may no longer read as an incomplete verification path',
);
assert.match(
a, /A `NOT RUN` result is never on its own a finding/,
'the rubric must say in-band that an absent measurement is not a defect',
);
assert.match(
a, /not-a-command/,
'the reviewer must be told the reason string the runner emits for a sentence that named a flag or a path',
);
});
test('D-04: trekreview runs the success-criteria commands and hands the reviewer the result', () => {
const t = read('commands/trekreview.md');
const phase = (t.split("\n## Phase 4.5 — ")[1] || '').split('\n## ')[0];
assert.ok(phase.length > 0, 'trekreview.md must carry the phase that runs the success-criteria checks');
assert.match(phase, /criteria-runner\.mjs" \\\n --brief .* --evidence/, 'the phase must invoke the runner in --evidence mode');
assert.match(phase, /sc_evidence_block/, 'the phase must name the captured block the reviewer prompt carries');
assert.match(
t.split('\n## Phase 5 — ')[1] || '', /sc_evidence_block/,
'the reviewer-launch phase must hand the block over — a block nobody passes is not evidence',
);
});
// Re-measured 2026-09-18: the plan's `## Verification` section
// is the one artifact BOTH /trekplan (writer) and lib/verification/criteria-runner.mjs
// (reader) touch, and nothing pinned them to the same format. Measured: nine
// ordinary shapes of that section parsed to zero criteria, which fells the
// single-session run. The writer must pin the heading string and the two forms
// the reader reads. Fix the SOURCE: if the runner learns a new form, say so here.
test('trekplan pins the ## Verification heading and a format the criteria runner reads', () => {
const t = read('commands/trekplan.md');
const section = (t.split('8. **Verification**')[1] || '').split('\n9. ')[0];
assert.ok(section.length > 0, 'trekplan.md must still carry the Verification instruction');
assert.match(
section, /exactly `## Verification`/,
'the heading string is load-bearing: `## Verification (acceptance)` and `### Verification` parse to nothing',
);
assert.match(
section, /criteria runner|criteria-runner/,
'the instruction must name the reader it is writing for',
);
assert.match(
section, /- \[ \] `/,
'the bullet form the runner reads must be shown, not described',
);
assert.match(section, /```bash/, 'the shell-tagged fence form must be shown too');
});
test('trekexecute Phase 7 says NO_CRITERIA out loud instead of failing silently', () => {
const phase7 = (read('commands/trekexecute.md').split('\n## Phase 7 — ')[1] || '').split('\n## ')[0];
assert.match(
phase7, /NO_CRITERIA/,
'a section the runner could not read must be reported as such, not as a bare exit 1',
);
assert.match(
phase7, /source line|`\{plan_path\}:/,
'the report must carry the line the unreadable section starts on',
);
});
// Model-alias facts, verified 2026-10-02 against code.claude.com/docs/en/model-config
// (Anthropic API) and code.claude.com/docs/en/sub-agents: `opus` -> Opus 5.5 and
// `sonnet` -> Sonnet 5.5, both default effort `medium`; `fable` -> Fable 5.1, default
// `high`; a subagent without `effort:` inherits the session level. Live surfaces state
// what is true now; historical records keep what was true when written and are never
// rewritten. When an alias moves again, re-verify against the docs and fix the SOURCE.
const ALIAS_LIVE_SURFACES = [
'CLAUDE.md', 'README.md', 'docs/architecture.md', 'docs/operations.md',
...listMd('commands').map((f) => `commands/${f}`),
...listMd('agents').map((f) => `agents/${f}`),
];
const RETIRED_ALIAS_TARGETS = [
{ term: 'Opus 4.8', re: /Opus 4\.8/g },
{ term: 'Sonnet 4.6', re: /Sonnet 4\.6/g },
{ term: 'Fable 5', re: /Fable 5(?!\.\d)/g },
];
// History inside a live surface: an entry must hit EXACTLY its count (a stale one fails).
const ALIAS_HISTORY_EXEMPT = [
{ path: 'docs/operations.md', term: 'Sonnet 4.6', count: 1, reason: 'Path C measurement, closed 2026-05-05 on CC v2.1.128' },
];
const ALIAS_HISTORICAL = ['CHANGELOG.md'];
function aliasHits(rel, re) { return (read(rel).match(re) || []).length; }
test('model aliases: no live surface names a retired alias target as current', () => {
const offenders = [];
for (const rel of ALIAS_LIVE_SURFACES) {
for (const { term, re } of RETIRED_ALIAS_TARGETS) {
const n = aliasHits(rel, re);
const ex = ALIAS_HISTORY_EXEMPT.find((e) => e.path === rel && e.term === term);
const allowed = ex ? ex.count : 0;
if (n !== allowed) offenders.push(`${rel}: ${term} x${n} (allowed ${allowed})`);
}
}
assert.deepEqual(offenders, [], `retired alias targets on live surfaces: ${offenders.join(' · ')}`);
});
test('model aliases: known-positive control — the same query still finds every retired term in history', () => {
// Verifiseringsloven ansikt 4: the empty result above is a measurement only if the
// query CAN find. Every retired term must still hit in a historical record or an
// exact-count exemption.
const silent = RETIRED_ALIAS_TARGETS.filter(({ term, re }) =>
!ALIAS_HISTORICAL.some((rel) => aliasHits(rel, re) > 0)
&& !ALIAS_HISTORY_EXEMPT.some((e) => e.term === term)).map(({ term }) => term);
assert.deepEqual(silent, [], `no known-positive left for: ${silent.join(', ')}`);
});
test('model aliases: architecture.md states the current targets, defaults and session inheritance', () => {
const t = read('docs/architecture.md');
const axes = (t.split('\n## Model & effort\n')[1] || '').split('\n## ')[0];
assert.ok(axes.length > 0, 'docs/architecture.md must still carry the Model & effort section');
assert.match(axes, /Opus 5\.5[^\n]*\n?[^\n]*`medium`/, 'opus -> Opus 5.5, default effort `medium`');
assert.match(axes, /Sonnet 5\.5/, 'sonnet -> Sonnet 5.5');
assert.match(axes, /Fable 5\.1[\s\S]{0,120}`high`/, 'fable -> Fable 5.1, default effort `high`');
assert.match(axes, /inherits? the session/i, 'a subagent without `effort:` inherits the session level');
assert.ok(!/inherit the model default/.test(axes), 'unset `effort:` inherits the session, not the model default');
assert.ok(!/overrides effort entirely/.test(axes),
'`MAX_THINKING_TOKENS=0` has no effect on Opus 5.5, Sonnet 5.5 or the Fable models');
assert.match(axes, /maxEffortLevel/, 'effort is capped by `maxEffortLevel` / an organization cap, not by `availableModels`');
// README's alias sentence went with its §Cost profile (cleanup R-b).
for (const f of ['CLAUDE.md']) {
for (const target of ['Opus 5.5', 'Sonnet 5.5', 'Fable 5.1']) {
assert.ok(read(f).includes(target), `${f} must name the current alias target ${target}`);
}
}
});
// A `## Brief Quality` section belongs to the exhausted and force-stopped
// branches only; the passed-gate branch must not tell the model to write one.
test('trekbrief: the passed-gate branch names no `## Brief Quality`, the exhausted branch still does', () => {
const text = read('commands/trekbrief.md');
const passStart = text.indexOf('**If gate passes:**');
const passEnd = text.indexOf('**If gate fails AND iteration count < the class cap:**');
assert.ok(passStart >= 0 && passEnd > passStart, 'passed-gate branch markers');
assert.ok(!text.slice(passStart, passEnd).includes('## Brief Quality'), 'the passed-gate branch names `## Brief Quality`');
const exStart = text.indexOf('**If gate fails AND iteration count == the class cap (loop exhausted):**');
const exEnd = text.indexOf('### Step 4f', exStart);
assert.ok(exStart > passEnd && exEnd > exStart, 'exhausted branch markers');
assert.ok(text.slice(exStart, exEnd).includes('## Brief Quality'), 'the exhausted branch must still name `## Brief Quality`');
});
// --- effort floor: no live doc claims retrieval agents run at medium ------------
// Operator rule (2026-10-05): no agent is pinned below `high`. The model DEFAULT
// sentence ("Opus 5.5 ... default reasoning effort `medium`") stays true and is not a pin.
const RETRIEVAL_MEDIUM = /retrieval(?: agents)?\s*(?:→|->|at)\s*`medium`/i;
test('effort floor: no live doc claims retrieval agents run at medium', () => {
const hits = [];
for (const f of ['docs/architecture.md', 'CLAUDE.md', 'README.md']) {
read(f).split('\n').forEach((l, i) => { if (RETRIEVAL_MEDIUM.test(l)) hits.push(`${f}:${i + 1}`); });
}
assert.deepEqual(hits, [], 'the four retrieval agents pin `high` now');
});
test('effort floor: known-positive control — the query finds the old phrases, not the model default', () => {
for (const phrase of ['**Retrieval agents → `medium`:**', '(retrieval → `medium`, adversarial', 'retrieval at `medium`, adversarial']) {
assert.match(phrase, RETRIEVAL_MEDIUM, phrase);
}
assert.doesNotMatch('both with default reasoning effort `medium`', RETRIEVAL_MEDIUM);
});
test('effort floor: architecture.md names the four retrieval agents as high', () => {
const axes = (read('docs/architecture.md').split('\n## Model & effort\n')[1] || '').split('\n## ')[0];
const bullet = axes.split('\n- ').find((b) => ['task-finder', 'git-historian', 'dependency-tracer', 'architecture-mapper'].every((a) => b.includes(a)));
assert.ok(bullet, 'one bullet in §Model & effort names all four retrieval agents');
assert.match(bullet, /`high`/);
assert.doesNotMatch(bullet, /`medium`/);
});