From a4f94c87aabe1fb3d3afb691ff9acb27c6aa68bf Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 12 May 2026 18:50:35 -0400 Subject: [PATCH] fix: align planner plan contract with phase index (#3436) * test: add planner/query contract regressions (#3430) * fix: align planner plan contract with phase index (#3430) * docs: add changeset for #3430 * fix: keep planner contract docs within size budget (#3430) * docs: set changeset pr for #3430 --- .changeset/silly-jaguars-sing.md | 5 +++ agents/gsd-planner.md | 4 +- sdk/src/query/phase.test.ts | 23 ++++++++++ sdk/src/query/phase.ts | 9 ++++ .../bug-3430-planner-phase-contract.test.cjs | 44 +++++++++++++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 .changeset/silly-jaguars-sing.md create mode 100644 tests/bug-3430-planner-phase-contract.test.cjs diff --git a/.changeset/silly-jaguars-sing.md b/.changeset/silly-jaguars-sing.md new file mode 100644 index 000000000..ce83abf1c --- /dev/null +++ b/.changeset/silly-jaguars-sing.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3436 +--- +**`/gsd-plan-phase` now documents phase-plan-index-compatible plan conventions** — planner guidance uses canonical `depends_on` and SUMMARY forms, and native phase indexing warns when it ignores noncanonical plan filenames. diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index 37e39c7bb..b23a1d3c8 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -426,7 +426,7 @@ phase: XX-name plan: NN type: execute wave: N # Execution wave (1, 2, 3...) -depends_on: [] # Plan IDs this plan requires +depends_on: [] # Use `01-01`/`01-01-auth-hardening` files_modified: [] # Files this plan touches autonomous: true # false if plan has checkpoints requirements: [] # REQUIRED — Requirement IDs from ROADMAP this plan addresses. MUST NOT be empty. @@ -496,7 +496,7 @@ Output: [Artifacts created] -After completion, create `.planning/phases/XX-name/{phase}-{plan}-SUMMARY.md` +Create `.planning/phases/XX-name/{padded_phase}-{plan}-SUMMARY.md` when done ``` diff --git a/sdk/src/query/phase.test.ts b/sdk/src/query/phase.test.ts index 6b6477120..2be0e4a59 100644 --- a/sdk/src/query/phase.test.ts +++ b/sdk/src/query/phase.test.ts @@ -505,4 +505,27 @@ describe('phasePlanIndex', () => { expect(planA!.depends_on).toEqual([]); expect(planB!.depends_on).toEqual(['15-01']); }); + + it('#3430: native phase-plan-index warns about noncanonical plan-shaped files it cannot index', async () => { + const phase16 = join(tmpDir, '.planning', 'phases', '16-warning'); + await mkdir(phase16, { recursive: true }); + await writeFile(join(phase16, '16-PLAN-01-eval-harness.md'), [ + '---', + 'phase: 16-warning', + 'plan: 01', + 'wave: 1', + 'autonomous: true', + 'depends_on: []', + '---', + 'Noncanonical plan filename.', + ].join('\n')); + + const result = await phasePlanIndex(['16'], tmpDir); + const data = result.data as Record; + + expect(data.plans).toEqual([]); + expect(data.warnings).toEqual([ + 'Ignored noncanonical plan files: 16-PLAN-01-eval-harness.md', + ]); + }); }); diff --git a/sdk/src/query/phase.ts b/sdk/src/query/phase.ts index 4fbd249a7..f42272f83 100644 --- a/sdk/src/query/phase.ts +++ b/sdk/src/query/phase.ts @@ -278,6 +278,11 @@ export const phasePlanIndex: QueryHandler = async (args, projectDir, workstream) const phaseFiles = await readdir(phaseDir); const planFiles = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort(); const summaryFiles = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'); + const nonCanonicalPlanFiles = phaseFiles.filter((f) => ( + f.toLowerCase().endsWith('.md') + && /(^|-)plan(-|\.)/i.test(f) + && !(f.endsWith('-PLAN.md') || f === 'PLAN.md') + )).sort(); // Build set of plan IDs with summaries — match the planId derivation logic const completedPlanIds = new Set( @@ -433,6 +438,10 @@ export const phasePlanIndex: QueryHandler = async (args, projectDir, workstream) let hasCheckpoints = false; const warnings: string[] = []; + if (nonCanonicalPlanFiles.length > 0) { + warnings.push(`Ignored noncanonical plan files: ${nonCanonicalPlanFiles.join(', ')}`); + } + for (const raw of rawPlans) { if (!raw.autonomous) { hasCheckpoints = true; diff --git a/tests/bug-3430-planner-phase-contract.test.cjs b/tests/bug-3430-planner-phase-contract.test.cjs new file mode 100644 index 000000000..fadca144c --- /dev/null +++ b/tests/bug-3430-planner-phase-contract.test.cjs @@ -0,0 +1,44 @@ +// allow-test-rule: source-text-is-the-product +// Planner markdown is the deployed planning contract; these checks lock the +// exact canonical forms that downstream phase-plan-index accepts. + +'use strict'; + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const PLANNER_PATH = path.join(__dirname, '..', 'agents', 'gsd-planner.md'); + +function readPlanner() { + return fs.readFileSync(PLANNER_PATH, 'utf8'); +} + +test('#3430: planner SUMMARY instruction uses canonical padded phase/plan form', () => { + const content = readPlanner(); + assert.match( + content, + /Create `\.planning\/phases\/XX-name\/\{padded_phase\}-\{plan\}-SUMMARY\.md` when done/, + 'planner must instruct executors to write SUMMARY files in canonical padded-phase form' + ); + assert.doesNotMatch( + content, + /After completion, create `\.planning\/phases\/XX-name\/\{phase\}-\{plan\}-SUMMARY\.md`/, + 'planner must not instruct the broken {phase}-{plan}-SUMMARY.md form' + ); +}); + +test('#3430: planner depends_on docs show canonical in-phase plan ids', () => { + const content = readPlanner(); + assert.match( + content, + /depends_on:[^\n]*Use `01-01`\/`01-01-auth-hardening`/, + 'planner must document canonical depends_on examples that phase-plan-index resolves' + ); + assert.doesNotMatch( + content, + /depends_on:[^\n]*01-trust\/01/, + 'planner must not document phase-slug/plan-number depends_on examples as canonical' + ); +});