diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 2a007321e..6dfddd479 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -1024,6 +1024,17 @@ function cmdInitManager(cwd, raw) { // Dependency satisfaction: check if all depends_on phases are complete const completedNums = new Set(phases.filter(p => p.disk_status === 'complete').map(p => p.number)); + + // Also include phases from previously shipped milestones — they are all + // complete by definition (a milestone only ships when all phases are done). + // rawContent is the full ROADMAP.md (including
-wrapped shipped + // milestone sections that extractCurrentMilestone strips out). + const _allCompletedPattern = /-\s*\[x\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi; + let _allMatch; + while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) { + completedNums.add(_allMatch[1]); + } + for (const phase of phases) { if (!phase.depends_on || /^none$/i.test(phase.depends_on.trim())) { phase.deps_satisfied = true; diff --git a/tests/init-manager-deps.test.cjs b/tests/init-manager-deps.test.cjs new file mode 100644 index 000000000..25f843a4d --- /dev/null +++ b/tests/init-manager-deps.test.cjs @@ -0,0 +1,120 @@ +/** + * Tests for bug #2267: deps_satisfied should include phases from shipped milestones. + * + * Root cause: completedNums was built only from the current milestone's phases, + * so a dependency on a phase from a previously shipped milestone was never + * satisfied — even though all prior-milestone phases are complete by definition. + */ + +const { describe, test, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +describe('init manager — cross-milestone dependency satisfaction', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + /** + * Write a ROADMAP.md that has: + * - A shipped previous milestone (v1.0) inside a
block containing + * Phase 5 marked [x] complete. + * - A current active milestone (v2.0) containing Phase 6 that depends on + * Phase 5. + */ + function writeRoadmapWithShippedMilestone(dir) { + const content = [ + '# Roadmap', + '', + '
', + 'v1.0 — Initial Release (Shipped)', + '', + '## Roadmap v1.0: Initial Release', + '', + '- [x] **Phase 5: Auth**', + '', + '### Phase 5: Auth', + '**Goal:** Add authentication', + '', + '
', + '', + '## Roadmap v2.0: Dashboard', + '', + '- [ ] **Phase 6: Dashboard**', + '', + '### Phase 6: Dashboard', + '**Goal:** Build dashboard', + '**Depends on:** Phase 5', + '', + ].join('\n'); + + fs.writeFileSync(path.join(dir, '.planning', 'ROADMAP.md'), content); + } + + function writeStateWithMilestone(dir, version) { + fs.writeFileSync( + path.join(dir, '.planning', 'STATE.md'), + `---\nmilestone: ${version}\n---\n# State\n` + ); + } + + test('phase depending on a shipped-milestone phase has deps_satisfied: true', () => { + writeRoadmapWithShippedMilestone(tmpDir); + writeStateWithMilestone(tmpDir, 'v2.0'); + + const result = runGsdTools('init manager', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + + // Only the current milestone's phases should appear in the phases array + assert.strictEqual(output.phases.length, 1, 'Should have exactly one phase from the current milestone'); + + const phase6 = output.phases[0]; + assert.strictEqual(phase6.number, '6', 'Should be Phase 6'); + + // Phase 6 depends on Phase 5 from the prior milestone — must be satisfied + assert.strictEqual( + phase6.deps_satisfied, + true, + 'Phase 6 dep on shipped Phase 5 should be satisfied' + ); + }); + + test('phase depending on a non-existent phase has deps_satisfied: false', () => { + writeRoadmapWithShippedMilestone(tmpDir); + writeStateWithMilestone(tmpDir, 'v2.0'); + + // Add a second phase in the current milestone that depends on a phantom phase + const roadmapPath = path.join(tmpDir, '.planning', 'ROADMAP.md'); + const existing = fs.readFileSync(roadmapPath, 'utf-8'); + const withExtra = existing + [ + '### Phase 7: Extra', + '**Goal:** Extra work', + '**Depends on:** Phase 99', + '', + ].join('\n'); + fs.writeFileSync(roadmapPath, withExtra); + + const result = runGsdTools('init manager', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + const phase7 = output.phases.find(p => p.number === '7'); + assert.ok(phase7, 'Phase 7 should be in the output'); + + assert.strictEqual( + phase7.deps_satisfied, + false, + 'Phase 7 dep on non-existent Phase 99 should not be satisfied' + ); + }); +});