From e6e33602c314ffbf62b9f72017aa0b868a6e94ff Mon Sep 17 00:00:00 2001 From: Gabriel Rodrigues Garcia Date: Mon, 13 Apr 2026 11:59:11 -0300 Subject: [PATCH] fix(init): ignore archived phases from prior milestones sharing a phase number (#2186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a new milestone reuses a phase number that exists in an archived milestone (e.g., v2.0 Phase 2 while v1.0-phases/02-old-feature exists), findPhaseInternal falls through to the archive and returns the old phase. init plan-phase and init execute-phase then emitted archived values for phase_dir, phase_slug, has_context, has_research, and *_path fields, while phase_req_ids came from the current ROADMAP — producing a silent inconsistency that pointed downstream agents at a shipped phase from a previous milestone. cmdInitPhaseOp already guarded against this (see lines 617-642); apply the same guard in cmdInitPlanPhase, cmdInitExecutePhase, and cmdInitVerifyWork: if findPhaseInternal returns an archived match and the current ROADMAP.md has the phase, discard the archived phaseInfo so the ROADMAP fallback path produces clean values. Adds three regression tests covering plan-phase, execute-phase, and verify-work under the shared-number scenario. --- get-shit-done/bin/lib/init.cjs | 30 +++++++++++++++ tests/init.test.cjs | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 214baa2ba..8a704b813 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 2fed3fd08..07c6ece84 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) // ─────────────────────────────────────────────────────────────────────────────