chore(cleanup): R-b R3 — profiles, phase signals and the effort dialog removed, one fixed path per command

The four profiles, the resolver, the profile validator and docs/profiles.md go;
/trekbrief Phase 3.5 and the brief's phase_signals checks go (an older brief's
key is ignored); MANIFEST_PROFILE_DRIFT and profile_used go. Every Profile,
Composition rule, Sequencing gate and High-effort section leaves the commands.
Decision 209: /trekresearch runs the full swarm with contrarian-researcher as
wave 2 every run; /trekreview and the coordinator run all four passes with no
high-effort normalization. Voyage agents are spawned with no model (their own
model: opus applies); the two built-in spawns pass model "opus". --profile
stays in the parser as a retired flag that swallows its value. §Model & effort
moves to docs/architecture.md; the sequencing-gate eval case goes.

Files removed: 44 (43 from the R3 list + tests/commands/trekreview.test.mjs,
whose every test pinned removed text). Files edited: 43.
Tests: 2467 (2466 pass / 0 fail / 1 skipped) before, 2309 (2309 pass / 0 fail /
0 skipped) after. Durable check: all three groups green, none pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-07 21:12:57 +01:00
commit 2a35d067d9
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
87 changed files with 225 additions and 4967 deletions

View file

@ -1,118 +1,22 @@
// tests/commands/trekbrief.test.mjs
// v5.1 prose-pin tests + v5.1.1 runtime SC1 tests.
//
// Pattern D prose-pins kept as doc-anchors for the .md file. Runtime tests
// added per finding 350853 (BLOCKER SC1) + a7f4f95a (MAJOR Plan Step 5 drift).
//
// SC1 re-interpretation (per plan Step 10 amendment): "asserts on 4
// AskUserQuestion calls" → "asserts resolvePhaseSignal returns non-null for
// all 4 entries in PHASE_SIGNAL_PHASES when applied to a brief with a
// committed phase_signals block." See brief amendment for full rationale.
// Pattern D prose-pins kept as doc-anchors for commands/trekbrief.md.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
import { validateBriefContent, PHASE_SIGNAL_PHASES, EFFORT_LEVELS } from '../../lib/validators/brief-validator.mjs';
import { BASE_ALLOWED_MODELS } from '../../lib/validators/profile-validator.mjs';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const COMMAND_FILE = join(ROOT, 'commands', 'trekbrief.md');
const REVIEWER_FILE = join(ROOT, 'agents', 'brief-reviewer.md');
const TEMPLATE_FILE = join(ROOT, 'templates', 'trekbrief-template.md');
const FIXTURE = (name) => join(ROOT, 'tests', 'fixtures', name);
function read() {
return readFileSync(COMMAND_FILE, 'utf8');
}
function readFixture(name) {
return readFileSync(FIXTURE(name), 'utf8');
}
function frontmatterOf(text) {
const doc = parseDocument(text);
return doc.parsed && doc.parsed.frontmatter;
}
// --- Pattern D prose-pins (doc-anchors) ---
test('trekbrief — Phase 3.5 heading is present', () => {
const text = read();
assert.match(text, /^## Phase 3\.5 — Per-phase effort dialog$/m,
'Phase 3.5 heading missing from commands/trekbrief.md');
});
test('trekbrief — Phase 3.5 references all 4 downstream phases', () => {
const text = read();
const startIdx = text.indexOf('## Phase 3.5');
assert.ok(startIdx >= 0, 'Phase 3.5 not found');
const section = text.slice(startIdx, text.indexOf('## Phase 4', startIdx));
for (const phase of ['research', 'plan', 'execute', 'review']) {
assert.ok(section.includes(phase),
`Phase 3.5 missing reference to "${phase}"`);
}
});
test('trekbrief — Phase 3.5 documents phase_signals_partial force-stop', () => {
const text = read();
assert.ok(text.includes('phase_signals_partial'),
'phase_signals_partial not mentioned in /trekbrief command prose');
});
// --- v5.1.1 runtime SC1 tests ---
test('trekbrief — SC1: resolvePhaseSignal returns non-null for all 4 phases on committed brief (brief-effort-low)', () => {
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
for (const phase of PHASE_SIGNAL_PHASES) {
const r = resolvePhaseSignal(fm, phase);
assert.ok(r && typeof r === 'object',
`phase=${phase}: resolver must return non-null for committed brief; got ${JSON.stringify(r)}`);
assert.ok(typeof r.effort === 'string',
`phase=${phase}: resolver result must include effort`);
}
});
test('trekbrief — SC1: each of 4 phases has both effort AND model on full-signals fixture', () => {
const fm = frontmatterOf(readFixture('brief-with-phase-signals.md'));
for (const phase of PHASE_SIGNAL_PHASES) {
const r = resolvePhaseSignal(fm, phase);
assert.ok(r && typeof r === 'object', `phase=${phase}: must resolve`);
assert.ok(EFFORT_LEVELS.includes(r.effort),
`phase=${phase}: effort "${r.effort}" not in EFFORT_LEVELS`);
if ('model' in r) {
assert.ok(BASE_ALLOWED_MODELS.includes(r.model),
`phase=${phase}: model "${r.model}" not in [${BASE_ALLOWED_MODELS.join(', ')}]`);
}
}
});
test('trekbrief — SC1: missing phase_signals + brief_version 2.1 triggers BRIEF_V51_MISSING_SIGNALS', () => {
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
assert.equal(r.valid, false);
assert.ok(
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
`gate must fire; errors=${JSON.stringify(r.errors)}`,
);
});
// --- v5.9 — fable tier option in the Phase 3.5 loop ---
test('trekbrief — v5.9 Phase 3.5 canonical mapping contains the fable row and offers 4 options', () => {
const text = read();
const startIdx = text.indexOf('## Phase 3.5');
assert.ok(startIdx >= 0, 'Phase 3.5 not found');
const section = text.slice(startIdx, text.indexOf('## Phase 4', startIdx));
assert.ok(section.includes('fable → {effort: high, model: fable}'),
'Phase 3.5 canonical mapping must contain the fable tier row');
assert.ok(section.includes('with 4 options'),
'Phase 3.5 loop must offer 4 options (AskUserQuestion maxItems: 4)');
});
// --- v5.5 — framing enforcement + TL;DR + memory-alignment prose-pins ---
test('trekbrief — v5.5 Phase 2.5 framing declaration heading present', () => {
@ -174,35 +78,3 @@ test('trekbrief — v5.5 template carries framing field, 2.2, and TL;DR section'
'template frontmatter must include the framing field');
assert.match(tpl, /^## TL;DR$/m, 'template must include the ## TL;DR section');
});
test('trekbrief — SC1: phase_signals_partial: true does NOT trigger the gate', () => {
const partial = `---
type: trekbrief
brief_version: "2.1"
created: 2026-05-14
task: "Partial brief"
slug: partial-brief
project_dir: .claude/projects/2026-05-14-partial-brief/
research_topics: 0
research_status: complete
auto_research: false
interview_turns: 2
source: fixture
phase_signals_partial: true
---
# Task
## Intent
Stop early.
## Goal
Test partial mode.
## Success Criteria
- gate does not fire.
`;
const r = validateBriefContent(partial, { strict: true });
assert.equal(r.valid, true, `errors=${JSON.stringify(r.errors)}`);
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
});

View file

@ -1,84 +1,21 @@
// tests/commands/trekexecute.test.mjs
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekexecute.
// Low effort runs the plan as one session, step after step; it sets no --gates
// (fix round 4-5a, #33).
// Prose-pin and runtime tests for /trekexecute.
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
import { sectionText } from '../helpers/md-section.mjs';
import { childEnv } from '../helpers/child-env.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const COMMAND_FILE = join(ROOT, 'commands', 'trekexecute.md');
const PHASE = 'execute';
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
function frontmatterOf(text) {
const doc = parseDocument(text);
return doc.parsed && doc.parsed.frontmatter;
}
// --- Pattern D prose-pins ---
test('trekexecute — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
const text = read();
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
'/trekexecute must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
assert.ok(text.includes('phase_signals'),
'/trekexecute must reference phase_signals (v5.1 composition rule)');
});
test('trekexecute — low-effort path runs one session, step after step, and sets no --gates open', () => {
const text = read();
const compIdx = text.indexOf('## Composition rule (v5.1)');
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
const section = text.slice(compIdx, compIdx + 2000);
assert.doesNotMatch(section, /--gates open/, 'Low effort must not set --gates open');
assert.match(section.replace(/\s+/g, ' '), /`effort == 'low'` runs the plan as one session, step after step \(Phase 2\.5\)/,
'Low-effort path must run the plan as one session, step after step');
});
// --- v5.1.1 runtime SC4 + SC7 ---
test('trekexecute — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'low');
assert.equal(r.model, 'sonnet');
});
test('trekexecute — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'standard');
assert.equal(r.model, undefined);
});
test('trekexecute — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'high');
assert.equal(r.model, 'opus');
});
test('trekexecute — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
assert.equal(r.valid, false);
assert.ok(
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
);
});
// --- S38 loop-discipline hardening: completion gate (Step 2) ---
@ -201,9 +138,9 @@ test('trekexecute Phase 8: the state-file block imports through the plugin root
}
});
// --- Phase 1 --project: the brief check and the execute resolver -------------
// Phase 1's --project branch runs the two lines /trekplan runs, for the execute
// phase, and halts only on BRIEF_V51_MISSING_SIGNALS.
// --- Phase 1 --project: the brief check -------------------------------------
// Phase 1's --project branch runs the brief-validator line /trekplan runs, and
// never halts on what it reports.
/** Phase 1's rule 1 (the --project branch), as lines. */
function projectRule() {
@ -215,35 +152,25 @@ function projectRule() {
return sec.slice(start, end).join('\n');
}
test('trekexecute Phase 1 --project runs brief-validator --soft --json and the execute resolver, and halts only on BRIEF_V51_MISSING_SIGNALS', () => {
test('trekexecute Phase 1 --project runs brief-validator --soft --json and never halts on its errors', () => {
const rule = projectRule();
const validator = libLine(rule, 'validators/brief-validator.mjs', '--soft --json');
const resolver = libLine(rule, 'profiles/resolver.mjs', '--resolve-phase-model --phase execute');
assert.equal(validator, 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/brief-validator.mjs --soft --json "{dir}/brief.md"');
assert.equal(resolver,
'node ${CLAUDE_PLUGIN_ROOT}/lib/profiles/resolver.mjs --resolve-phase-model --phase execute --brief-path "{dir}/brief.md" [--profile {profile}] --json');
const f = flat(rule);
for (const s of [
'The validator exits 1 when its `errors` are not empty; read its JSON either way.',
'Halt only when the validator\'s `errors` hold `BRIEF_V51_MISSING_SIGNALS`, with the message from "Sequencing gate surface" below.',
'Any other error is printed in one line and the run continues.',
'A validator or resolver that cannot run (no JSON) is "not measured" in one line, and the run continues.',
'Capture the resolver\'s JSON as `phase_signal_result` (Composition rule below).',
'No `{dir}/brief.md`: skip both lines.',
'Any error is printed in one line and the run continues.',
'A validator that cannot run (no JSON) is "not measured" in one line, and the run continues.',
'No `{dir}/brief.md`: skip the line.',
]) assert.ok(f.includes(s), `the --project rule is missing: ${s}`);
// The two lines run as printed: the validator names the one halting code for a
// 2.1 brief without signals, and the resolver hands Phase 2.5 its low effort.
// The line runs as printed: a brief with an error exits 1 and still prints JSON.
const dir = mkdtempSync(join(tmpdir(), 'trekexecute-project-'));
try {
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-v21-no-signals.md'), join(dir, 'brief.md'));
writeFileSync(join(dir, 'brief.md'), '---\ntype: trekbrief\n---\n\n# Task\n');
const v = bash(validator.split('{dir}').join(dir), ROOT);
assert.equal(v.status, 1, `validator: ${v.stdout}${v.stderr}`);
assert.ok(JSON.parse(v.stdout).errors.some((e) => e.code === 'BRIEF_V51_MISSING_SIGNALS'), v.stdout);
copyFileSync(join(ROOT, 'tests', 'fixtures', 'brief-effort-low.md'), join(dir, 'brief.md'));
const r = bash(resolver.split('{dir}').join(dir).replace(' [--profile {profile}]', ''), ROOT);
assert.equal(r.status, 0, `resolver: ${r.stdout}${r.stderr}`);
assert.equal(JSON.parse(r.stdout).effort, 'low');
assert.ok(JSON.parse(v.stdout).errors.some((e) => e.code === 'BRIEF_MISSING_FIELD'), v.stdout);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
@ -362,36 +289,9 @@ test('trekexecute merge lock: Phase 7.2 step 4 run on a BLOCK review of an older
assert.equal(JSON.parse(r.stdout).verdict, 'BLOCK');
});
// --- The resolver runs in Phase 1; low effort is one session; no gates escalation
// (fix round 4-5a, #33).
// --- No gates escalation (fix round 4-5a, #33).
test('[#33] the Composition rule says the resolver runs in Phase 1, and no line says Phase 2.4 runs it', () => {
assert.doesNotMatch(flat(read()), /invoked in Phase 2\.4/);
assert.ok(flat(sectionText(read(), '## Composition rule (v5.1)')).includes('invoked in Phase 1 alongside the sequencing-gate brief-validator call'),
'the Composition rule does not say the resolver is invoked in Phase 1');
});
test('[#33] sibling: Composition rule has no "uniformity and stats" and no gates auto-escalation', () => {
const f = flat(sectionText(read(), '## Composition rule (v5.1)'));
assert.doesNotMatch(f, /uniformity and stats/, 'the Composition rule still says the model is returned for stats');
assert.ok(f.includes('it is returned for cross-command uniformity.'),
'the Composition rule does not say why the model is returned');
assert.doesNotMatch(f, /gates auto-escalation/);
});
test('[#33] Phase 2.5 names low effort as one session, steps in order, never while a wave is in flight', () => {
const sec = sectionText(read(), '## Phase 2.5 — Execution strategy decision').split('\n');
const start = sec.findIndex((l) => l.startsWith('**Run as single session (foreground)** when ANY of these are true:'));
assert.ok(start !== -1, 'Phase 2.5 must still list when it runs as a single session');
const end = sec.findIndex((l, i) => i > start && l.trim() === '');
const bullets = sec.slice(start + 1, end);
assert.ok(bullets.includes("- `phase_signal_result.effort == 'low'` → one session, steps in order, unless a wave is in flight: when "
+ '`.claude/trekplan-sessions/{slug}/orders/wave.json` exists in the main checkout (the first entry of `git worktree list`), '
+ 'this bullet does not apply and the multi-session path below collects the wave. The check only reads the file and never stops the run.'),
`the single-session list does not name low effort: ${bullets.join(' | ')}`);
});
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left, and high effort says it changes nothing', () => {
test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_override is left', () => {
const text = read();
const f = flat(text);
assert.doesNotMatch(f, /gates_mode = 'closed'/);
@ -399,9 +299,6 @@ test('[#33] no gates_mode = \'closed\', "strictest" or --event gates_mode_flag_o
assert.doesNotMatch(f, /halts immediately/);
assert.doesNotMatch(text, /--event gates_mode_flag_override/);
assert.doesNotMatch(f, /gates_mode = '(adaptive|open)'/);
assert.equal(flat(sectionText(text, '### High-effort behavior (v5.1.1)')),
'### High-effort behavior (v5.1.1) High effort changes nothing in `/trekexecute`: the step helper and the controller '
+ 'already run opus, and every gate already runs. No `gates_mode` is set from the effort.');
});
// --- Phase 8 writes the merge evidence file before the merge call (4.5b2, MEV) --

View file

@ -1,76 +1,17 @@
// tests/commands/trekplan.test.mjs
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekplan.
// Prose-pin tests for /trekplan.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const COMMAND_FILE = join(ROOT, 'commands', 'trekplan.md');
const PHASE = 'plan';
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
function frontmatterOf(text) {
const doc = parseDocument(text);
return doc.parsed && doc.parsed.frontmatter;
}
// --- Pattern D prose-pins (kept) ---
test('trekplan — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
const text = read();
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
'/trekplan must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
assert.ok(text.includes('phase_signals'),
'/trekplan must reference phase_signals (v5.1 composition rule)');
});
test('trekplan — low-effort path references --quick equivalent', () => {
const text = read();
const compIdx = text.indexOf('## Composition rule (v5.1)');
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
const section = text.slice(compIdx, compIdx + 2000);
assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent');
});
// --- v5.1.1 runtime SC4 + SC7 tests ---
test('trekplan — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'low');
assert.equal(r.model, 'sonnet');
});
test('trekplan — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'standard');
assert.equal(r.model, undefined);
});
test('trekplan — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'high');
assert.equal(r.model, 'opus');
});
test('trekplan — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
assert.equal(r.valid, false);
assert.ok(
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
);
});
// --- S31 / V15: export trim — drop pr|issue|markdown, keep headless as a --decompose alias ---

View file

@ -1,63 +1,21 @@
// tests/commands/trekresearch.test.mjs
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekresearch.
// Prose-pin tests for /trekresearch.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const COMMAND_FILE = join(ROOT, 'commands', 'trekresearch.md');
const PHASE = 'research';
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
function frontmatterOf(text) {
const doc = parseDocument(text);
return doc.parsed && doc.parsed.frontmatter;
}
// --- Pattern D prose-pins ---
test('trekresearch — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
const text = read();
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
'/trekresearch must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
assert.ok(text.includes('phase_signals'),
'/trekresearch must reference phase_signals (v5.1 composition rule)');
});
test('trekresearch — low-effort path references --quick equivalent', () => {
const text = read();
// Bound the Composition rule section by the next `###` heading rather than a
// magic 2000-character window: a fixed count silently drops the match as soon
// as prose is inserted above it, turning a real pin into a no-op.
const sectionOf = (doc) => {
const compIdx = doc.indexOf('## Composition rule (v5.1)');
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
const nextHeading = doc.indexOf('\n### ', compIdx);
return nextHeading > compIdx ? doc.slice(compIdx, nextHeading) : doc.slice(compIdx);
};
// (a) positive: the low-effort path is documented inside the bounded section.
assert.match(sectionOf(text), /--quick/, 'Low-effort path must mention --quick equivalent');
// (b) negative: an actual removal must still be caught — a bound that can
// never fail proves nothing.
const mutated = text.replace(/--quick/g, '--removed');
assert.doesNotMatch(sectionOf(mutated), /--quick/,
'heading-bounded slice must still fail on a genuine removal');
});
// --- Phase 5 slice (heading-bounded) ---
// Same bounding discipline as the Composition-rule pin above: slice from the
// phase heading to the NEXT phase heading, never a fixed character window.
// Slice from the phase heading to the NEXT phase heading, never a fixed character window.
function phaseSlice(doc, startHeading, endHeading) {
const start = doc.indexOf(startHeading);
assert.ok(start >= 0, `${startHeading} missing`);
@ -70,52 +28,15 @@ function phase5(doc) {
return phaseSlice(doc, '## Phase 5 —', '## Phase 6 —');
}
test('trekresearch — High-effort behavior gives standard effort the wave-2 sentence and keeps the low effort sentence verbatim', () => {
const text = read();
const flatText = text.replace(/\s+/g, ' ');
test('trekresearch — Phase 4 runs the full swarm with contrarian-researcher as wave 2 every time (decision 209)', () => {
const flatText = read().replace(/\s+/g, ' ');
assert.ok(
flatText.includes('Every run launches the FULL swarm for its scope: all 5 local agents and all 4 external agents, and `contrarian-researcher` runs every time, as wave 2 (see Launch rules)'),
'Phase 4 must state the one fixed path: the full swarm, contrarian-researcher as wave 2 every run',
);
assert.ok(
!flatText.includes('use the existing conditional triggers'),
'standard effort must not point at a conditional trigger that nothing defines',
);
assert.ok(
flatText.includes('Standard effort (or absent): the selected agents run as in Phase 4, and `contrarian-researcher`, when selected, runs as wave 2 after the other agents return (see Launch rules).'),
'the standard-effort sentence must say the contrarian runs as wave 2 when selected',
);
assert.ok(
text.includes('Low effort: inline research only, no agent swarm'),
'the low-effort sentence must survive the Phase 5 rewrite verbatim',
);
});
// --- v5.1.1 runtime SC4 + SC7 ---
test('trekresearch — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'low');
assert.equal(r.model, 'sonnet');
});
test('trekresearch — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'standard');
assert.equal(r.model, undefined);
});
test('trekresearch — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'high');
assert.equal(r.model, 'opus');
});
test('trekresearch — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
assert.equal(r.valid, false);
assert.ok(
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
'no effort must point at a conditional trigger that nothing defines',
);
});
@ -134,17 +55,14 @@ function paragraphAt(text, marker) {
return flat(lines.slice(start, end).join(' '));
}
test('trekresearch — Phase 5 follow-ups take phase_signal_result.model, else opus', () => {
test('trekresearch — Phase 5 follow-ups: a built-in agent is spawned with model opus, at most 2', () => {
const text = read();
assert.doesNotMatch(flat(phase5(text)), /model: "opus"/);
const part = paragraphAt(phase5(text), 'launch a targeted follow-up agent');
assert.match(part, /Maximum 2 follow-ups\./);
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/);
assert.ok(part.includes("A follow-up may be one of Claude Code's built-in agents: Spawn it with `model: \"opus\"`: a built-in agent has no Voyage pin."), part);
});
test('trekresearch — promises no maxTurns budget', () => {
const text = read();
assert.doesNotMatch(text, /maxTurns/);
assert.match(text, /- convention-scanner: medium\+ codebases only \(50\+ files\)/);
});

View file

@ -1,74 +0,0 @@
// tests/commands/trekreview.test.mjs
// v5.1 prose-pin tests + v5.1.1 runtime SC4 + SC7 tests for /trekreview.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolvePhaseSignal } from '../../lib/profiles/phase-signal-resolver.mjs';
import { validateBriefContent } from '../../lib/validators/brief-validator.mjs';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const COMMAND_FILE = join(ROOT, 'commands', 'trekreview.md');
const PHASE = 'review';
function read() { return readFileSync(COMMAND_FILE, 'utf8'); }
function readFixture(name) { return readFileSync(join(ROOT, 'tests', 'fixtures', name), 'utf8'); }
function frontmatterOf(text) {
const doc = parseDocument(text);
return doc.parsed && doc.parsed.frontmatter;
}
// --- Pattern D prose-pins ---
test('trekreview — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
const text = read();
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
'/trekreview must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
assert.ok(text.includes('phase_signals'),
'/trekreview must reference phase_signals (v5.1 composition rule)');
});
test('trekreview — low-effort path references --quick equivalent', () => {
const text = read();
const compIdx = text.indexOf('## Composition rule (v5.1)');
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
const section = text.slice(compIdx, compIdx + 2000);
assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent');
});
// --- v5.1.1 runtime SC4 + SC7 ---
test('trekreview — SC4: low-effort fixture → resolver returns {effort: low, model: sonnet}', () => {
const fm = frontmatterOf(readFixture('brief-effort-low.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'low');
assert.equal(r.model, 'sonnet');
});
test('trekreview — SC4: standard-effort fixture → resolver returns {effort: standard, model: undefined}', () => {
const fm = frontmatterOf(readFixture('brief-effort-standard.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'standard');
assert.equal(r.model, undefined);
});
test('trekreview — SC4: high-effort fixture → resolver returns {effort: high, model: opus}', () => {
const fm = frontmatterOf(readFixture('brief-effort-high.md'));
const r = resolvePhaseSignal(fm, PHASE);
assert.equal(r.effort, 'high');
assert.equal(r.model, 'opus');
});
test('trekreview — SC7: brief_version 2.1 + no phase_signals + no partial → BRIEF_V51_MISSING_SIGNALS', () => {
// Falsification via brief-v21-no-signals fixture: validator must catch missing signals.
const r = validateBriefContent(readFixture('brief-v21-no-signals.md'), { strict: true });
assert.equal(r.valid, false);
assert.ok(
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
`sequencing gate must fire; errors=${JSON.stringify(r.errors)}`,
);
});

View file

@ -1,45 +0,0 @@
---
type: trekbrief
brief_version: "2.1"
created: 2026-07-02
task: "Fixture: high-effort all phases on fable (v5.9 allowlist test)"
slug: brief-effort-fable
project_dir: .claude/projects/2026-07-02-brief-effort-fable/
research_topics: 0
research_status: complete
auto_research: false
interview_turns: 4
source: fixture
phase_signals:
- phase: research
effort: high
model: fable
- phase: plan
effort: high
model: fable
- phase: execute
effort: high
model: fable
- phase: review
effort: high
model: fable
---
# Task: High-effort fable fixture
## Intent
Test fixture for the v5.9 fable model tier — all 4 phases at the
high effort tier with explicit fable model overrides. Mirrors
brief-effort-high.md with `model: opus` replaced by `model: fable`.
## Goal
Resolver returns `{effort: 'high', model: 'fable'}` for each of the 4
PHASE_SIGNAL_PHASES.
## Success Criteria
- Validator passes with no BRIEF_INVALID_MODEL.
- resolvePhaseSignal(fm, phase).effort === 'high' for all 4 phases.
- resolvePhaseSignal(fm, phase).model === 'fable' for all 4 phases.

View file

@ -1,45 +0,0 @@
---
type: trekbrief
brief_version: "2.1"
created: 2026-05-14
task: "Fixture: high-effort all phases (v5.1.1 runtime test)"
slug: brief-effort-high
project_dir: .claude/projects/2026-05-14-brief-effort-high/
research_topics: 0
research_status: complete
auto_research: false
interview_turns: 4
source: fixture
phase_signals:
- phase: research
effort: high
model: opus
- phase: plan
effort: high
model: opus
- phase: execute
effort: high
model: opus
- phase: review
effort: high
model: opus
---
# Task: High-effort fixture
## Intent
Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the
high effort tier with explicit opus model overrides. Mirrors the
production-grade premium-profile scenario.
## Goal
Resolver returns `{effort: 'high', model: 'opus'}` for each of the 4
PHASE_SIGNAL_PHASES.
## Success Criteria
- Validator passes.
- resolvePhaseSignal(fm, phase).effort === 'high' for all 4 phases.
- resolvePhaseSignal(fm, phase).model === 'opus' for all 4 phases.

View file

@ -1,43 +0,0 @@
---
type: trekbrief
brief_version: "2.1"
created: 2026-05-14
task: "Fixture: low-effort all phases (v5.1.1 runtime test)"
slug: brief-effort-low
project_dir: .claude/projects/2026-05-14-brief-effort-low/
research_topics: 0
research_status: complete
auto_research: false
interview_turns: 4
source: fixture
phase_signals:
- phase: research
effort: low
model: sonnet
- phase: plan
effort: low
model: sonnet
- phase: execute
effort: low
model: sonnet
- phase: review
effort: low
model: sonnet
---
# Task: Low-effort fixture
## Intent
Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the lowest
effort tier with explicit sonnet model overrides.
## Goal
Resolver returns `{effort: 'low', model: 'sonnet'}` for each of the 4
PHASE_SIGNAL_PHASES.
## Success Criteria
- Validator passes.
- resolvePhaseSignal(fm, phase) is non-null for all 4 phases.

View file

@ -1,42 +0,0 @@
---
type: trekbrief
brief_version: "2.1"
created: 2026-05-14
task: "Fixture: standard-effort all phases, no model (v5.1.1 runtime test)"
slug: brief-effort-standard
project_dir: .claude/projects/2026-05-14-brief-effort-standard/
research_topics: 0
research_status: complete
auto_research: false
interview_turns: 4
source: fixture
phase_signals:
- phase: research
effort: standard
- phase: plan
effort: standard
- phase: execute
effort: standard
- phase: review
effort: standard
---
# Task: Standard-effort fixture (no model override)
## Intent
Test fixture for v5.1.1 runtime resolver tests — all 4 phases at the
standard tier WITHOUT explicit model fields. This is the operator-skipped
model path that should fall through to the profile.
## Goal
Resolver returns `{effort: 'standard', model: undefined}` for each of the 4
PHASE_SIGNAL_PHASES. The orchestrator-model path then falls through to the
active profile's phase_models.
## Success Criteria
- Validator passes.
- resolvePhaseSignal(fm, phase).model is undefined.
- resolvePhaseSignal(fm, phase).effort is 'standard'.

View file

@ -1,32 +0,0 @@
---
type: trekbrief
brief_version: "2.1"
created: 2026-05-14
task: "Fixture: v5.1 brief WITHOUT phase_signals or partial (falsification target)"
slug: brief-v21-no-signals
project_dir: .claude/projects/2026-05-14-brief-v21-no-signals/
research_topics: 0
research_status: complete
auto_research: false
interview_turns: 4
source: fixture
---
# Task: brief_version 2.1 without phase_signals
## Intent
Falsification fixture for the v5.1 sequencing gate. The brief declares
`brief_version: "2.1"` but omits BOTH `phase_signals` AND
`phase_signals_partial: true`. The brief-validator MUST emit
`BRIEF_V51_MISSING_SIGNALS` for this file — the runtime test for the
sequencing gate asserts the error code fires.
## Goal
Validate that brief-validator catches the missing-signals scenario.
## Success Criteria
- brief-validator returns valid: false.
- errors contains BRIEF_V51_MISSING_SIGNALS.

View file

@ -1,42 +0,0 @@
---
type: trekbrief
brief_version: "2.1"
created: 2026-05-13
task: "Add per-phase effort dialog to /trekbrief"
slug: phase-signals-example
project_dir: .claude/projects/2026-05-13-phase-signals-example/
research_topics: 2
research_status: complete
auto_research: false
interview_turns: 6
source: interview
phase_signals:
- phase: research
effort: low
model: sonnet
- phase: plan
effort: standard
- phase: execute
effort: high
model: opus
- phase: review
effort: standard
---
# Task: Phase-signals example
## Intent
A minimal brief that exercises the v5.1 phase_signals additive field with a
mix of effort levels and model overrides. Used by tests/validators to confirm
the validator accepts well-formed signals across the supported tier matrix.
## Goal
Validator returns valid: true. annotate.mjs strips phase_signals from the
rendered HTML body (frontmatter stays in source).
## Success Criteria
- Validator passes.
- annotate.mjs determinism: re-run produces byte-identical HTML.

View file

@ -1,31 +0,0 @@
---
type: trekbrief
brief_version: "2.0"
created: 2026-05-13
task: "Backward-compat fixture for v5.0-style brief"
slug: legacy-brief-example
project_dir: .claude/projects/2026-05-13-legacy-brief-example/
research_topics: 0
research_status: complete
auto_research: false
interview_turns: 3
source: interview
---
# Task: Legacy brief example
## Intent
A pre-v5.1 brief that pre-dates the phase_signals field. Used by
tests/validators to confirm backward-compatibility: the brief is accepted
without phase_signals as long as brief_version is < 2.1.
## Goal
Validator returns valid: true. The sequencing gate
(BRIEF_V51_MISSING_SIGNALS) does NOT fire for brief_version 2.0.
## Success Criteria
- Validator passes.
- No BRIEF_V51_MISSING_SIGNALS error in r.errors.

View file

@ -1,57 +0,0 @@
---
plan_version: "1.7"
profile: economy
phase_models:
- phase: brief
model: sonnet
- phase: research
model: sonnet
- phase: plan
model: sonnet
- phase: execute
model: sonnet
- phase: review
model: sonnet
- phase: continue
model: sonnet
---
# Test plan — profile drift fixture
Frontmatter declares `profile: economy`. Step 1 manifest has matching
profile_used. Step 2 manifest declares `profile_used: premium` — the
drift case Step 20 of v4.1 plan-validator must catch in --strict mode.
## Implementation Plan
### Step 1: matching profile
- Files: a.ts
- Manifest:
```yaml
manifest:
expected_paths:
- a.ts
min_file_count: 1
commit_message_pattern: "^feat:"
bash_syntax_check: []
forbidden_paths: []
must_contain: []
profile_used: economy
```
### Step 2: drift to premium
- Files: b.ts
- Manifest:
```yaml
manifest:
expected_paths:
- b.ts
min_file_count: 1
commit_message_pattern: "^feat:"
bash_syntax_check: []
forbidden_paths: []
must_contain: []
profile_used: premium
```

View file

@ -1,26 +0,0 @@
---
plan_version: "1.7"
profile: balanced
phase_models:
- phase: brief
model: sonnet
- phase: research
model: sonnet
- phase: plan
model: opus
- phase: execute
model: sonnet
- phase: review
model: opus
- phase: continue
model: sonnet
---
# Test plan (with profile)
This fixture has explicit profile + phase_models in frontmatter.
## Implementation Plan
### Step 1: Stub
- Files: src/stub.mjs

View file

@ -1,15 +0,0 @@
---
plan_version: "1.6"
---
# Test plan (v4.0-style, no profile field)
This fixture is a v4.0-style plan WITHOUT the v4.1 profile/phase_models fields.
Used by tests/lib/profile-application.test.mjs to verify backward-compat
edge-case: resolveTrekcontinueProfile returns {profile: 'premium', profile_source: 'default'}
without throwing when the plan has no profile concept.
## Implementation Plan
### Step 1: Stub
- Files: src/stub.mjs

View file

@ -1,21 +0,0 @@
---
profile_version: "1.0"
name: invalid-enum
phase_models:
- phase: brief
model: sonnet
- phase: research
model: sonnet
- phase: plan
model: opus
- phase: execute
model: sonnet
- phase: review
model: opus
- phase: continue
model: sonnet
parallel_agents_min: 4
parallel_agents_max: 6
external_research_enabled: "yes"
brief_reviewer_iter_cap: 2
---

View file

@ -1,21 +0,0 @@
---
profile_version: "1.0"
name: invalid-model
phase_models:
- phase: brief
model: sonnet
- phase: research
model: sonnet
- phase: plan
model: gpt-4
- phase: execute
model: sonnet
- phase: review
model: opus
- phase: continue
model: sonnet
parallel_agents_min: 2
parallel_agents_max: 4
external_research_enabled: false
brief_reviewer_iter_cap: 2
---

View file

@ -1,153 +0,0 @@
// tests/integration/profile-jaccard-smoke.test.mjs
// SC #18 — cross-tier Jaccard smoke-test for v4.1 model profiles.
//
// Pairs the 4 parked-synthetic fixtures from Step 17:
// profile-plan-run-economy-{1,2}.md × profile-plan-run-premium-{1,2}.md
//
// Asserts that every cross-tier pair clears CROSS_TIER_JACCARD_FLOOR
// after string-normalisering (lowercase, strip backticks/parens, collapse
// whitespace). The pre-gates run BEFORE Jaccard:
// 1. Frontmatter parses cleanly on both fixtures
// 2. Step-count parity (±20 %) — hard fail independent of Jaccard
//
// Empirically calibrated, NOT literature-canonical (see
// research/02-jaccard-syntese-quality.md). arXiv:2412.12148: there is no
// universal threshold; 0.55 is conservative starting point per Step 17
// calibration file (tests/synthetic/profile-jaccard-calibration.md).
//
// Plan-critic-fallback (auto-tighten if Jaccard insufficient) is NOT in
// v4.1 — deferred to v4.2 per research/02 Recommendation #5.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve, join } from 'node:path';
import { jaccardSimilarity } from '../../lib/parsers/jaccard.mjs';
import { normalizeSteps, checkStepCountParity } from '../../lib/parsers/profile-jaccard.mjs';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..', '..');
// Empirically calibrated, NOT literature-canonical.
// See tests/synthetic/profile-jaccard-calibration.md for derivation.
const CROSS_TIER_JACCARD_FLOOR = 0.55;
const ECONOMY_FIXTURES = [
'tests/synthetic/profile-plan-run-economy-1.md',
'tests/synthetic/profile-plan-run-economy-2.md',
];
const PREMIUM_FIXTURES = [
'tests/synthetic/profile-plan-run-premium-1.md',
'tests/synthetic/profile-plan-run-premium-2.md',
];
function loadSteps(rel) {
const text = readFileSync(join(ROOT, rel), 'utf-8');
const doc = parseDocument(text);
assert.ok(
doc.valid,
`frontmatter of ${rel} did not parse: ${(doc.errors || []).map((e) => e.message).join(', ')}`,
);
const steps = doc.parsed.frontmatter && doc.parsed.frontmatter.steps;
assert.ok(
Array.isArray(steps) && steps.length > 0,
`frontmatter.steps of ${rel} is missing or empty`,
);
return steps;
}
// --- Pre-gate 1: structural frontmatter integrity (acts as plan-validator
// pre-gate for synthetic frontmatter-only fixtures; real plan-md goes
// through node lib/validators/plan-validator.mjs --strict separately).
test('profile-jaccard-smoke — pre-gate: all 4 fixtures parse cleanly with frontmatter.steps', () => {
for (const rel of [...ECONOMY_FIXTURES, ...PREMIUM_FIXTURES]) {
const steps = loadSteps(rel);
assert.ok(steps.length >= 10, `${rel}: < 10 steps (got ${steps.length})`);
// Sanity: all entries are non-empty strings
for (const s of steps) {
assert.equal(typeof s, 'string', `${rel}: non-string step: ${JSON.stringify(s)}`);
assert.ok(s.trim().length > 0, `${rel}: empty step entry`);
}
}
});
// --- Pre-gate 2: step-count parity (±20 % cross-tier).
test('profile-jaccard-smoke — pre-gate: step-count parity ±20% across cross-tier pairs', () => {
for (const eFix of ECONOMY_FIXTURES) {
for (const pFix of PREMIUM_FIXTURES) {
const eSteps = loadSteps(eFix);
const pSteps = loadSteps(pFix);
const r = checkStepCountParity(eSteps, pSteps, 0.34);
// Note: synthetic economy=30, premium=40 → ratio = 10/40 = 0.25.
// We allow 0.34 here because empirical cross-tier may exceed 0.20
// when one tier prunes verification steps. Tighten in v4.2 once
// empirical data lands.
assert.ok(r.ok, `${eFix} × ${pFix}: ${r.message}`);
}
}
});
// --- Cross-tier Jaccard: every pair must clear floor (after normalisering).
test('profile-jaccard-smoke — cross-tier Jaccard ≥ floor for all 4 economy×premium pairs', () => {
const pairs = [];
for (const eFix of ECONOMY_FIXTURES) {
for (const pFix of PREMIUM_FIXTURES) {
const eSteps = normalizeSteps(loadSteps(eFix));
const pSteps = normalizeSteps(loadSteps(pFix));
const sim = jaccardSimilarity(eSteps, pSteps);
pairs.push({ eFix, pFix, sim });
}
}
// Report all pairs in failure message for diagnostic clarity.
const failures = pairs.filter((p) => p.sim < CROSS_TIER_JACCARD_FLOOR);
if (failures.length > 0) {
const summary = pairs
.map((p) => ` ${p.eFix.split('/').pop()} × ${p.pFix.split('/').pop()}: ${p.sim.toFixed(3)}`)
.join('\n');
assert.fail(
`${failures.length}/${pairs.length} cross-tier pairs below floor ${CROSS_TIER_JACCARD_FLOOR}:\n${summary}`,
);
}
// Sanity-floor: at least 4 pairs measured (2×2 cross product).
assert.equal(pairs.length, 4, 'expected 4 cross-tier pairs (2 economy × 2 premium)');
});
// --- Intra-tier sanity: same-profile pairs must have HIGHER Jaccard than
// cross-tier (otherwise the smoke-test is not actually discriminating).
test('profile-jaccard-smoke — intra-tier Jaccard > cross-tier mean (sanity for discriminator)', () => {
const intraEconomy = jaccardSimilarity(
normalizeSteps(loadSteps(ECONOMY_FIXTURES[0])),
normalizeSteps(loadSteps(ECONOMY_FIXTURES[1])),
);
const intraPremium = jaccardSimilarity(
normalizeSteps(loadSteps(PREMIUM_FIXTURES[0])),
normalizeSteps(loadSteps(PREMIUM_FIXTURES[1])),
);
let crossSum = 0;
let crossN = 0;
for (const eFix of ECONOMY_FIXTURES) {
for (const pFix of PREMIUM_FIXTURES) {
crossSum += jaccardSimilarity(
normalizeSteps(loadSteps(eFix)),
normalizeSteps(loadSteps(pFix)),
);
crossN += 1;
}
}
const crossMean = crossSum / crossN;
assert.ok(
intraEconomy > crossMean,
`intra-tier Jaccard (economy: ${intraEconomy.toFixed(3)}) must exceed cross-tier mean (${crossMean.toFixed(3)})`,
);
assert.ok(
intraPremium > crossMean,
`intra-tier Jaccard (premium: ${intraPremium.toFixed(3)}) must exceed cross-tier mean (${crossMean.toFixed(3)})`,
);
});

View file

@ -281,3 +281,8 @@ test('trekreview --answers: the parser accepts it as a boolean, and the usage li
assert.equal(usage.status, 1, usage.stdout + usage.stderr);
assert.match(usage.stderr, /Usage: \/trekreview --project <dir> [^\n]*\[--answers\]/, `the usage line names --answers: ${usage.stderr}`);
});
test('retired --profile swallows its value: --profile economy leaves positional untouched', () => {
const r = parseArgs('--profile economy .claude/projects/x', 'trekcontinue');
assert.deepEqual(r.positional, ['.claude/projects/x'], `the retired flag's value is no positional: ${JSON.stringify(r)}`);
});

View file

@ -169,10 +169,9 @@ test('[#1] sibling: commands/trekexecute.md: `gates_mode` changes nothing in `/t
'trekexecute does not say gates_mode changes nothing there and the one stop is the merge into main');
});
test('[#1] sibling: docs/architecture.md does not call --gates open the executor\'s low effort', () => {
test('[#1] sibling: docs/architecture.md does not name --gates open', () => {
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', () => {

View file

@ -124,8 +124,7 @@ test('computeVerdict — counts each severity tier', () => {
test('reasonablenessFilter — citation-less is REFUTED, unknown rule_key is UNVERIFIED, severity mismatch corrected', () => {
// Contract change (fail-closed): only `no-citation` refutes. An ad-hoc
// rule_key is a real defect wearing the wrong label — v5.1.1 high-effort mode
// already keeps those, normalised to PLAN_EXECUTE_DRIFT.
// rule_key is a real defect wearing the wrong label.
const r = reasonablenessFilter([
{ file: 'x.mjs', line: 1, rule_key: 'NOPE_KEY', severity: 'BLOCKER' }, // unknown → unverified
{ file: '', line: 1, rule_key: 'MISSING_TEST', severity: 'MAJOR' }, // no file → drop

View file

@ -833,7 +833,7 @@ test('runCriteria: a nested `node --test` on a failing test is red even when NOD
function briefWith(sections) {
return [
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', 'phase_signals_partial: true', '---', '',
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
...sections,
].join('\n');

View file

@ -7,8 +7,8 @@
//
// 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, profile tables from
// the yaml, version-sync) and survive harmless rewording. These are the
// (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,
@ -22,7 +22,6 @@ import { readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseDocument } from '../../lib/util/frontmatter.mjs';
import { resolveProfile, loadProfile } from '../../lib/profiles/resolver.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
@ -310,27 +309,6 @@ test('S23: Phase-9 dedup hand-off uses --stdin and the reviewers emit a json fin
}
});
// --- S20 — anti-false-claim: operations.md custom-profile prose must match findProfilePath ---
//
// findProfilePath (lib/profiles/resolver.mjs:57) resolves built-in FIRST, then
// repo-root voyage-profiles/, then ~/.claude/voyage-profiles/. Custom profiles
// live in voyage-profiles/ (NOT lib/profiles/), must use NEW names, and CANNOT
// shadow a built-in. The old prose claimed the opposite (lib/profiles/<custom>;
// "override built-ins of the same name"; "alphabetical … precedence") — guard it.
test('operations.md custom-profile prose matches findProfilePath resolution order [S20]', () => {
const ops = read('docs/operations.md');
for (const bad of ['lib/profiles/<custom>', 'alphabetical', 'override built-ins of the same name']) {
assert.ok(
!ops.includes(bad),
`docs/operations.md: false custom-profile claim "${bad}" contradicts findProfilePath — fix the prose, not this test`,
);
}
assert.ok(
ops.includes('voyage-profiles/'),
'docs/operations.md must point custom profiles at voyage-profiles/ (not lib/profiles/)',
);
});
// --- 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]
@ -371,113 +349,6 @@ test('operations.md + command-modes.md describe the real autonomy-gate, not a fa
}
});
// --- v4.1 Step 21 — pin --profile + phase_models on the 6 commands ---
//
// CLAUDE.md / README.md pinning is deferred to Step 22 (post-write of
// those documents). Step 21 only verifies command-file content, which
// was written in Step 7 (Wave 3).
const PIPELINE_COMMANDS = [
'trekbrief.md',
'trekresearch.md',
'trekplan.md',
'trekexecute.md',
'trekreview.md',
'trekcontinue.md',
];
test('every pipeline command-file documents the --profile flag (SC #20)', () => {
for (const f of PIPELINE_COMMANDS) {
const text = read(`commands/${f}`);
assert.match(
text,
/--profile\b/,
`commands/${f}: --profile flag is required documentation in v4.1`,
);
}
});
test('command-files mentioning model profiles use canonical name `phase_models`', () => {
// Reject legacy / brainstormed alternatives that would confuse readers.
const FORBIDDEN = ['model_per_phase', 'phase_to_model', 'profile_phase_models'];
for (const f of PIPELINE_COMMANDS) {
const text = read(`commands/${f}`);
for (const bad of FORBIDDEN) {
assert.ok(
!text.includes(bad),
`commands/${f}: forbidden alias "${bad}" — canonical name is "phase_models"`,
);
}
}
});
test('at least one pipeline command-file references `phase_models` canonical name', () => {
// Sanity: not every command has to enumerate phase_models inline (e.g.
// trekbrief and trekcontinue may only mention --profile), but ≥ 1
// command-file must spell out the canonical name so the regression test
// pins drift.
let mentioned = 0;
for (const f of PIPELINE_COMMANDS) {
if (read(`commands/${f}`).includes('phase_models')) mentioned += 1;
}
assert.ok(
mentioned >= 1,
`expected ≥ 1 command-file to mention canonical name "phase_models", got ${mentioned}`,
);
});
// --- v4.1 Step 22 — post-write CLAUDE.md / README.md pinning ---
//
// Plan-critic Blocker 2 fix: Step 21 only pinned commands/*.md (which
// are written in Step 7 / Wave 3). Step 22 writes the top-level docs
// and extends pinning here so doc-consistency stays green AFTER Step 22.
test('CLAUDE.md documents --profile flag', () => {
const md = read('CLAUDE.md');
assert.match(
md,
/--profile\b/,
'CLAUDE.md must document the --profile flag (v4.1 SC #20)',
);
});
test('canonical name `phase_models` documented in docs/profiles.md', () => {
// v5.x doc consolidation (67f6dd5) moved profile/phase detail OUT of
// CLAUDE.md and into docs/. docs/profiles.md is now the canonical home for
// phase_models; CLAUDE.md only links to it via "Reference docs". Assert the
// canonical name lives in the canonical doc and no legacy alias crept in.
// (was: read('CLAUDE.md') — v4.1 SC #20.)
const md = read('docs/profiles.md');
assert.match(
md,
/phase_models/,
'docs/profiles.md must use canonical name "phase_models" (v4.1 SC #20)',
);
for (const bad of ['model_per_phase', 'phase_to_model', 'profile_phase_models']) {
assert.ok(
!md.includes(bad),
`docs/profiles.md must NOT use legacy alias "${bad}"`,
);
}
});
test('README.md documents --profile flag for all 6 commands', () => {
// SG1: README flag-table coverage is gating for SC #20. README is the
// primary discovery surface for new users.
const md = read('README.md');
// Top-level Profile system section is required so the flag is
// discoverable independent of per-command tables.
assert.match(md, /## Profile system/, 'README.md missing top-level "## Profile system" section');
// Every per-command Modes table must include --profile (count of
// --profile occurrences should be ≥ 6 — one per command + Profile
// system section).
const profileMentions = (md.match(/--profile\b/g) || []).length;
assert.ok(
profileMentions >= 6,
`README.md must mention --profile ≥ 6 times (one per command + section), got ${profileMentions}`,
);
});
test('CHANGELOG.md has v4.1.0 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(
@ -487,19 +358,6 @@ test('CHANGELOG.md has v4.1.0 entry', () => {
);
});
test('docs/profiles.md exists and documents Custom.yaml authoring', () => {
const dp = read('docs/profiles.md');
assert.ok(dp.length > 1000, 'docs/profiles.md must be substantive (> 1000 chars)');
// Must document custom-profile authoring (Step 22 manifest must_contain
// pattern: "Custom.yaml" — case-insensitive match handled here as
// /[Cc]ustom[. ]/ to allow either "custom.yaml" or "Custom profile" prose).
assert.match(
dp,
/[Cc]ustom\.yaml|[Cc]ustom profile|<custom>\.yaml/,
'docs/profiles.md must document custom profile authoring',
);
});
test('commands/trekplan.md Phase 8 seals Opus-4.7 schema-drift defense', () => {
const cmd = read('commands/trekplan.md');
// Locate Phase 8 section
@ -714,7 +572,7 @@ test('operational files no longer reference trekrevise (v5.0.0 removal)', () =>
}
});
// --- v5.1 — phase_signals + brief_version 2.1 ---
// --- v5.1 — brief_version 2.1 ---
test('v5.5 — templates/trekbrief-template.md declares brief_version: "2.2" (quoted)', () => {
const t = read('templates/trekbrief-template.md');
@ -722,67 +580,11 @@ test('v5.5 — templates/trekbrief-template.md declares brief_version: "2.2" (qu
'trekbrief-template.md must declare brief_version: "2.2" (quoted) — unquoted parses as Number and bypasses sequencing gate; v5.5 bumped 2.1→2.2 for framing enforcement');
});
test('v5.1 — templates/trekbrief-template.md contains phase_signals: block', () => {
const t = read('templates/trekbrief-template.md');
assert.match(t, /^phase_signals:$/m,
'trekbrief-template.md must contain a phase_signals: block in frontmatter');
});
test('v5.1 — HANDOVER-CONTRACTS.md schema row includes phase_signals + phase_signals_partial', () => {
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('| `phase_signals` |'),
'HANDOVER-CONTRACTS must add a phase_signals row to the Handover 1 schema table');
assert.ok(t.includes('| `phase_signals_partial` |'),
'HANDOVER-CONTRACTS must add a phase_signals_partial row to the Handover 1 schema table');
});
test('v5.1 — phase_signals documented in docs/HANDOVER-CONTRACTS.md', () => {
// v5.x doc consolidation (67f6dd5) moved phase_signals out of CLAUDE.md.
// phase_signals is a Handover 1 (brief) field, so docs/HANDOVER-CONTRACTS.md
// is its canonical home (the schema rows are pinned separately above).
// CLAUDE.md only links to it. (was: read('CLAUDE.md') — v5.1.)
const t = read('docs/HANDOVER-CONTRACTS.md');
assert.ok(t.includes('phase_signals'),
'docs/HANDOVER-CONTRACTS.md must document phase_signals (v5.1)');
});
test('v5.1 — voyage README.md mentions phase_signals', () => {
const t = read('README.md');
assert.ok(t.includes('phase_signals'),
'voyage README.md must mention phase_signals (v5.1 "What\'s new" bullet)');
});
// --- v5.1.1 — High-effort behavior sub-section per command (Step 10) ---
test('v5.1.1 — commands/trekplan.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
const t = read('commands/trekplan.md');
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
'trekplan.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (Adversarial Pass 2, unavailable/skipped since v5.10.1)');
});
test('v5.1.1 — commands/trekresearch.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
const t = read('commands/trekresearch.md');
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
'trekresearch.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (contrarian-researcher always-on)');
});
test('v5.1.1 — commands/trekreview.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
const t = read('commands/trekreview.md');
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
'trekreview.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (skip Pass 3 + coordinator normalization)');
});
test('v5.1.1 — commands/trekexecute.md contains ### High-effort behavior (v5.1.1) sub-section', () => {
const t = read('commands/trekexecute.md');
assert.match(t, /^### High-effort behavior \(v5\.1\.1\)$/m,
'trekexecute.md must contain ### High-effort behavior (v5.1.1) sub-section under Composition rule (high effort changes nothing in /trekexecute)');
});
// --- 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:
// phase_signals stays optional; brief_version 2.1 is the frozen baseline (S3 decision).
// 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');
@ -808,10 +610,8 @@ test('v5.5 — brief_version 2.2 is current in the public-contract schema table
'Handover 1 schema table must mark brief_version "2.2" as current');
});
test('v5.5 — phase_signals stays optional: not promoted to required', () => {
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(!t.includes('v5.4 may promote'),
'the speculative "v5.4 may promote phase_signals to required" line must be resolved (S3 froze the shape)');
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'),
@ -851,103 +651,6 @@ test('v5.5 — voyage README.md mentions framing enforcement / brief_version 2.2
'voyage README.md must carry a "What\'s new" note for framing enforcement (brief_version 2.2)');
});
// --- S15 — default-profile name + per-phase model claims (cost-claim truth-pass) ---
// The resolver's hardcoded default (lib/profiles/resolver.mjs) is the single
// source of truth for which built-in profile a fresh user gets. README,
// docs/profiles.md, and docs/operations.md must all mark THAT profile as the
// default and must not mark any other built-in as default. Guards the
// code-vs-docs contradiction the S14 audit surfaced (Addendum finding #3).
const PROFILE_DOCS = ['README.md', 'docs/profiles.md', 'docs/operations.md'];
test('S15: default-profile name is consistent across resolver + all profile docs', () => {
// Empty env so the operator's VOYAGE_PROFILE does not mask the hardcoded default.
const { profile: def, profile_source } = resolveProfile({}, {});
assert.equal(profile_source, 'default', 'resolveProfile({}, {}) must report source=default');
assert.equal(def, 'premium', 'resolver hardcoded default is premium (operator decision 2026-05-13, commit 40d8742)');
const OTHERS = ['economy', 'balanced', 'premium', 'fable'].filter((p) => p !== def);
for (const doc of PROFILE_DOCS) {
const body = read(doc);
assert.ok(
body.includes(`\`${def}\` (default)`),
`${doc}: profile table must mark \`${def}\` as "(default)" to match resolver`,
);
for (const other of OTHERS) {
assert.ok(
!body.includes(`\`${other}\` (default)`),
`${doc}: must NOT mark \`${other}\` as "(default)" — resolver default is ${def}`,
);
assert.ok(
!body.includes(`Default \`${other}\``),
`${doc}: lookup-order must not say "Default \`${other}\`" — resolver default is ${def}`,
);
}
}
});
test('S15: profile tables encode each built-in yaml phase_models exactly', () => {
// Column order in every profile table: Profile | Brief | Research | Plan | Execute | Review | Continue | Use case
const PHASES = ['brief', 'research', 'plan', 'execute', 'review', 'continue'];
for (const name of ['economy', 'balanced', 'premium', 'fable']) {
const pm = loadProfile(name).phase_models; // {brief:'opus', ...}
const expected = PHASES.map((ph) => pm[ph]);
for (const doc of PROFILE_DOCS) {
const row = read(doc)
.split('\n')
.find((l) => new RegExp(`^\\|\\s*\`${name}\``).test(l));
assert.ok(row, `${doc}: profile table is missing a \`${name}\` row`);
const cells = row.split('|').map((c) => c.trim());
// cells[0] is '' (leading pipe), cells[1] is the name, cells[2..7] are the 6 models
const docModels = cells.slice(2, 8);
assert.deepEqual(
docModels,
expected,
`${doc}: \`${name}\` row models ${JSON.stringify(docModels)} must equal lib/profiles/${name}.yaml ${JSON.stringify(expected)}`,
);
}
}
});
// --- S34 (V30) — economy is self-declared experimental until the cross-tier
// Jaccard floor (0.55) is empirically calibrated (Step-17 calibration deferred
// to v4.2). The status must be visible in BOTH the profile data
// (lib/profiles/economy.yaml `experimental: true`) and every profile doc's
// economy row. Fix the SOURCE doc/profile, not these pins.
test('S34: economy profile row is flagged experimental across all profile docs', () => {
for (const doc of PROFILE_DOCS) {
const body = read(doc);
const row = body.split('\n').find((l) => /^\|\s*`economy`/.test(l));
assert.ok(row, `${doc}: profile table missing an \`economy\` row`);
assert.match(
row,
/experimental/i,
`${doc}: the \`economy\` table row must flag it experimental ` +
`(uncalibrated Jaccard floor, S34/V30) — matches lib/profiles/economy.yaml experimental: true`,
);
}
});
test('S34: economy experimental flag agrees with parked-synthetic calibration status', () => {
// economy.yaml experimental:true is honest ONLY while calibration is still
// parked-synthetic. When empirical calibration lands (status: empirical),
// drop experimental:true in the SAME change — this pin enforces the linkage.
const eco = parseDocument(read('lib/profiles/economy.yaml')).parsed.frontmatter;
const cal = parseDocument(read('tests/synthetic/profile-jaccard-calibration.md')).parsed.frontmatter;
if (cal.status === 'parked-synthetic') {
assert.equal(
eco.experimental,
true,
'while calibration is parked-synthetic, economy.yaml must stay experimental: true',
);
} else {
assert.notEqual(
eco.experimental,
true,
'calibration is no longer parked-synthetic — drop economy experimental: true in the same change',
);
}
});
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
@ -962,7 +665,7 @@ test('S15: README cost prose does not resurrect the false Sonnet-swarm claim', (
for (const phrase of BANNED) {
assert.ok(
!md.includes(phrase),
`README.md must not claim "${phrase}" — sub-agents are opus-pinned; default profile runs Opus on every phase`,
`README.md must not claim "${phrase}" — sub-agents are opus-pinned`,
);
}
});
@ -1070,7 +773,7 @@ test('S32: trekbrief Phase 3 delegates Q&A turn-taking to AskUserQuestion (V01
);
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 3.5', p3Start);
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),
@ -1125,29 +828,6 @@ test('S38: forward-guard — no product-facing doc asserts main-context relief u
);
});
// ── v5.9 (fable-tier step 6) — composed-resolver wiring pin ────────────────
// The four pipeline commands must resolve {effort, model} via the composed
// CLI (`resolver.mjs --resolve-phase-model`, brief > profile > default). A
// direct `phase-signal-resolver.mjs --brief` invocation in a command Bash
// block is the brief-only CLI: it silently drops the profile layer (the
// AP2-1 footgun — `--profile <x>` would never reach sub-agent spawns again).
// If this pin fails, re-wire the command to the composed CLI — do not relax
// the pin.
for (const cmd of ['trekresearch', 'trekplan', 'trekreview', 'trekexecute']) {
test(`v5.9: commands/${cmd}.md invokes the composed resolver, not the brief-only CLI`, () => {
const text = read(`commands/${cmd}.md`);
assert.ok(
text.includes('--resolve-phase-model'),
`commands/${cmd}.md must invoke resolver.mjs --resolve-phase-model (composed brief > profile > default)`,
);
assert.ok(
!/phase-signal-resolver\.mjs --brief/.test(text),
`commands/${cmd}.md must not invoke the brief-only phase-signal-resolver CLI — the profile layer would be silently dropped`,
);
});
}
// ── 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:
@ -1221,11 +901,7 @@ test('v5.9: no commands/*.md frontmatter carries a model: key (session inheritan
// 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. So the two
// v5.1.1 assertion messages above (which describe the high-effort rules) are not
// covered by the scanner and were found stale by review, not by npm test — they
// match on the heading regex, so a wrong message never fails. When the high-effort
// rules change, re-read those messages by hand.
// 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 = [
@ -1275,20 +951,10 @@ test('S95: known-positive control — the historical records still DO carry gemi
);
});
test('S95: trekplan high-effort Pass 2 degrades explicitly instead of failing', () => {
const t = read('commands/trekplan.md');
const section = t.split('\n### High-effort behavior (v5.1.1)\n')[1];
assert.ok(section, 'trekplan.md must still carry the High-effort behavior sub-section');
const head = section.split('\n## ')[0];
assert.match(
head, /unavailable, skipped/,
'the dropped Pass 2 must read "unavailable, skipped" explicitly — a removed pass that is silent is indistinguishable from one that failed',
);
});
// End-state defect D-07: since v5.9.0 no command pins `model:`, so the orchestrator follows
// the session model, and sub-agent models are resolved at the spawn sites (brief signal >
// profile > agent frontmatter). No user-facing doc may claim otherwise. Fix the SOURCE.
// 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(
@ -1304,15 +970,19 @@ test('D-07: no doc claims an opus orchestrator or ships a recipe for model: line
}
});
test('D-07: trekresearch launch rules inject the resolved model instead of a blanket opus', () => {
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 resolution (brief > profile > agent frontmatter, wired in v5.9.0)',
'a blanket opus rule contradicts the file\'s own rule: a Voyage agent keeps its own pin, only a built-in agent gets opus',
);
assert.match(rules, /phase_signal_result\.model/, 'the launch rules must name the resolved model the spawn sites inject');
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
@ -1447,7 +1117,7 @@ test('trekexecute Phase 7 says NO_CRITERIA out loud instead of failing silently'
// 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/profiles.md', 'docs/operations.md',
'CLAUDE.md', 'README.md', 'docs/architecture.md', 'docs/operations.md',
...listMd('commands').map((f) => `commands/${f}`),
...listMd('agents').map((f) => `agents/${f}`),
];
@ -1487,10 +1157,10 @@ test('model aliases: known-positive control — the same query still finds every
assert.deepEqual(silent, [], `no known-positive left for: ${silent.join(', ')}`);
});
test('model aliases: profiles.md states the current targets, defaults and session inheritance', () => {
const t = read('docs/profiles.md');
const axes = (t.split('\n## Model & effort axes\n')[1] || '').split('\n## ')[0];
assert.ok(axes.length > 0, 'docs/profiles.md must still carry the Model & effort axes section');
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`');
@ -1499,7 +1169,8 @@ test('model aliases: profiles.md states the current targets, defaults and sessio
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`');
for (const f of ['CLAUDE.md', 'README.md']) {
// 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}`);
}
@ -1527,7 +1198,7 @@ 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/profiles.md', 'CLAUDE.md', 'README.md']) {
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');
@ -1540,29 +1211,10 @@ test('effort floor: known-positive control — the query finds the old phrases,
assert.doesNotMatch('both with default reasoning effort `medium`', RETRIEVAL_MEDIUM);
});
test('effort floor: profiles.md names the four retrieval agents as high', () => {
const axes = (read('docs/profiles.md').split('\n## Model & effort axes\n')[1] || '').split('\n## ')[0];
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 axes names all four retrieval agents');
assert.ok(bullet, 'one bullet in §Model & effort names all four retrieval agents');
assert.match(bullet, /`high`/);
assert.doesNotMatch(bullet, /`medium`/);
});
// The resolver returns `effort` only when the brief's phase signal sets one;
// no command says it always does.
const RESOLVER_COMMANDS = ['trekplan', 'trekexecute', 'trekresearch', 'trekreview'];
test('resolver: no command says the resolver always returns {effort, model, source}', () => {
for (const f of readdirSync(join(ROOT, 'commands')).filter((x) => x.endsWith('.md'))) {
assert.doesNotMatch(read(`commands/${f}`), /\{effort, model, source\}/, f);
}
});
test('resolver: each resolver-using command says `effort` comes only from the brief signal, else standard', () => {
for (const c of RESOLVER_COMMANDS) {
const t = read(`commands/${c}.md`);
const hits = t.match(/\{model, source\}[^\n]*\n?[^\n]*effort[^\n]*only when the brief's phase signal sets one/g) ?? [];
assert.ok(hits.length >= 1, `${c}: no statement that effort is only present with a brief signal`);
assert.match(t, /without it[^\n]*\n?[^\n]*`?standard`?/i, c);
}
});

View file

@ -91,43 +91,8 @@ test('extension does NOT break REQUIRED_KEYS contract', () => {
}
});
// v4.1 Step 3 — OPTIONAL_STRING_KEYS dispatch (profile_used)
// v4.1 Step 3 — OPTIONAL_STRING_KEYS dispatch; its one key went in cleanup R-b.
test('OPTIONAL_STRING_KEYS exports profile_used', () => {
assert.deepEqual(
[...OPTIONAL_STRING_KEYS].sort(),
['profile_used'].sort(),
'OPTIONAL_STRING_KEYS export drift — pin contract',
);
});
test('profile_used: economy parses successfully (SC #10 forward-compat)', () => {
const r = parseManifest(bodyWithExtras(' profile_used: economy'));
assert.equal(r.valid, true, JSON.stringify(r.errors));
assert.equal(r.parsed.profile_used, 'economy');
});
test('profile_used: numeric rejected with MANIFEST_OPTIONAL_TYPE', () => {
const r = parseManifest(bodyWithExtras(' profile_used: 42'));
assert.equal(r.valid, false);
const found = r.errors.find(e => e.code === 'MANIFEST_OPTIONAL_TYPE');
assert.ok(found, `expected MANIFEST_OPTIONAL_TYPE, got: ${JSON.stringify(r.errors)}`);
assert.match(found.message, /profile_used/);
assert.match(found.message, /string/);
});
test('absence of profile_used: field is NOT in parsed (NOT defaulted, unlike boolean)', () => {
const r = parseManifest(bodyOnlyRequired());
assert.equal(r.valid, true, JSON.stringify(r.errors));
// Absence semantics differ from boolean: parsed should NOT contain the key
assert.equal('profile_used' in r.parsed, false,
'profile_used must NOT be auto-defaulted when absent — string-key semantics');
});
test('profile_used works alongside boolean optional keys (skip_commit_check + memory_write)', () => {
const r = parseManifest(bodyWithExtras(' skip_commit_check: true\n memory_write: true\n profile_used: balanced'));
assert.equal(r.valid, true, JSON.stringify(r.errors));
assert.equal(r.parsed.skip_commit_check, true);
assert.equal(r.parsed.memory_write, true);
assert.equal(r.parsed.profile_used, 'balanced');
test('OPTIONAL_STRING_KEYS exports no key', () => {
assert.deepEqual([...OPTIONAL_STRING_KEYS], [], 'OPTIONAL_STRING_KEYS export drift — pin contract');
});

View file

@ -1,112 +0,0 @@
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { execFileSync } from 'node:child_process';
import { writeFileSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { resolvePhaseSignal, resolvePhaseSignalFromFile } from '../../lib/profiles/phase-signal-resolver.mjs';
const FULL_SIGNALS_FM = {
phase_signals: [
{ phase: 'research', effort: 'low', model: 'sonnet' },
{ phase: 'plan', effort: 'standard' },
{ phase: 'execute', effort: 'high', model: 'opus' },
{ phase: 'review', effort: 'standard', model: 'sonnet' },
],
};
test('resolvePhaseSignal — returns {effort, model} for all 4 phases on full-signals brief', () => {
for (const phase of ['research', 'plan', 'execute', 'review']) {
const r = resolvePhaseSignal(FULL_SIGNALS_FM, phase);
assert.ok(r && typeof r === 'object', `phase=${phase} should resolve non-null`);
assert.ok(typeof r.effort === 'string', `phase=${phase} should have effort`);
}
});
test('resolvePhaseSignal — returns null when brief has no phase_signals', () => {
const r = resolvePhaseSignal({ task: 'x' }, 'plan');
assert.equal(r, null);
});
test('resolvePhaseSignal — returns partial {effort} with model undefined when signal omits model', () => {
const r = resolvePhaseSignal(FULL_SIGNALS_FM, 'plan');
assert.equal(r.effort, 'standard');
assert.equal(r.model, undefined);
assert.ok(!('model' in r), 'model key should be absent when not in signal');
});
test('resolvePhaseSignal — returns null when phase is not in PHASE_SIGNAL_PHASES', () => {
assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'brief'), null);
assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'continue'), null);
assert.equal(resolvePhaseSignal(FULL_SIGNALS_FM, 'nonsense'), null);
});
test('resolvePhaseSignal — defensive: null/non-object input returns null', () => {
assert.equal(resolvePhaseSignal(null, 'plan'), null);
assert.equal(resolvePhaseSignal(undefined, 'plan'), null);
assert.equal(resolvePhaseSignal('string', 'plan'), null);
assert.equal(resolvePhaseSignal({ phase_signals: 'not-array' }, 'plan'), null);
});
test('resolvePhaseSignal — drops model not in BASE_ALLOWED_MODELS (defense-in-depth gate)', () => {
// MAJOR fix (S4): line that copies `model` must gate against the same
// allowlist brief-validator uses (BASE_ALLOWED_MODELS = ['sonnet','opus','fable']),
// mirroring how effort is gated against EFFORT_LEVELS. A brief that slipped
// validation (hand-edited, validation skipped) must not hand a junk model
// string to a command that then spawns an agent with `model: <junk>`.
const fm = {
phase_signals: [
{ phase: 'plan', effort: 'high', model: 'gpt-4' }, // invalid model
{ phase: 'review', effort: 'standard', model: 'haiku' }, // not in BASE_ALLOWED_MODELS
],
};
const plan = resolvePhaseSignal(fm, 'plan');
assert.equal(plan.effort, 'high', 'valid effort still resolves');
assert.equal(plan.model, undefined, 'invalid model "gpt-4" must be dropped');
assert.ok(!('model' in plan), 'model key absent when value not in allowlist');
const review = resolvePhaseSignal(fm, 'review');
assert.equal(review.model, undefined, 'haiku must be dropped (not in BASE_ALLOWED_MODELS)');
assert.ok(!('model' in review), 'model key absent for haiku');
});
test('resolvePhaseSignal — keeps valid models (sonnet, opus, fable) after gating', () => {
const fm = {
phase_signals: [
{ phase: 'research', effort: 'low', model: 'sonnet' },
{ phase: 'execute', effort: 'high', model: 'opus' },
{ phase: 'review', effort: 'high', model: 'fable' },
],
};
assert.equal(resolvePhaseSignal(fm, 'research').model, 'sonnet');
assert.equal(resolvePhaseSignal(fm, 'execute').model, 'opus');
assert.equal(resolvePhaseSignal(fm, 'review').model, 'fable');
});
test('resolvePhaseSignalFromFile + CLI shim — writes JSON to stdout, exit 0', () => {
const fixture = join(tmpdir(), `phase-signal-test-${process.pid}.md`);
writeFileSync(fixture, `---
type: trekbrief
brief_version: "2.1"
phase_signals:
- phase: plan
effort: high
model: opus
---
# x
`);
try {
// Programmatic invocation
const r = resolvePhaseSignalFromFile(fixture, 'plan');
assert.deepEqual(r, { effort: 'high', model: 'opus' });
// CLI shim
const helperPath = new URL('../../lib/profiles/phase-signal-resolver.mjs', import.meta.url).pathname;
const out = execFileSync('node', [helperPath, '--brief', fixture, '--phase', 'plan', '--json'], {
encoding: 'utf-8',
});
const parsed = JSON.parse(out.trim());
assert.deepEqual(parsed, { effort: 'high', model: 'opus' });
} finally {
try { unlinkSync(fixture); } catch { /* swallow */ }
}
});

View file

@ -151,12 +151,13 @@ function paraAfter(rel, marker) {
return flat(lines.slice(start, i).join('\n'));
}
// ---- spawn model: phase_signal_result.model, the agent's own model: as the fallback ----
// ---- spawn model: a Voyage agent gets no model:, a built-in agent gets opus ----
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\./;
const NO_MODEL = "This spawn passes no `model:`: the agent's own `model: opus` applies.";
const BUILT_IN = 'Spawn it with `model: "opus"`: a built-in agent has no Voyage pin.';
test('spawn model: commands/trekplan.md pins no spawn to `model: "opus"`', () => {
assert.doesNotMatch(PLAN_CMD, /model: "opus"/);
test('spawn model: commands/trekplan.md pins `model: "opus"` once, at the built-in Explore deep-dive', () => {
assert.equal(PLAN_CMD.split('model: "opus"').length - 1, 1, 'expected exactly one `model: "opus"` in commands/trekplan.md');
});
for (const [where, marker] of [
@ -164,32 +165,25 @@ for (const [where, marker] of [
['### 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`.'],
['session-decomposer (Phase 1.5)', 'Launch the **session-decomposer** agent:'],
]) {
test(`spawn model: ${where}: the spawn takes phase_signal_result.model, the agent's own model: as the fallback`, () => {
test(`spawn model: ${where}: the spawn passes no model:, the agent's own model: opus applies`, () => {
const part = paraAfter('commands/trekplan.md', marker);
assert.match(part, PASS_MODEL, `${where}: missing the resolved-model paragraph after "${marker}"`);
assert.ok(part.includes(NO_MODEL), `${where}: missing the no-model sentence after "${marker}": ${part}`);
assert.doesNotMatch(part, /model: "opus"/);
});
}
test('spawn model: session-decomposer (Phase 1.5): --decompose has no resolved model, so the spawn passes no model:', () => {
const part = paraAfter('commands/trekplan.md', 'Launch the **session-decomposer** agent:');
assert.ok(part.includes('Phase 1 runs the model resolver only for `--project`, so `--decompose` has no resolved model: this spawn '
+ 'passes no `model:`, and the agent\'s own `model:` applies.'), `the session-decomposer paragraph does not say it passes no model: ${part}`);
assert.ok(!part.includes('phase_signal_result'), 'the session-decomposer spawn still names phase_signal_result');
assert.doesNotMatch(part, /model: "opus"/);
});
test('spawn model: convention-scanner (Phase 5): the spawn takes phase_signal_result.model, the frontmatter as the fallback', () => {
test('spawn model: convention-scanner (Phase 5): the spawn passes no model:, its own model: opus applies', () => {
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.ok(part.includes('(no `model:`; its own `model: opus` applies)'), part);
assert.doesNotMatch(part, /model: "opus"/);
});
test('spawn model: deep-dives (Phase 6): the spawn takes phase_signal_result.model, else opus', () => {
test('spawn model: deep-dives (Phase 6): the built-in Explore agent is spawned with model 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"/);
assert.ok(part.includes('(subagent_type: "Explore", Claude Code\'s built-in agent)'), part);
assert.ok(part.includes(BUILT_IN), part);
});
// ---- Phase 4 sizing: no turn budget, and the count it runs ---------------------

View file

@ -4,9 +4,7 @@
// can be checked offline: that each grader can see what it claims to see.
//
// - the new case for veikart steg 1's gate exists, and its fixture really
// reaches the intent gate (passes the sequencing gate, has no marker)
// - case 4 (plan-halts-without-phase-signals) can only be stopped by the
// sequencing gate: its fixture carries a current intent approval
// reaches the intent gate (passes the brief validator, has no marker)
// - review-requires-project: the error line is written by CODE (the arg
// parser), and the grader reads the parser's code from the trace
// - grader weaknesses the PM re-measurement found (2026-09-23):
@ -77,14 +75,13 @@ test('proevesett: a case exists for the steg 1 intent gate, with its graders', (
const re = new RegExp(n.pattern);
assert.match(inTrace({ valid: false, errors: [{ code: 'BRIEF_INTENT_NOT_APPROVED' }] }, 2), re);
assert.doesNotMatch(COMMAND_PROSE, re, 'the pattern must not be satisfiable by quoting the command prose');
assert.doesNotMatch(inTrace({ valid: false, errors: [{ code: 'BRIEF_V51_MISSING_SIGNALS' }] }, 2), re);
const noAgent = grader(`evals/${GATE_CASE}/graders/no-agent.md`);
assert.deepEqual([noAgent.type, noAgent.tool, noAgent.min, noAgent.max], ['tool_used', 'Agent', '0', '0']);
const noPlan = grader(`evals/${GATE_CASE}/graders/no-plan-file.md`);
assert.deepEqual([noPlan.type, noPlan.path, noPlan.exists], ['file_exists', '**/plan.md', 'false']);
});
test('proevesett: the gate case fixture passes the sequencing gate and stops at the intent gate', () => {
test('proevesett: the gate case fixture passes the brief validator and stops at the intent gate', () => {
const ws = mkdtempSync(join(tmpdir(), 'proevesett-'));
try {
const s = spawnSync('bash', [join(EVALS, GATE_CASE, 'scaffold.sh')], { cwd: ws, encoding: 'utf8' });
@ -98,54 +95,6 @@ test('proevesett: the gate case fixture passes the sequencing gate and stops at
} finally { rmSync(ws, { recursive: true, force: true }); }
});
// ---- case 4: only the sequencing gate may stop it ----
// Measured 2026-09-23: with no intent marker the child sometimes ran the
// intent gate first and halted there (PM: 6 of 12 on dc9b480, 7 of 10 on
// 66e1fa1). The halt was right, but the case then measured the wrong gate.
const SEQ_CASE = 'plan-halts-without-phase-signals';
test('plan-halts-without-phase-signals: the fixture carries a current intent approval and fails only the sequencing gate', () => {
const ws = mkdtempSync(join(tmpdir(), 'proevesett-'));
try {
const s = spawnSync('bash', [join(EVALS, SEQ_CASE, 'scaffold.sh')], { cwd: ws, encoding: 'utf8' });
assert.equal(s.status, 0, s.stderr);
const brief = join(ws, 'proj', 'brief.md');
const g = spawnSync('node', [join(ROOT, 'lib/validators/intent-approval.mjs'), '--check', '--json', brief], { encoding: 'utf8' });
assert.equal(g.status, 0, `the intent gate must pass so only the sequencing gate can stop the run: ${g.stdout}`);
const v = spawnSync('node', [join(ROOT, 'lib/validators/brief-validator.mjs'), '--soft', '--json', brief], { encoding: 'utf8' });
assert.deepEqual(JSON.parse(v.stdout).errors.map((e) => e.code), ['BRIEF_V51_MISSING_SIGNALS']);
} finally { rmSync(ws, { recursive: true, force: true }); }
});
test('plan-halts-without-phase-signals: a halt at the intent gate fails the case', () => {
assert.ok(graders(SEQ_CASE).includes('no-intent-halt.md'), graders(SEQ_CASE).join(','));
const g = grader(`evals/${SEQ_CASE}/graders/no-intent-halt.md`);
assert.deepEqual([g.type, g.target, g.match], ['regex', 'trace', 'not_contains']);
const re = new RegExp(g.pattern);
for (const code of ['BRIEF_INTENT_NOT_APPROVED', 'BRIEF_INTENT_APPROVAL_STALE', 'BRIEF_INTENT_APPROVAL_INVALID']) {
assert.match(inTrace({ valid: false, errors: [{ code }] }, 2), re, `${code} must fail the case`);
}
assert.doesNotMatch(inTrace({ valid: true, errors: [], warnings: [] }, 2), re);
assert.doesNotMatch(inTrace({ valid: false, errors: [{ code: 'BRIEF_V51_MISSING_SIGNALS' }] }, 2), re);
assert.doesNotMatch(COMMAND_PROSE, re, 'the command prose must not trip the grader');
});
// Known-positive, measured 2026-09-23: with the check in brief-validator.mjs
// disabled, the child still halted on the prose rule and named phase_signals
// (3 of 3 passed). The halt alone does not show that the validator fired.
test('plan-halts-without-phase-signals: the validator\'s own code must be in the trace', () => {
assert.ok(graders(SEQ_CASE).includes('names-code.md'), graders(SEQ_CASE).join(','));
const g = grader(`evals/${SEQ_CASE}/graders/names-code.md`);
assert.deepEqual([g.type, g.target], ['regex', 'trace']);
const re = new RegExp(g.pattern);
assert.match(inTrace({ valid: false, errors: [{ code: 'BRIEF_V51_MISSING_SIGNALS' }] }, 2), re);
assert.doesNotMatch(inTrace({ valid: true, errors: [], warnings: [] }, 2), re);
assert.doesNotMatch(inTrace({ valid: false, errors: [{ code: 'BRIEF_INTENT_NOT_APPROVED' }] }, 2), re);
assert.doesNotMatch(COMMAND_PROSE, re, 'the pattern must not be satisfiable by quoting the command prose');
assert.doesNotMatch(read('lib/validators/brief-validator.mjs'), re, 'nor by reading the validator source');
});
// ---- case 5: the missing-flag error comes from code ----
test('review-requires-project: the arg parser, run as trekreview.md spells it, prints the error (exit 1)', () => {

View file

@ -1,244 +0,0 @@
// tests/lib/profile-application.test.mjs
// SC #5-#9 + backward-compat edge-case for lib/profiles/resolver.mjs.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import {
loadProfile,
resolveProfile,
resolveTrekcontinueProfile,
validateProfileFile,
findProfilePath,
} from '../../lib/profiles/resolver.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, '..', '..');
// SC #5: loadProfile returns matrix-match for all 6 phase_models
test('SC #5: loadProfile("economy") returns flattened phase_models with all 6 phases', () => {
const p = loadProfile('economy');
assert.equal(p.name, 'economy');
assert.equal(p.phase_models.brief, 'sonnet');
assert.equal(p.phase_models.research, 'sonnet');
assert.equal(p.phase_models.plan, 'sonnet');
assert.equal(p.phase_models.execute, 'sonnet');
assert.equal(p.phase_models.review, 'sonnet');
assert.equal(p.phase_models.continue, 'sonnet');
assert.equal(p.parallel_agents_min, 2);
assert.equal(p.parallel_agents_max, 3);
assert.equal(p.external_research_enabled, false);
assert.equal(p.brief_reviewer_iter_cap, 1);
});
test('SC #5: loadProfile("balanced") returns mixed phase_models', () => {
const p = loadProfile('balanced');
assert.equal(p.phase_models.plan, 'opus');
assert.equal(p.phase_models.review, 'opus');
assert.equal(p.phase_models.brief, 'sonnet');
assert.equal(p.phase_models.execute, 'sonnet');
});
test('SC #5: loadProfile("premium") returns all-opus', () => {
const p = loadProfile('premium');
for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) {
assert.equal(p.phase_models[phase], 'opus', `premium ${phase} should be opus`);
}
});
test('SC #5: loadProfile("fable") returns all-fable (BUILTIN_NAMES canary)', () => {
// Throws PROFILE_NOT_FOUND if 'fable' regresses out of BUILTIN_NAMES —
// the silent-fallback-to-premium failure mode this pin exists to catch.
const p = loadProfile('fable');
assert.equal(p.name, 'fable');
for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) {
assert.equal(p.phase_models[phase], 'fable', `fable ${phase} should be fable`);
}
assert.equal(p.parallel_agents_min, 6);
assert.equal(p.parallel_agents_max, 8);
assert.equal(p.external_research_enabled, true);
assert.equal(p.brief_reviewer_iter_cap, 3);
});
test('SC #5: loadProfile throws PROFILE_NOT_FOUND for unknown profile', () => {
try {
loadProfile('does-not-exist-xyz');
assert.fail('expected throw');
} catch (e) {
assert.equal(e.cause, 'PROFILE_NOT_FOUND');
assert.match(e.message, /not found/);
assert.ok(Array.isArray(e.attempted), 'should expose attempted paths');
}
});
// SC #6: env-var fallback flag > env > default
test('SC #6: resolveProfile flag > env > default', () => {
// flag wins
const r1 = resolveProfile({ flags: { '--profile': 'balanced' } }, { VOYAGE_PROFILE: 'economy' });
assert.equal(r1.profile, 'balanced');
assert.equal(r1.profile_source, 'flag');
// env wins when no flag
const r2 = resolveProfile({ flags: {} }, { VOYAGE_PROFILE: 'economy' });
assert.equal(r2.profile, 'economy');
assert.equal(r2.profile_source, 'env');
// default when neither
const r3 = resolveProfile({ flags: {} }, {});
assert.equal(r3.profile, 'premium');
assert.equal(r3.profile_source, 'default');
});
// SC #7: performance — loadProfile 1000 iter < 50ms average (allowing some headroom)
test('SC #7: loadProfile 1000-iter performance < 50ms average', () => {
const iterations = 1000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
loadProfile('economy');
}
const elapsed = performance.now() - start;
const avgMs = elapsed / iterations;
assert.ok(avgMs < 50, `loadProfile too slow: ${avgMs.toFixed(3)}ms average over ${iterations} iter`);
});
// SC #8: custom.yaml from repo-root trumps ~/.claude/
test('SC #8: custom profile from <cwd>/voyage-profiles/<name>.yaml takes precedence over ~/.claude/', () => {
const tmpRepo = mkdtempSync(join(tmpdir(), 'voyage-resolver-repo-'));
const tmpHome = mkdtempSync(join(tmpdir(), 'voyage-resolver-home-'));
try {
// Place custom profile in repo and home — repo should win
mkdirSync(join(tmpRepo, 'voyage-profiles'), { recursive: true });
mkdirSync(join(tmpHome, '.claude', 'voyage-profiles'), { recursive: true });
writeFileSync(join(tmpRepo, 'voyage-profiles', 'mycustom.yaml'),
`---
profile_version: "1.0"
name: mycustom-repo
phase_models:
- phase: brief
model: sonnet
- phase: research
model: sonnet
- phase: plan
model: sonnet
- phase: execute
model: sonnet
- phase: review
model: sonnet
- phase: continue
model: sonnet
parallel_agents_min: 1
parallel_agents_max: 2
external_research_enabled: false
brief_reviewer_iter_cap: 1
---
`);
writeFileSync(join(tmpHome, '.claude', 'voyage-profiles', 'mycustom.yaml'),
`---
profile_version: "1.0"
name: mycustom-home
phase_models:
- phase: brief
model: opus
- phase: research
model: opus
- phase: plan
model: opus
- phase: execute
model: opus
- phase: review
model: opus
- phase: continue
model: opus
parallel_agents_min: 1
parallel_agents_max: 2
external_research_enabled: true
brief_reviewer_iter_cap: 3
---
`);
const found = findProfilePath('mycustom', { cwd: tmpRepo, home: tmpHome });
assert.ok(found.path, `expected to find mycustom; attempted: ${found.attempted.join(', ')}`);
assert.ok(found.path.startsWith(tmpRepo),
`expected repo-rot win (path under ${tmpRepo}), got: ${found.path}`);
const p = loadProfile('mycustom', { cwd: tmpRepo, home: tmpHome });
assert.equal(p.name, 'mycustom-repo', 'repo profile should win');
assert.equal(p.phase_models.brief, 'sonnet');
} finally {
rmSync(tmpRepo, { recursive: true, force: true });
rmSync(tmpHome, { recursive: true, force: true });
}
});
test('SC #8: missing profile error message includes both attempted paths', () => {
const tmpRepo = mkdtempSync(join(tmpdir(), 'voyage-resolver-empty-'));
const tmpHome = mkdtempSync(join(tmpdir(), 'voyage-resolver-emptyhome-'));
try {
try {
loadProfile('not-a-real-profile', { cwd: tmpRepo, home: tmpHome });
assert.fail('expected throw');
} catch (e) {
assert.equal(e.cause, 'PROFILE_NOT_FOUND');
// Both attempted paths should be in the error message for diagnostic clarity
const msg = e.message;
assert.match(msg, /voyage-profiles\/not-a-real-profile\.yaml/);
assert.match(msg, /\.claude\/voyage-profiles\/not-a-real-profile\.yaml/);
}
} finally {
rmSync(tmpRepo, { recursive: true, force: true });
rmSync(tmpHome, { recursive: true, force: true });
}
});
// SC #9: resolveTrekcontinueProfile inheritance from plan-frontmatter
test('SC #9: resolveTrekcontinueProfile inherits from plan-frontmatter (profile: balanced)', () => {
const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-with-profile.md');
const r = resolveTrekcontinueProfile(planPath, { flags: {} });
assert.equal(r.profile, 'balanced');
assert.equal(r.profile_source, 'inheritance');
});
test('SC #9: resolveTrekcontinueProfile flag overrides plan-frontmatter (advisory)', () => {
const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-with-profile.md');
const advisories = [];
const fakeConsole = { error: (m) => advisories.push(m) };
const r = resolveTrekcontinueProfile(planPath,
{ flags: { '--profile': 'economy' } },
{ console: fakeConsole });
assert.equal(r.profile, 'economy');
assert.equal(r.profile_source, 'flag');
assert.equal(advisories.length, 1, 'expected one advisory message');
assert.match(advisories[0], /balanced.*economy/);
assert.match(advisories[0], /\[voyage\]/);
});
// Backward-compat edge-case: v4.0-style plan WITHOUT profile field
test('Backward-compat: resolveTrekcontinueProfile on v4.0 plan without profile field returns default premium', () => {
const planPath = join(REPO_ROOT, 'tests', 'fixtures', 'plan-without-profile.md');
const r = resolveTrekcontinueProfile(planPath, { flags: {} });
assert.equal(r.profile, 'premium');
assert.equal(r.profile_source, 'default');
});
test('Backward-compat: resolveTrekcontinueProfile with non-existent plan path returns default premium', () => {
const r = resolveTrekcontinueProfile('/tmp/does-not-exist-plan-xyz.md', { flags: {} });
assert.equal(r.profile, 'premium');
assert.equal(r.profile_source, 'default');
});
// validateProfileFile re-export sanity
test('validateProfileFile re-exports validateProfile (locked-interface compat)', () => {
const r = validateProfileFile(join(REPO_ROOT, 'lib', 'profiles', 'economy.yaml'));
assert.equal(r.valid, true);
});

View file

@ -1,41 +0,0 @@
// tests/lib/profile-flag-coverage.test.mjs
// SC #4 (docs side): every command file must document --profile + VOYAGE_PROFILE.
// /trekcontinue.md must additionally describe profile-arv (inheritance) policy.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const COMMANDS_DIR = join(__dirname, '..', '..', 'commands');
const COMMAND_FILES = [
'trekbrief.md',
'trekresearch.md',
'trekplan.md',
'trekexecute.md',
'trekreview.md',
'trekcontinue.md',
];
for (const filename of COMMAND_FILES) {
test(`${filename} documents --profile flag`, () => {
const content = readFileSync(join(COMMANDS_DIR, filename), 'utf-8');
assert.match(content, /--profile/,
`${filename} must contain --profile flag documentation`);
});
test(`${filename} mentions VOYAGE_PROFILE env-var`, () => {
const content = readFileSync(join(COMMANDS_DIR, filename), 'utf-8');
assert.match(content, /VOYAGE_PROFILE/,
`${filename} must mention VOYAGE_PROFILE env-var (resolution order)`);
});
}
test('trekcontinue.md documents inheritance policy (profile arves fra plan-frontmatter)', () => {
const content = readFileSync(join(COMMANDS_DIR, 'trekcontinue.md'), 'utf-8');
assert.match(content, /inheritance/,
'trekcontinue.md must describe profile-arv (inheritance) policy from plan-frontmatter');
});

View file

@ -1,89 +0,0 @@
// tests/lib/profile-resolver.test.mjs
// v5.1.1 SC5 — non-interference cases for resolvePhaseModel().
// Verifies the new highest-priority lookup step (brief.phase_signals[phase].model)
// wins over --profile flag and VOYAGE_PROFILE env; falls through cleanly when
// no brief signal is present.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolvePhaseModel } from '../../lib/profiles/resolver.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, '..', '..');
const FIXTURE = (name) => join(REPO_ROOT, 'tests', 'fixtures', name);
test('resolvePhaseModel — Case 1: brief signal wins over VOYAGE_PROFILE env', () => {
// brief-effort-low.md pins all 4 phases to model: sonnet.
// env says premium (would normally select opus). Brief must win.
const r = resolvePhaseModel('research', FIXTURE('brief-effort-low.md'), [], { VOYAGE_PROFILE: 'premium' });
assert.equal(r.model, 'sonnet', `brief signal should beat env; got ${JSON.stringify(r)}`);
assert.equal(r.source, 'brief-signal');
});
test('resolvePhaseModel — Case 2: brief signal wins over --profile flag', () => {
// brief-effort-high.md pins all 4 phases to model: opus.
// flag says economy (would normally select sonnet). Brief must win.
const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), ['--profile', 'economy'], {});
assert.equal(r.model, 'opus', `brief signal should beat flag; got ${JSON.stringify(r)}`);
assert.equal(r.source, 'brief-signal');
});
test('resolvePhaseModel — Case 3: no phase_signals → fallthrough to --profile flag', () => {
// brief-without-phase-signals fixture lacks phase_signals entirely.
// --profile balanced is set. Should return balanced.phase_models.plan (= opus per yaml).
const r = resolvePhaseModel('plan', FIXTURE('brief-without-phase-signals.md'), ['--profile', 'balanced'], {});
assert.equal(r.model, 'opus', `balanced.plan should be opus; got ${JSON.stringify(r)}`);
assert.equal(r.source, 'flag');
});
test('resolvePhaseModel — Case 4: phase not in PHASE_SIGNAL_PHASES falls through gracefully', () => {
// brief-effort-high.md has signals for the 4 supported phases.
// Asking for 'continue' (not in PHASE_SIGNAL_PHASES) must fall through.
// --profile premium is set, so continue resolves to premium.phase_models.continue (= opus).
const r = resolvePhaseModel('continue', FIXTURE('brief-effort-high.md'), ['--profile', 'premium'], {});
assert.equal(r.model, 'opus', `premium.continue should be opus; got ${JSON.stringify(r)}`);
assert.ok(r.source !== 'brief-signal', 'continue must not resolve via brief-signal');
});
test('resolvePhaseModel — Case 5 (defensive): missing brief file falls through cleanly', () => {
// Non-existent path. Must not throw; must fall through to flag/env/default.
const r = resolvePhaseModel('plan', '/nonexistent/brief.md', ['--profile', 'economy'], {});
assert.equal(r.model, 'sonnet', 'economy.plan should be sonnet on fallthrough');
assert.equal(r.source, 'flag');
});
test('resolvePhaseModel — Case 6 (defensive): null briefPath falls through to default', () => {
// null briefPath, no flag, no env → default = premium.
const r = resolvePhaseModel('plan', null, [], {});
assert.equal(r.model, 'opus', 'premium.plan default = opus');
assert.equal(r.source, 'default');
});
// v5.9 — fable profile composition (brief SC 4) + effort passthrough coherence
test('resolvePhaseModel — --profile fable: all six phases resolve model fable (no brief signal)', () => {
for (const phase of ['brief', 'research', 'plan', 'execute', 'review', 'continue']) {
const r = resolvePhaseModel(phase, null, ['--profile', 'fable'], {});
assert.equal(r.model, 'fable', `fable.${phase} should be fable; got ${JSON.stringify(r)}`);
assert.equal(r.source, 'flag');
}
});
test('resolvePhaseModel — brief signal (opus) beats --profile fable (composition precedence pin)', () => {
// brief-effort-high.md pins execute to model: opus. Brief must beat the fable profile.
const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), ['--profile', 'fable'], {});
assert.equal(r.model, 'opus', `brief signal should beat fable profile; got ${JSON.stringify(r)}`);
assert.equal(r.source, 'brief-signal');
});
test('resolvePhaseModel — composed output carries {effort, model} atomically (v5.9 passthrough)', () => {
// brief-effort-high.md: execute → {effort: high, model: opus}. The composed
// resolver must return BOTH fields from one call — the split-CLI design this
// passthrough replaced could drift effort and model apart.
const r = resolvePhaseModel('execute', FIXTURE('brief-effort-high.md'), [], {});
assert.equal(r.effort, 'high', `effort must pass through; got ${JSON.stringify(r)}`);
assert.equal(r.model, 'opus');
assert.equal(r.source, 'brief-signal');
});

View file

@ -9,8 +9,8 @@
// files (never tracked) and this test. A term is a plain substring, except a
// flag, which must stand as its own token (`--engine`, `--profile`).
//
// When this test fails, remove the mention; add an EXEMPT entry only for a
// line that must keep the term, with the reason.
// Every group always runs. When this test fails, remove the mention; add an
// EXEMPT entry only for a line that must keep the term, with the reason.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
@ -45,13 +45,25 @@ export const GROUPS = {
],
};
// Groups whose removal stage has not run yet. Each stage of R-b empties its
// own entry; REMOVED_SURFACES_ALL=1 checks every group (the red proof).
const PENDING = new Set(['R3']);
// Named exceptions: "path:line" -> reason. Each must still hit (a stale
// exemption fails below).
const EXEMPT = new Map([
['docs/HANDOVER-CONTRACTS.md:81', 'public contract: an older brief\'s key is ignored (decisions 179/191)'],
['docs/proevesett.md:139', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
['docs/proevesett.md:143', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
['docs/proevesett.md:146', 'dated v1/v2 measurement record of the fixed test set, kept unedited'],
['lib/parsers/arg-parser.mjs:16', 'retired flag swallows its value'],
['lib/parsers/arg-parser.mjs:20', 'retired flag swallows its value'],
['lib/parsers/arg-parser.mjs:25', 'retired flag swallows its value'],
['lib/parsers/arg-parser.mjs:30', 'retired flag swallows its value'],
['lib/parsers/arg-parser.mjs:36', 'retired flag swallows its value'],
['lib/parsers/arg-parser.mjs:41', 'retired flag swallows its value'],
['lib/parsers/arg-parser.mjs:46', 'retired flag swallows its value'],
['tests/lib/arg-parser.test.mjs:285', 'retired flag swallows its value'],
['tests/lib/arg-parser.test.mjs:286', 'retired flag swallows its value'],
['tests/validators/brief-validator.test.mjs:200', 'older brief stays valid (decision 191)'],
['tests/validators/brief-validator.test.mjs:203', 'older brief stays valid (decision 191)'],
['tests/validators/brief-validator.test.mjs:205', 'older brief stays valid (decision 191)'],
]);
const matches = (term, line) => (typeof term === 'string' ? line.includes(term) : term.test(line));
@ -108,10 +120,7 @@ test('scope leaves out history, local files and this test', () => {
});
for (const group of Object.keys(GROUPS)) {
test(`${group}: no tracked file names a removed surface`, (t) => {
if (PENDING.has(group) && process.env.REMOVED_SURFACES_ALL !== '1') {
return t.skip(`${group} is not removed yet`);
}
test(`${group}: no tracked file names a removed surface`, () => {
const all = tracked();
assert.ok(all.length > 100, `git ls-files returned ${all.length} files; refusing to pass on an empty scan`);
const hits = scan(ROOT, all, GROUPS[group]).filter(h => !EXEMPT.has(h));

View file

@ -1043,7 +1043,7 @@ test('review 630 r9 / f8087ca4 criteria-runner.mjs:172: a check refused for its
// (2) A brief SC with such a comment is BRIEF_SC_NOT_RUNNABLE, not prose-only.
const brief = (sc) => [
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', 'phase_signals_partial: true', '---', '',
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
'## Success Criteria', '', sc, '',
].join('\n');

View file

@ -1,53 +0,0 @@
// tests/parsers/arg-parser-profile.test.mjs
// SC #4: --profile valued flag MUST be recognized on all 6 voyage commands.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { parseArgs } from '../../lib/parsers/arg-parser.mjs';
const COMMANDS = ['trekbrief', 'trekresearch', 'trekplan', 'trekexecute', 'trekreview', 'trekcontinue'];
for (const cmd of COMMANDS) {
test(`${cmd} — --profile economy parses as valued`, () => {
const r = parseArgs('--profile economy', cmd);
assert.equal(r.flags['--profile'], 'economy', `${cmd} should accept --profile economy`);
assert.equal(r.errors.length, 0, `${cmd} should have no errors`);
assert.equal(r.unknown.length, 0, `${cmd} should not mark --profile as unknown`);
});
}
test('trekplan — --profile without value emits ARG_MISSING_VALUE', () => {
const r = parseArgs('--profile', 'trekplan');
const missing = r.errors.find((e) => e.code === 'ARG_MISSING_VALUE');
assert.ok(missing, 'must surface ARG_MISSING_VALUE');
assert.match(missing.message, /--profile/);
});
test('trekplan — --profile economy --quick combines correctly', () => {
const r = parseArgs('--profile economy --quick', 'trekplan');
assert.equal(r.flags['--profile'], 'economy');
assert.equal(r.flags['--quick'], true);
});
test('trekplan — --profile economy --gates open: --gates is unknown (parsed inline by command prose, not in FLAG_SCHEMA)', () => {
// Edge case from plan.md Step 2 (per plan-critic minor): --gates is intentionally
// NOT in FLAG_SCHEMA; commands parse it inline. Verify --profile still parses cleanly
// and --gates ends up in unknown[] / positional[] rather than colliding with --profile.
const r = parseArgs('--profile economy --gates open', 'trekplan');
assert.equal(r.flags['--profile'], 'economy');
// --gates is unknown to FLAG_SCHEMA; it lands in unknown[] and 'open' becomes positional
assert.ok(r.unknown.includes('--gates'), `expected --gates in unknown, got: ${JSON.stringify(r.unknown)}`);
assert.ok(r.positional.includes('open'), `expected 'open' as positional, got: ${JSON.stringify(r.positional)}`);
});
test('trekexecute — --profile balanced --project /tmp/p combines correctly', () => {
const r = parseArgs('--profile balanced --project /tmp/p', 'trekexecute');
assert.equal(r.flags['--profile'], 'balanced');
assert.equal(r.flags['--project'], '/tmp/p');
});
test('trekcontinue — --profile premium parses without --project', () => {
// trekcontinue had empty valued[] before v4.1 — sanity check the array is now extended
const r = parseArgs('--profile premium', 'trekcontinue');
assert.equal(r.flags['--profile'], 'premium');
});

View file

@ -386,14 +386,12 @@ test('review-coordinator Pass 3 — only an engine finding is marked, and the on
assert.match(p3, /verdict gate refuses \(exit 2\) every other marked or lowered shape/);
});
test('review-coordinator — quick mode and high-effort normalization each still apply the brief-conflict rule', () => {
test('review-coordinator — quick mode still applies the brief-conflict rule', () => {
const coord = readDoc('agents', 'review-coordinator.md');
const pass3 = coord.indexOf('### Pass 3');
assert.ok(pass3 >= 0, 'Pass 3 heading');
const quick = cut(coord.slice(pass3), 'In `quick` mode, skip this pass entirely.', '\n\n', 'quick-mode paragraph');
assert.match(quick, /brief-conflict rule[^.]*still applies/, 'the quick-mode paragraph says the rule still applies');
const high = cut(coord, '**High-effort normalization (v5.1.1):**', '\n### ', 'high-effort paragraph');
assert.match(high, /brief-conflict rule[^.]*still applies/, 'the high-effort paragraph says the rule still applies');
});
test('review-coordinator Pass 2 — the trim rule judges a lowerable brief conflict as MINOR, inside its own sentence', () => {

View file

@ -1,106 +0,0 @@
---
type: trekplan-jaccard-calibration
plan_version: "1.7"
created: 2026-05-09
status: parked-synthetic
threshold: 0.55
threshold_basis: "research/02 conservative starting value (arXiv:2412.12148)"
empirical_runs: 0
synthetic_runs: 4
ramp_target: v4.2
---
# Cross-tier Jaccard calibration — voyage v4.1
## Status: PARKED-SYNTHETIC
> **Profile linkage (S34/V30).** Because this calibration is still
> parked-synthetic, `lib/profiles/economy.yaml` carries `experimental: true`
> and every profile doc flags the `economy` row experimental. When the
> empirical calibration in *How to replace* below lands (`status: empirical`),
> drop `experimental: true` in the **same change** — the linkage is guarded by
> `tests/lib/doc-consistency.test.mjs` ("economy experimental flag agrees with
> parked-synthetic calibration status").
Empirical Jaccard calibration was deferred from v4.1 because the four
required `/trekplan` invocations cost an estimated $60-120 of LLM-budget
that was not authorized for the v4.1-execute-4b session. Per Step 17
escalate-handler, this file documents:
1. The synthetic placeholder fixtures used by Step 18's smoke-test, and
2. The pinned conservative threshold (`0.55`) from research/02.
## Threshold rationale
`threshold: 0.55` is pinned per research/02 (Recommendation #5):
> "There is no universal Jaccard threshold for cross-model plan
> agreement. arXiv:2412.12148 reports 0.45–0.65 for n=10 task-pair
> samples on coding tasks. We recommend a *conservative starting value
> of 0.55* — this absorbs intra-tier variance and most cross-tier drift,
> while still flagging severe disagreement (e.g. when one tier produces
> a fundamentally different decomposition strategy)."
The 0.55 floor is enforced by `tests/integration/profile-jaccard-smoke.test.mjs`
(Step 18) as a module-local constant `CROSS_TIER_JACCARD_FLOOR`. The test
also gates on a structural pre-check (step-count parity ±20 % and
plan-validator strict pass on both fixtures) — these are *non-negotiable*
even when Jaccard happens to clear 0.55.
## Synthetic fixture pairs
The four parked-synthetic plan-runs in `tests/synthetic/`:
| run-A | run-B | jaccard (synthetic, normalized) |
|-------|-------|---------------------------------|
| profile-plan-run-economy-1.md | profile-plan-run-premium-1.md | 0.707 |
| profile-plan-run-economy-1.md | profile-plan-run-premium-2.md | 0.707 |
| profile-plan-run-economy-2.md | profile-plan-run-premium-1.md | 0.750 |
| profile-plan-run-economy-2.md | profile-plan-run-premium-2.md | 0.750 |
Intra-tier (sanity): economy-1 × economy-2 = 0.935;
premium-1 × premium-2 = 0.905. Intra-tier > cross-tier confirms the
fixtures discriminate.
Min observed cross-tier (synthetic): 0.707. Min minus 0.05 buffer = 0.657.
We pin `threshold: 0.55` — the lower of (research/02 conservative value)
vs (min - 0.05 buffer). This is the same rule plan.md Step 17 prescribes:
`floor(min(jaccard_values), 2) - 0.05` or `0.55`, whichever is lower.
Synthetic Jaccards above are *expected* values for the placeholder
fixtures; real LLM runs will likely differ. The 0.55 pin remains valid
across that uncertainty.
## When to replace these fixtures
Trigger empirical calibration when **any** of the following holds:
1. Cross-tier Jaccard smoke-test (Step 18) flips from green to red on a
real plan run — indicates the synthetic threshold no longer reflects
reality and needs re-grounding.
2. v4.2 ROADMAP item "empirical Jaccard calibration" is approved and
$60-120 LLM-budget is authorized.
3. A new profile is added (`balanced` already exists; if a fourth tier
like `frontier` is added, recalibrate against premium baseline).
## How to replace
1. Run `/trekplan --profile economy --brief examples/01-add-verbose-flag/brief.md`
twice. Save each plan's `steps:` frontmatter to
`profile-plan-run-economy-{1,2}.md` (overwrite synthetic content).
Update `status: parked-synthetic` → `status: empirical`.
2. Same for `--profile premium`, twice.
3. Recompute the four cross-tier Jaccards. Update the table above.
4. Repin threshold: `min(jaccard_values, 2) - 0.05` or 0.55, whichever
lower. (Tighter is fine; do not loosen below 0.55.)
5. Run `tests/integration/profile-jaccard-smoke.test.mjs` — must pass.
6. Update `empirical_runs: 4`, `synthetic_runs: 0`,
`status: empirical`, `ramp_target: stabilized` in this frontmatter.
## Fallback strategy in the meantime
Until real calibration is run, operators are advised to use the
`balanced` profile (sonnet for most phases, opus for plan + review) as
the lowest-risk choice. `balanced` was selected as the v4.1 default in
`commands/trekplan.md` Phase 5.5 specifically to avoid stress-testing
the cross-tier Jaccard floor with parked-synthetic data.

View file

@ -1,83 +0,0 @@
---
type: trekplan-synthetic
plan_version: "1.7"
created: 2026-05-09
task: "Add --verbose flag to CLI"
slug: verbose-flag
run_id: economy-1
profile_used: economy
status: parked-synthetic
steps:
- "Add config entry for verbose flag in package.json"
- "Define types for verbose mode in types.ts"
- "Update parseArgs to recognize --verbose flag"
- "Pass verbose context through main entry point"
- "Add log level enum (silent, normal, verbose)"
- "Wire log level into logger module"
- "Replace console.log with logger.info in handler.ts"
- "Add tests for parseArgs --verbose recognition"
- "Add tests for log level enum mapping"
- "Update README with --verbose flag documentation"
- "Add CHANGELOG entry for verbose flag"
- "Bump package.json minor version"
- "Add lint rule blocking direct console usage"
- "Run lint and fix new violations"
- "Add CLI integration test for --verbose end-to-end"
- "Add fixture file for verbose log capture"
- "Document verbose output format in docs/cli.md"
- "Add jsdoc for new logger API"
- "Verify all existing tests pass with verbose disabled"
- "Add backward-compat test for legacy quiet behavior"
- "Update help text to list --verbose flag"
- "Add usage example to docs/quickstart.md"
- "Verify CI matrix runs on Node 18 and 20"
- "Update .gitignore for verbose log dump files"
- "Add cleanup logic for stale verbose logs"
- "Verify exit code on verbose mode error"
- "Add stderr routing for warnings in verbose"
- "Update troubleshooting guide with verbose flag"
- "Verify version sync across all docs"
- "Document verbose changes in release notes"
---
# Synthetic plan run economy-1 — Add --verbose flag to CLI (PARKED)
This fixture is a SYNTHETIC PLACEHOLDER for empirical Jaccard calibration
that requires live LLM-budget ($60-120 for 4 plan-runs). Marked
`status: parked-synthetic` per the Step 17 escalate-handler.
## Why parked
Per NEXT-SESSION-PROMPT.local.md fallback: "Hvis Step 17 LLM-budget
blokkerer: dokumentér `economy`-Plan som `parked` i kalibrasjons-fil og
fortsett med Step 18-19 ved bruk av `balanced` som lavterskel-profil."
The session running v4.1-execute-4b did not have authorization for live
LLM invocation against `/trekplan --profile economy --brief
examples/01-add-verbose-flag/brief.md`. Synthetic fixtures here represent
the *shape* of what such a run would produce — a near-subset of the
`premium` plan's steps (covering the same task surface) but with ~25 %
fewer sub-verification entries (no edge-case-collision step, no security
audit step, no PII test, no benchmark, etc).
## How this fixture is consumed
`tests/integration/profile-jaccard-smoke.test.mjs` (Step 18) reads the
`steps` array from the frontmatter and pairs it with the corresponding
`premium` fixtures to compute cross-tier Jaccard.
When real LLM budget is approved (deferred to v4.2), regenerate this
fixture by running the actual command and overwriting the frontmatter
`steps` array. Update `status: parked-synthetic` → `status: empirical`.
## Step-shape rationale
Economy profile uses sonnet for all phases (per
`lib/profiles/economy.yaml`). Empirical observation from research/02:
sonnet plans tend toward fewer verification entries, fewer edge-case
branches, and slightly less granular decomposition than opus plans. The
30 entries here represent the typical "skip the marginal sub-verification"
behaviour while keeping wording aligned with what an opus run would
produce on the same brief — modeling the realistic expectation that
profile choice changes *what* steps get included more than *how* the
included ones are phrased.

View file

@ -1,63 +0,0 @@
---
type: trekplan-synthetic
plan_version: "1.7"
created: 2026-05-09
task: "Add --verbose flag to CLI"
slug: verbose-flag
run_id: economy-2
profile_used: economy
status: parked-synthetic
steps:
- "Add config entry for verbose flag in package.json"
- "Define types for verbose mode in types.ts"
- "Update parseArgs to recognize --verbose flag"
- "Pass verbose context through main entry point"
- "Add log level enum (silent, normal, verbose)"
- "Wire log level into logger module"
- "Replace console.log with logger.info in handler.ts"
- "Add tests for parseArgs --verbose recognition"
- "Add tests for log level enum mapping"
- "Update README with --verbose flag documentation"
- "Add CHANGELOG entry for verbose flag"
- "Bump package.json minor version"
- "Add lint rule blocking direct console usage"
- "Run lint and fix new violations"
- "Add CLI integration test for --verbose end-to-end"
- "Add fixture file for verbose log capture"
- "Document verbose output format in docs/cli.md"
- "Add jsdoc for new logger API"
- "Verify all existing tests pass with verbose disabled"
- "Add backward-compat test for legacy quiet behavior"
- "Update help text to list --verbose flag"
- "Add usage example to docs/quickstart.md"
- "Verify CI matrix runs on Node 18 and 20"
- "Update .gitignore for verbose log dump files"
- "Add cleanup logic for stale verbose logs"
- "Verify exit code on verbose mode error"
- "Add stderr routing for warnings in verbose"
- "Update troubleshooting guide with verbose flag"
- "Verify version sync across all docs"
- "Add timestamp prefix in verbose log lines"
---
# Synthetic plan run economy-2 — Add --verbose flag to CLI (PARKED)
Companion fixture to `profile-plan-run-economy-1.md`. Same `economy`
profile, simulated as a second run of the same brief, with the final
step replaced (release notes → timestamp prefix) to model intra-tier
variance.
See `profile-plan-run-economy-1.md` for full parked-synthetic rationale.
## Intra-tier Jaccard
Economy-1 vs economy-2 share 29/30 step titles (final step differs);
union = 31. Jaccard = 29/31 ≈ 0.935 — well above any reasonable
cross-tier floor. This is the expected intra-tier band: small variance
because the same profile produces near-identical plans modulo language
drift.
When real LLM-budget runs replace this synthetic, the empirical
intra-tier Jaccard is expected to land in the 0.85–0.95 band per
research/02. Cross-tier (economy vs premium) is the discriminating
measurement and is documented in `profile-jaccard-calibration.md`.

View file

@ -1,80 +0,0 @@
---
type: trekplan-synthetic
plan_version: "1.7"
created: 2026-05-09
task: "Add --verbose flag to CLI"
slug: verbose-flag
run_id: premium-1
profile_used: premium
status: parked-synthetic
steps:
- "Add config entry for verbose flag in package.json"
- "Define types for verbose mode in types.ts"
- "Update parseArgs to recognize --verbose flag"
- "Pass verbose context through main entry point"
- "Add log level enum (silent, normal, verbose)"
- "Wire log level into logger module"
- "Replace console.log with logger.info in handler.ts"
- "Add tests for parseArgs --verbose recognition"
- "Add tests for log level enum mapping"
- "Update README with --verbose flag documentation"
- "Add CHANGELOG entry for verbose flag"
- "Bump package.json minor version"
- "Add lint rule blocking direct console usage"
- "Run lint and fix new violations"
- "Add CLI integration test for --verbose end-to-end"
- "Add fixture file for verbose log capture"
- "Document verbose output format in docs/cli.md"
- "Add jsdoc for new logger API"
- "Verify all existing tests pass with verbose disabled"
- "Add backward-compat test for legacy quiet behavior"
- "Add edge-case test for repeated --verbose flags"
- "Add edge-case test for --verbose with --silent collision"
- "Update help text to list --verbose flag"
- "Add usage example to docs/quickstart.md"
- "Verify CI matrix runs on Node 18 and 20"
- "Add npm script for verbose mode debugging"
- "Run security audit on logger dependency tree"
- "Verify no PII leaks in verbose log output"
- "Add manual test checklist to CONTRIBUTING.md"
- "Update .gitignore for verbose log dump files"
- "Add cleanup logic for stale verbose logs"
- "Add unit test for cleanup logic"
- "Verify exit code on verbose mode error"
- "Add stderr routing for warnings in verbose"
- "Add timestamp prefix in verbose log lines"
- "Add test for timestamp format"
- "Update troubleshooting guide with verbose flag"
- "Verify version sync across all docs"
- "Add benchmark for verbose log emission cost"
- "Document benchmark methodology in PERF.md"
---
# Synthetic plan run premium-1 — Add --verbose flag to CLI (PARKED)
This fixture is a SYNTHETIC PLACEHOLDER for empirical Jaccard calibration
that requires live LLM-budget ($60-120 for 4 plan-runs). Marked
`status: parked-synthetic` per the Step 17 escalate-handler.
## Why parked
Same rationale as `profile-plan-run-economy-1.md`. The session running
v4.1-execute-4b did not have authorization for live LLM invocation. This
fixture mirrors the existing baseline `plan-run-A.md` (40 steps, opus
granularity) since premium profile uses opus for `plan` and `review`
phases per `lib/profiles/premium.yaml`.
## Step-shape rationale
Premium profile uses opus for plan + review phases (per
`lib/profiles/premium.yaml`). Empirical observation from research/02:
opus plans tend toward finer-grained steps, more explicit verification
entries, and richer edge-case decomposition than sonnet plans. The 40
entries here capture the level of detail typical of an opus run.
## Cross-tier Jaccard pairing
Paired with `profile-plan-run-economy-1.md` and `-economy-2.md` in
`tests/integration/profile-jaccard-smoke.test.mjs` (Step 18). Expected
cross-tier Jaccard for the parked-synthetic run-pair is documented in
`profile-jaccard-calibration.md`.

View file

@ -1,73 +0,0 @@
---
type: trekplan-synthetic
plan_version: "1.7"
created: 2026-05-09
task: "Add --verbose flag to CLI"
slug: verbose-flag
run_id: premium-2
profile_used: premium
status: parked-synthetic
steps:
- "Add config entry for verbose flag in package.json"
- "Define types for verbose mode in types.ts"
- "Update parseArgs to recognize --verbose flag"
- "Pass verbose context through main entry point"
- "Add log level enum (silent, normal, verbose)"
- "Wire log level into logger module"
- "Replace console.log with logger.info in handler.ts"
- "Add tests for parseArgs --verbose recognition"
- "Add tests for log level enum mapping"
- "Update README with --verbose flag documentation"
- "Add CHANGELOG entry for verbose flag"
- "Bump package.json minor version"
- "Add lint rule blocking direct console usage"
- "Run lint and fix new violations"
- "Add CLI integration test for --verbose end-to-end"
- "Add fixture file for verbose log capture"
- "Document verbose output format in docs/cli.md"
- "Add jsdoc for new logger API"
- "Verify all existing tests pass with verbose disabled"
- "Add backward-compat test for legacy quiet behavior"
- "Add edge-case test for repeated --verbose flags"
- "Add edge-case test for --verbose with --silent collision"
- "Update help text to list --verbose flag"
- "Add usage example to docs/quickstart.md"
- "Verify CI matrix runs on Node 18 and 20"
- "Add npm script for verbose mode debugging"
- "Run security audit on logger dependency tree"
- "Verify no PII leaks in verbose log output"
- "Add manual test checklist to CONTRIBUTING.md"
- "Update .gitignore for verbose log dump files"
- "Add cleanup logic for stale verbose logs"
- "Add unit test for cleanup logic"
- "Verify exit code on verbose mode error"
- "Add stderr routing for warnings in verbose"
- "Add timestamp prefix in verbose log lines"
- "Add test for timestamp format"
- "Update troubleshooting guide with verbose flag"
- "Verify version sync across all docs"
- "Add benchmark for verbose log capture overhead"
- "Document overhead methodology in PERF.md"
---
# Synthetic plan run premium-2 — Add --verbose flag to CLI (PARKED)
Companion to `profile-plan-run-premium-1.md`. Same `premium` profile,
simulated as a second run with two terminal steps replaced
(emission cost / benchmark methodology → capture overhead / overhead
methodology) to model intra-tier variance.
## Intra-tier Jaccard
Premium-1 vs premium-2 share 38/40 step titles; union = 42.
Jaccard = 38/42 ≈ 0.905 — matches the existing baseline plan-run-A vs
plan-run-B floor (≥ 0.833 in plan-determinism.test.mjs).
## Cross-tier Jaccard rationale
Pairing premium fixtures (40 steps) against economy fixtures (30 steps)
yields ~30 shared titles (after string-normalisering), with union ~40.
Conservative cross-tier Jaccard ≈ 30/40 = 0.75 in this synthetic — but
the calibration file pins a *more conservative* floor (0.55) per
research/02 to absorb empirical variance once real runs replace these
fixtures. See `profile-jaccard-calibration.md` for threshold derivation.

View file

@ -27,7 +27,6 @@ project_dir: .claude/projects/2026-10-06-dark-mode-toggle/
research_topics: 0
research_status: complete
framing: new-direction
phase_signals_partial: true
${extra}---
# Task

View file

@ -69,7 +69,6 @@ auto_research: false
interview_turns: 5
source: interview
framing: new-direction
phase_signals_partial: true
---
# Task: JWT auth
@ -116,28 +115,7 @@ Find what was missed.
- All BLOCKER findings get a fix-plan.
`;
// signals blocks
const GOOD_SIGNALS = `phase_signals:
- phase: plan
effort: high
model: opus
`;
const BAD_PHASE_SIGNALS = `phase_signals:
- phase: nonsense
effort: high
`;
const BAD_EFFORT_SIGNALS = `phase_signals:
- phase: plan
effort: turbo
`;
const BAD_MODEL_SIGNALS = `phase_signals:
- phase: plan
effort: high
model: gpt4
`;
// convenience builders
const v21 = (s) => s.replace('brief_version: "2.0"', 'brief_version: "2.1"');
const withLine = (s, line) => s.replace('source: interview\n', `source: interview\n${line}`);
// ---- the canonical in-scope set + a class-balanced pair for each -------------
@ -151,12 +129,6 @@ const IN_SCOPE_CODES = [
'BRIEF_MISSING_SECTION',
'BRIEF_INVALID_FRAMING',
'BRIEF_MISSING_FRAMING',
'BRIEF_V51_MISSING_SIGNALS',
'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE',
'BRIEF_INVALID_PHASE_SIGNALS',
'BRIEF_INVALID_PHASE_SIGNAL_PHASE',
'BRIEF_INVALID_EFFORT',
'BRIEF_INVALID_MODEL',
];
const CASES = {
@ -192,30 +164,6 @@ const CASES = {
fail: GOOD_BRIEF_22.replace('framing: new-direction\n', ''),
pass: GOOD_BRIEF_22,
},
BRIEF_V51_MISSING_SIGNALS: {
fail: v21(GOOD_BRIEF),
pass: GOOD_BRIEF, // 2.0 — gate inactive
},
BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE: {
fail: withLine(v21(GOOD_BRIEF), `phase_signals_partial: true\n${GOOD_SIGNALS}`),
pass: withLine(v21(GOOD_BRIEF), 'phase_signals_partial: true\n'),
},
BRIEF_INVALID_PHASE_SIGNALS: {
fail: withLine(GOOD_BRIEF, 'phase_signals: notalist\n'),
pass: withLine(v21(GOOD_BRIEF), GOOD_SIGNALS),
},
BRIEF_INVALID_PHASE_SIGNAL_PHASE: {
fail: withLine(v21(GOOD_BRIEF), BAD_PHASE_SIGNALS),
pass: withLine(v21(GOOD_BRIEF), GOOD_SIGNALS),
},
BRIEF_INVALID_EFFORT: {
fail: withLine(v21(GOOD_BRIEF), BAD_EFFORT_SIGNALS),
pass: withLine(v21(GOOD_BRIEF), GOOD_SIGNALS),
},
BRIEF_INVALID_MODEL: {
fail: withLine(v21(GOOD_BRIEF), BAD_MODEL_SIGNALS),
pass: withLine(v21(GOOD_BRIEF), GOOD_SIGNALS),
},
};
test('gate coverage — CASES keys exactly match IN_SCOPE_CODES (no silent gap)', () => {

View file

@ -157,127 +157,10 @@ test('validateBrief — wrong-type error message includes accepted set', () => {
assert.ok(/trekreview/.test(wrongType.message));
});
// --- v5.1 — phase_signals additive field + sequencing gate ---
const SIGNALS_BLOCK = `phase_signals:
- phase: research
effort: standard
- phase: plan
effort: high
model: opus
- phase: execute
effort: low
model: sonnet
- phase: review
effort: standard
`;
test('validateBrief — v5.1 well-formed phase_signals accepted', () => {
const t = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
.replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`);
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, true, JSON.stringify(r.errors));
});
test('validateBrief — pre-v5.1 brief without phase_signals accepted (backward-compat)', () => {
const r = validateBriefContent(GOOD_BRIEF, { strict: true });
assert.equal(r.valid, true, JSON.stringify(r.errors));
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
});
test('validateBrief — v5.1+ brief missing phase_signals + partial emits BRIEF_V51_MISSING_SIGNALS', () => {
const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"');
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, false);
assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
});
test('validateBrief — v5.1+ brief with phase_signals_partial: true accepted', () => {
const t = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
.replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n');
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, true, JSON.stringify(r.errors));
});
test('validateBrief — phase_signals + phase_signals_partial both set rejected (mutually exclusive)', () => {
const t = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
.replace('source: interview\n', `source: interview\nphase_signals_partial: true\n${SIGNALS_BLOCK}`);
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, false);
assert.ok(r.errors.find(e => e.code === 'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE'));
});
test('validateBrief — phase_signals with unknown phase rejected', () => {
const BAD_SIGNALS = `phase_signals:
- phase: nonsense
effort: standard
`;
const t = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
.replace('source: interview\n', `source: interview\n${BAD_SIGNALS}`);
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, false);
assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_PHASE_SIGNAL_PHASE'));
});
// --- v5.1.1 regression: YAML-number bypass closed ---
// Findings 3c834097 + df1435a2: v5.1.0 shipped with an unquoted `brief_version: 2.1`
// template. parseScalar coerces unquoted "2.1" to Number 2.1, and the original gate
// guarded `typeof === 'string'`, silently bypassing the sequencing check. v5.1.1
// coerces via String() so both shapes trigger the gate.
test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 without signals triggers gate', () => {
const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: 2.1');
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, false);
assert.ok(
r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'),
`gate must fire for unquoted brief_version: 2.1 (YAML Number); errors=${JSON.stringify(r.errors)}`,
);
});
test('validateBrief — v5.1.1: QUOTED brief_version "2.1" without signals triggers gate (regression guard)', () => {
const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"');
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, false);
assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
});
test('validateBrief — v5.1.1: UNQUOTED brief_version 2.1 WITH phase_signals is valid (positive case)', () => {
const t = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: 2.1')
.replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`);
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, true, JSON.stringify(r.errors));
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
});
// --- v5.9 — fable model tier (BASE_ALLOWED_MODELS widened to three values) ---
test('validateBrief — v5.9: fable phase_signals fixture accepted (no BRIEF_INVALID_MODEL)', () => {
const t = readFileSync(new URL('../fixtures/brief-effort-fable.md', import.meta.url), 'utf-8');
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, true, JSON.stringify(r.errors));
assert.ok(!r.errors.find(e => e.code === 'BRIEF_INVALID_MODEL'));
});
test('validateBrief — v5.9: unknown model gpt5 in phase_signals rejected with BRIEF_INVALID_MODEL', () => {
const t = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
.replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK.replace('model: opus', 'model: gpt5')}`);
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, false);
assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_MODEL'),
`expected BRIEF_INVALID_MODEL for gpt5, got: ${JSON.stringify(r.errors)}`);
});
// --- v5.5 — framing enforcement + obligatory TL;DR (gated at brief_version ≥ 2.2) ---
// Operator decision (S6, option A1): framing + TL;DR are hard BLOCKERs for briefs
// declaring brief_version ≥ 2.2; existing 2.0/2.1 briefs stay valid (forward-compat,
// mirroring the phase_signals ≥ 2.1 precedent). The framing ENUM check fires on any
// declaring brief_version ≥ 2.2; existing 2.0/2.1 briefs stay valid (forward-compat).
// The framing ENUM check fires on any
// version when the field is present but malformed.
const GOOD_BRIEF_22 = `---
@ -293,7 +176,6 @@ auto_research: false
interview_turns: 5
source: interview
framing: new-direction
phase_signals_partial: true
---
# Task: JWT auth
@ -315,6 +197,18 @@ What success looks like.
- All tests pass.
`;
// An older brief may still carry a phase_signals block (removed in cleanup R-b,
// decision 191): the key is ignored, with no error and no warning.
test('validateBrief — a 2.2 brief that still carries phase_signals validates with no error or warning about it', () => {
const t = GOOD_BRIEF_22.replace('framing: new-direction\n',
'framing: new-direction\nphase_signals:\n - phase: plan\n effort: high\n - phase: brief\n effort: xhigh\n');
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, true, JSON.stringify(r.errors));
assert.deepEqual(r.errors, []);
assert.deepEqual(r.warnings.filter(w => /signal/i.test(w.code + ' ' + w.message)), []);
});
test('validateBrief — v5.5 well-formed 2.2 brief (framing + TL;DR) accepted', () => {
const r = validateBriefContent(GOOD_BRIEF_22, { strict: true });
assert.equal(r.valid, true, JSON.stringify(r.errors));
@ -367,8 +261,7 @@ test('validateBrief — v5.5 TL;DR exceeding 5 lines emits BRIEF_TLDR_TOO_LONG w
test('validateBrief — v5.5 backward-compat: 2.1 brief without framing/TL;DR stays valid', () => {
const t = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
.replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n');
.replace('brief_version: "2.0"', 'brief_version: "2.1"');
const r = validateBriefContent(t, { strict: true });
assert.equal(r.valid, true, JSON.stringify(r.errors));
assert.ok(!r.errors.find(e => e.code === 'BRIEF_MISSING_FRAMING'));
@ -385,8 +278,7 @@ test('validateBrief — v5.5 trekreview brief not subject to framing/TL;DR gate'
// S18 — opt-in --min-brief-version gate. Warns (never blocks) when a brief
// declares a version below the caller's floor, sidestepping framing enforcement.
const BRIEF_21 = GOOD_BRIEF
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
.replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n');
.replace('brief_version: "2.0"', 'brief_version: "2.1"');
test('validateBrief — S18 min-version: older brief below floor warns but does not block', () => {
const r = validateBriefContent(BRIEF_21, { strict: true, minBriefVersion: '2.2' });
@ -457,7 +349,7 @@ const codes = (list) => list.map((e) => e.code);
function briefWith(sections) {
return [
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', 'phase_signals_partial: true', '---', '',
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
...sections,
].join('\n');

View file

@ -42,7 +42,6 @@ auto_research: false
interview_turns: 4
source: interview
framing: new-direction
phase_signals_partial: true
---
# Task: Dark-mode toggle

View file

@ -1,68 +0,0 @@
// tests/validators/plan-validator-profile-drift.test.mjs
// SC #20 — MANIFEST_PROFILE_DRIFT warning per brief Assumptions block 7.
// In strict mode, plan-validator must emit a warning (NOT an error) when a
// step manifest's profile_used differs from the plan's frontmatter profile.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { validatePlanContent } from '../../lib/validators/plan-validator.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..', '..');
function loadFixture(name) {
return readFileSync(resolve(ROOT, 'tests/fixtures', name), 'utf-8');
}
const DRIFT_FIXTURE = loadFixture('plan-profile-drift.md');
test('drift detected in strict mode — emits MANIFEST_PROFILE_DRIFT warning, not error', () => {
const r = validatePlanContent(DRIFT_FIXTURE, { strict: true });
assert.equal(r.valid, true, `plan must remain valid; errors: ${JSON.stringify(r.errors)}`);
const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT');
assert.equal(drift.length, 1, `expected 1 drift warning, got ${drift.length}: ${JSON.stringify(drift)}`);
assert.match(drift[0].message, /step 2/, `warning message must reference step 2: ${drift[0].message}`);
assert.match(drift[0].message, /premium/, 'warning must include offending profile_used value');
assert.match(drift[0].message, /economy/, 'warning must include plan-level profile value');
});
test('drift NOT detected in soft mode — strict gate honored', () => {
const r = validatePlanContent(DRIFT_FIXTURE, { strict: false });
const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT');
assert.equal(
drift.length,
0,
'MANIFEST_PROFILE_DRIFT must only emit in strict mode (per brief assumption 7)',
);
});
test('matching profile — no drift warning emitted', () => {
// Same fixture body, but rewrite step 2 profile_used to match plan profile.
// Use /g to catch both the doc-comment mention and the actual manifest entry.
const matching = DRIFT_FIXTURE.replace(/profile_used: premium/g, 'profile_used: economy');
const r = validatePlanContent(matching, { strict: true });
assert.equal(r.valid, true);
const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT');
assert.equal(
drift.length,
0,
`no drift expected when all step profile_used match plan profile; got ${JSON.stringify(drift)}`,
);
});
test('plan without frontmatter profile — no drift warnings emitted', () => {
// If plan-level profile is absent, step-level profile_used can be anything
// without triggering drift (drift is only meaningful relative to a baseline).
const noProfile = DRIFT_FIXTURE.replace(/profile: economy\n/, '');
const r = validatePlanContent(noProfile, { strict: true });
const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT');
assert.equal(
drift.length,
0,
'no plan-level profile means no baseline; drift detection must be silent',
);
});

View file

@ -1,276 +0,0 @@
// tests/validators/profile-validator.test.mjs
// SC #1, #2, #3: profile-validator validates lib/profiles/{economy,balanced,premium,fable}.yaml
// (innebygde profiler) plus rejects invalid models and invalid enum types.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import {
validateProfile,
validateProfileContent,
PROFILE_REQUIRED_FIELDS,
PROFILE_REQUIRED_PHASES,
BASE_ALLOWED_MODELS,
} from '../../lib/validators/profile-validator.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, '..', '..');
// SC #1: alle 4 innebygde profiler grønne
for (const profileName of ['economy', 'balanced', 'premium', 'fable']) {
test(`SC #1: lib/profiles/${profileName}.yaml validates clean`, () => {
const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', `${profileName}.yaml`));
assert.equal(r.valid, true,
`expected valid, got errors: ${JSON.stringify(r.errors, null, 2)}`);
assert.equal(r.errors.length, 0);
// Spot-check: all 6 phases present
const phases = r.parsed.frontmatter.phase_models.map(p => p.phase);
for (const required of PROFILE_REQUIRED_PHASES) {
assert.ok(phases.includes(required), `${profileName} missing phase: ${required}`);
}
});
}
// SC #2: PROFILE_INVALID_MODEL fired when phase_models[N].model not in allowlist
test('SC #2: profile-invalid-model.yaml rejected with PROFILE_INVALID_MODEL at phase_models[2].model', () => {
const r = validateProfile(join(REPO_ROOT, 'tests', 'fixtures', 'profile-invalid-model.yaml'));
assert.equal(r.valid, false);
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_MODEL');
assert.ok(found, `expected PROFILE_INVALID_MODEL, got: ${JSON.stringify(r.errors)}`);
assert.equal(found.location, 'phase_models[2].model',
`expected location phase_models[2].model, got ${found.location}`);
assert.match(found.message, /gpt-4/);
});
// SC #3: PROFILE_INVALID_ENUM for wrong-type values
test('SC #3: profile-invalid-enum.yaml rejected with PROFILE_INVALID_ENUM (external_research_enabled is string)', () => {
const r = validateProfile(join(REPO_ROOT, 'tests', 'fixtures', 'profile-invalid-enum.yaml'));
assert.equal(r.valid, false);
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_ENUM' && /external_research_enabled/.test(e.message));
assert.ok(found, `expected PROFILE_INVALID_ENUM for external_research_enabled, got: ${JSON.stringify(r.errors)}`);
assert.match(found.message, /boolean/);
});
// VOYAGE_ALLOW_HAIKU env-var allows haiku model
test('VOYAGE_ALLOW_HAIKU=1 allows haiku in phase_models', () => {
const haikuProfile = `---
profile_version: "1.0"
name: haiku-allowed
phase_models:
- phase: brief
model: haiku
- phase: research
model: sonnet
- phase: plan
model: opus
- phase: execute
model: sonnet
- phase: review
model: opus
- phase: continue
model: sonnet
parallel_agents_min: 2
parallel_agents_max: 4
external_research_enabled: false
brief_reviewer_iter_cap: 1
---
`;
// Default env: haiku rejected
const denied = validateProfileContent(haikuProfile, { env: { /* no VOYAGE_ALLOW_HAIKU */ } });
assert.equal(denied.valid, false);
const haikuErr = denied.errors.find(e => e.code === 'PROFILE_INVALID_MODEL' && /haiku/.test(e.message));
assert.ok(haikuErr, `expected haiku rejection: ${JSON.stringify(denied.errors)}`);
assert.match(haikuErr.message, /VOYAGE_ALLOW_HAIKU/);
// With opt-in: haiku accepted
const allowed = validateProfileContent(haikuProfile, { env: { VOYAGE_ALLOW_HAIKU: '1' } });
assert.equal(allowed.valid, true,
`expected valid with VOYAGE_ALLOW_HAIKU=1, got: ${JSON.stringify(allowed.errors)}`);
});
// Fable tier (v5.9): fable accepted under DEFAULT env (no opt-in flag),
// unknown models still rejected — the allowlist gate must demonstrably fire.
test('fable accepted in phase_models under default env (no env flag)', () => {
const fableProfile = `---
profile_version: "1.0"
name: fable-inline
phase_models:
- phase: brief
model: fable
- phase: research
model: fable
- phase: plan
model: fable
- phase: execute
model: fable
- phase: review
model: fable
- phase: continue
model: fable
parallel_agents_min: 6
parallel_agents_max: 8
external_research_enabled: true
brief_reviewer_iter_cap: 3
---
`;
const r = validateProfileContent(fableProfile, { env: { /* default: no flags */ } });
assert.equal(r.valid, true,
`expected fable accepted under default env, got: ${JSON.stringify(r.errors)}`);
assert.equal(r.errors.length, 0);
});
test('unknown model gpt5 rejected with PROFILE_INVALID_MODEL under default env', () => {
const gpt5Profile = `---
profile_version: "1.0"
name: gpt5-inline
phase_models:
- phase: brief
model: gpt5
- phase: research
model: sonnet
- phase: plan
model: opus
- phase: execute
model: sonnet
- phase: review
model: opus
- phase: continue
model: sonnet
parallel_agents_min: 2
parallel_agents_max: 4
external_research_enabled: false
brief_reviewer_iter_cap: 1
---
`;
const r = validateProfileContent(gpt5Profile, { env: { /* default: no flags */ } });
assert.equal(r.valid, false);
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_MODEL' && /gpt5/.test(e.message));
assert.ok(found, `expected PROFILE_INVALID_MODEL for gpt5, got: ${JSON.stringify(r.errors)}`);
});
// BASE_ALLOWED_MODELS allowlist drift-pin (mirrors the PROFILE_REQUIRED_FIELDS pin)
test('BASE_ALLOWED_MODELS export drift-pin', () => {
assert.deepEqual(
[...BASE_ALLOWED_MODELS],
['sonnet', 'opus', 'fable'],
'BASE_ALLOWED_MODELS contract drift — pin contract',
);
});
// Required fields presence
test('PROFILE_MISSING_FIELD when name absent', () => {
const r = validateProfileContent(`---
profile_version: "1.0"
phase_models:
- phase: brief
model: sonnet
- phase: research
model: sonnet
- phase: plan
model: opus
- phase: execute
model: sonnet
- phase: review
model: opus
- phase: continue
model: sonnet
parallel_agents_min: 2
parallel_agents_max: 4
external_research_enabled: false
brief_reviewer_iter_cap: 1
---
`);
assert.equal(r.valid, false);
const found = r.errors.find(e => e.code === 'PROFILE_MISSING_FIELD' && /name/.test(e.message));
assert.ok(found, `expected PROFILE_MISSING_FIELD for name, got: ${JSON.stringify(r.errors)}`);
});
// PROFILE_NOT_FOUND for missing file
test('PROFILE_NOT_FOUND for non-existent file', () => {
const r = validateProfile('/tmp/does-not-exist-profile-xyz.yaml');
assert.equal(r.valid, false);
assert.ok(r.errors.find(e => e.code === 'PROFILE_NOT_FOUND'));
});
// REQUIRED_FIELDS frontmatter contract drift-pin
test('PROFILE_REQUIRED_FIELDS export drift-pin', () => {
assert.deepEqual(
[...PROFILE_REQUIRED_FIELDS].sort(),
['brief_reviewer_iter_cap', 'external_research_enabled', 'name',
'parallel_agents_max', 'parallel_agents_min', 'phase_models'].sort(),
'PROFILE_REQUIRED_FIELDS contract drift — pin contract',
);
});
test('PROFILE_REQUIRED_PHASES export drift-pin', () => {
assert.deepEqual(
[...PROFILE_REQUIRED_PHASES].sort(),
['brief', 'research', 'plan', 'execute', 'review', 'continue'].sort(),
'PROFILE_REQUIRED_PHASES contract drift — pin contract',
);
});
// S34 (V30) — economy carries an explicit `experimental` marker.
// The cross-tier Jaccard floor (0.55) rests on parked-synthetic fixtures;
// empirical Step-17 calibration is deferred to v4.2 ($60-120 LLM budget,
// unauthorized). Until a real calibration lands, `economy` is self-declared
// experimental in the profile DATA — not only in prose — so the status
// survives a docs rewrite. See tests/synthetic/profile-jaccard-calibration.md.
test('S34: economy.yaml declares experimental: true (uncalibrated Jaccard floor)', () => {
const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', 'economy.yaml'));
assert.equal(r.valid, true, `economy.yaml should validate: ${JSON.stringify(r.errors)}`);
assert.equal(r.parsed.frontmatter.experimental, true,
'economy.yaml must carry experimental: true until the Jaccard floor is empirically calibrated (S34/V30)');
});
test('S34: premium + balanced are NOT flagged experimental', () => {
for (const name of ['premium', 'balanced']) {
const r = validateProfile(join(REPO_ROOT, 'lib', 'profiles', `${name}.yaml`));
assert.equal(r.valid, true, `${name}.yaml should validate: ${JSON.stringify(r.errors)}`);
assert.notEqual(r.parsed.frontmatter.experimental, true,
`${name}.yaml must not be flagged experimental`);
}
});
test('S34: validator type-checks experimental as boolean (PROFILE_INVALID_ENUM)', () => {
const bad = `---
profile_version: "1.0"
name: bad-experimental
phase_models:
- phase: brief
model: sonnet
- phase: research
model: sonnet
- phase: plan
model: sonnet
- phase: execute
model: sonnet
- phase: review
model: sonnet
- phase: continue
model: sonnet
parallel_agents_min: 2
parallel_agents_max: 3
external_research_enabled: false
brief_reviewer_iter_cap: 1
experimental: "yes"
---
`;
const r = validateProfileContent(bad);
assert.equal(r.valid, false, 'experimental: "yes" (string) must be rejected');
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_ENUM' && /experimental/.test(e.message));
assert.ok(found, `expected PROFILE_INVALID_ENUM for experimental, got: ${JSON.stringify(r.errors)}`);
assert.match(found.message, /boolean/);
});

View file

@ -171,7 +171,7 @@ test('runner: a failing verification is never verified end to end', () => {
function brief(criteria) {
return [
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s',
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', 'phase_signals_partial: true', '---', '',
'research_topics: 0', 'research_status: skipped', 'framing: new-direction', '---', '',
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why.', '', '## Goal', '', 'What.', '',
'## Success Criteria', '', ...criteria, '',
].join('\n');