Files
gstack/test/autoplan-phase-order.test.ts
T
ac20ef1e13 v1.91.16.0 fix: safe installs and upgrades, skills start everywhere, truthful completion (parallel wave) (#3014)
* fix(make-pdf,design): real TOC page numbers and anchors; design models move to gpt-5.5 + gpt-image-2 (#2903, #2807)

* ci: pin every action to a SHA, Scorecard, checksummed Bun, scoped Codex CLI in the CI image, macOS named-regressions job (#1948, #1706, #1997)

* fix(cso,browse): Windows/macOS lease identity, retention skips bad runs, truthful 'not assessed' reports, env.kv false positive, Windows browse spawn fixes (#2894, #2912, #2048, #2637, #2805)

* fix(outside-voice): one Codex model precedence for probe and dispatch, bounded timeouts with KILL escalation, probe cache identity, guarded mktemp, skills hidden in nested Codex (#2776, #2914, #2787, #2881, #2847)

* feat(setup): install registry, ./setup --status, staged activation, per-scope migrations, host tiers and conformance kit, OpenCode/Hermes fixes, Copilot CLI (experimental), disabled_skills, slimmer CLAUDE.md (#1925, #2879, #2347, #2338, #2626, #2629, #2825, #2015, #393, #1206, #2096)

Co-authored-by: Andrey Esipov <61357035+andrey-esipov@users.noreply.github.com>
Co-authored-by: Matthew Williams <5863886+ridermw@users.noreply.github.com>
Co-authored-by: Rocky Wu <92764750+lolisaigao1234@users.noreply.github.com>

* fix(preamble): skills start in worktree-isolated Claude Code sessions — one literal skill-start line, context-recovery helper, no eval in skill blocks, $N token tripwire, honest plan-mode precedence (#2763, #2896, #2851)

* fix(setup): per-install renders for renamed, vendored and custom-config installs, space-safe alias roots, per-host /cso launcher path (#1882, #2906)

* test: free tests never write the real HOME (home-write tripwire), no orphaned browse daemons, symlinked-TMPDIR (macOS) fixes, gbrain-on-PATH isolation (#2895, #2816, #2829, #2841, #2982, #2911, #2909, #2908, #2910)

* chore: integrate wave follow-ups — regenerate all hosts, README model docs, TODOS, touchfiles, duration seeds, Bun 1.3 telemetry test fix, replay fixtures pinned to their recorded bytes

* v1.91.14.0: release metadata

* test(browse): group-kill cleanup falls back to the leader when macOS returns EPERM for a live group

* ci: Codex login steps run under bash (the CI container's default sh rejects pipefail)

* fix: full-suite repairs — private state roots for 7 real-HOME writers, truthful home-guard attribution with --attribute-home, gstack-paths shim in ship-hook fixtures, Copilot in team-init probes, test-of-test detector, cookie review fixture pinned to the approved excerpt

* fix(windows): LF for Dockerfiles, normalized home-guard exclusions, path.relative fixture containment, ship push test spawn timeouts

* test(design-detect): widen the whole-scan budget test's timing margins so a loaded macOS runner can't time out the single-batch call

* fix(evals): qa protocol names defined for the judge, async PTY judge so overlapping sessions keep their cleanup deadline, bws setup cases use the standard JUDGE_MS tier

* test(pty): the non-blocking judge case is POSIX-only (its fake judge is a shebang script, like the PTY harness it covers)

* test(evals): ship-coverage-audit names absolute skill paths, uses Read, and skips the Step 7 artifact nothing consumes (removes the mechanism behind 3 of 4 historical timeouts)

* test: deflake terminal-agent concurrent startup (write the placeholder record before spawning, like the product) and qa-evidence resumed pipe (barrier on a full high-water mark instead of a sleep)

* fix(design): daemon log stream handles open errors instead of crashing the daemon; shutdown test awaits the stubbed exit

* test(evals): skillify checks the product's resolved state root (no GSTACK_HOME bypass), negative cases check every root; tpa shims live outside the agent's cwd and answer consistently

* test(evals): coverage-audit detector splits parallel columns at │ rails and [+] groups; coverage prompts name absolute skill paths; TODOs from the gate eval census

* fix(plan-eng-review): scope gate always sends the menu (no headless 'Scope pending' exit); PTY harness ends idle turn-ends and API-error panels promptly with a precise diagnostic

* test(evals): one position-bound coverage-read rule credits read-only && chains (labels, context ranges, git displays in either order) and still rejects forged output

* fix(ship): documentation section has one saved source inserted by cat (never retyped); spawned-marker evidence is the child output; refreshed codex/factory ship goldens

* test(docsync): the byte-exact insert positive is POSIX-only (the bash-command observer refuses backslash paths by design)

* test(evals): plan-design-with-ui focus-title rule accepts the comma-less 'dimensions or focus' wording (replayed on the captured CI run, negative controls kept)

* fix(qa-callers): exploratory preflight says the scope/method Reads come after exploratory.md; design-review scan check credits an executed scan proven by one engine run over the changed files

---------

Co-authored-by: Andrey Esipov <61357035+andrey-esipov@users.noreply.github.com>
Co-authored-by: Matthew Williams <5863886+ridermw@users.noreply.github.com>
Co-authored-by: Rocky Wu <92764750+lolisaigao1234@users.noreply.github.com>
2026-10-03 07:54:09 -07:00

414 lines
24 KiB
TypeScript

/**
* /autoplan phase-order pin (free, static).
*
* The pipeline order is a deliberate design decision (2026-08-25, user-directed):
* CEO → Design (if UI scope) → DX (if developer-facing scope) → Eng, ALWAYS LAST.
* Eng is the required shipping gate — it must review the FINAL amended plan, so
* every other phase's amendments land before it. The original order buried Eng
* mid-pipeline (CEO → Design → Eng → DX), which let DX findings land AFTER the
* gate had signed off — the gate validated a stale plan.
*
* These assertions pin the template so a refactor can't silently restore the
* old order. No paid eval runs the whole chain; the production phase-publication
* hook enforces the order at runtime (autoplan-publication-guard.test.ts).
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import { tmpdir } from 'node:os';
import { prepareMethodology, createSnapshot, preparePhaseClose } from '../bin/gstack-autoplan-snapshot';
import { SECTION } from '../scripts/resolvers/sections';
import { HOST_PATHS, type TemplateContext } from '../scripts/resolvers/types';
import { ALL_HOST_CONFIGS } from '../hosts';
const ROOT = path.join(import.meta.dir, '..');
const read = (p: string) => fs.readFileSync(path.join(ROOT, p), 'utf-8');
const phases = [
{ child: 'ceo', id: '1', next: ['2'] },
{ child: 'design', id: '2', next: ['2.5', '3'] },
{ child: 'dx', id: '2.5', next: ['3'] },
{ child: 'eng', id: '3', next: ['4'] },
];
// Exercise the actual packet's bound report data; the shared close procedure
// owns the separate native-message operation that consumes those fields.
const closePackets = new Map<string, ReturnType<typeof preparePhaseClose> & { text: string }>();
function closePacket(phase: string) {
if (closePackets.has(phase)) return closePackets.get(phase)!;
const dir = fs.mkdtempSync(path.join(tmpdir(), 'autoplan-order-'));
try {
const active = path.join(dir, 'active.md'), restore = path.join(dir, 'restore.md');
const original = '## Implementation plan\nKeep the documented behavior.\n## Review record\n';
fs.writeFileSync(active, original); fs.writeFileSync(restore, original);
const skill = `plan-${phase === 'dx' ? 'devex' : phase}-review/SKILL.md`;
const method = prepareMethodology(phase, path.join(ROOT, skill), restore).methodologyPath;
const checkpoint = createSnapshot(phase, active, restore, method).snapshotPath;
fs.appendFileSync(active, `<!-- autoplan-accepted:${phase} -->\nNone: current behavior is retained.\n<!-- /autoplan-accepted:${phase} -->\n`);
const packet = preparePhaseClose(phase, active, checkpoint, restore, method);
const text = fs.readFileSync(packet.closePacketPath, 'utf8');
const result = { ...packet, text }; closePackets.set(phase, result); return result;
} finally { fs.rmSync(dir, {recursive: true, force: true}); }
}
function closeContent(phase: string) { return closePacket(phase).text; }
const flat = (text: string) => text.replace(/\s+/g, ' ');
function ordered(text: string, anchors: string[]) {
const positions = anchors.map(anchor => text.indexOf(anchor));
expect(positions.every(position => position >= 0), `missing one of ${JSON.stringify(anchors)}`).toBe(true);
expect(positions).toEqual([...positions].sort((a, b) => a - b));
}
describe('autoplan phase order (Eng always last)', () => {
const tmpl = read('autoplan/SKILL.md.tmpl');
test('Sequential Execution block names Eng as the terminal phase', () => {
const block = tmpl.split('## Sequential Execution')[1]?.split('---')[0] ?? '';
expect(block).toMatch(/CEO → Design.*→ DX.*→ Eng/s);
// The old order must not resurface anywhere in the template.
expect(tmpl).not.toContain('CEO → Design → Eng → DX');
});
test('phase headings appear in the new order: 1, 2, 2.5, 3', () => {
ordered(tmpl, ['## Phase 1: CEO Review', '## Phase 2: Design Review', '## Phase 2.5: DX Review', '## Phase 3: Eng Review']);
// No stale Phase 3.5 heading or transition marker survives.
expect(tmpl).not.toContain('Phase 3.5');
});
test.each(phases)('carved child completion and handoff IDs match the pipeline: %j', ({ child, id, next }) => {
const section = read(`autoplan/sections/${child}-phase.md.tmpl`);
const report = closePacket(child).report;
const announced = [report.number];
const handoff = report.next;
expect(section.trim().endsWith('{{SECTION:phase-close}}')).toBe(true);
const pointer = section.indexOf('{{SECTION:phase-close}}');
expect(pointer).toBeGreaterThan(section.indexOf('**Close this phase:**'));
expect(section.slice(pointer).trim()).toBe('{{SECTION:phase-close}}');
const generated = read(`autoplan/sections/${child}-phase.md`);
expect(generated).toContain('Read `~/.claude/skills/gstack/autoplan/sections/phase-close.md` and execute it');
expect(announced).toEqual([id]);
expect([...handoff.matchAll(/Phase (\d+(?:\.\d+)?)/g)].map(m => m[1])).toEqual(next);
// Catch obsolete Phase 3.5 references anywhere in any carved child,
// including prose or prompts that could contradict otherwise-correct headings.
const known = new Set(['0', '0.5', '1', '2', '2.5', '3', '4']);
const mentioned = [...section.matchAll(/\bPhase (\d+(?:\.\d+)?)\b/gi)].map(m => m[1]);
expect(mentioned.filter(id => !known.has(id))).toEqual([]);
});
test('each later Codex voice receives only already-completed phase context', () => {
const dx = read('autoplan/sections/dx-phase.md.tmpl');
const priorContext = [...dx.matchAll(/^\s*(CEO|Design|Eng|DX): <insert /gm)].map(m => m[1]);
expect(priorContext).toEqual(['CEO', 'Design']);
// Eng's Codex voice sees every prior phase's consensus, DX included.
expect(read('autoplan/sections/eng-phase.md.tmpl')).toContain(
'DX: <insert DX consensus table summary',
);
});
test('single final gate: premises queue for the gate, never a mid-run stop', () => {
expect(flat(tmpl)).toMatch(/never auto-decide user challenges/i);
expect(flat(tmpl)).toMatch(/ask once at final approval gate, never mid-run/i);
expect(tmpl).not.toContain('Premise gate passed (user confirmed)');
const ceo = read('autoplan/sections/ceo-phase.md.tmpl');
expect(ceo).not.toContain('GATE: Present premises to user for confirmation');
expect(flat(ceo)).toContain('as User Challenges for Phase 4');
expect(flat(ceo)).toMatch(/never stop mid-pipeline/i);
});
test('generated workflow loads each complete skill at its own phase boundary', () => {
const skill = read('autoplan/SKILL.md');
const phase0 = skill.slice(skill.indexOf('### Step 3:'), skill.indexOf('## Phase 1:'));
const setup = flat(phase0.split('**Section skip list')[0]!);
expect(setup).toMatch(/never prefetch future phases/i);
expect(setup).toMatch(/missing skill: report phase and setup repair/i);
// Locating paths at intake does not load or execute their future phases.
expect(phase0.split('**Section skip list')[0]!).not.toMatch(/^Read `[^`]+\/SKILL\.md` in full now/gm);
const owners = [
{ id: '1', name: 'ceo', next: '## Phase 2:' },
{ id: '2', name: 'design', next: '## Phase 2.5:' },
{ id: '2.5', name: 'devex', next: '## Phase 3:' },
{ id: '3', name: 'eng', next: '## Decision Audit Trail' },
];
for (const { id, name, next } of owners) {
const start = skill.indexOf(`## Phase ${id}:`);
const end = skill.indexOf(next, start);
expect(start).toBeGreaterThan(-1);
expect(end).toBeGreaterThan(start);
const block = skill.slice(start, end);
const child = name === 'devex' ? 'dx' : name;
expect(block).toContain(`/autoplan/sections/${child}-phase.md`);
// The phase checkpoint binds the installed host's complete methodology.
// A second runtime-root Read would select another harness's skill.
expect(block).not.toMatch(/^Read `[^`]+\/SKILL\.md` in full now/gm);
const phase = read(`autoplan/sections/${child}-phase.md`);
const load = phase.indexOf('Before dispatch, Read `methodologyPath`');
expect(load).toBeGreaterThanOrEqual(0);
expect(phase).toContain(`methodology ${child} "<REVIEW_SKILL>" "<RESTORE_PATH>"`);
expect(phase).toContain('per `readRanges`');
expect(load).toBeLessThan(phase.indexOf(`create ${child} `));
if (id === '2' || id === '2.5') {
expect(block.indexOf('**Skip condition:**')).toBeLessThan(block.indexOf('> **STOP.**'));
}
}
const gate = skill.indexOf('## Phase 4: Final Approval Gate');
expect(gate).toBeGreaterThan(-1);
expect(skill.indexOf('Read `~/.claude/skills/gstack/autoplan/sections/tasks-aggregator.md`'))
.toBeGreaterThan(gate);
});
});
describe('autoplan phase execution checkpoints', () => {
const tmpl = read('autoplan/SKILL.md.tmpl');
const phases = ['ceo', 'design', 'dx', 'eng'];
test('loads full review skills at phase entry instead of prefetching future phases', () => {
const intake = flat(tmpl.split('### Step 3:')[1]?.split('## Phase 0.5:')[0] ?? '');
expect(intake).toContain('`<REVIEW_SKILL>`');
expect(intake).toMatch(/never prefetch future phases/i);
for (const phase of phases) {
const section = read(`autoplan/sections/${phase}-phase.md.tmpl`);
expect(section).toMatch(/^Before dispatch, Read \{\{AUTOPLAN_REVIEW_FILE:plan-[a-z-]+:with-sections\}\}/);
const load = section.split('**Override rules:**')[0]!;
expect(load).toContain('per `readRanges`');
expect(load).toMatch(/to EOF/);
expect(load).toMatch(/skip-listed: load only/i);
expect(section.indexOf(':with-sections}}')).toBeLessThan(section.indexOf('create ' + phase));
expect(section).toContain(`create ${phase} "<ACTIVE_PLAN>" "<RESTORE_PATH>" "<methodologyPath>"`);
}
});
for (const phase of phases) {
test(`${phase} places a blocking native dispatch and its completion wait before outside review`, () => {
const section = read(`autoplan/sections/${phase}-phase.md.tmpl`);
const native = section.indexOf(`**{{NATIVE_LABEL}} ${phase === 'design' ? 'design' : phase === 'dx' ? 'DX' : phase === 'ceo' ? 'CEO' : 'eng'} subagent**`);
const outside = section.indexOf('{{OUTSIDE_INVOCATION:autoplan}}');
expect(native).toBeGreaterThan(-1);
expect(native).toBeLessThan(outside);
const dispatch = section.slice(native, outside);
const words = flat(dispatch);
expect(dispatch).toContain('run_in_background: false');
expect(dispatch).toContain('isAsync: true');
expect(words).toMatch(/final tool call/i);
expect(words).toMatch(/end response immediately/i);
expect(words).toMatch(/no further tool calls\/review until/i);
expect(dispatch).toContain('`nativePromptPath` to EOF');
expect(words).toMatch(/INPUT must match snapshot phase\/hash/i);
expect(words).toMatch(/retry invalid input once/i);
expect(words).toMatch(/no inline substitute/i);
// Provider preflight, timeout and native fallback remain at every call.
// The invocation states its own outer gate; a second, larger number would contradict it.
expect(section).not.toContain('Outer tool timeout');
expect(flat(section)).toMatch(/both retain the native pass/i);
expect(section).toContain(`{{OUTSIDE_PROVENANCE:${phase}}}`);
expect(section).toContain(phase === 'ceo' ? 'Outside disabled/unavailable' : 'Missing/disabled');
expect(section).toContain('N/A');
expect(section).toMatch(/primary cannot replace/i);
expect(section).toMatch(/(never|not) CONFIRMED/);
});
}
test('the parent completes only the current phase and cannot waive native work for context pressure', () => {
const contract = flat(tmpl.split('## Sequential Execution')[1]?.split('---')[0] ?? '');
expect(contract).toMatch(/keep one phase active/i);
expect(contract).toMatch(/never draft future-phase reviews or outputs/i);
expect(contract).toMatch(/after compaction, reload current phase instructions/i);
ordered(contract, ['Load its phase instructions', 'create the fresh snapshot', 'nativeDispatchPrompt',
'Consume the native terminal result', 'remaining primary review sections']);
ordered(contract, ['`phase-close`', 'parent completion message', 'Only after the message has been sent may the driver',
'after Eng, proceed to final synthesis/approval']);
expect(contract).toMatch(/send the parent completion message/i);
expect(contract).toMatch(/do not load its review or close steps/i);
expect(contract).toMatch(/resume its first incomplete numbered operation/i);
expect(contract).toContain('step 6 (Publish)');
expect(contract).toMatch(/a missing gate means the current phase remains open/i);
expect(contract).toMatch(/do not prove uptake/i);
expect(contract).toMatch(/pending is not unavailable/i);
expect(contract).toMatch(/never skip native passes\/required sections/i);
expect(contract).toMatch(/never read raw agent transcripts/i);
const rerun = flat(tmpl.split('**Starting an affected-phase rerun:**')[1]!.split('---')[0]!);
expect(rerun).toMatch(/verbatim/i);
expect(rerun).toContain('`baselineEdits.record` and `sourceSha256`');
expect(flat(tmpl)).toContain('`amend-input`');
});
test('each phase binds its fixed amendment checkpoint before loading the shared close', () => {
for (const [phase, number] of [['ceo', '1'], ['design', '2'], ['dx', '2.5'], ['eng', '3']]) {
const section = read(`autoplan/sections/${phase}-phase.md.tmpl`);
const barrier = section.indexOf('**Close this phase:**');
const pointer = section.indexOf('{{SECTION:phase-close}}');
expect(barrier).toBeGreaterThan(-1);
expect(pointer).toBeGreaterThan(barrier);
expect(closePacket(phase).report.number).toBe(number);
expect(section.match(/\{\{SECTION:phase-close\}\}/g)).toHaveLength(1);
const binding = flat(section.slice(barrier, pointer));
const checkpoint = phase === 'ceo' ? 'CEO_STEP0_CHECKPOINT' : `${phase.toUpperCase()}_INPUT`;
expect(binding).toContain(`Use phase \`${phase}\`, checkpoint \`<${checkpoint}>\``);
expect(binding).toContain('`methodologyPath`');
expect(binding).toMatch(/afresh/i);
expect(binding).toMatch(/review exports do not replace it/i);
expect(section.slice(pointer).trim()).toBe('{{SECTION:phase-close}}');
}
});
test('the shared close separates complete readback, verification, publication and driver return', () => {
const template = read('autoplan/sections/phase-close.md.tmpl');
const close = flat(template);
const stages = ['1. **Finish and save the review.**', '2. **Reconcile accepted requirements.**',
"3. **Prepare this phase's close packet.**", '4. **Read the complete current packet.**',
'5. **Verify the current implementation.**', '6. **Publish the parent report.**', '7. **Return to the driver.**'];
const positions = stages.map(stage => template.indexOf(stage));
ordered(template, stages);
expect(close).toMatch(/terminal reviewer results/i);
expect(close).toMatch(/INPUT to its voice snapshot/i);
expect(close).toMatch(/a pending reviewer keeps the phase open/i);
expect(close).toMatch(/unavailable\/disabled voices receive no completion credit/i);
expect(close).toMatch(/user challenges preserve the original requirements/i);
expect(close).toMatch(/a `None` record must explain/i);
expect(close).toMatch(/keep the amendment checkpoint fixed/i);
expect(template).toContain('prepare-close "<PHASE>" "<ACTIVE_PLAN>" "<AMENDMENT_CHECKPOINT>" "<RESTORE_PATH>" "<methodologyPath>"');
expect(close).toContain('`readRanges`');
expect(close).toContain("exact `offset` and `limit`");
expect(close).toMatch(/through EOF/i);
expect(close).toMatch(/do not advance on a request without its result/i);
expect(close).toMatch(/returns to step 3/i);
const verification = flat(template.slice(positions[4], positions[5]));
expect(verification).toMatch(/compare the complete current implementation with accepted decisions/i);
expect(verification).toContain('Recheck step 1');
expect(verification).toMatch(/keep this phase open/i);
expect(close).not.toContain('The packet owns the close continuation');
expect(read('autoplan/sections/phase-close.md')).toContain(template.trim());
});
test('native publication precedes driver continuation without a repair bypass or user wait', () => {
const close = flat(read('autoplan/sections/phase-close.md.tmpl'));
const publish = close.indexOf('6. **Publish the parent report.**');
const report = close.indexOf('**Phase <report.number> complete.**');
const continueAt = close.indexOf('7. **Return to the driver.**');
expect(publish).toBeGreaterThan(-1);
expect(report).toBeGreaterThan(publish);
expect(continueAt).toBeGreaterThan(report);
expect(close.slice(publish, report)).toMatch(/send the filled report[^.]*visible parent assistant text/i);
expect(close.slice(publish, report)).toMatch(/before any next-phase tool call/i);
expect(close.slice(publish, report)).toMatch(/confirmed counts require both voices/i);
expect(close.slice(continueAt)).toMatch(/in the same turn/i);
expect(close.slice(continueAt)).toMatch(/the driver alone advances phases/i);
expect(close.slice(continueAt)).toMatch(/do not wait for a “continue” reply/i);
expect(close.slice(continueAt)).toMatch(/a skip is never a completion/i);
expect(close).toMatch(/printing it through Bash does not publish it/i);
expect(close).not.toContain('This message contains no tool calls');
});
test('all four phase formats consume the current packet data in the shared publication step', () => {
const totals: Record<string, string> = {ceo: '6', design: 'rows in the completed design litmus scorecard', dx: '6', eng: '6'};
const numbers: Record<string, string> = {ceo: '1', design: '2', dx: '2.5', eng: '3'};
const publication = read('autoplan/sections/phase-close.md.tmpl').split('6. **Publish the parent report.**')[1]!.split('7. **Return')[0]!;
expect(publication).toContain('**Phase <report.number> complete.**');
expect(publication).toContain('Outside review: <completed: N concerns / unavailable / disabled>');
expect(publication).toContain('Native subagent: <completed: N issues / unavailable>');
expect(publication).toContain('N/A (voice coverage missing)');
expect(publication).toContain('X/<report.total> native+outside confirmed');
expect(publication).toContain('`report.includeDxMetrics`');
expect(publication).toContain('DX overall: <score>/10. TTHW: <observed> min → <target> min.');
expect(publication).toContain('`report.next`');
for (const child of phases) {
const packet = closePacket(child);
expect(packet.report.number).toBe(numbers[child]);
expect(packet.report.total).toBe(totals[child]);
expect(packet.report.includeDxMetrics).toBe(child === 'dx');
expect(packet.phaseComplete).toBe(false);
const continuation = packet.text.split('## Return to the close procedure')[1]!;
expect(continuation).toContain('The following unfilled template is not a completed report');
expect(continuation).toContain(`**Phase ${numbers[child]} complete.**`);
expect(continuation).toContain(`Passing to <applicable ${packet.report.next}>.`);
expect(continuation).toContain('Preparation and a Read result complete neither verification nor publication');
const caller = read(`autoplan/sections/${child}-phase.md.tmpl`).split('**Close this phase:**')[1]!;
expect(caller.trim().endsWith('{{SECTION:phase-close}}')).toBe(true);
expect(caller).not.toContain('**Phase ');
expect(caller).not.toContain('Passing to ');
}
});
test('Design hands off to conditional DX and DX never requests a future Eng result', () => {
const design = read('autoplan/sections/design-phase.md.tmpl');
const dx = read('autoplan/sections/dx-phase.md.tmpl');
expect(closePacket('design').report.next).toContain('Phase 2.5 (DX Review) if DX scope was detected; otherwise Phase 3 (Eng Review)');
expect(design).not.toContain('> Passing to Phase 3.');
expect(dx).toContain("Design: <insert Design consensus summary, or 'skipped, no UI scope'>");
expect(dx).not.toContain('Eng: <insert Eng consensus summary>');
});
});
describe('autoplan current implementation-plan identity', () => {
test('pins the assigned active plan and keeps accepted amendments separate from review analyses', () => {
const intake = flat(read('autoplan/SKILL.md.tmpl').split('## Phase 0: Intake')[1]?.split('### Step 2:')[0] ?? '');
expect(intake).toContain('ACTIVE_PLAN (harness-assigned plan, else SOURCE_PLAN)');
expect(intake).toMatch(/save plan amendments and review artifacts to ACTIVE_PLAN/i);
expect(intake).toMatch(/final approval request in the conversation/i);
expect(intake).toMatch(/init backs up SOURCE_PLAN exactly/i);
expect(intake).toContain('init "<SOURCE_PLAN>" "<ACTIVE_PLAN>" "<RESTORE_PATH>"');
expect(intake).toMatch(/on helper errors, stop/i);
expect(intake).toContain('`## Review record`');
// Binding belongs to the lazy execution site, not a stale intake variable.
expect(intake).not.toContain('Bind `<review_plan_path>`');
});
test('DX scope consumes the deterministic full-input result and permits only enabling overrides', () => {
const intake = flat(read('autoplan/SKILL.md.tmpl').split('### Step 2: Read context')[1]?.split('### Step 3:')[0] ?? '');
expect(intake).toContain('scope "<ACTIVE_PLAN>"');
expect(intake).toContain('`dxRequired`');
expect(intake).toMatch(/threshold is 2\+ term matches/);
expect(intake).toContain('`--developer-tool` or `--agent-primary`');
expect(intake).toMatch(/no context label can negate a positive result/i);
});
test('every native and outside call site binds the fresh snapshot, retaining requested outside consensus', () => {
for (const phase of ['ceo', 'design', 'dx', 'eng']) {
const section = read(`autoplan/sections/${phase}-phase.md.tmpl`);
const bind = section.indexOf("**Bind phase input:**");
const native = section.indexOf('subagent**');
const outside = section.indexOf('{{OUTSIDE_INVOCATION:autoplan}}');
expect(bind).toBeGreaterThan(-1);
expect(bind).toBeLessThan(native);
expect(native).toBeLessThan(outside);
const preparation = section.slice(bind, native);
expect(preparation).toContain(`create ${phase} "<ACTIVE_PLAN>" "<RESTORE_PATH>"`);
expect(preparation).toContain('`snapshotPath` as `<' + phase.toUpperCase() + '_INPUT>`');
expect(preparation).toContain('`Review record`');
expect(flat(section)).toMatch(/send its `nativeDispatchPrompt` verbatim/i);
expect(section).toContain('Read `snapshot.json` beside `<' + phase.toUpperCase() + '_INPUT>`');
expect(section).toContain('Reads `nativePromptPath` to EOF');
expect(section).toContain(`Outside prompt: inline the full contents of <${phase.toUpperCase()}_INPUT>`);
const checkpoint = phase === 'ceo' ? 'CEO_STEP0_CHECKPOINT' : `${phase.toUpperCase()}_INPUT`;
expect(section).toContain(`Use phase \`${phase}\`, checkpoint \`<${checkpoint}>\``);
expect(section).toContain('{{SECTION:phase-close}}');
expect(section).not.toContain('<review_plan_path>');
expect(section).not.toContain('<plan_path>');
expect(section).toMatch(/no summaries or prior reviews/i);
}
expect(read('autoplan/sections/phase-close.md.tmpl')).toContain('prepare-close "<PHASE>" "<ACTIVE_PLAN>" "<AMENDMENT_CHECKPOINT>"');
expect(read('autoplan/sections/eng-phase.md.tmpl')).toContain('DX: <insert DX consensus table summary');
});
});
describe('phase-close control ownership across installed hosts', () => {
test.each(ALL_HOST_CONFIGS)('$name loads or inlines the same numbered close procedure', host => {
const ctx = {host: host.name, paths: HOST_PATHS[host.name], skillName: 'autoplan', tmplPath: ''} as TemplateContext;
const rendered = SECTION(ctx, ['phase-close']);
const template = read('autoplan/sections/phase-close.md.tmpl').trimEnd();
if (host.name === 'claude') {
expect(rendered).toContain(`${ctx.paths.skillRoot}/autoplan/sections/phase-close.md`);
expect(rendered).toContain('and execute it');
} else {
expect(rendered).toBe(template);
ordered(rendered, ['4. **Read the complete current packet.**', '5. **Verify the current implementation.**',
'6. **Publish the parent report.**', '7. **Return to the driver.**']);
expect(rendered).toMatch(/in the same turn/i);
expect(rendered).toMatch(/do not wait for a “continue” reply/i);
expect(rendered).not.toContain('The packet owns the close continuation');
}
});
});