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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user