From 8084f626ba5ab1cd103e4aa0fce3c61a94042f3c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 2 Jul 2026 10:24:53 -0400 Subject: [PATCH] fix(#1912): init.progress fails safe in workstream mode with no active ws (#1918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmdInitProgress used planningDir(cwd), which resolves to root .planning when no active workstream and no --ws/GSD_WORKSTREAM is set — regardless of mode:workstream. So /gsd-progress confidently reported a stale root milestone with no signal it was stale. Fail safe: when .planning/workstreams/ has workstreams AND no active workstream is resolved, error with an actionable hint naming the available workstreams and the --ws / `workstream set` fix. Flat mode (no workstreams dir) and --ws are unchanged. Regression in tests/init.test.cjs: errors when workstreams exist but none active (no stale root report); succeeds with --ws; flat mode unchanged. Closes #1912 --- .changeset/lively-lynx-snooze.md | 5 ++++ src/init.cts | 26 +++++++++++++++++ tests/init.test.cjs | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 .changeset/lively-lynx-snooze.md diff --git a/.changeset/lively-lynx-snooze.md b/.changeset/lively-lynx-snooze.md new file mode 100644 index 000000000..084679130 --- /dev/null +++ b/.changeset/lively-lynx-snooze.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1918 +--- +**`/gsd-progress` no longer reports a stale root milestone in workstream mode** — in a multi-workstream project with no active workstream set, `gsd-tools query init.progress` silently fell back to root `.planning/STATE.md` (often stale) and reported it confidently. It now fails safe with an actionable error naming the available workstreams and the `--ws`/`workstream set` fix, so a stale root value is never reported. Flat mode and `--ws ` are unchanged. (#1912) diff --git a/src/init.cts b/src/init.cts index 1a2294bab..34ec9acb5 100644 --- a/src/init.cts +++ b/src/init.cts @@ -71,6 +71,7 @@ const { planningPaths, planningDir, planningRoot, + getActiveWorkstream, findContextMdIn, } = planningWorkspace; @@ -1683,6 +1684,31 @@ function cmdInitProgress(cwd: string, raw: boolean): void { const milestone = getMilestoneInfo(cwd) as unknown as Record; const _slashRuntime = resolveRuntime(cwd); + // #1912: fail safe in workstream mode with no active workstream. With no active + // workstream and no --ws, planningDir(cwd) resolves to root .planning — silently + // reporting a stale root milestone. Require an explicit workstream instead. + // Mirror planningDir's resolution (GSD_WORKSTREAM env > stored active pointer) so + // an explicit --ws (which sets GSD_WORKSTREAM) satisfies the check. + const _wsRoot = path.join(planningRoot(cwd), 'workstreams'); + let _availableWorkstreams: string[] = []; + try { + _availableWorkstreams = fs + .readdirSync(_wsRoot, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + } catch { + /* no workstreams dir → flat mode */ + } + const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd); + if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) { + error( + `init.progress requires a workstream in workstream mode — no active workstream is set, so root STATE.md (likely stale) would be reported. ` + + `Pass --ws or run ${formatGsdSlash('workstream set', _slashRuntime) as string} first. ` + + `Available workstreams: ${_availableWorkstreams.join(', ')}`, + ); + } + const phasesDir = path.join(planningDir(cwd), 'phases'); const phases: Record[] = []; let currentPhase: Record | null = null; diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 871786122..f4b1f7242 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -2220,6 +2220,54 @@ describe('init handlers honor GSD_WORKSTREAM (ADR-0006 planningPaths consumption }); }); +// ───────────────────────────────────────────────────────────────────────────── +// #1912: init.progress fails safe in workstream mode with no active workstream +// ───────────────────────────────────────────────────────────────────────────── + +describe('#1912 — init.progress fails safe in workstream mode with no active workstream', () => { + let tmpDir; + + beforeEach(() => { tmpDir = createTempProject(); }); + afterEach(() => { cleanup(tmpDir); }); + + function seedWs(name, milestoneVersion) { + const wsDir = path.join(tmpDir, '.planning', 'workstreams', name); + fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true }); + fs.writeFileSync( + path.join(wsDir, 'STATE.md'), + `# State\n\n**Status:** executing\n**Milestone:** ${milestoneVersion}\n`, + ); + fs.writeFileSync( + path.join(wsDir, 'ROADMAP.md'), + `# Roadmap\n\n## Milestones\n- ${milestoneVersion} Test (Phase 1)\n\n## Phases\n### Phase 1: X\n**Goal:** do x\n`, + ); + return wsDir; + } + + test('errors (does NOT report stale root) when workstreams exist but none active', () => { + seedWs('alpha', 'v9.0'); + seedWs('beta', 'v9.0'); + // Stale root STATE — the misleading value that must never be silently reported. + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'milestone: v7.1\nstatus: executing\n'); + // No active-workstream pointer, no --ws. + const result = runGsdTools('init progress', tmpDir); + assert.equal(result.success, false, 'should fail safe rather than report the stale root milestone'); + assert.match(result.error || '', /workstream|--ws/i, 'error should name the workstream requirement'); + }); + + test('succeeds with --ws (reads the named workstream, not root)', () => { + seedWs('alpha', 'v9.0'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'milestone: v7.1\nstatus: executing\n'); + const result = runGsdTools('init progress --ws alpha', tmpDir); + assert.ok(result.success, `should succeed with --ws: ${result.error}`); + }); + + test('flat mode (no workstreams dir) is unchanged', () => { + const result = runGsdTools('init progress', tmpDir); + assert.ok(result.success, `flat mode should still work: ${result.error}`); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // roadmap analyze command // ─────────────────────────────────────────────────────────────────────────────