fix(query): workstream resolution in init.milestone-op and roadmap.analyze (#3196)

- initMilestoneOp now accepts and propagates the workstream parameter:
  relPlanningPath(workstream) replaces the hardcoded '.planning' dir,
  getMilestoneInfo gets workstream passed, extractCurrentMilestone gets
  workstream passed, archiveDir is derived from planningDir not root.

- resolveQueryRuntimeContext now reads .planning/active-workstream as a
  third-priority fallback after --ws flag and GSD_WORKSTREAM env var,
  completing the documented resolution chain for all query handlers.

Closes #3196

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-06 21:05:27 -04:00
parent 608da536fd
commit 8054959417
5 changed files with 369 additions and 7 deletions

View File

@@ -1,3 +1,5 @@
import { join } from 'node:path';
import { readFileSync, existsSync } from 'node:fs';
import { findProjectRoot } from './helpers.js';
import { validateWorkstreamName } from '../workstream-utils.js';
@@ -11,6 +13,35 @@ export interface QueryRuntimeContext {
ws?: string;
}
/**
* Read the active workstream from `.planning/active-workstream` file.
*
* Mirrors the logic in workstream.ts:getActiveWorkstream — returns null
* when the file is missing, empty, contains invalid characters, or names
* a workstream directory that doesn't exist on disk.
*/
function readActiveWorkstreamFile(projectDir: string): string | null {
const filePath = join(projectDir, '.planning', 'active-workstream');
try {
const name = readFileSync(filePath, 'utf-8').trim();
if (!name || !validateWorkstreamName(name)) return null;
const wsDir = join(projectDir, '.planning', 'workstreams', name);
if (!existsSync(wsDir)) return null;
return name;
} catch {
return null;
}
}
/**
* Resolve the runtime context for a query invocation.
*
* Workstream resolution priority:
* 1. `--ws <name>` flag (input.ws)
* 2. `GSD_WORKSTREAM` environment variable
* 3. `.planning/active-workstream` file
* 4. Root `.planning/` (no workstream)
*/
export function resolveQueryRuntimeContext(input: QueryRuntimeContextInput): QueryRuntimeContext {
const projectDir = findProjectRoot(input.projectDir);
@@ -22,8 +53,13 @@ export function resolveQueryRuntimeContext(input: QueryRuntimeContextInput): Que
}
const envWs = process.env.GSD_WORKSTREAM;
if (envWs && validateWorkstreamName(envWs)) {
return { projectDir, ws: envWs };
}
const fileWs = readActiveWorkstreamFile(projectDir);
return {
projectDir,
ws: envWs && validateWorkstreamName(envWs) ? envWs : undefined,
ws: fileWs ?? undefined,
};
}