* feat(sdk): add queued_phases to init.manager (closes #2497) Surfaces the milestone immediately AFTER the active one so the /gsd-manager dashboard can preview upcoming phases without mixing them into the active phases grid. Changes: - roadmap.ts: exports two new helpers - extractPhasesFromSection(section): parses phase number / name / goal / depends_on using the same pattern initManager uses for the active milestone, so queued phases have identical shape. - extractNextMilestoneSection(content, projectDir): resolves the current milestone via the STATE-first path (matching upstream PR #2508) then scans for the next ## milestone heading. Shipped milestones are stripped first so they can't shadow the real next. Returns null when the active milestone is the last one. - init-complex.ts: initManager now exposes - queued_phases: Array<{ number, name, display_name, goal, depends_on, dep_phases, deps_display }> - queued_milestone_version: string | null - queued_milestone_name: string | null Existing phases array is unchanged — callers that only care about the active milestone see no behavior difference. Scope note: PR #2508 (merged upstream 2026-04-21) superseded the #2495 + #2496 portions of this branch's original submission. This commit is the rebased remainder contributing only #2497 on top of upstream's new helpers. Test coverage (7 new tests, all passing): - roadmap.test.ts: +5 tests - extractPhasesFromSection parses multiple phases with goal + deps - extractPhasesFromSection returns [] when no phase headings - extractNextMilestoneSection returns the milestone after the STATE-resolved active one - extractNextMilestoneSection returns null when active is last - extractNextMilestoneSection returns null when no version found - init-complex.test.ts: +4 tests under `queued_phases (#2497)` - surfaces next milestone with version + name metadata - queued entries carry name / deps_display / display_name - queued phases are NOT mixed into active phases list - returns [] + nulls when active is the last milestone All 51 tests in roadmap.test.ts + init-complex.test.ts pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(workflows): render queued_phases section in /gsd-manager dashboard Surfaces the new `queued_phases` / `queued_milestone_version` / `queued_milestone_name` fields from init.manager (SDK #2497) in a compact preview section directly below the main active-milestone table. Changes to workflows/manager.md: - Initialize step: parse the optional trio (queued_milestone_version, queued_milestone_name, queued_phases) alongside the existing init.manager fields. Treat missing as empty for backward compatibility with older SDK versions. - Dashboard step: new "Queued section (next milestone preview)" rendered between the main active-milestone grid and the Recommendations section. Renders only when queued_phases is non-empty; skipped entirely when absent or empty (e.g. active milestone is the last one). - Queued rows render without D/P/E columns since the phases haven't been discussed yet — just number, display_name, deps_display, and a fixed "· Queued" status. - Success criterion added: queued section renders when non-empty and is skipped when absent. Queued phases are deliberately NOT eligible for the Continue action menu; they live in a future milestone. The preview exists for situational awareness only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ import {
|
||||
roadmapGetPhase,
|
||||
getMilestoneInfo,
|
||||
extractCurrentMilestone,
|
||||
extractNextMilestoneSection,
|
||||
extractPhasesFromSection,
|
||||
stripShippedMilestones,
|
||||
} from './roadmap.js';
|
||||
|
||||
@@ -474,3 +476,99 @@ describe('roadmapAnalyze', () => {
|
||||
expect((data1.phases as unknown[]).length).toBe((data2.phases as unknown[]).length);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── extractPhasesFromSection + extractNextMilestoneSection (#2497) ──────
|
||||
|
||||
describe('extractPhasesFromSection', () => {
|
||||
it('parses phase number, name, goal, and depends_on from a milestone section', () => {
|
||||
const section = [
|
||||
'',
|
||||
'### Phase 31: Email Schema',
|
||||
'**Goal**: Set up Prisma models.',
|
||||
'**Depends on**: None',
|
||||
'',
|
||||
'### Phase 32: Today\'s Sheets',
|
||||
'**Goal**: Port the GAS sender.',
|
||||
'**Depends on**: Phase 31',
|
||||
'',
|
||||
].join('\n');
|
||||
const phases = extractPhasesFromSection(section);
|
||||
expect(phases).toEqual([
|
||||
{ number: '31', name: 'Email Schema', goal: 'Set up Prisma models.', depends_on: 'None' },
|
||||
{ number: '32', name: "Today's Sheets", goal: 'Port the GAS sender.', depends_on: 'Phase 31' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns empty array when section has no phase headings', () => {
|
||||
expect(extractPhasesFromSection('no phases here\njust prose.')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractNextMilestoneSection', () => {
|
||||
const MULTI = [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'## Milestone v1.0: Old — ✅ SHIPPED 2026-01-01',
|
||||
'',
|
||||
'Shipped stuff.',
|
||||
'',
|
||||
'## Milestone v2.0.5: Current Milestone',
|
||||
'',
|
||||
'### Phase 35: Audit',
|
||||
'**Goal**: Audit schemas.',
|
||||
'',
|
||||
'## Milestone v2.1: Daily Emails',
|
||||
'',
|
||||
'### Phase 31: Schema',
|
||||
'**Goal**: Build schema.',
|
||||
'**Depends on**: None',
|
||||
'',
|
||||
'### Phase 32: Sending',
|
||||
'**Goal**: Send emails.',
|
||||
'**Depends on**: Phase 31',
|
||||
'',
|
||||
'## Milestone v2.2: Later',
|
||||
'',
|
||||
'### Phase 99: Future',
|
||||
'**Goal**: Later work.',
|
||||
].join('\n');
|
||||
|
||||
it('returns the milestone immediately after the active one (STATE-driven)', async () => {
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), MULTI);
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'STATE.md'),
|
||||
'---\nmilestone: v2.0.5\nmilestone_name: Current Milestone\n---\n',
|
||||
);
|
||||
const next = await extractNextMilestoneSection(MULTI, tmpDir);
|
||||
expect(next).not.toBeNull();
|
||||
expect(next!.version).toBe('v2.1');
|
||||
expect(next!.name).toBe('Daily Emails');
|
||||
// Phases parse correctly from the returned section — only v2.1 phases,
|
||||
// not v2.2's Phase 99.
|
||||
const phases = extractPhasesFromSection(next!.section).map(p => p.number);
|
||||
expect(phases).toEqual(['31', '32']);
|
||||
});
|
||||
|
||||
it('returns null when the active milestone is the last one in ROADMAP', async () => {
|
||||
const roadmap = [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'## Milestone v2.0.5: Last One',
|
||||
'',
|
||||
'### Phase 35: Final',
|
||||
'**Goal**: Final work.',
|
||||
].join('\n');
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), roadmap);
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'STATE.md'),
|
||||
'---\nmilestone: v2.0.5\n---\n',
|
||||
);
|
||||
const next = await extractNextMilestoneSection(roadmap, tmpDir);
|
||||
expect(next).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when no current milestone can be resolved', async () => {
|
||||
const next = await extractNextMilestoneSection('# Roadmap\nno milestones\n', tmpDir);
|
||||
expect(next).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user