Repository: gsd-build/get-shit-done (2026-04-13T11:48:41Z)

Issue:
init plan-phase / execute-phase return archived phase when current milestone reuses the same phase number
## GSD Version
1.34.2 (reproduced on `main` at 1.35.0-dev)

## Runtime
Claude Code

## OS / Node / Shell
macOS, Node v22.22.1, zsh

## Installation Method
Local (`~/.claude/get-shit-done/`)

## Summary

`node gsd-tools.cjs init plan-phase <N>` returns the wrong phase when the current milestone reuses a phase number that also exists in an archived milestone's `.planning/milestones/v*-phases/` directory.

`phase_dir`, `phase_slug`, `phase_name`, `has_context`, `has_research`, `context_path`, `research_path` all point at the archived phase from the prior milestone, while `phase_req_ids` correctly comes from the current ROADMAP. The mismatch silently steers downstream agents (gsd-planner, gsd-phase-researcher) at shipped work from a previous milestone.

## Reproduction

Directory layout (a project on v2.0 where v1.0 shipped and was archived):

```
.planning/
  ROADMAP.md                         # current v2.0 roadmap, has "Phase 2: New Feature"
  phases/                            # (empty — Phase 2 not started yet)
  milestones/
    v1.0-phases/
      02-old-feature/
        2-CONTEXT.md                 # old v1.0 artifacts
        2-RESEARCH.md
```

Running:

```
node gsd-tools.cjs init plan-phase 2
```

returns:

```json
{
  "phase_found": true,
  "phase_dir": ".planning/milestones/v1.0-phases/02-old-feature",
  "phase_name": "old-feature",
  "phase_slug": "old-feature",
  "has_context": true,
  "has_research": true,
  "context_path": ".planning/milestones/v1.0-phases/02-old-feature/2-CONTEXT.md",
  "research_path": ".planning/milestones/v1.0-phases/02-old-feature/2-RESEARCH.md",
  "phase_req_ids": "NEW-01, NEW-02"
}
```

`phase_req_ids` is correctly scoped to the current milestone (via `getRoadmapPhaseInternal`'s `extractCurrentMilestone`), but everything coming from `findPhaseInternal` points at the archived phase. The result is internally inconsistent.

## Expected

Since `ROADMAP.md` has Phase 2 in the current milestone and `.planning/phases/` has no Phase 2 directory yet, init should return the ROADMAP fallback (no directory, clean slate) — same behavior `cmdInitPhaseOp` already implements for discuss-phase.

```json
{
  "phase_found": true,
  "phase_dir": null,
  "phase_name": "New Feature",
  "phase_slug": "new-feature",
  "has_context": false,
  "has_research": false,
  "phase_req_ids": "NEW-01, NEW-02"
}
```

## Root Cause

`findPhaseInternal` (in `get-shit-done/bin/lib/core.cjs`) falls through to archived milestone directories when nothing is found in `.planning/phases/`. The archived match's `archived: version` flag is set but never checked by `cmdInitPlanPhase` / `cmdInitExecutePhase` / `cmdInitVerifyWork`, so an archived phase silently masquerades as the current one.

`cmdInitPhaseOp` already guards against this (lines 621–642 in `get-shit-done/bin/lib/init.cjs` on current main). The three other init entry points do not.

## Impact

Hit in real use while running `/gsd-plan-phase 2` on a project where v1.0 was archived and v2.0's Phase 2 started — the orchestrator was routed at `.planning/milestones/v1.0-phases/02-session-context-pool/` artifacts (a shipped browser-service phase) instead of the current milestone's UI theming phase. Had to manually override `phase_dir`.

## Fix

PR: #TBD (linked once opened from fork).


Discussion:
PR opened: #2186
Branch `fix/2185-init-plan-phase-execute-phase-return-arc` created.

```bash
git fetch origin && git checkout fix/2185-init-plan-phase-execute-phase-return-arc
```
Fix submitted in PR #2186. The `init plan-phase`, `init execute-phase`, and `init verify-work` commands now ignore archived phases from prior milestones when the current milestone's ROADMAP has a matching phase number. Closes automatically when PR #2186 merges.



Patch:
diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs
index 214baa2baf..8a704b8135 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);


Test patch:
diff --git a/tests/init.test.cjs b/tests/init.test.cjs
index 2fed3fd08e..07c6ece84d 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)
 // ─────────────────────────────────────────────────────────────────────────────