voyage/tests/lib/skills-brief-prose-pins.test.mjs
Kjell Tore Guttormsen 8295601965
chore(cleanup): remove the June trials, orchestrator docs, dormant agent, caller-less code and the fix-round chain
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>
2026-11-07 12:51:54 +01:00

170 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// tests/lib/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/);
});