fix(2129): exclude 999.x backlog phases from next-phase and all_complete (#2135)

* test(2129): add failing tests for 999.x backlog phase exclusion

Bug A: phase complete reports 999.1 as next phase instead of 3
Bug B: init manager returns all_complete:false when only 999.x is incomplete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(2129): exclude 999.x backlog phases from next-phase scan and all_complete check

In cmdPhaseComplete, backlog phases (999.x) on disk were picked as the
next phase when intervening milestone phases had no directory yet. Now
the filesystem scan skips any directory whose phase number starts with 999.

In cmdInitManager, all_complete compared completed count against the full
phase list including 999.x stubs, making it impossible to reach true when
backlog items existed. Now the check uses only non-backlog phases.

Closes #2129

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-11 23:50:25 -04:00
committed by GitHub
parent 637daa831b
commit eb03ba3dd8
4 changed files with 125 additions and 2 deletions

View File

@@ -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,

View File

@@ -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;

View File

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

View File

@@ -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
// ─────────────────────────────────────────────────────────────────────────────