// tests/lib/skills-brief-prose-pins.test.mjs // Wave 3, order 7e: the `interviewing` skill and its wiring into /trekbrief. // // What is pinned, and why: // • the skill's form is not pinned here: the form check // (lib/skills/skill-lint.mjs) and the routing test (lib/skills/routing.mjs) // run over every skill in `npm test` and are the authority (contract S1); // the routing test is also what shows the skill does not collide with // /trekbrief (cosine >= 0.75 fails, never lowered); // • the interview form lives in the skill: at most four questions per call, // the recommended answer first, facts looked up, the rest written down, // the stop rule, and the two failures the red baseline showed (an answer // that decides nothing written in as a decision; scope nobody asked for); // • /trekbrief asks for it by name, and for `voyage:writing-criteria` (S3, // order 7c) where the success criteria are written, and names both // review skills (S3 + S4) where it starts brief-reviewer; // • the text that moved is gone from /trekbrief (one source), while what is // /trekbrief's own contract stays: the size class and its budget, the // framing question, the storage question, the approval and the answers // that decide nothing; // • the skill says plainly that the interview's form is prose only. // // S3 and S4 are made by orders 7c and 7d. Their skills need not exist in this // branch: the names are the contract, written here before the merge. // Counted as doc pins by lib/util/test-census.mjs (file name *-prose-pins). import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { existsSync, readFileSync } from 'node:fs'; 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'); const SKILL = 'skills/interviewing/SKILL.md'; const skill = () => { assert.ok(existsSync(join(ROOT, SKILL)), `${SKILL} missing`); return read(SKILL); }; const brief = read('commands/trekbrief.md'); /** 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); } /** Text from `### ` up to the next `### ` or `## ` heading. */ function subsection(text, heading) { const start = text.indexOf(`\n### ${heading}`); assert.ok(start >= 0, `missing ### ${heading}`); const rest = text.slice(start + 5); const m = rest.match(/\n#{2,3} /); return text.slice(start, m ? start + 5 + m.index : undefined); } const flat = (t) => t.replace(/\s+/g, ' '); // ---- the skill carries the interview form ---- test('pin: the interviewing skill exists', () => { skill(); }); test('pin: interviewing — at most four questions per AskUserQuestion call, recommended answer first', () => { const p = flat(section(skill(), 'Process')); assert.match(p, /at most (4|four) questions per `AskUserQuestion` call/i); assert.match(p, /recommended answer first/i); assert.match(p, /\(Recommended\)/); assert.match(p, /consequence of each option/i); }); test('pin: interviewing — facts are looked up, never asked', () => { const p = flat(section(skill(), 'Process')); assert.match(p, /never ask[^.]*fact/i); }); test('pin: interviewing — the rest is ranked and written down: assumptions and a Deferred list', () => { const p = flat(section(skill(), 'Process')); assert.match(p, /Impact × Uncertainty/); assert.match(p, /[Dd]eferred/); assert.match(p, /assumed, not asked/); }); test('pin: interviewing — the stop rule: the next three answers can be predicted', () => { assert.match(flat(section(skill(), 'Process')), /next three answers/); }); test('pin: interviewing — the two measured failures: an answer that decides nothing, and unasked scope', () => { const p = flat(section(skill(), 'Process')); assert.match(p, /not confirmed — the answer was/); assert.match(p, /never as the person's (choice|decision)/i); assert.match(p, /nobody asked for/i); const flags = section(skill(), 'Red flags'); assert.match(flags, /[Ww]hatever you think/); }); test('pin: interviewing — the interview form is prose only; the brief validator checks the result', () => { const enforced = flat(section(skill(), 'Enforced by')); assert.ok(enforced.includes('lib/validators/brief-validator.mjs'), 'names the brief validator'); assert.match(enforced, /prose only/); assert.match(enforced, /not the form of the interview/); }); test('pin: interviewing — credits its sources with commit and licence', () => { const credits = section(skill(), 'Credits'); for (const src of [ 'mattpocock/skills @c55ee46', 'skills/productivity/grilling/SKILL.md', 'addyosmani/agent-skills @9d0c60d', 'skills/interview-me/SKILL.md', 'github/spec-kit @2e941fe', 'templates/commands/clarify.md', ]) assert.ok(credits.includes(src), `credit ${src}`); assert.equal((credits.match(/\bMIT\b/g) || []).length, 3, 'one licence per source'); }); // ---- /trekbrief asks for the skills by name (S3, S4, interviewing) ---- test('pin: /trekbrief Phase 3 asks for voyage:interviewing by name, and the command may use the Skill tool', () => { assert.match(section(brief, 'Phase 3'), /`voyage:interviewing`/); assert.match(section(brief, 'Phase 3'), /Call the Skill tool/); assert.match(brief.split('\n---\n')[0], /\nallowed-tools:[^\n]*\bSkill\b/, 'the command may use the Skill tool'); }); test('pin: /trekbrief names voyage:writing-criteria (S3) where the success criteria are asked and written', () => { const bank = subsection(brief, 'Question bank'); const sc = bank.slice(bank.indexOf('**Success Criteria**'), bank.indexOf('**Research Plan**')); assert.match(sc, /`voyage:writing-criteria`/); const draft = brief.slice(brief.indexOf('**Step 4a'), brief.indexOf('**Step 4b')); assert.match(draft, /\*\*Success Criteria:\*\*[^\n]*\n?[^\n]*`voyage:writing-criteria`/); }); test('pin: /trekbrief names both review skills where it starts brief-reviewer (S3 + S4)', () => { const step = brief.slice(brief.indexOf('**Step 4c'), brief.indexOf('**Step 4d')); assert.match(step, /`voyage:writing-criteria`/); assert.match(step, /`voyage:reviewing-adversarially`/); }); // ---- one source: the moved text is gone from /trekbrief ---- const MOVED = [ 'Never ask the operator a fact a helper can look up', 'Rank the rest by Impact × Uncertainty', 'The tool takes at most 4 questions per call', "Stop asking as soon as you can predict the operator's next three answers", 'Each question is a full sentence in plain words', 'The round, the recommended answer first and "never ask a fact": mattpocock/skills@c55ee46', 'Impact × Uncertainty and the Deferred list: GitHub Spec Kit', 'The stop rule and the answers that decide nothing: addyosmani/agent-skills@9d0c60d', ]; test('pin: the interview text that moved into the skill is no longer in /trekbrief', () => { const b = flat(brief); for (const s of MOVED) assert.ok(!b.includes(s), `still in commands/trekbrief.md: «${s}»`); }); // ---- what stays: /trekbrief's own contract ---- test('pin: /trekbrief keeps its own contract — class, budget, framing first, storage, hollow answers, approval', () => { const p3 = flat(section(brief, 'Phase 3')); assert.match(p3, /### Step 3b — Ceremony by size/); assert.match(p3, /\*\*Budget per class\*\*/); assert.match(p3, /framing[^\n]*first/i); assert.match(p3, /artifact_store/); for (const w of ['høres bra ut', 'gjør som du vil']) assert.ok(p3.includes(w), `Phase 3 keeps «${w}»`); assert.match(p3, /empty reply/); assert.match(section(brief, 'Phase 2.5'), /hollow answer/i); assert.match(section(brief, 'Phase 4h'), /høres bra ut/); });