diff --git a/.changeset/rapid-pumas-click.md b/.changeset/rapid-pumas-click.md new file mode 100644 index 000000000..6b057bcb7 --- /dev/null +++ b/.changeset/rapid-pumas-click.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1917 +--- +**`milestone complete --ws` now archives into the workstream instead of root** — the archive paths (MILESTONES.md, the milestones/ archive dir, and the per-version MILESTONE-AUDIT.md) were hardcoded to root `.planning/`, so a workstream milestone close scattered its artifacts into root and never produced a workstream-local archive. They now derive from the workstream-aware planning base (`planningPaths(cwd).planning`); flat-mode (no --ws) is unchanged. (#1911) diff --git a/src/milestone.cts b/src/milestone.cts index 7dcfb6fdf..58a0ad9e5 100644 --- a/src/milestone.cts +++ b/src/milestone.cts @@ -130,8 +130,13 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo const roadmapPath = planningPaths(cwd).roadmap; const reqPath = planningPaths(cwd).requirements; const statePath = planningPaths(cwd).state; - const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md'); - const archiveDir = path.join(cwd, '.planning', 'milestones'); + // #1911: derive the archive base from the workstream-aware planning root so + // `milestone complete --ws` archives into the workstream, not root. planningPaths(cwd).planning + // resolves to the workstream base when GSD_WORKSTREAM is set and to root .planning otherwise + // (flat mode is a no-op). + const planningBase = planningPaths(cwd).planning; + const milestonesPath = path.join(planningBase, 'MILESTONES.md'); + const archiveDir = path.join(planningBase, 'milestones'); const phasesDir = planningPaths(cwd).phases; const today = new Date().toISOString().split('T')[0]; const milestoneName = options.name || version; @@ -283,7 +288,7 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo } // Archive audit file if exists - const auditFile = path.join(cwd, '.planning', `${version}-MILESTONE-AUDIT.md`); + const auditFile = path.join(planningBase, `${version}-MILESTONE-AUDIT.md`); if (fs.existsSync(auditFile)) { retryRenameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)); } diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index fcd50955a..20e38cbdc 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -716,3 +716,46 @@ describe('milestone complete explicit version scope (#3043)', () => { } }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// #1911: milestone complete --ws must archive to the workstream, not root +// ───────────────────────────────────────────────────────────────────────────── + +describe('#1911 — milestone complete --ws archives to the workstream', () => { + let tmpDir; + + beforeEach(() => { tmpDir = createTempProject(); }); + afterEach(() => cleanup(tmpDir)); + + test('--ws archives roadmap/requirements into the workstream milestones dir, not root', () => { + const wsBase = path.join(tmpDir, '.planning', 'workstreams', 'ws1'); + fs.mkdirSync(path.join(wsBase, 'phases', '01-foo'), { recursive: true }); + fs.writeFileSync(path.join(wsBase, 'STATE.md'), 'milestone: v2.0\nstatus: executing\n'); + fs.writeFileSync( + path.join(wsBase, 'ROADMAP.md'), + '# Roadmap\n## Milestones\n- v2.0 Test (Phases 1) — IN PROGRESS\n## Phases\n### Phase 1: Foo\n**Goal:** foo\n', + ); + fs.writeFileSync(path.join(wsBase, 'REQUIREMENTS.md'), '# Requirements\n- [ ] REQ-01\n'); + fs.writeFileSync(path.join(wsBase, 'phases', '01-foo', '01-SUMMARY.md'), '---\none-liner: foo done\n---\n# Summary\n'); + // Root milestones dir pre-exists; it must NOT receive the workstream archive. + fs.mkdirSync(path.join(tmpDir, '.planning', 'milestones'), { recursive: true }); + + const result = runGsdTools('milestone complete v2.0 --ws ws1 --force', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + // Archive lands inside the workstream. + assert.ok( + fs.existsSync(path.join(wsBase, 'milestones', 'v2.0-ROADMAP.md')), + 'v2.0-ROADMAP.md should be archived into the workstream milestones dir', + ); + assert.ok( + fs.existsSync(path.join(wsBase, 'milestones', 'v2.0-REQUIREMENTS.md')), + 'v2.0-REQUIREMENTS.md should be archived into the workstream milestones dir', + ); + // And NOT in root. + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v2.0-ROADMAP.md')), + 'must not archive to root .planning/milestones/ in workstream mode', + ); + }); +});