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>
159 lines
6.4 KiB
JavaScript
159 lines
6.4 KiB
JavaScript
// tests/lib/agent-frontmatter.test.mjs
|
|
// Pin the agent-frontmatter contract from Steps 1-3 of plan-v2:
|
|
// every agents/*.md MUST declare:
|
|
// - model: (one of opus | sonnet | haiku)
|
|
// - tools: (allowlist) OR disallowedTools: (denylist), at least one
|
|
// No agent includes the `Agent` tool (orchestration runs in the session).
|
|
//
|
|
// When this test fails, fix the agent file — do NOT relax the assertion to
|
|
// hide drift. The contract is what /trek* commands rely on for
|
|
// disciplined model selection + tool scoping.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(HERE, '..', '..');
|
|
const AGENTS_DIR = join(ROOT, 'agents');
|
|
|
|
const ALLOWED_MODELS = new Set(['opus', 'sonnet', 'haiku']);
|
|
|
|
function read(rel) {
|
|
return readFileSync(join(ROOT, rel), 'utf-8');
|
|
}
|
|
|
|
function extractFrontmatter(text) {
|
|
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
function hasTopLevelKey(fm, key) {
|
|
return new RegExp(`^${key}\\s*:`, 'm').test(fm);
|
|
}
|
|
|
|
function getTopLevelValue(fm, key) {
|
|
const m = fm.match(new RegExp(`^${key}\\s*:\\s*(.+?)\\s*$`, 'm'));
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
const agentFiles = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md'));
|
|
|
|
test('every agents/*.md declares a model: field', () => {
|
|
assert.ok(agentFiles.length > 0, 'No agent files found under agents/');
|
|
for (const f of agentFiles) {
|
|
const fm = extractFrontmatter(read(`agents/${f}`));
|
|
assert.ok(fm, `agents/${f}: missing YAML frontmatter block`);
|
|
assert.ok(
|
|
hasTopLevelKey(fm, 'model'),
|
|
`agents/${f}: required \`model:\` field missing from frontmatter`,
|
|
);
|
|
const value = getTopLevelValue(fm, 'model');
|
|
assert.ok(
|
|
value && ALLOWED_MODELS.has(value),
|
|
`agents/${f}: model: "${value}" must be one of ${[...ALLOWED_MODELS].join(' | ')}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('every agents/*.md declares tools: or disallowedTools:', () => {
|
|
for (const f of agentFiles) {
|
|
const fm = extractFrontmatter(read(`agents/${f}`));
|
|
assert.ok(fm, `agents/${f}: missing YAML frontmatter block`);
|
|
assert.ok(
|
|
hasTopLevelKey(fm, 'tools') || hasTopLevelKey(fm, 'disallowedTools'),
|
|
`agents/${f}: required \`tools:\` (allowlist) or \`disallowedTools:\` (denylist) field missing`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('every agents/*.md frontmatter name matches its filename', () => {
|
|
for (const f of agentFiles) {
|
|
const fm = extractFrontmatter(read(`agents/${f}`));
|
|
assert.ok(fm, `agents/${f}: missing frontmatter`);
|
|
const expected = f.replace(/\.md$/, '');
|
|
const value = getTopLevelValue(fm, 'name');
|
|
assert.equal(
|
|
value,
|
|
expected,
|
|
`agents/${f}: frontmatter name="${value}" should match filename "${expected}"`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('agents do NOT include the Agent tool (no recursive swarming)', () => {
|
|
for (const f of agentFiles) {
|
|
const fm = extractFrontmatter(read(`agents/${f}`));
|
|
assert.ok(fm, `agents/${f}: missing frontmatter`);
|
|
const tools = getTopLevelValue(fm, 'tools');
|
|
if (tools === null) continue; // disallowedTools-only agent — fine
|
|
assert.ok(
|
|
!/\bAgent\b/.test(tools),
|
|
`agents/${f}: must NOT include the Agent tool ` +
|
|
`(orchestration runs in the session) — got tools: ${tools}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
// M4 (v5.7.1): examples-relocation invariant. The 34 <example> blocks belong in
|
|
// agent BODIES, not in the always-loaded `description:` frontmatter (voyage
|
|
// launches its agents by name, so the auto-selection examples are cost without
|
|
// function there). This pins the migration: examples cannot regress into
|
|
// frontmatter, and cannot be silently lost during the move.
|
|
function bodyOf(text) {
|
|
// Slice the file content AFTER the closing frontmatter `---`.
|
|
// Do NOT split on /^---$/m — a horizontal rule in the body would mis-split.
|
|
const m = text.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
return m ? text.slice(m[0].length) : text;
|
|
}
|
|
|
|
test('no agents/*.md frontmatter contains an <example> block (M4: examples live in the body)', () => {
|
|
for (const f of agentFiles) {
|
|
const fm = extractFrontmatter(read(`agents/${f}`));
|
|
assert.ok(fm !== null, `agents/${f}: missing frontmatter`);
|
|
assert.equal(
|
|
(fm.match(/<example>/g) || []).length,
|
|
0,
|
|
`agents/${f}: <example> blocks must live in the body, not the always-loaded description: frontmatter (M4)`,
|
|
);
|
|
}
|
|
});
|
|
|
|
// Floor lowered 34 -> 32 in v5.10.1: `gemini-bridge` (2 examples) was DELETED as an
|
|
// agent by operator directive, not relocated. The pin guards M4's "relocation moves,
|
|
// never deletes" invariant, so it must move only when an agent legitimately leaves the
|
|
// inventory — never to absorb examples that went missing from a surviving agent.
|
|
test('agent bodies retain at least 32 <example> blocks (M4: relocation moves, never deletes)', () => {
|
|
let total = 0;
|
|
for (const f of agentFiles) {
|
|
total += (bodyOf(read(`agents/${f}`)).match(/<example>/g) || []).length;
|
|
}
|
|
assert.ok(
|
|
total >= 32,
|
|
`expected >= 32 <example> blocks across agent bodies (16 agents x 2), got ${total} ` +
|
|
`— examples may have been deleted instead of relocated (M4)`,
|
|
);
|
|
});
|
|
|
|
// Operator rule (2026-10-05): "standard" is Opus 5.5 at reasoning effort High. A pinned
|
|
// `effort:` overrides the session level, so a pin below `high` runs that agent below the
|
|
// floor whatever the session is set to. Unpinned agents follow the session.
|
|
const EFFORT_FLOOR = new Set(['high', 'xhigh', 'max']);
|
|
const RETRIEVAL_AGENTS = ['architecture-mapper.md', 'dependency-tracer.md', 'git-historian.md', 'task-finder.md'];
|
|
|
|
test('no agents/*.md pins a reasoning effort below high', () => {
|
|
const pinned = agentFiles
|
|
.map((f) => [f, getTopLevelValue(extractFrontmatter(read(`agents/${f}`)) || '', 'effort')])
|
|
.filter(([, v]) => v !== null);
|
|
assert.ok(pinned.length > 0, 'no agent carries effort: - the check would prove nothing');
|
|
const below = pinned.filter(([, v]) => !EFFORT_FLOOR.has(v)).map(([f, v]) => `agents/${f}: effort: ${v}`);
|
|
assert.deepEqual(below, [], 'fix the agent file - the floor is the operator\'s rule');
|
|
});
|
|
|
|
test('the four retrieval agents pin effort: high', () => {
|
|
for (const f of RETRIEVAL_AGENTS) {
|
|
assert.equal(getTopLevelValue(extractFrontmatter(read(`agents/${f}`)), 'effort'), 'high', `agents/${f}`);
|
|
}
|
|
});
|