fix(sdk): forward --ws workstream flag through query dispatch (#2546)
* fix(sdk): forward --ws workstream flag through query dispatch (closes #2524) - cli.ts: pass args.ws as workstream to registry.dispatch() - registry.ts: add workstream? param to dispatch(), thread to handler - utils.ts: add optional workstream? to QueryHandler type signature - helpers.ts: planningPaths() accepts workstream? and uses relPlanningPath() - All ~26 query handlers updated to receive and pass workstream to planningPaths() - Config/commit/intel handlers use _workstream (project-global, not scoped) - Add failing-then-passing test: tests/bug-2524-sdk-query-ws-flag.test.cjs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): forward workstream to all downstream query helpers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(test): rewrite #2524 test as static source assertions — no sdk/dist build in CI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -121,8 +121,9 @@ export function replaceInCurrentMilestone(
|
||||
export async function readModifyWriteRoadmapMd(
|
||||
projectDir: string,
|
||||
modifier: (content: string) => string | Promise<string>,
|
||||
workstream?: string,
|
||||
): Promise<string> {
|
||||
const roadmapPath = planningPaths(projectDir).roadmap;
|
||||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||||
const lockPath = await acquireStateLock(roadmapPath);
|
||||
try {
|
||||
let content: string;
|
||||
@@ -152,14 +153,14 @@ export async function readModifyWriteRoadmapMd(
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { phase_number, padded, name, slug, directory, naming_mode }
|
||||
*/
|
||||
export const phaseAdd: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseAdd: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const description = args[0];
|
||||
if (!description) {
|
||||
throw new GSDError('description required for phase add', ErrorClassification.Validation);
|
||||
}
|
||||
assertNoNullBytes(description, 'description');
|
||||
|
||||
const configPath = planningPaths(projectDir).config;
|
||||
const configPath = planningPaths(projectDir, workstream).config;
|
||||
let config: Record<string, unknown> = {};
|
||||
try {
|
||||
config = JSON.parse(await readFile(configPath, 'utf-8'));
|
||||
@@ -206,7 +207,7 @@ export const phaseAdd: QueryHandler = async (args, projectDir) => {
|
||||
|
||||
assertSafePhaseDirName(dirName);
|
||||
|
||||
const dirPath = join(planningPaths(projectDir).phases, dirName);
|
||||
const dirPath = join(planningPaths(projectDir, workstream).phases, dirName);
|
||||
|
||||
// Create directory with .gitkeep so git tracks empty folders
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
@@ -224,7 +225,7 @@ export const phaseAdd: QueryHandler = async (args, projectDir) => {
|
||||
return rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
|
||||
}
|
||||
return rawContent + phaseEntry;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
if (!dirName) {
|
||||
throw new GSDError('Phase directory name was not computed', ErrorClassification.Execution);
|
||||
@@ -238,7 +239,7 @@ export const phaseAdd: QueryHandler = async (args, projectDir) => {
|
||||
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
||||
name: description,
|
||||
slug,
|
||||
directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir).phases, dirName))),
|
||||
directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir, workstream).phases, dirName))),
|
||||
naming_mode: config.phase_naming || 'sequential',
|
||||
};
|
||||
|
||||
@@ -255,7 +256,7 @@ export const phaseAdd: QueryHandler = async (args, projectDir) => {
|
||||
*
|
||||
* @param args - Either `--descriptions` followed by a JSON array string, or one description per arg (`--raw` ignored)
|
||||
*/
|
||||
export const phaseAddBatch: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseAddBatch: QueryHandler = async (args, projectDir, workstream) => {
|
||||
let descriptions: string[];
|
||||
const descIdx = args.indexOf('--descriptions');
|
||||
if (descIdx !== -1 && args[descIdx + 1] !== undefined) {
|
||||
@@ -284,14 +285,14 @@ export const phaseAddBatch: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
}
|
||||
|
||||
const roadmapPath = planningPaths(projectDir).roadmap;
|
||||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||||
if (!existsSync(roadmapPath)) {
|
||||
throw new GSDError('ROADMAP.md not found', ErrorClassification.Validation);
|
||||
}
|
||||
|
||||
let config: Record<string, unknown> = {};
|
||||
try {
|
||||
config = JSON.parse(await readFile(planningPaths(projectDir).config, 'utf-8'));
|
||||
config = JSON.parse(await readFile(planningPaths(projectDir, workstream).config, 'utf-8'));
|
||||
} catch { /* use defaults */ }
|
||||
|
||||
const projectCode = (config.project_code as string) || '';
|
||||
@@ -321,7 +322,7 @@ export const phaseAddBatch: QueryHandler = async (args, projectDir) => {
|
||||
if (num > maxPhase) maxPhase = num;
|
||||
}
|
||||
|
||||
const phasesOnDisk = planningPaths(projectDir).phases;
|
||||
const phasesOnDisk = planningPaths(projectDir, workstream).phases;
|
||||
if (existsSync(phasesOnDisk)) {
|
||||
const entries = await readdir(phasesOnDisk, { withFileTypes: true });
|
||||
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
||||
@@ -352,7 +353,7 @@ export const phaseAddBatch: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
|
||||
assertSafePhaseDirName(dirName);
|
||||
const dirPath = join(planningPaths(projectDir).phases, dirName);
|
||||
const dirPath = join(planningPaths(projectDir, workstream).phases, dirName);
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
await writeFile(join(dirPath, '.gitkeep'), '', 'utf-8');
|
||||
|
||||
@@ -373,13 +374,13 @@ export const phaseAddBatch: QueryHandler = async (args, projectDir) => {
|
||||
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
||||
name: description,
|
||||
slug,
|
||||
directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir).phases, dirName))),
|
||||
directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir, workstream).phases, dirName))),
|
||||
naming_mode: config.phase_naming || 'sequential',
|
||||
});
|
||||
}
|
||||
|
||||
return rawContent;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
return { data: { phases: added, count: added.length } };
|
||||
};
|
||||
@@ -397,7 +398,7 @@ export const phaseAddBatch: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { phase_number, after_phase, name, slug, directory }
|
||||
*/
|
||||
export const phaseInsert: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseInsert: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const afterPhase = args[0];
|
||||
const description = args[1];
|
||||
|
||||
@@ -424,7 +425,7 @@ export const phaseInsert: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
|
||||
// Calculate next decimal by scanning both directories AND ROADMAP.md entries
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const normalizedBase = normalizePhaseName(afterPhase);
|
||||
const decimalSet = new Set<number>();
|
||||
|
||||
@@ -453,7 +454,7 @@ export const phaseInsert: QueryHandler = async (args, projectDir) => {
|
||||
// Optional project code prefix
|
||||
let insertConfig: Record<string, unknown> = {};
|
||||
try {
|
||||
insertConfig = JSON.parse(await readFile(planningPaths(projectDir).config, 'utf-8'));
|
||||
insertConfig = JSON.parse(await readFile(planningPaths(projectDir, workstream).config, 'utf-8'));
|
||||
} catch { /* use defaults */ }
|
||||
const projectCode = (insertConfig.project_code as string) || '';
|
||||
assertSafeProjectCode(projectCode);
|
||||
@@ -488,7 +489,7 @@ export const phaseInsert: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
|
||||
return rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
if (!decimalPhase) {
|
||||
throw new GSDError('Decimal phase was not computed', ErrorClassification.Execution);
|
||||
@@ -502,7 +503,7 @@ export const phaseInsert: QueryHandler = async (args, projectDir) => {
|
||||
after_phase: afterPhase,
|
||||
name: description,
|
||||
slug,
|
||||
directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir).phases, dirName))),
|
||||
directory: toPosixPath(relative(projectDir, join(planningPaths(projectDir, workstream).phases, dirName))),
|
||||
};
|
||||
|
||||
return { data: result };
|
||||
@@ -518,8 +519,9 @@ export const phaseInsert: QueryHandler = async (args, projectDir) => {
|
||||
async function findPhaseDir(
|
||||
projectDir: string,
|
||||
phase: string,
|
||||
workstream?: string,
|
||||
): Promise<{ dirPath: string; dirName: string; phaseName: string | null } | null> {
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const normalized = normalizePhaseName(phase);
|
||||
|
||||
try {
|
||||
@@ -573,7 +575,7 @@ function normalizeScaffoldArgs(args: string[]): string[] {
|
||||
return [type, phase, ...(name !== undefined && name !== '' ? [name] : [])];
|
||||
}
|
||||
|
||||
export const phaseScaffold: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseScaffold: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const normalized = normalizeScaffoldArgs(args);
|
||||
const type = normalized[0];
|
||||
const phase = normalized[1];
|
||||
@@ -609,7 +611,7 @@ export const phaseScaffold: QueryHandler = async (args, projectDir) => {
|
||||
const slug = generateSlugInternal(name);
|
||||
const dirNameNew = `${padded}-${slug}`;
|
||||
assertSafePhaseDirName(dirNameNew, 'scaffold phase directory');
|
||||
const phasesParent = planningPaths(projectDir).phases;
|
||||
const phasesParent = planningPaths(projectDir, workstream).phases;
|
||||
await mkdir(phasesParent, { recursive: true });
|
||||
const dirPath = join(phasesParent, dirNameNew);
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
@@ -624,7 +626,7 @@ export const phaseScaffold: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
|
||||
// For context/uat/verification types, find the phase directory
|
||||
const phaseInfo = phase ? await findPhaseDir(projectDir, phase) : null;
|
||||
const phaseInfo = phase ? await findPhaseDir(projectDir, phase, workstream) : null;
|
||||
if (phase && !phaseInfo) {
|
||||
throw new GSDError(`Phase ${phase} directory not found`, ErrorClassification.Blocked);
|
||||
}
|
||||
@@ -819,6 +821,7 @@ async function updateRoadmapAfterPhaseRemoval(
|
||||
targetPhase: string,
|
||||
isDecimal: boolean,
|
||||
removedInt: number,
|
||||
workstream?: string,
|
||||
): Promise<void> {
|
||||
await readModifyWriteRoadmapMd(projectDir, (content) => {
|
||||
const escaped = escapeRegex(targetPhase);
|
||||
@@ -884,7 +887,7 @@ async function updateRoadmapAfterPhaseRemoval(
|
||||
}
|
||||
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
}
|
||||
|
||||
// ─── phaseRemove handler ───────────────────────────────────────────────
|
||||
@@ -901,14 +904,14 @@ async function updateRoadmapAfterPhaseRemoval(
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { removed, directory_deleted, renamed_directories, renamed_files, roadmap_updated, state_updated }
|
||||
*/
|
||||
export const phaseRemove: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseRemove: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const targetPhase = args[0];
|
||||
if (!targetPhase) {
|
||||
throw new GSDError('phase number required for phase remove', ErrorClassification.Validation);
|
||||
}
|
||||
assertNoNullBytes(targetPhase, 'targetPhase');
|
||||
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const phasesDir = paths.phases;
|
||||
|
||||
if (!existsSync(paths.roadmap)) {
|
||||
@@ -964,7 +967,7 @@ export const phaseRemove: QueryHandler = async (args, projectDir) => {
|
||||
} catch { /* intentionally empty — renaming is best-effort */ }
|
||||
|
||||
// Update ROADMAP.md
|
||||
await updateRoadmapAfterPhaseRemoval(projectDir, targetPhase, isDecimal, parseInt(normalized, 10));
|
||||
await updateRoadmapAfterPhaseRemoval(projectDir, targetPhase, isDecimal, parseInt(normalized, 10), workstream);
|
||||
|
||||
// Update STATE.md: decrement total_phases
|
||||
let stateUpdated = false;
|
||||
@@ -1109,18 +1112,18 @@ function updatePerformanceMetricsSection(
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with completion details and warnings
|
||||
*/
|
||||
export const phaseComplete: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseComplete: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phaseNum = args[0];
|
||||
if (!phaseNum) {
|
||||
throw new GSDError('phase number required for phase complete', ErrorClassification.Validation);
|
||||
}
|
||||
assertNoNullBytes(phaseNum, 'phaseNum');
|
||||
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Step A: Validate phase exists and get info
|
||||
const phaseInfo = await findPhaseDir(projectDir, phaseNum);
|
||||
const phaseInfo = await findPhaseDir(projectDir, phaseNum, workstream);
|
||||
if (!phaseInfo) {
|
||||
throw new GSDError(`Phase ${phaseNum} not found`, ErrorClassification.Validation);
|
||||
}
|
||||
@@ -1246,7 +1249,7 @@ export const phaseComplete: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
|
||||
return roadmapContent;
|
||||
});
|
||||
}, workstream);
|
||||
}
|
||||
|
||||
// Step E: Find next phase — filesystem first, then ROADMAP.md fallback
|
||||
@@ -1255,7 +1258,7 @@ export const phaseComplete: QueryHandler = async (args, projectDir) => {
|
||||
let isLastPhase = true;
|
||||
|
||||
try {
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir, workstream);
|
||||
const entries = await readdir(paths.phases, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
||||
.filter(isDirInMilestone)
|
||||
@@ -1408,8 +1411,8 @@ export const phaseComplete: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { cleared: count }
|
||||
*/
|
||||
export const phasesClear: QueryHandler = async (args, projectDir) => {
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
export const phasesClear: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const confirm = Array.isArray(args) && args.includes('--confirm');
|
||||
let cleared = 0;
|
||||
|
||||
@@ -1446,8 +1449,8 @@ export const phasesClear: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { archived: count, version, archive_directory }
|
||||
*/
|
||||
export const phasesList: QueryHandler = async (args, projectDir) => {
|
||||
const paths = planningPaths(projectDir);
|
||||
export const phasesList: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const phasesDir = paths.phases;
|
||||
|
||||
const typeIdx = args.indexOf('--type');
|
||||
@@ -1510,14 +1513,14 @@ export const phasesList: QueryHandler = async (args, projectDir) => {
|
||||
return { data: { directories: dirs, count: dirs.length } };
|
||||
};
|
||||
|
||||
export const phaseNextDecimal: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseNextDecimal: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const basePhase = args[0];
|
||||
if (!basePhase) {
|
||||
throw new GSDError('base phase number required', ErrorClassification.Validation);
|
||||
}
|
||||
assertNoNullBytes(basePhase, 'basePhase');
|
||||
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const phasesDir = paths.phases;
|
||||
const normalized = normalizePhaseName(basePhase);
|
||||
const decimalSet = new Set<number>();
|
||||
@@ -1567,16 +1570,16 @@ export const phaseNextDecimal: QueryHandler = async (args, projectDir) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const phasesArchive: QueryHandler = async (args, projectDir) => {
|
||||
export const phasesArchive: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const version = args[0];
|
||||
if (!version) {
|
||||
throw new GSDError('version required for phases archive', ErrorClassification.Validation);
|
||||
}
|
||||
assertNoNullBytes(version, 'version');
|
||||
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const phasesDir = paths.phases;
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir, workstream);
|
||||
|
||||
const archiveDir = join(paths.planning, 'milestones', `${version}-phases`);
|
||||
await mkdir(archiveDir, { recursive: true });
|
||||
@@ -1627,7 +1630,7 @@ function extractOneLinerFromBody(content: string): string | null {
|
||||
/**
|
||||
* Query handler for `milestone.complete` — port of `cmdMilestoneComplete` from `milestone.cjs`.
|
||||
*/
|
||||
export const milestoneComplete: QueryHandler = async (args, projectDir) => {
|
||||
export const milestoneComplete: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const version = args[0];
|
||||
if (!version) {
|
||||
throw new GSDError('version required for milestone complete (e.g., v1.0)', ErrorClassification.Validation);
|
||||
@@ -1637,7 +1640,7 @@ export const milestoneComplete: QueryHandler = async (args, projectDir) => {
|
||||
const nameOpt = parseMultiwordArg(args, 'name');
|
||||
const archivePhases = args.includes('--archive-phases');
|
||||
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const roadmapPath = paths.roadmap;
|
||||
const reqPath = paths.requirements;
|
||||
const statePath = paths.state;
|
||||
@@ -1649,7 +1652,7 @@ export const milestoneComplete: QueryHandler = async (args, projectDir) => {
|
||||
|
||||
await mkdir(archiveDir, { recursive: true });
|
||||
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir, workstream);
|
||||
|
||||
let phaseCount = 0;
|
||||
let totalPlans = 0;
|
||||
@@ -1754,7 +1757,7 @@ export const milestoneComplete: QueryHandler = async (args, projectDir) => {
|
||||
`${version} milestone completed and archived`,
|
||||
);
|
||||
return next;
|
||||
});
|
||||
}, workstream);
|
||||
}
|
||||
|
||||
let phasesArchived = false;
|
||||
|
||||
Reference in New Issue
Block a user