fix: phase-plan-index returns null/empty for files_modified, objective, task_count (#734)

cmdPhasePlanIndex had 3 mismatches with the canonical XML plan format
defined in templates/phase-prompt.md:

- files_modified: looked up fm['files-modified'] (hyphen) but plans use
  files_modified (underscore). Now checks underscore first, hyphen fallback.
- objective: read from YAML frontmatter but plans put it in <objective>
  XML tag. Now extracts first line from the tag, falls back to frontmatter.
- task_count: matched ## Task N markdown headings but plans use <task>
  XML tags. Now counts XML tags first, markdown fallback.

All three fixes preserve backward compat with legacy markdown-style plans.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ROMB
2026-02-27 19:58:36 +01:00
committed by GitHub
parent b69a8de83b
commit 3a417522c9
2 changed files with 236 additions and 7 deletions

View File

@@ -193,6 +193,11 @@ function cmdFindPhase(cwd, phase, raw) {
}
}
function extractObjective(content) {
const m = content.match(/<objective>\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 <task> tags (canonical) or ## Task N markdown (legacy)
const xmlTasks = content.match(/<task[\s>]/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,

View File

@@ -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]
---
<objective>
Build main application shell
Purpose: Entry point
Output: App component
</objective>
<tasks>
<task type="auto">
<name>Task 1: Create App component</name>
<files>src/App.tsx</files>
<action>Create component</action>
<verify>npm run build</verify>
<done>Component renders</done>
</task>
</tasks>
`
);
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 <objective> 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: []
---
<objective>
Build main application shell
Purpose: Entry point for the SPA
Output: App.tsx with routing
</objective>
<tasks>
<task type="auto">
<name>Task 1: Scaffold</name>
<files>src/App.tsx</files>
<action>Create shell</action>
<verify>build passes</verify>
<done>App renders</done>
</task>
</tasks>
`
);
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 <objective> XML tag first line'
);
});
test('task_count: counts <task> 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: []
---
<objective>
Create UI components
</objective>
<tasks>
<task type="auto">
<name>Task 1: Header</name>
<files>src/Header.tsx</files>
<action>Create header</action>
<verify>build</verify>
<done>Header renders</done>
</task>
<task type="auto">
<name>Task 2: Footer</name>
<files>src/Footer.tsx</files>
<action>Create footer</action>
<verify>build</verify>
<done>Footer renders</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>UI components</what-built>
<how-to-verify>Visit localhost:3000</how-to-verify>
<resume-signal>Type approved</resume-signal>
</task>
</tasks>
`
);
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 <task> 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]
---
<objective>
Implement complete Chat feature as vertical slice.
Purpose: Self-contained chat that can run parallel to other features.
Output: Chat component, API endpoints.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create Chat component</name>
<files>src/components/Chat.tsx</files>
<action>Build chat UI with message list and input</action>
<verify>npm run build</verify>
<done>Chat component renders messages</done>
</task>
<task type="auto">
<name>Task 2: Create Chat API</name>
<files>src/app/api/chat/route.ts</files>
<action>GET /api/chat and POST /api/chat endpoints</action>
<verify>curl tests pass</verify>
<done>CRUD operations work</done>
</task>
</tasks>
<verification>
- [ ] npm run build succeeds
- [ ] API endpoints respond correctly
</verification>
`
);
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 <task> XML tags');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// state-snapshot command
// ─────────────────────────────────────────────────────────────────────────────