diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index a092ffa27..214baa2ba 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -1104,7 +1104,9 @@ function cmdInitManager(cwd, raw) { return true; }); - const completedCount = phases.filter(p => p.disk_status === 'complete').length; + // Exclude backlog phases (999.x) from completion accounting (#2129) + const nonBacklogPhases = phases.filter(p => !/^999(?:\.|$)/.test(p.number)); + const completedCount = nonBacklogPhases.filter(p => p.disk_status === 'complete').length; // Read manager flags from config (passthrough flags for each step) // Validate: flags must be CLI-safe (only --flags, alphanumeric, hyphens, spaces) @@ -1135,7 +1137,7 @@ function cmdInitManager(cwd, raw) { in_progress_count: phases.filter(p => ['partial', 'planned', 'discussed', 'researched'].includes(p.disk_status)).length, recommended_actions: filteredActions, waiting_signal: waitingSignal, - all_complete: completedCount === phases.length && phases.length > 0, + all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0, project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), roadmap_exists: true, state_exists: true, diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 3e8450bfc..9bdf70e8c 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -838,9 +838,11 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { .sort((a, b) => comparePhaseNum(a, b)); // Find the next phase directory after current + // Skip backlog phases (999.x) — they are parked ideas, not sequential work (#2129) for (const dir of dirs) { const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i); if (dm) { + if (/^999(?:\.|$)/.test(dm[1])) continue; if (comparePhaseNum(dm[1], phaseNum) > 0) { nextPhaseNum = dm[1]; nextPhaseName = dm[2] || null; diff --git a/tests/init-manager.test.cjs b/tests/init-manager.test.cjs index 1c6d5cb39..8f1d37794 100644 --- a/tests/init-manager.test.cjs +++ b/tests/init-manager.test.cjs @@ -531,4 +531,46 @@ describe('init manager', () => { assert.strictEqual(output.response_language, undefined); }); + + test('all_complete is true when non-backlog phases are complete and 999.x exists (#2129)', () => { + writeState(tmpDir); + writeRoadmap(tmpDir, [ + { number: '1', name: 'Setup', complete: true }, + { number: '2', name: 'Core', complete: true }, + { number: '3', name: 'Polish', complete: true }, + { number: '999.1', name: 'Backlog idea' }, + ]); + + // Scaffold completed phases on disk + scaffoldPhase(tmpDir, 1, { slug: 'setup', plans: 2, summaries: 2 }); + scaffoldPhase(tmpDir, 2, { slug: 'core', plans: 1, summaries: 1 }); + scaffoldPhase(tmpDir, 3, { slug: 'polish', plans: 1, summaries: 1 }); + + const result = runGsdTools('init manager', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.all_complete, true, 'all_complete should be true when only 999.x phases remain incomplete'); + }); + + test('all_complete false with incomplete non-backlog phase still produces recommended_actions (#2129)', () => { + writeState(tmpDir); + writeRoadmap(tmpDir, [ + { number: '1', name: 'Setup', complete: true }, + { number: '2', name: 'Core', complete: true }, + { number: '3', name: 'Polish' }, + { number: '999.1', name: 'Backlog idea' }, + ]); + + scaffoldPhase(tmpDir, 1, { slug: 'setup', plans: 1, summaries: 1 }); + scaffoldPhase(tmpDir, 2, { slug: 'core', plans: 1, summaries: 1 }); + // Phase 3 has no directory — should trigger discuss recommendation + + const result = runGsdTools('init manager', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.all_complete, false, 'all_complete should be false with phase 3 incomplete'); + assert.ok(output.recommended_actions.length > 0, 'recommended_actions should not be empty when non-backlog phases remain'); + }); }); diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index 338fb2596..b23b31cad 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -2330,6 +2330,83 @@ describe('phase complete updates Performance Metrics', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// phase complete — backlog phase (999.x) exclusion (#2129) +// ───────────────────────────────────────────────────────────────────────────── + +describe('phase complete excludes 999.x backlog from next-phase (#2129)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('next phase skips 999.x backlog dirs and falls back to roadmap', () => { + // ROADMAP defines phases 1, 2, 3 and a backlog 999.1 + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + [ + '# Roadmap', + '', + '- [ ] Phase 1: Setup', + '- [ ] Phase 2: Core', + '- [ ] Phase 3: Polish', + '- [ ] Phase 999.1: Backlog idea', + '', + '### Phase 1: Setup', + '**Goal:** Initial setup', + '', + '### Phase 2: Core', + '**Goal:** Build core', + '', + '### Phase 3: Polish', + '**Goal:** Polish everything', + '', + '### Phase 999.1: Backlog idea', + '**Goal:** Parked idea', + ].join('\n') + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + [ + '# State', + '', + '**Current Phase:** 02', + '**Status:** In progress', + '**Current Plan:** 02-01', + '**Last Activity:** 2025-01-01', + '**Last Activity Description:** Working', + ].join('\n') + ); + + // Phase 1 and 2 exist on disk, phase 3 does NOT exist yet, 999.1 DOES exist + const p1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + + const p2 = path.join(tmpDir, '.planning', 'phases', '02-core'); + fs.mkdirSync(p2, { recursive: true }); + fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p2, '02-01-SUMMARY.md'), '# Summary'); + + // Backlog stub on disk — this is what triggers the bug + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '999.1-backlog-idea'), { recursive: true }); + + const result = runGsdTools('phase complete 2', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Should find phase 3 from roadmap, NOT 999.1 from filesystem + assert.strictEqual(output.next_phase, '3', 'next_phase should be 3, not 999.1'); + assert.strictEqual(output.is_last_phase, false, 'should not be last phase'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // milestone complete command // ─────────────────────────────────────────────────────────────────────────────