R4-R7 of the cleanup list (decision 188): the June trial docs and fixtures, the three orchestrator reference docs, the dormant synthesis-agent, code with no caller (project-discovery, bash-normalize, fidelity-diff, test-census, the next-session prompt validator, arm B and the --workflow flag) and the fix-round chain. Living docs repointed; the reasons for in-session orchestration and the all-opus pin are now written in docs/architecture.md and docs/profiles.md. New tests/lib/living-paths.test.mjs: living tracked text names only plugin paths that exist (seen red with 22 dead pointers before this commit). docs/T1-cc26-delegated-orchestration.md and lib/review/gold-scorer.mjs stay until R-b: the frozen end-state registry names them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
7.8 KiB
JavaScript
170 lines
7.8 KiB
JavaScript
// 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.
|
||
|
||
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 `## <heading>` 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 `### <heading>` 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/);
|
||
});
|