fix(state): empty slice plan stays in planning, not summarizing (#454)

A plan file with zero tasks caused `find(t => !t.done)` to return
undefined, which was treated as "all tasks done" → summarizing phase.
Now requires `tasks.length > 0` before entering summarizing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson 2026-03-15 10:04:57 -06:00
parent 65acf67762
commit e147b2dfdf
2 changed files with 57 additions and 1 deletions

View file

@ -470,7 +470,7 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
};
const activeTaskEntry = slicePlan.tasks.find(t => !t.done);
if (!activeTaskEntry) {
if (!activeTaskEntry && slicePlan.tasks.length > 0) {
// All tasks done but slice not marked complete
return {
activeMilestone,
@ -491,6 +491,27 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
};
}
// Empty plan — no tasks defined yet, stay in planning phase
if (!activeTaskEntry) {
return {
activeMilestone,
activeSlice,
activeTask: null,
phase: 'planning',
recentDecisions: [],
blockers: [],
nextAction: `Slice ${activeSlice.id} has a plan file but no tasks. Add tasks to the plan.`,
activeBranch: activeBranch ?? undefined,
registry,
requirements,
progress: {
milestones: milestoneProgress,
slices: sliceProgress,
tasks: taskProgress,
},
};
}
const activeTask: ActiveRef = {
id: activeTaskEntry.id,
title: activeTaskEntry.title,

View file

@ -651,6 +651,41 @@ Continue from step 2.
}
}
// ─── Empty plan (zero tasks) stays in planning, not summarizing (#454) ──
console.log('\n=== empty plan → planning (not summarizing) ===');
{
const base = createFixtureBase();
try {
writeRoadmap(base, 'M001', `---
id: M001
title: "Test"
---
# M001: Test
## Vision
Test
## Success Criteria
- Done
## Slices
- [ ] **S01: Empty slice** \`risk:low\` \`depends:[]\`
> Test
## Boundary Map
_None_
`);
writePlan(base, 'M001', 'S01', `---
slice: S01
---
# S01 Plan
## Tasks
`);
const state = await deriveState(base);
assertEq(state.phase, 'planning', 'empty plan stays in planning');
assertEq(state.activeSlice?.id, 'S01', 'active slice is S01');
assertEq(state.activeTask, null, 'no active task');
} finally {
cleanup(base);
}
}
report();
}