fix(state): treat zero-slice roadmap as pre-planning instead of blocked (#1826)

Fixes #1785

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-21 14:38:59 -04:00 committed by GitHub
parent 1a70f9daea
commit b9a2a9a37b
2 changed files with 46 additions and 0 deletions

View file

@ -550,6 +550,30 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
};
}
// ── 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");

View file

@ -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();
}