diff --git a/src/resources/extensions/gsd/state.ts b/src/resources/extensions/gsd/state.ts index 738d13a88..c9f85b54e 100644 --- a/src/resources/extensions/gsd/state.ts +++ b/src/resources/extensions/gsd/state.ts @@ -550,6 +550,30 @@ async function _deriveStateImpl(basePath: string): Promise { }; } + // ── Zero-slice roadmap guard (#1785) ───────────────────────────────── + // A stub roadmap (placeholder text, no slice definitions) has a truthy + // roadmap object but an empty slices array. Without this check the + // slice-finding loop below finds nothing and returns phase: "blocked". + // An empty slices array means the roadmap still needs slice definitions, + // so the correct phase is pre-planning. + if (activeRoadmap.slices.length === 0) { + return { + activeMilestone, + activeSlice: null, + activeTask: null, + phase: 'pre-planning', + recentDecisions: [], + blockers: [], + nextAction: `Milestone ${activeMilestone.id} has a roadmap but no slices defined. Add slices to the roadmap.`, + registry, + requirements, + progress: { + milestones: milestoneProgress, + slices: { done: 0, total: 0 }, + }, + }; + } + // Check if active milestone needs validation or completion (all slices done) if (isMilestoneComplete(activeRoadmap)) { const validationFile = resolveMilestoneFile(basePath, activeMilestone.id, "VALIDATION"); diff --git a/src/resources/extensions/gsd/tests/derive-state.test.ts b/src/resources/extensions/gsd/tests/derive-state.test.ts index 550cb567f..c228107a4 100644 --- a/src/resources/extensions/gsd/tests/derive-state.test.ts +++ b/src/resources/extensions/gsd/tests/derive-state.test.ts @@ -957,6 +957,28 @@ slice: S01 } } + // ─── Test: zero-slice roadmap → pre-planning, not blocked (#1785) ──── + console.log('\n=== zero-slice roadmap → pre-planning, not blocked (#1785) ==='); + { + const base = createFixtureBase(); + try { + // Write a stub roadmap with zero slices (placeholder text, no slice definitions) + writeRoadmap(base, 'M001', `# M001: Stub Milestone\n\n**Vision:** Placeholder.\n\n## Slices\n\n_No slices defined yet._\n`); + + const state = await deriveState(base); + + assertEq(state.phase, 'pre-planning', 'phase is pre-planning when roadmap has zero slices'); + assertTrue(state.activeMilestone !== null, 'activeMilestone is set'); + assertEq(state.activeMilestone?.id, 'M001', 'activeMilestone is M001'); + assertEq(state.activeSlice, null, 'activeSlice is null'); + assertEq(state.activeTask, null, 'activeTask is null'); + assertEq(state.blockers.length, 0, 'no blockers reported'); + assertTrue(state.nextAction.includes('M001'), 'nextAction references M001'); + } finally { + cleanup(base); + } + } + report(); }