fix(#1912): init.progress fails safe in workstream mode with no active ws (#1918)

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 <name> 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
This commit is contained in:
Tom Boucher
2026-07-02 10:24:53 -04:00
committed by GitHub
parent 05cd55d43b
commit 8084f626ba
3 changed files with 79 additions and 0 deletions

View File

@@ -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 <name>` are unchanged. (#1912)

View File

@@ -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<string, unknown>;
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 <name> or run ${formatGsdSlash('workstream set', _slashRuntime) as string} first. ` +
`Available workstreams: ${_availableWorkstreams.join(', ')}`,
);
}
const phasesDir = path.join(planningDir(cwd), 'phases');
const phases: Record<string, unknown>[] = [];
let currentPhase: Record<string, unknown> | null = null;

View File

@@ -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
// ─────────────────────────────────────────────────────────────────────────────