diff --git a/src/resources/extensions/gsd/state.ts b/src/resources/extensions/gsd/state.ts index 9aa14e85c..6d15b1c5b 100644 --- a/src/resources/extensions/gsd/state.ts +++ b/src/resources/extensions/gsd/state.ts @@ -470,7 +470,7 @@ async function _deriveStateImpl(basePath: string): Promise { }; 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 { }; } + // 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, diff --git a/src/resources/extensions/gsd/tests/derive-state.test.ts b/src/resources/extensions/gsd/tests/derive-state.test.ts index b750bd058..6c97d31c0 100644 --- a/src/resources/extensions/gsd/tests/derive-state.test.ts +++ b/src/resources/extensions/gsd/tests/derive-state.test.ts @@ -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(); }