diff --git a/src/resources/extensions/gsd/state.ts b/src/resources/extensions/gsd/state.ts index f14f83ad5..dc09a9003 100644 --- a/src/resources/extensions/gsd/state.ts +++ b/src/resources/extensions/gsd/state.ts @@ -79,7 +79,12 @@ export async function getActiveMilestoneId(basePath: string): Promise { for (const mid of milestoneIds) { const rf = resolveMilestoneFile(basePath, mid, "ROADMAP"); const rc = rf ? await loadFile(rf) : null; - if (!rc) continue; + if (!rc) { + // No roadmap — milestone is complete if it has a summary + const sf = resolveMilestoneFile(basePath, mid, "SUMMARY"); + if (sf) completeMilestoneIds.add(mid); + continue; + } const rmap = parseRoadmap(rc); if (!isMilestoneComplete(rmap)) continue; const sf = resolveMilestoneFile(basePath, mid, "SUMMARY"); @@ -134,7 +144,18 @@ export async function deriveState(basePath: string): Promise { const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP"); const content = roadmapFile ? await loadFile(roadmapFile) : null; if (!content) { - // No roadmap yet — treat as incomplete/active + // No roadmap — check if a summary exists (completed milestone without roadmap) + const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY"); + if (summaryFile) { + const summaryContent = await loadFile(summaryFile); + const summaryTitle = summaryContent + ? (parseSummary(summaryContent).title || mid) + : mid; + registry.push({ id: mid, title: summaryTitle, status: 'complete' }); + completeMilestoneIds.add(mid); + continue; + } + // No roadmap and no summary — treat as incomplete/active if (!activeMilestoneFound) { activeMilestone = { id: mid, title: mid }; activeMilestoneFound = true; diff --git a/src/resources/extensions/gsd/tests/derive-state.test.ts b/src/resources/extensions/gsd/tests/derive-state.test.ts index 4f485a718..210afbe78 100644 --- a/src/resources/extensions/gsd/tests/derive-state.test.ts +++ b/src/resources/extensions/gsd/tests/derive-state.test.ts @@ -618,6 +618,58 @@ Continue from step 2. } } + // ═══ Milestone with summary but no roadmap → complete ═══════════════════ + { + console.log('\n=== milestone with summary and no roadmap → complete ==='); + const base = createFixtureBase(); + try { + // M001, M002: completed milestones with summaries but no roadmaps + const m1dir = join(base, '.gsd', 'milestones', 'M001'); + mkdirSync(m1dir, { recursive: true }); + writeFileSync(join(m1dir, 'M001-SUMMARY.md'), '---\nid: M001\n---\n# Bootstrap\nDone.'); + + const m2dir = join(base, '.gsd', 'milestones', 'M002'); + mkdirSync(m2dir, { recursive: true }); + writeFileSync(join(m2dir, 'M002-SUMMARY.md'), '---\nid: M002\n---\n# Core Features\nDone.'); + + // M003: active milestone with a roadmap + writeRoadmap(base, 'M003', '# M003: Polish\n## Slices\n- [ ] **S01: Cleanup**'); + + const state = await deriveState(base); + + assertEq(state.phase, 'planning', 'summary-no-roadmap: phase is planning (active is M003)'); + assertEq(state.activeMilestone?.id, 'M003', 'summary-no-roadmap: active milestone is M003'); + assertEq(state.activeMilestone?.title, 'Polish', 'summary-no-roadmap: active title is Polish'); + assertEq(state.registry.length, 3, 'summary-no-roadmap: registry has 3 entries'); + assertEq(state.registry[0]?.status, 'complete', 'summary-no-roadmap: M001 is complete'); + assertEq(state.registry[0]?.title, 'Bootstrap', 'summary-no-roadmap: M001 title from summary'); + assertEq(state.registry[1]?.status, 'complete', 'summary-no-roadmap: M002 is complete'); + assertEq(state.registry[1]?.title, 'Core Features', 'summary-no-roadmap: M002 title from summary'); + assertEq(state.registry[2]?.status, 'active', 'summary-no-roadmap: M003 is active'); + assertEq(state.progress?.milestones?.done, 2, 'summary-no-roadmap: milestones done = 2'); + assertEq(state.progress?.milestones?.total, 3, 'summary-no-roadmap: milestones total = 3'); + } finally { + cleanup(base); + } + } + + // ═══ All milestones have summary but no roadmap → complete ═════════════ + { + console.log('\n=== all milestones summary-only → complete ==='); + const base = createFixtureBase(); + try { + const m1dir = join(base, '.gsd', 'milestones', 'M001'); + mkdirSync(m1dir, { recursive: true }); + writeFileSync(join(m1dir, 'M001-SUMMARY.md'), '---\ntitle: Done\n---\nAll done.'); + + const state = await deriveState(base); + assertEq(state.phase, 'complete', 'all-summary-only: phase is complete'); + assertEq(state.registry[0]?.status, 'complete', 'all-summary-only: M001 is complete'); + } finally { + cleanup(base); + } + } + // ═════════════════════════════════════════════════════════════════════════ // Results // ═════════════════════════════════════════════════════════════════════════