mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
* 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>
414 lines
24 KiB
TypeScript
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');
|
|
}
|
|
});
|
|
});
|