* 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:
@@ -229,4 +229,92 @@ describe('initManager', () => {
|
||||
expect(typeof flags.plan).toBe('string');
|
||||
expect(typeof flags.execute).toBe('string');
|
||||
});
|
||||
|
||||
// ── queued_phases (#2497) ─────────────────────────────────────────────
|
||||
describe('queued_phases (#2497)', () => {
|
||||
const MULTI_MILESTONE = [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'## Milestone v1.0: Old — ✅ SHIPPED 2026-01-01',
|
||||
'',
|
||||
'Shipped.',
|
||||
'',
|
||||
'## Milestone v2.0.5: Current',
|
||||
'',
|
||||
'### Phase 35: Audit',
|
||||
'**Goal**: Audit schemas.',
|
||||
'**Depends on**: None',
|
||||
'',
|
||||
'## 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');
|
||||
|
||||
beforeEach(async () => {
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), MULTI_MILESTONE);
|
||||
await writeFile(join(tmpDir, '.planning', 'STATE.md'), [
|
||||
'---',
|
||||
'milestone: v2.0.5',
|
||||
'milestone_name: Current',
|
||||
'---',
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('surfaces the next milestone in queued_phases with metadata', async () => {
|
||||
const result = await initManager([], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
expect(data.queued_milestone_version).toBe('v2.1');
|
||||
expect(data.queued_milestone_name).toBe('Daily Emails');
|
||||
const queued = data.queued_phases as Record<string, unknown>[];
|
||||
expect(queued.map(p => p.number)).toEqual(['31', '32']);
|
||||
// Only the NEXT milestone's phases appear — not v2.2's Phase 99.
|
||||
expect(queued.find(p => p.number === '99')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('queued_phases entries carry name, deps_display, and display_name', async () => {
|
||||
const result = await initManager([], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
const queued = data.queued_phases as Record<string, unknown>[];
|
||||
const p32 = queued.find(p => p.number === '32');
|
||||
expect(p32).toBeDefined();
|
||||
expect(p32!.name).toBe('Sending');
|
||||
expect(p32!.deps_display).toBe('31');
|
||||
expect(typeof p32!.display_name).toBe('string');
|
||||
});
|
||||
|
||||
it('does NOT mix queued phases into the active phases list', async () => {
|
||||
const result = await initManager([], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
const active = (data.phases as Record<string, unknown>[]).map(p => p.number);
|
||||
// Active milestone is v2.0.5 → only Phase 35 belongs here.
|
||||
expect(active).toContain('35');
|
||||
expect(active).not.toContain('31');
|
||||
expect(active).not.toContain('32');
|
||||
});
|
||||
|
||||
it('returns empty queued_phases and null metadata when active is last milestone', async () => {
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), [
|
||||
'## Milestone v2.0.5: Only Milestone',
|
||||
'',
|
||||
'### Phase 35: Audit',
|
||||
'**Goal**: Final.',
|
||||
].join('\n'));
|
||||
const result = await initManager([], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
expect(data.queued_phases).toEqual([]);
|
||||
expect(data.queued_milestone_version).toBeNull();
|
||||
expect(data.queued_milestone_name).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user