// tests/lib/intent-prose-pins.test.mjs // Order 2 (P1 — intent as a stored artifact): the prose pins for /trekbrief's // ceremony by size, the question round, answers that are not approval, the // approval log and the commit; and /trekplan's Phase 1 log gate and Phase 11 // plan approval. The command lines are EXTRACTED from the prose and executed, // so removing a line, or pointing it at the wrong module, fails here. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const read = (rel) => readFileSync(join(ROOT, rel), 'utf8'); /** Text from `## ` up to the next `## ` heading. */ function section(text, heading) { const start = text.indexOf(`\n## ${heading}`); assert.ok(start >= 0, `missing ## ${heading}`); const next = text.indexOf('\n## ', start + 4); return text.slice(start, next < 0 ? undefined : next); } /** The single line in `text` that runs `lib/` with `verb`. */ function line(text, mod, verb) { const esc = mod.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); const re = new RegExp(`^\\s*node \\$\\{CLAUDE_PLUGIN_ROOT\\}/lib/${esc}\\b`); const hits = text.split('\n').filter((l) => re.test(l) && l.includes(verb)); assert.equal(hits.length, 1, `expected exactly one \`${mod} ${verb}\` line; found ${hits.length}`); return hits[0].trim(); } function bash(cmd, cwd) { return spawnSync('bash', ['-c', cmd.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)], { cwd, encoding: 'utf8' }); } const brief = read('commands/trekbrief.md'); const plan = read('commands/trekplan.md'); // ---- /trekbrief: ceremony by size ---- test('trekbrief: three classes, heaviest in doubt, said aloud, overridable, only upward mid-task', () => { const p3 = section(brief, 'Phase 3'); for (const c of ['small', 'bounded', 'architectural']) assert.ok(p3.includes(`\`${c}\``), `class ${c}`); assert.match(p3, /heaviest/i); assert.match(p3, /aloud/i); assert.match(p3, /override/i); assert.match(p3, /only (ever )?(moves? )?up/i); assert.match(p3, /`ceremony:/, 'the class is written to the brief (K1)'); }); // The interview form moved into the skill /trekbrief asks for by name (order // 7e, one source): the same assertions, now read where the text lives. const interviewing = () => read('skills/interviewing/SKILL.md'); test('trekbrief: the round — all answerable questions at once, recommendation first, no fact questions', () => { const p3 = section(brief, 'Phase 3'); const iv = interviewing(); assert.match(p3, /Use `AskUserQuestion` for every question/); assert.match(p3, /`voyage:interviewing`/, 'Phase 3 asks for the interview form by name'); assert.doesNotMatch(brief, /Ask one question at a time/, 'the one-at-a-time rule is replaced by the round'); assert.doesNotMatch(iv, /Ask one question at a time/, 'the one-at-a-time rule is replaced by the round'); assert.match(p3, /one round/i); assert.match(iv, /one round/i); assert.match(iv, /recommend(ed|ation)[^.\n]*first/i); assert.match(iv, /never ask[^.\n]*fact/i); assert.match(iv, /Impact × Uncertainty/); assert.match(iv, /[Dd]eferred/); assert.match(iv, /next three answers/); }); test('trekbrief: the budget for a small task is at most 5 questions and fits one AskUserQuestion call', () => { const p3 = section(brief, 'Phase 3'); const m = p3.match(/`small`[^\n|]*\|\s*(\d+)/); assert.ok(m, 'the budget table must give a number for `small`'); assert.ok(Number(m[1]) <= 5 && Number(m[1]) <= 4, `small budget ${m[1]} must be ≤ 5 and fit one call (max 4 questions)`); assert.match(p3, /framing/i, 'framing is one of the round\'s questions'); assert.match(p3, /artifact_store/, 'the storage choice is one of the round\'s questions'); }); test('trekbrief: credits every borrowing of the round and the classes', () => { // The classes stay in /trekbrief with their credits; the round's credits // moved with the round into the skill. const p3 = section(brief, 'Phase 3'); for (const src of ['obra/superpowers@8ca22db', 'kiro.dev']) { assert.ok(p3.includes(src), `credit for ${src}`); } const iv = interviewing(); for (const re of [/mattpocock\/skills ?@c55ee46/, /GitHub Spec Kit/, /addyosmani\/agent-skills ?@9d0c60d/]) { assert.match(iv, re, `credit for ${re}`); } }); test('trekbrief: hard rule 8 no longer says Phase 3 has no cap', () => { assert.doesNotMatch(brief, /Phase 3 has no cap/); }); // ---- /trekbrief Phase 4h: answers that are not approval, the log, the commit ---- test('trekbrief 4h: the answer check runs before the stamp, then append, then commit', () => { const p4h = section(brief, 'Phase 4h'); const ans = p4h.indexOf(line(p4h, 'util/approval-log.mjs', ' answer')); const stamp = p4h.indexOf('intent-approval.mjs --stamp'); const app = p4h.indexOf(line(p4h, 'util/approval-log.mjs', ' append')); const com = p4h.indexOf(line(p4h, 'util/artifact-commit.mjs', '--store')); assert.ok(ans >= 0 && ans < stamp && stamp < app && app < com, 'order: answer → stamp → append → commit'); assert.match(p4h, /høres bra ut/); assert.match(p4h, /gjør som du vil/); assert.match(p4h, /empty/i); }); test('trekbrief 4h: the answer line, run as written, refuses «høres bra ut» and «gjør som du vil»', () => { const p4h = section(brief, 'Phase 4h'); const start = p4h.indexOf(line(p4h, 'util/approval-log.mjs', ' answer')); const block = p4h.slice(start, p4h.indexOf('```', start)).trim(); // the heredoc, up to the fence const d = mkdtempSync(join(tmpdir(), 'intent-pins-')); try { for (const [answer, code] of [['høres bra ut', 1], ['gjør som du vil', 1], ['', 1], ['Approve', 0]]) { const r = bash(block.split('{OPERATOR_ANSWER}').join(answer), d); assert.equal(r.status, code, `«${answer}» → ${r.status} (${r.stdout}${r.stderr})`); } } finally { rmSync(d, { recursive: true, force: true }); } }); test('trekbrief 4h: append binds the intent hash; commit uses the operator\'s storage choice', () => { const p4h = section(brief, 'Phase 4h'); const app = line(p4h, 'util/approval-log.mjs', ' append'); assert.match(app, /--kind intent/); assert.match(app, /--subject "\{BRIEF_PATH\}"/); assert.match(app, /--sha256 "\{INTENT_HASH\}"/); const com = line(p4h, 'util/artifact-commit.mjs', '--store'); assert.match(com, /--store \{ARTIFACT_STORE\}/); assert.match(com, /--artifact "\{BRIEF_PATH\}"/); }); // ---- /trekplan Phase 1: the log gate ---- function gateLine() { const read1 = plan.slice(plan.indexOf('### Read the brief'), plan.indexOf('## Phase 1.5')); return line(read1, 'util/approval-log.mjs', ' verify'); } test('trekplan Phase 1: the log gate sits after the marker check, before planning, and says halt', () => { const readIdx = plan.indexOf('### Read the brief'); const check = plan.indexOf('intent-approval.mjs --check'); const gate = plan.indexOf(gateLine()); const p15 = plan.indexOf('## Phase 1.5'); assert.ok(readIdx < check && check < gate && gate < p15); const after = plan.slice(gate, gate + 2000); assert.match(after, /\*\*halt\*\*/i); for (const s of ['broken', 'diverged', 'not-logged', 'mismatch']) assert.ok(after.includes(`\`${s}\``), s); assert.match(gateLine(), /--kind intent --subject "\{brief_path\}" --sha256 "\{intent_approved_hash\}"/); }); test('trekplan Phase 1: the gate line, run as written, stops (exit 1) when the brief\'s log is broken', () => { const d = mkdtempSync(join(tmpdir(), 'intent-pins-gate-')); try { const H = 'd'.repeat(64); const logCli = join(ROOT, 'lib', 'util', 'approval-log.mjs'); writeFileSync(join(d, 'brief.md'), 'x'); spawnSync(process.execPath, [logCli, 'append', '--kind', 'intent', '--subject', 'brief.md', '--sha256', H], { cwd: d }); spawnSync(process.execPath, [logCli, 'append', '--kind', 'plan', '--subject', 'plan.md', '--sha256', H], { cwd: d }); const cmd = gateLine().split('{brief_path}').join('brief.md').split('{intent_approved_hash}').join(`sha256:${H}`); assert.equal(bash(cmd, d).status, 0, 'intact log with the brief\'s approval passes'); const p = join(d, '.claude', 'voyage', 'approvals.jsonl'); const text = readFileSync(p, 'utf8'); writeFileSync(p, text.replace('"intent"', '"intenT"')); const r = bash(cmd, d); assert.equal(r.status, 1, r.stdout + r.stderr); assert.equal(JSON.parse(r.stdout).status, 'broken'); } finally { rmSync(d, { recursive: true, force: true }); } }); test('trekplan Phase 1: reads ceremony (K1, absent = architectural) and artifact_store (absent = local)', () => { const read1 = plan.slice(plan.indexOf('### Read the brief'), plan.indexOf('### When the input is type:trekreview')); assert.match(read1, /`ceremony`[^\n]*absent[^\n]*`architectural`/); assert.match(read1, /`artifact_store`[^\n]*absent[^\n]*`local`/); }); // ---- /trekplan Phase 11: plan approval before any handoff ---- test('trekplan Phase 11: plan approval (answer check → append plan → commit) comes before every handoff', () => { const p11 = section(plan, 'Phase 11'); const ans = p11.indexOf(line(p11, 'util/approval-log.mjs', ' answer')); const app = p11.indexOf(line(p11, 'util/approval-log.mjs', ' append')); const com = p11.indexOf(line(p11, 'util/artifact-commit.mjs', '--store')); const save = p11.indexOf('### "save"'); assert.ok(ans >= 0 && ans < app && app < com && com < save); assert.match(line(p11, 'util/approval-log.mjs', ' append'), /--kind plan --subject "\{plan_path\}" --sha256-of "\{plan_path\}"/); assert.match(line(p11, 'util/artifact-commit.mjs', '--store'), /--store \{artifact_store\} --artifact "\{plan_path\}"/); assert.match(p11, /AskUserQuestion/); });