fix(trekplan): explorers and reviewers read only in the repo and get the brief
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
305ac9284f
commit
e6755bd886
2 changed files with 128 additions and 12 deletions
99
tests/commands/trekplan-explorer-prompts.test.mjs
Normal file
99
tests/commands/trekplan-explorer-prompts.test.mjs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// tests/commands/trekplan-explorer-prompts.test.mjs
|
||||
// Prose pins: every agent /trekplan sends to explore or review is told to read
|
||||
// local files only inside the repo under exploration (plus the paths its prompt
|
||||
// gives it), and every exploring agent gets the brief path.
|
||||
// Blocks are found by a named list; a block that cannot be found fails, so an
|
||||
// empty scan is never green.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { findSection } from '../../lib/parsers/plan-schema.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const PLAN_CMD = readFileSync(join(ROOT, 'commands', 'trekplan.md'), 'utf8');
|
||||
|
||||
const STAY_LINE =
|
||||
'Read local files only inside the repo under exploration, plus paths this prompt gives you (the brief, the plan, and templates it names).';
|
||||
|
||||
/** The text of a level-2 section, up to the next `## ` heading outside a fence. */
|
||||
function section(heading) {
|
||||
const s = findSection(PLAN_CMD, heading.replace(/^## /, ''), 2);
|
||||
assert.ok(s, `missing ${heading}`);
|
||||
return s.text;
|
||||
}
|
||||
|
||||
/** Collapse line wraps so a sentence wrapped across lines still matches. */
|
||||
const flat = (s) => s.replace(/\s+/g, ' ');
|
||||
|
||||
/**
|
||||
* The quoted prompt that follows `marker` inside `text`: from `Prompt: "` to
|
||||
* the next closing `"`. Fails when the marker or the prompt is missing, or
|
||||
* when the first `Prompt: "` after the marker sits in a later paragraph.
|
||||
*/
|
||||
function promptAfter(text, marker, label) {
|
||||
const at = text.indexOf(marker);
|
||||
assert.ok(at !== -1, `${label}: marker ${JSON.stringify(marker)} not found`);
|
||||
const open = text.indexOf('Prompt: "', at);
|
||||
assert.ok(open !== -1, `${label}: no Prompt: after the marker`);
|
||||
assert.ok(!text.slice(at, open).includes('\n\n'),
|
||||
`${label}: the next Prompt: lies in another paragraph, not in this block`);
|
||||
const start = open + 'Prompt: "'.length;
|
||||
const close = text.indexOf('"', start);
|
||||
assert.ok(close !== -1, `${label}: prompt never closes`);
|
||||
return text.slice(start, close);
|
||||
}
|
||||
|
||||
const P4B = section('## Phase 4b — Brief review');
|
||||
const P5 = section('## Phase 5 — Parallel exploration (specialized agents + research)');
|
||||
const P6 = section('## Phase 6 — Targeted deep-dives');
|
||||
const P9 = section('## Phase 9 — Adversarial review (bounded critic loop)');
|
||||
|
||||
function phase6Rule() {
|
||||
const start = P6.indexOf('spawn a targeted deep-dive agent');
|
||||
const end = P6.indexOf('Launch up to 3 deep-dive agents');
|
||||
assert.ok(start !== -1 && end > start, 'Phase 6 deep-dive rule not found');
|
||||
return P6.slice(start, end);
|
||||
}
|
||||
|
||||
// [label, block text getter, carries {brief_path}?]
|
||||
const BLOCKS = [
|
||||
['architecture-mapper', () => promptAfter(P5, '**architecture-mapper**', 'architecture-mapper'), true],
|
||||
['dependency-tracer', () => promptAfter(P5, '**dependency-tracer**', 'dependency-tracer'), true],
|
||||
['risk-assessor', () => promptAfter(P5, '**risk-assessor**', 'risk-assessor'), true],
|
||||
['task-finder', () => promptAfter(P5, '**task-finder**', 'task-finder'), true],
|
||||
['test-strategist', () => promptAfter(P5, '**test-strategist**', 'test-strategist'), true],
|
||||
['git-historian', () => promptAfter(P5, '**git-historian**', 'git-historian'), true],
|
||||
['convention-scanner', () => promptAfter(P5, '**Convention Scanner**', 'convention-scanner'), true],
|
||||
['research-scout', () => promptAfter(P5, 'launch **research-scout**', 'research-scout'), true],
|
||||
['Phase 6 deep-dive rule', phase6Rule, true],
|
||||
['brief-reviewer', () => promptAfter(P4B, '**brief-reviewer**', 'brief-reviewer'), false],
|
||||
['plan-critic', () => promptAfter(P9, '**plan-critic**', 'plan-critic'), false],
|
||||
['scope-guardian', () => promptAfter(P9, '**scope-guardian**', 'scope-guardian'), false],
|
||||
];
|
||||
|
||||
test('all 12 named blocks are found (an empty scan fails)', () => {
|
||||
assert.equal(BLOCKS.length, 12);
|
||||
let found = 0;
|
||||
for (const [label, get] of BLOCKS) {
|
||||
const text = get();
|
||||
assert.ok(text.trim().length > 0, `${label}: empty block`);
|
||||
found += 1;
|
||||
}
|
||||
assert.equal(found, 12);
|
||||
});
|
||||
|
||||
for (const [label, get] of BLOCKS) {
|
||||
test(`${label} carries the stay-in-repo line`, () => {
|
||||
assert.ok(flat(get()).includes(STAY_LINE), `${label} does not carry: ${STAY_LINE}`);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [label, get, needsBrief] of BLOCKS) {
|
||||
if (!needsBrief) continue;
|
||||
test(`${label} is given the brief path`, () => {
|
||||
assert.ok(get().includes('{brief_path}'), `${label} is not given {brief_path}`);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue