diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 214baa2b..8a704b81 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -58,6 +58,16 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) { const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + // If findPhaseInternal matched an archived phase from a prior milestone, but + // the phase exists in the current milestone's ROADMAP.md, ignore the archive + // match — we are initializing a new phase in the current milestone that + // happens to share a number with an archived one. Without this, phase_dir, + // phase_slug and related fields would point at artifacts from a previous + // milestone. + if (phaseInfo?.archived && roadmapPhase?.found) { + phaseInfo = null; + } + // Fallback to ROADMAP.md if no phase directory exists yet if (!phaseInfo && roadmapPhase?.found) { const phaseName = roadmapPhase.phase_name; @@ -181,6 +191,16 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) { const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + // If findPhaseInternal matched an archived phase from a prior milestone, but + // the phase exists in the current milestone's ROADMAP.md, ignore the archive + // match — we are planning a new phase in the current milestone that happens + // to share a number with an archived one. Without this, phase_dir, + // phase_slug, has_context and has_research would point at artifacts from a + // previous milestone. + if (phaseInfo?.archived && roadmapPhase?.found) { + phaseInfo = null; + } + // Fallback to ROADMAP.md if no phase directory exists yet if (!phaseInfo && roadmapPhase?.found) { const phaseName = roadmapPhase.phase_name; @@ -552,6 +572,16 @@ function cmdInitVerifyWork(cwd, phase, raw) { const config = loadConfig(cwd); let phaseInfo = findPhaseInternal(cwd, phase); + // If findPhaseInternal matched an archived phase from a prior milestone, but + // the phase exists in the current milestone's ROADMAP.md, ignore the archive + // match — same pattern as cmdInitPhaseOp. + if (phaseInfo?.archived) { + const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + if (roadmapPhase?.found) { + phaseInfo = null; + } + } + // Fallback to ROADMAP.md if no phase directory exists yet if (!phaseInfo) { const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 2fed3fd0..07c6ece8 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -352,6 +352,76 @@ describe('init commands ROADMAP fallback when phase directory does not exist (#1 }); }); +// ───────────────────────────────────────────────────────────────────────────── +// init ignores archived phases from prior milestones that share a phase number +// ───────────────────────────────────────────────────────────────────────────── + +describe('init commands ignore archived phases from prior milestones sharing a number', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + // Current milestone ROADMAP has Phase 2 but no disk directory yet + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + '# v2.0 Roadmap\n\n### Phase 2: New Feature\n**Goal:** New v2.0 feature\n**Requirements**: NEW-01, NEW-02\n**Plans:** TBD\n' + ); + // Prior milestone archive has a shipped Phase 2 with different slug and artifacts + const archivedDir = path.join(tmpDir, '.planning', 'milestones', 'v1.0-phases', '02-old-feature'); + fs.mkdirSync(archivedDir, { recursive: true }); + fs.writeFileSync(path.join(archivedDir, '2-CONTEXT.md'), '# OLD v1.0 Phase 2 context'); + fs.writeFileSync(path.join(archivedDir, '2-RESEARCH.md'), '# OLD v1.0 Phase 2 research'); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('init plan-phase prefers current ROADMAP entry over archived v1.0 phase of same number', () => { + const result = runGsdTools('init plan-phase 2', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true); + assert.strictEqual(output.phase_name, 'New Feature', + 'phase_name must come from current ROADMAP.md, not archived v1.0'); + assert.strictEqual(output.phase_slug, 'new-feature'); + assert.strictEqual(output.phase_dir, null, + 'phase_dir must be null — current milestone has no directory yet'); + assert.strictEqual(output.has_context, false, + 'has_context must not inherit archived v1.0 artifacts'); + assert.strictEqual(output.has_research, false, + 'has_research must not inherit archived v1.0 artifacts'); + assert.ok(!output.context_path, + 'context_path must not point at archived v1.0 file'); + assert.ok(!output.research_path, + 'research_path must not point at archived v1.0 file'); + assert.strictEqual(output.phase_req_ids, 'NEW-01, NEW-02'); + }); + + test('init execute-phase prefers current ROADMAP entry over archived v1.0 phase of same number', () => { + const result = runGsdTools('init execute-phase 2', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true); + assert.strictEqual(output.phase_name, 'New Feature'); + assert.strictEqual(output.phase_slug, 'new-feature'); + assert.strictEqual(output.phase_dir, null); + assert.strictEqual(output.phase_req_ids, 'NEW-01, NEW-02'); + }); + + test('init verify-work prefers current ROADMAP entry over archived v1.0 phase of same number', () => { + const result = runGsdTools('init verify-work 2', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true); + assert.strictEqual(output.phase_name, 'New Feature'); + assert.strictEqual(output.phase_dir, null); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // cmdInitTodos (INIT-01) // ─────────────────────────────────────────────────────────────────────────────