diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 05023fdeb..4e4cbff60 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -193,6 +193,11 @@ function cmdFindPhase(cwd, phase, raw) { } } +function extractObjective(content) { + const m = content.match(/\s*\n?\s*(.+)/); + return m ? m[1].trim() : null; +} + function cmdPhasePlanIndex(cwd, phase, raw) { if (!phase) { error('phase required for phase-plan-index'); @@ -242,9 +247,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) { const content = fs.readFileSync(planPath, 'utf-8'); const fm = extractFrontmatter(content); - // Count tasks (## Task N patterns) - const taskMatches = content.match(/##\s*Task\s*\d+/gi) || []; - const taskCount = taskMatches.length; + // Count tasks: XML tags (canonical) or ## Task N markdown (legacy) + const xmlTasks = content.match(/]/gi) || []; + const mdTasks = content.match(/##\s*Task\s*\d+/gi) || []; + const taskCount = xmlTasks.length || mdTasks.length; // Parse wave as integer const wave = parseInt(fm.wave, 10) || 1; @@ -259,10 +265,11 @@ function cmdPhasePlanIndex(cwd, phase, raw) { hasCheckpoints = true; } - // Parse files-modified + // Parse files_modified (underscore is canonical; also accept hyphenated for compat) let filesModified = []; - if (fm['files-modified']) { - filesModified = Array.isArray(fm['files-modified']) ? fm['files-modified'] : [fm['files-modified']]; + const fmFiles = fm['files_modified'] || fm['files-modified']; + if (fmFiles) { + filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles]; } const hasSummary = completedPlanIds.has(planId); @@ -274,7 +281,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) { id: planId, wave, autonomous, - objective: fm.objective || null, + objective: extractObjective(content) || fm.objective || null, files_modified: filesModified, task_count: taskCount, has_summary: hasSummary, diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index 21d6d2951..f1888eb3c 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -358,6 +358,228 @@ objective: Manual review needed }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// phase-plan-index — canonical XML format (template-aligned) +// ───────────────────────────────────────────────────────────────────────────── + +describe('phase-plan-index canonical format', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('files_modified: underscore key is parsed correctly', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '04-ui'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '04-01-PLAN.md'), + `--- +wave: 1 +autonomous: true +files_modified: [src/App.tsx, src/index.ts] +--- + + +Build main application shell + +Purpose: Entry point +Output: App component + + + + + Task 1: Create App component + src/App.tsx + Create component + npm run build + Component renders + + +` + ); + + const result = runGsdTools('phase-plan-index 04', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.deepStrictEqual( + output.plans[0].files_modified, + ['src/App.tsx', 'src/index.ts'], + 'files_modified with underscore should be parsed' + ); + }); + + test('objective: extracted from XML tag, not frontmatter', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '04-ui'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '04-01-PLAN.md'), + `--- +wave: 1 +autonomous: true +files_modified: [] +--- + + +Build main application shell + +Purpose: Entry point for the SPA +Output: App.tsx with routing + + + + + Task 1: Scaffold + src/App.tsx + Create shell + build passes + App renders + + +` + ); + + const result = runGsdTools('phase-plan-index 04', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual( + output.plans[0].objective, + 'Build main application shell', + 'objective should come from XML tag first line' + ); + }); + + test('task_count: counts XML tags', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '04-ui'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '04-01-PLAN.md'), + `--- +wave: 1 +autonomous: true +files_modified: [] +--- + + +Create UI components + + + + + Task 1: Header + src/Header.tsx + Create header + build + Header renders + + + + Task 2: Footer + src/Footer.tsx + Create footer + build + Footer renders + + + + UI components + Visit localhost:3000 + Type approved + + +` + ); + + const result = runGsdTools('phase-plan-index 04', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual( + output.plans[0].task_count, + 3, + 'should count all 3 XML tags' + ); + }); + + test('all three fields work together in canonical plan format', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '04-ui'); + fs.mkdirSync(phaseDir, { recursive: true }); + + fs.writeFileSync( + path.join(phaseDir, '04-01-PLAN.md'), + `--- +phase: 04-ui +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: [src/components/Chat.tsx, src/app/api/chat/route.ts] +autonomous: true +requirements: [R1, R2] +--- + + +Implement complete Chat feature as vertical slice. + +Purpose: Self-contained chat that can run parallel to other features. +Output: Chat component, API endpoints. + + + +@~/.claude/get-shit-done/workflows/execute-plan.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md + + + + + Task 1: Create Chat component + src/components/Chat.tsx + Build chat UI with message list and input + npm run build + Chat component renders messages + + + + Task 2: Create Chat API + src/app/api/chat/route.ts + GET /api/chat and POST /api/chat endpoints + curl tests pass + CRUD operations work + + + + +- [ ] npm run build succeeds +- [ ] API endpoints respond correctly + +` + ); + + const result = runGsdTools('phase-plan-index 04', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + const plan = output.plans[0]; + assert.strictEqual(plan.objective, 'Implement complete Chat feature as vertical slice.', 'objective from XML tag'); + assert.deepStrictEqual(plan.files_modified, ['src/components/Chat.tsx', 'src/app/api/chat/route.ts'], 'files_modified with underscore'); + assert.strictEqual(plan.task_count, 2, 'task_count from XML tags'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // state-snapshot command // ─────────────────────────────────────────────────────────────────────────────