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:
@@ -408,7 +408,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
||||
}
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
} else {
|
||||
const result = await registry.dispatch(matched.cmd, matched.args, args.projectDir);
|
||||
const result = await registry.dispatch(matched.cmd, matched.args, args.projectDir, args.ws);
|
||||
let output: unknown = result.data;
|
||||
|
||||
if (pickField) {
|
||||
|
||||
@@ -491,8 +491,8 @@ export interface AuditOpenResult {
|
||||
/**
|
||||
* Same structured result as `gsd-tools.cjs audit-open` (JSON).
|
||||
*/
|
||||
export function auditOpenArtifacts(projectDir: string): AuditOpenResult {
|
||||
const planDir = planningPaths(projectDir).planning;
|
||||
export function auditOpenArtifacts(projectDir: string, workstream?: string): AuditOpenResult {
|
||||
const planDir = planningPaths(projectDir, workstream).planning;
|
||||
|
||||
const debugSessions = (() => {
|
||||
try { return scanDebugSessions(planDir); } catch { return [{ scan_error: true }]; }
|
||||
@@ -707,9 +707,9 @@ export function formatAuditReport(auditResult: AuditOpenResult): string {
|
||||
/**
|
||||
* `audit-open` / `audit.open` — optional `--json` for structured JSON only (default adds formatted report string).
|
||||
*/
|
||||
export const auditOpen: QueryHandler = async (args, projectDir) => {
|
||||
export const auditOpen: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const jsonOnly = args.includes('--json');
|
||||
const result = auditOpenArtifacts(projectDir);
|
||||
const result = auditOpenArtifacts(projectDir, workstream);
|
||||
if (jsonOnly) {
|
||||
return { data: result };
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ async function readFileSafe(filePath: string): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export const checkGates: QueryHandler = async (args, projectDir) => {
|
||||
export const checkGates: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const workflow = args[0];
|
||||
if (!workflow) {
|
||||
throw new GSDError('workflow name required for check gates', ErrorClassification.Validation);
|
||||
@@ -51,7 +51,7 @@ export const checkGates: QueryHandler = async (args, projectDir) => {
|
||||
|
||||
const blockers: Blocker[] = [];
|
||||
const warnings: Warning[] = [];
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
|
||||
// Gate 1: .continue-here.md in project root
|
||||
const continueHerePath = join(projectDir, '.continue-here.md');
|
||||
@@ -82,7 +82,7 @@ export const checkGates: QueryHandler = async (args, projectDir) => {
|
||||
|
||||
// Gate 3: Verification debt — check VERIFICATION.md in phase dir if phase provided
|
||||
if (phaseNum) {
|
||||
const phaseRes = await findPhase([phaseNum], projectDir);
|
||||
const phaseRes = await findPhase([phaseNum], projectDir, workstream);
|
||||
const pdata = phaseRes.data as Record<string, unknown>;
|
||||
if (pdata.found && pdata.directory) {
|
||||
const phaseDirFull = join(projectDir, pdata.directory as string);
|
||||
|
||||
@@ -95,7 +95,7 @@ export function sanitizeCommitMessage(text: string): string {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with commit result
|
||||
*/
|
||||
export const commit: QueryHandler = async (args, projectDir) => {
|
||||
export const commit: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const allArgs = [...args];
|
||||
|
||||
// Extract flags
|
||||
@@ -180,7 +180,7 @@ export const commit: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { can_commit, reason, commit_docs, staged_files }
|
||||
*/
|
||||
export const checkCommit: QueryHandler = async (_args, projectDir) => {
|
||||
export const checkCommit: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
const paths = planningPaths(projectDir);
|
||||
|
||||
let commitDocs = true;
|
||||
@@ -227,7 +227,7 @@ export const checkCommit: QueryHandler = async (_args, projectDir) => {
|
||||
|
||||
// ─── commitToSubrepo ─────────────────────────────────────────────────────
|
||||
|
||||
export const commitToSubrepo: QueryHandler = async (args, projectDir) => {
|
||||
export const commitToSubrepo: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const filesIdx = args.indexOf('--files');
|
||||
const endIdx = filesIdx >= 0 ? filesIdx : args.length;
|
||||
const knownFlags = new Set(['--force', '--amend', '--no-verify']);
|
||||
|
||||
@@ -215,7 +215,7 @@ function setConfigValue(obj: Record<string, unknown>, dotPath: string, value: un
|
||||
* @returns QueryResult matching gsd-tools `config-set` JSON: `{ updated, key, value, previousValue }`
|
||||
* @throws GSDError with Validation if key is invalid or args missing
|
||||
*/
|
||||
export const configSet: QueryHandler = async (args, projectDir) => {
|
||||
export const configSet: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const keyPath = args[0];
|
||||
const rawValue = args[1];
|
||||
if (!keyPath) {
|
||||
@@ -284,7 +284,7 @@ export const configSet: QueryHandler = async (args, projectDir) => {
|
||||
* @returns QueryResult with { set: true, profile, agents }
|
||||
* @throws GSDError with Validation if profile is invalid
|
||||
*/
|
||||
export const configSetModelProfile: QueryHandler = async (args, projectDir) => {
|
||||
export const configSetModelProfile: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const profileName = args[0];
|
||||
if (!profileName) {
|
||||
throw new GSDError(
|
||||
@@ -346,7 +346,7 @@ export const configSetModelProfile: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { created: true, path } or { created: false, reason }
|
||||
*/
|
||||
export const configNewProject: QueryHandler = async (args, projectDir) => {
|
||||
export const configNewProject: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const paths = planningPaths(projectDir);
|
||||
|
||||
// Idempotent: don't overwrite existing config
|
||||
@@ -472,7 +472,7 @@ export const configNewProject: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { ensured: true, section }
|
||||
*/
|
||||
export const configEnsureSection: QueryHandler = async (args, projectDir) => {
|
||||
export const configEnsureSection: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const sectionName = args[0];
|
||||
if (!sectionName) {
|
||||
throw new GSDError('Usage: config-ensure-section <section>', ErrorClassification.Validation);
|
||||
|
||||
@@ -79,7 +79,7 @@ export function getAgentToModelMapForProfile(normalizedProfile: string): Record<
|
||||
* @returns QueryResult with the config value at the given path
|
||||
* @throws GSDError with Validation classification if key missing or not found
|
||||
*/
|
||||
export const configGet: QueryHandler = async (args, projectDir) => {
|
||||
export const configGet: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const keyPath = args[0];
|
||||
if (!keyPath) {
|
||||
throw new GSDError('Usage: config-get <key.path>', ErrorClassification.Validation);
|
||||
@@ -127,7 +127,7 @@ export const configGet: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with `{ path: string }` absolute or project-relative resolution via planningPaths
|
||||
*/
|
||||
export const configPath: QueryHandler = async (_args, projectDir) => {
|
||||
export const configPath: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
const paths = planningPaths(projectDir);
|
||||
return { data: { path: paths.config } };
|
||||
};
|
||||
|
||||
@@ -21,8 +21,8 @@ const API_INDICATOR_RE = /route\.ts|controller\.|api\//i;
|
||||
const API_HEADING_RE = /\bAPI\b|endpoint|REST|GraphQL/i;
|
||||
const INFRA_RE = /docker|terraform|k8s|helm|infra/i;
|
||||
|
||||
async function roadmapHeadingForPhase(projectDir: string, phaseNum: string): Promise<string | null> {
|
||||
const roadmapPath = planningPaths(projectDir).roadmap;
|
||||
async function roadmapHeadingForPhase(projectDir: string, phaseNum: string, workstream?: string): Promise<string | null> {
|
||||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||||
let content: string;
|
||||
try {
|
||||
content = await readFile(roadmapPath, 'utf-8');
|
||||
@@ -34,14 +34,14 @@ async function roadmapHeadingForPhase(projectDir: string, phaseNum: string): Pro
|
||||
return m ? m[0] : null;
|
||||
}
|
||||
|
||||
export const detectPhaseType: QueryHandler = async (args, projectDir) => {
|
||||
export const detectPhaseType: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const raw = args[0];
|
||||
if (!raw) {
|
||||
throw new GSDError('phase number required for detect phase-type', ErrorClassification.Validation);
|
||||
}
|
||||
const phaseArg = normalizePhaseName(raw);
|
||||
|
||||
const phaseRes = await findPhase([raw], projectDir);
|
||||
const phaseRes = await findPhase([raw], projectDir, workstream);
|
||||
const pdata = phaseRes.data as Record<string, unknown>;
|
||||
const found = Boolean(pdata.found);
|
||||
|
||||
@@ -54,9 +54,9 @@ export const detectPhaseType: QueryHandler = async (args, projectDir) => {
|
||||
const phaseNumForRoadmap = (pdata.phase_number as string) || phaseArg;
|
||||
|
||||
// Read ROADMAP heading — try both normalized forms
|
||||
let heading = await roadmapHeadingForPhase(projectDir, phaseNumForRoadmap);
|
||||
let heading = await roadmapHeadingForPhase(projectDir, phaseNumForRoadmap, workstream);
|
||||
if (!heading && phaseNumForRoadmap !== phaseArg) {
|
||||
heading = await roadmapHeadingForPhase(projectDir, phaseArg);
|
||||
heading = await roadmapHeadingForPhase(projectDir, phaseArg, workstream);
|
||||
}
|
||||
|
||||
// Frontend detection
|
||||
|
||||
@@ -21,6 +21,7 @@ import { join, dirname, relative, resolve, isAbsolute, normalize } from 'node:pa
|
||||
import { realpath } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { GSDError, ErrorClassification } from '../errors.js';
|
||||
import { relPlanningPath } from '../workstream-utils.js';
|
||||
|
||||
// ─── Runtime-aware agents directory resolution ─────────────────────────────
|
||||
|
||||
@@ -406,14 +407,16 @@ export function normalizeMd(content: string): string {
|
||||
/**
|
||||
* Get common .planning file paths for a project directory.
|
||||
*
|
||||
* Simplified version (no workstream/project env vars).
|
||||
* When `workstream` is provided, all paths are rooted under
|
||||
* `.planning/workstreams/<workstream>` instead of `.planning`.
|
||||
* All paths returned in POSIX format.
|
||||
*
|
||||
* @param projectDir - Root project directory
|
||||
* @param workstream - Optional workstream name (see relPlanningPath)
|
||||
* @returns Object with paths to common .planning files
|
||||
*/
|
||||
export function planningPaths(projectDir: string): PlanningPaths {
|
||||
const base = join(projectDir, '.planning');
|
||||
export function planningPaths(projectDir: string, workstream?: string): PlanningPaths {
|
||||
const base = join(projectDir, relPlanningPath(workstream));
|
||||
return {
|
||||
planning: toPosixPath(base),
|
||||
state: toPosixPath(join(base, 'STATE.md')),
|
||||
|
||||
@@ -58,7 +58,7 @@ function pathExists(base: string, relPath: string): boolean {
|
||||
*
|
||||
* Port of cmdInitNewProject from init.cjs lines 296-399.
|
||||
*/
|
||||
export const initNewProject: QueryHandler = async (_args, projectDir) => {
|
||||
export const initNewProject: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
const config = await loadConfig(projectDir);
|
||||
|
||||
// Detect search API key availability from env vars and ~/.gsd/ files
|
||||
@@ -174,7 +174,7 @@ export const initNewProject: QueryHandler = async (_args, projectDir) => {
|
||||
*
|
||||
* Port of cmdInitProgress from init.cjs lines 1139-1284.
|
||||
*/
|
||||
export const initProgress: QueryHandler = async (_args, projectDir) => {
|
||||
export const initProgress: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
const config = await loadConfig(projectDir);
|
||||
const milestone = await getMilestoneInfo(projectDir);
|
||||
const paths = planningPaths(projectDir);
|
||||
@@ -322,7 +322,7 @@ export const initProgress: QueryHandler = async (_args, projectDir) => {
|
||||
*
|
||||
* Port of cmdInitManager from init.cjs lines 854-1137.
|
||||
*/
|
||||
export const initManager: QueryHandler = async (_args, projectDir) => {
|
||||
export const initManager: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
const config = await loadConfig(projectDir);
|
||||
const milestone = await getMilestoneInfo(projectDir);
|
||||
const paths = planningPaths(projectDir);
|
||||
|
||||
@@ -118,7 +118,7 @@ function searchArchMd(filePath: string, term: string): string[] {
|
||||
|
||||
const INTEL_DISABLED_MSG = 'Intel system disabled. Set intel.enabled=true in config.json to activate.';
|
||||
|
||||
export const intelStatus: QueryHandler = async (_args, projectDir) => {
|
||||
export const intelStatus: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
@@ -149,7 +149,7 @@ export const intelStatus: QueryHandler = async (_args, projectDir) => {
|
||||
return { data: { files, overall_stale: overallStale } };
|
||||
};
|
||||
|
||||
export const intelDiff: QueryHandler = async (_args, projectDir) => {
|
||||
export const intelDiff: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export const intelDiff: QueryHandler = async (_args, projectDir) => {
|
||||
return { data: { changed, added, removed } };
|
||||
};
|
||||
|
||||
export const intelSnapshot: QueryHandler = async (_args, projectDir) => {
|
||||
export const intelSnapshot: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
@@ -192,7 +192,7 @@ export const intelSnapshot: QueryHandler = async (_args, projectDir) => {
|
||||
return { data: { saved: true, timestamp, files: fileCount } };
|
||||
};
|
||||
|
||||
export const intelValidate: QueryHandler = async (_args, projectDir) => {
|
||||
export const intelValidate: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
@@ -219,7 +219,7 @@ export const intelValidate: QueryHandler = async (_args, projectDir) => {
|
||||
return { data: { valid: errors.length === 0, errors, warnings } };
|
||||
};
|
||||
|
||||
export const intelQuery: QueryHandler = async (args, projectDir) => {
|
||||
export const intelQuery: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const term = args[0] || '';
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
@@ -247,7 +247,7 @@ export const intelQuery: QueryHandler = async (args, projectDir) => {
|
||||
* Extract exports from a JS/CJS/ESM file — port of `intelExtractExports` in `intel.cjs` (lines 502–614).
|
||||
* Returns `{ file, exports, method }` with `file` as a resolved absolute path (matches `gsd-tools.cjs`).
|
||||
*/
|
||||
export const intelExtractExports: QueryHandler = async (args, projectDir) => {
|
||||
export const intelExtractExports: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const raw = args[0];
|
||||
if (!raw) {
|
||||
return { data: { file: '', exports: [], method: 'none' } };
|
||||
@@ -351,7 +351,7 @@ export const intelExtractExports: QueryHandler = async (args, projectDir) => {
|
||||
return { data: { file: filePath, exports, method } };
|
||||
};
|
||||
|
||||
export const intelPatchMeta: QueryHandler = async (args, projectDir) => {
|
||||
export const intelPatchMeta: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const raw = args[0];
|
||||
if (!raw) {
|
||||
return { data: { patched: false, error: 'File not found' } };
|
||||
@@ -391,7 +391,7 @@ export const intelPatchMeta: QueryHandler = async (args, projectDir) => {
|
||||
*
|
||||
* Port of `intelUpdate` from `intel.cjs` lines 314–321.
|
||||
*/
|
||||
export const intelUpdate: QueryHandler = async (_args, projectDir) => {
|
||||
export const intelUpdate: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
/** Resolve `.planning/phases/<dir>` for a phase token, or null. */
|
||||
async function resolvePhaseDir(phase: string, projectDir: string): Promise<string | null> {
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
async function resolvePhaseDir(phase: string, projectDir: string, workstream?: string): Promise<string | null> {
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const normalized = normalizePhaseName(phase);
|
||||
try {
|
||||
const entries = await readdir(phasesDir, { withFileTypes: true });
|
||||
@@ -40,7 +40,7 @@ type ArtifactType = 'context' | 'summary' | 'verification' | 'research';
|
||||
*
|
||||
* Args: `<phase>` `--type` `<context|summary|verification|research>`
|
||||
*/
|
||||
export const phaseListArtifacts: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseListArtifacts: QueryHandler = async (args, projectDir, workstream) => {
|
||||
if (!args[0]) {
|
||||
throw new GSDError('phase required', ErrorClassification.Validation);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export const phaseListArtifacts: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
const artifactType = rawType as ArtifactType;
|
||||
|
||||
const phaseDir = await resolvePhaseDir(phase, projectDir);
|
||||
const phaseDir = await resolvePhaseDir(phase, projectDir, workstream);
|
||||
if (!phaseDir) {
|
||||
return { data: { phase: normalizePhaseName(phase), type: artifactType, artifacts: [], error: 'Phase not found' } };
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export const phaseListArtifacts: QueryHandler = async (args, projectDir) => {
|
||||
*
|
||||
* Args: `<phase>` [`--with-schema` `<yamlKey>`]
|
||||
*/
|
||||
export const phaseListPlans: QueryHandler = async (args, projectDir) => {
|
||||
export const phaseListPlans: QueryHandler = async (args, projectDir, workstream) => {
|
||||
if (!args[0]) {
|
||||
throw new GSDError('phase required', ErrorClassification.Validation);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export const phaseListPlans: QueryHandler = async (args, projectDir) => {
|
||||
|
||||
const phase = args[0];
|
||||
const normalized = normalizePhaseName(phase);
|
||||
const phaseDir = await resolvePhaseDir(phase, projectDir);
|
||||
const phaseDir = await resolvePhaseDir(phase, projectDir, workstream);
|
||||
if (!phaseDir) {
|
||||
return {
|
||||
data: {
|
||||
|
||||
@@ -22,8 +22,9 @@ const UI_INDICATOR_RE = /UI|interface|frontend|component|layout|page|screen|view
|
||||
async function roadmapPhaseLineHasUiIndicators(
|
||||
projectDir: string,
|
||||
phaseNum: string,
|
||||
workstream?: string,
|
||||
): Promise<boolean> {
|
||||
const roadmapPath = planningPaths(projectDir).roadmap;
|
||||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||||
let content: string;
|
||||
try {
|
||||
content = await readFile(roadmapPath, 'utf-8');
|
||||
@@ -89,14 +90,14 @@ function inferNextStep(params: {
|
||||
return 'complete';
|
||||
}
|
||||
|
||||
export const checkPhaseReady: QueryHandler = async (args, projectDir) => {
|
||||
export const checkPhaseReady: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const raw = args[0];
|
||||
if (!raw) {
|
||||
throw new GSDError('phase number required for check phase-ready', ErrorClassification.Validation);
|
||||
}
|
||||
const phaseArg = normalizePhaseName(raw);
|
||||
|
||||
const phaseRes = await findPhase([raw], projectDir);
|
||||
const phaseRes = await findPhase([raw], projectDir, workstream);
|
||||
const pdata = phaseRes.data as Record<string, unknown>;
|
||||
const found = Boolean(pdata.found);
|
||||
|
||||
@@ -115,10 +116,10 @@ export const checkPhaseReady: QueryHandler = async (args, projectDir) => {
|
||||
|
||||
const phaseNumForRoadmap = (pdata.phase_number as string) || phaseArg;
|
||||
const has_ui_indicators =
|
||||
(await roadmapPhaseLineHasUiIndicators(projectDir, phaseNumForRoadmap)) ||
|
||||
(phaseNumForRoadmap !== phaseArg ? await roadmapPhaseLineHasUiIndicators(projectDir, phaseArg) : false);
|
||||
(await roadmapPhaseLineHasUiIndicators(projectDir, phaseNumForRoadmap, workstream)) ||
|
||||
(phaseNumForRoadmap !== phaseArg ? await roadmapPhaseLineHasUiIndicators(projectDir, phaseArg, workstream) : false);
|
||||
|
||||
const analysis = await roadmapAnalyze([], projectDir);
|
||||
const analysis = await roadmapAnalyze([], projectDir, workstream);
|
||||
const adata = analysis.data as { phases?: Array<Record<string, unknown>> };
|
||||
const phases = adata.phases ?? [];
|
||||
const deps = dependenciesMet(phases, phaseArg);
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
toPosixPath,
|
||||
planningPaths,
|
||||
} from './helpers.js';
|
||||
import { relPlanningPath } from '../workstream-utils.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────────────
|
||||
@@ -154,13 +155,13 @@ function extractObjective(content: string): string | null {
|
||||
* @returns QueryResult with PhaseInfo
|
||||
* @throws GSDError with Validation classification if phase identifier missing
|
||||
*/
|
||||
export const findPhase: QueryHandler = async (args, projectDir) => {
|
||||
export const findPhase: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phase = args[0];
|
||||
if (!phase) {
|
||||
throw new GSDError('phase identifier required', ErrorClassification.Validation);
|
||||
}
|
||||
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const normalized = normalizePhaseName(phase);
|
||||
|
||||
const notFound: PhaseInfo = {
|
||||
@@ -179,7 +180,7 @@ export const findPhase: QueryHandler = async (args, projectDir) => {
|
||||
};
|
||||
|
||||
// Search current phases first
|
||||
const relPhasesDir = '.planning/phases';
|
||||
const relPhasesDir = relPlanningPath(workstream) + '/phases';
|
||||
const current = await searchPhaseInDir(phasesDir, relPhasesDir, normalized);
|
||||
if (current) return { data: current };
|
||||
|
||||
@@ -221,13 +222,13 @@ export const findPhase: QueryHandler = async (args, projectDir) => {
|
||||
* @returns QueryResult with { phase, plans[], waves{}, incomplete[], has_checkpoints }
|
||||
* @throws GSDError with Validation classification if phase identifier missing
|
||||
*/
|
||||
export const phasePlanIndex: QueryHandler = async (args, projectDir) => {
|
||||
export const phasePlanIndex: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phase = args[0];
|
||||
if (!phase) {
|
||||
throw new GSDError('phase required for phase-plan-index', ErrorClassification.Validation);
|
||||
}
|
||||
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const normalized = normalizePhaseName(phase);
|
||||
|
||||
// Find phase directory
|
||||
|
||||
@@ -104,8 +104,8 @@ export const learningsQuery: QueryHandler = async (args) => {
|
||||
return { data: { learnings: results, count: results.length, tag } };
|
||||
};
|
||||
|
||||
export const learningsCopy: QueryHandler = async (_args, projectDir) => {
|
||||
const paths = planningPaths(projectDir);
|
||||
export const learningsCopy: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const learningsPath = join(paths.planning, 'LEARNINGS.md');
|
||||
if (!existsSync(learningsPath)) {
|
||||
return { data: { copied: false, total: 0, created: 0, skipped: 0, reason: 'No LEARNINGS.md found' } };
|
||||
|
||||
@@ -76,9 +76,9 @@ export async function determinePhaseStatus(
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with milestone progress data
|
||||
*/
|
||||
export const progressJson: QueryHandler = async (_args, projectDir) => {
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const milestone = await getMilestoneInfo(projectDir);
|
||||
export const progressJson: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const milestone = await getMilestoneInfo(projectDir, workstream);
|
||||
|
||||
const phases: Array<Record<string, unknown>> = [];
|
||||
let totalPlans = 0;
|
||||
@@ -128,8 +128,8 @@ export const progressJson: QueryHandler = async (_args, projectDir) => {
|
||||
* Progress bar line — port of `cmdProgressRender` `format === 'bar'` from commands.cjs (lines 588–593).
|
||||
* Uses the same plan/summary counts as `progressJson` / CJS (not `roadmap.analyze` percent).
|
||||
*/
|
||||
export const progressBar: QueryHandler = async (_args, projectDir) => {
|
||||
const json = await progressJson([], projectDir);
|
||||
export const progressBar: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const json = await progressJson([], projectDir, workstream);
|
||||
const d = json.data as {
|
||||
total_plans: number;
|
||||
total_summaries: number;
|
||||
@@ -148,8 +148,8 @@ export const progressBar: QueryHandler = async (_args, projectDir) => {
|
||||
/**
|
||||
* Markdown progress table — port of `cmdProgressRender` `format === 'table'` from commands.cjs (lines 575–587).
|
||||
*/
|
||||
export const progressTable: QueryHandler = async (_args, projectDir) => {
|
||||
const json = await progressJson([], projectDir);
|
||||
export const progressTable: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const json = await progressJson([], projectDir, workstream);
|
||||
const d = json.data as {
|
||||
milestone_version: string;
|
||||
milestone_name: string;
|
||||
@@ -183,14 +183,14 @@ export const progressTable: QueryHandler = async (_args, projectDir) => {
|
||||
/**
|
||||
* Statistics aggregate — port of `cmdStats` JSON/table output from commands.cjs lines 816–971.
|
||||
*/
|
||||
export const statsJson: QueryHandler = async (args, projectDir) => {
|
||||
export const statsJson: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const format = args[0] || 'json';
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const roadmapPath = planningPaths(projectDir).roadmap;
|
||||
const reqPath = planningPaths(projectDir).requirements;
|
||||
const statePath = planningPaths(projectDir).state;
|
||||
const milestone = await getMilestoneInfo(projectDir);
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||||
const reqPath = planningPaths(projectDir, workstream).requirements;
|
||||
const statePath = planningPaths(projectDir, workstream).state;
|
||||
const milestone = await getMilestoneInfo(projectDir, workstream);
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir, workstream);
|
||||
|
||||
const phasesByNumber = new Map<
|
||||
string,
|
||||
@@ -201,7 +201,7 @@ export const statsJson: QueryHandler = async (args, projectDir) => {
|
||||
let totalSummaries = 0;
|
||||
|
||||
try {
|
||||
const roadmapContent = await extractCurrentMilestone(await readFile(roadmapPath, 'utf-8'), projectDir);
|
||||
const roadmapContent = await extractCurrentMilestone(await readFile(roadmapPath, 'utf-8'), projectDir, workstream);
|
||||
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = headingPattern.exec(roadmapContent)) !== null) {
|
||||
@@ -348,8 +348,8 @@ export const statsJson: QueryHandler = async (args, projectDir) => {
|
||||
* Markdown statistics table — port of `cmdStats` `format === 'table'` from commands.cjs (lines 942–967).
|
||||
* Delegates to `statsJson` with `['table']` (same `rendered` string as CJS).
|
||||
*/
|
||||
export const statsTable: QueryHandler = async (_args, projectDir) => {
|
||||
return statsJson(['table'], projectDir);
|
||||
export const statsTable: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
return statsJson(['table'], projectDir, workstream);
|
||||
};
|
||||
|
||||
// ─── todoMatchPhase ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -110,10 +110,11 @@ export class QueryRegistry {
|
||||
* @param command - The command name to dispatch
|
||||
* @param args - Arguments to pass to the handler
|
||||
* @param projectDir - The project directory for context
|
||||
* @param workstream - Optional workstream name to scope .planning paths
|
||||
* @returns The query result from the handler
|
||||
* @throws GSDError if no handler is registered for the command
|
||||
*/
|
||||
async dispatch(command: string, args: string[], projectDir: string): Promise<QueryResult> {
|
||||
async dispatch(command: string, args: string[], projectDir: string, workstream?: string): Promise<QueryResult> {
|
||||
const handler = this.handlers.get(command);
|
||||
if (!handler) {
|
||||
throw new GSDError(
|
||||
@@ -121,7 +122,7 @@ export class QueryRegistry {
|
||||
ErrorClassification.Validation,
|
||||
);
|
||||
}
|
||||
return handler(args, projectDir);
|
||||
return handler(args, projectDir, workstream);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
} from './helpers.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
async function resolvePhaseDir(phase: string, projectDir: string): Promise<string | null> {
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
async function resolvePhaseDir(phase: string, projectDir: string, workstream?: string): Promise<string | null> {
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const normalized = normalizePhaseName(phase);
|
||||
try {
|
||||
const entries = await readdir(phasesDir, { withFileTypes: true });
|
||||
@@ -40,14 +40,14 @@ function normalizeReqList(v: unknown): string[] {
|
||||
/**
|
||||
* Args: `<phase>`
|
||||
*/
|
||||
export const requirementsExtractFromPlans: QueryHandler = async (args, projectDir) => {
|
||||
export const requirementsExtractFromPlans: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phase = args[0];
|
||||
if (!phase) {
|
||||
throw new GSDError('phase required', ErrorClassification.Validation);
|
||||
}
|
||||
|
||||
const normalized = normalizePhaseName(phase);
|
||||
const phaseDir = await resolvePhaseDir(phase, projectDir);
|
||||
const phaseDir = await resolvePhaseDir(phase, projectDir, workstream);
|
||||
if (!phaseDir) {
|
||||
return {
|
||||
data: {
|
||||
|
||||
@@ -14,13 +14,13 @@ import { escapeRegex, planningPaths } from './helpers.js';
|
||||
import { GSDError, ErrorClassification } from '../errors.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir) => {
|
||||
export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phaseNum = args[0];
|
||||
if (!phaseNum) {
|
||||
throw new GSDError('phase number required for roadmap update-plan-progress', ErrorClassification.Validation);
|
||||
}
|
||||
|
||||
const phaseResult = await findPhase([phaseNum], projectDir);
|
||||
const phaseResult = await findPhase([phaseNum], projectDir, workstream);
|
||||
const info = phaseResult.data as {
|
||||
found: boolean;
|
||||
plans: string[];
|
||||
@@ -49,7 +49,7 @@ export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir)
|
||||
const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
|
||||
const today = new Date().toISOString().split('T')[0]!;
|
||||
|
||||
const roadmapPath = planningPaths(projectDir).roadmap;
|
||||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||||
if (!existsSync(roadmapPath)) {
|
||||
return {
|
||||
data: {
|
||||
@@ -117,7 +117,7 @@ export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir)
|
||||
}
|
||||
|
||||
return roadmapContent;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
return {
|
||||
data: {
|
||||
|
||||
@@ -62,9 +62,9 @@ export function stripShippedMilestones(content: string): string {
|
||||
/**
|
||||
* Read milestone + name from STATE.md frontmatter when ROADMAP does not encode them.
|
||||
*/
|
||||
async function parseMilestoneFromState(projectDir: string): Promise<{ version: string; name: string } | null> {
|
||||
async function parseMilestoneFromState(projectDir: string, workstream?: string): Promise<{ version: string; name: string } | null> {
|
||||
try {
|
||||
const stateRaw = await readFile(planningPaths(projectDir).state, 'utf-8');
|
||||
const stateRaw = await readFile(planningPaths(projectDir, workstream).state, 'utf-8');
|
||||
const vm = stateRaw.match(/^milestone:\s*(.+)$/m);
|
||||
if (!vm) return null;
|
||||
const version = vm[1].trim().replace(/^["']|["']$/g, '');
|
||||
@@ -88,10 +88,10 @@ async function parseMilestoneFromState(projectDir: string): Promise<{ version: s
|
||||
* @param projectDir - Project root directory
|
||||
* @returns Object with version and name
|
||||
*/
|
||||
export async function getMilestoneInfo(projectDir: string): Promise<{ version: string; name: string }> {
|
||||
export async function getMilestoneInfo(projectDir: string, workstream?: string): Promise<{ version: string; name: string }> {
|
||||
try {
|
||||
// Priority 1: STATE.md frontmatter (authoritative for version; name only when real)
|
||||
const fromState = await parseMilestoneFromState(projectDir);
|
||||
const fromState = await parseMilestoneFromState(projectDir, workstream);
|
||||
const stateVersion = fromState?.version ?? null;
|
||||
const stateName = fromState && fromState.name !== 'milestone' ? fromState.name : null;
|
||||
if (stateVersion && stateName) {
|
||||
@@ -100,7 +100,7 @@ export async function getMilestoneInfo(projectDir: string): Promise<{ version: s
|
||||
// STATE.md has a version but no real name — fall through to ROADMAP for the name,
|
||||
// then override the version with the authoritative STATE.md value.
|
||||
|
||||
const roadmap = await readFile(planningPaths(projectDir).roadmap, 'utf-8');
|
||||
const roadmap = await readFile(planningPaths(projectDir, workstream).roadmap, 'utf-8');
|
||||
|
||||
// List-format: construction / blocked (legacy emoji)
|
||||
const barricadeMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
|
||||
@@ -137,7 +137,7 @@ export async function getMilestoneInfo(projectDir: string): Promise<{ version: s
|
||||
|
||||
return { version: stateVersion ?? 'v1.0', name: 'milestone' };
|
||||
} catch {
|
||||
const fromState = await parseMilestoneFromState(projectDir);
|
||||
const fromState = await parseMilestoneFromState(projectDir, workstream);
|
||||
if (fromState) return fromState;
|
||||
return { version: 'v1.0', name: 'milestone' };
|
||||
}
|
||||
@@ -152,11 +152,11 @@ export async function getMilestoneInfo(projectDir: string): Promise<{ version: s
|
||||
* @param projectDir - Working directory for reading STATE.md
|
||||
* @returns Content scoped to current milestone
|
||||
*/
|
||||
export async function extractCurrentMilestone(content: string, projectDir: string): Promise<string> {
|
||||
export async function extractCurrentMilestone(content: string, projectDir: string, workstream?: string): Promise<string> {
|
||||
// Get version from STATE.md frontmatter
|
||||
let version: string | null = null;
|
||||
try {
|
||||
const stateRaw = await readFile(planningPaths(projectDir).state, 'utf-8');
|
||||
const stateRaw = await readFile(planningPaths(projectDir, workstream).state, 'utf-8');
|
||||
const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m);
|
||||
if (milestoneMatch) {
|
||||
version = milestoneMatch[1].trim();
|
||||
@@ -294,7 +294,7 @@ function searchPhaseInContent(content: string, escapedPhase: string, phaseNum: s
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with phase section info or { found: false }
|
||||
*/
|
||||
export const roadmapGetPhase: QueryHandler = async (args, projectDir) => {
|
||||
export const roadmapGetPhase: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phaseNum = args[0];
|
||||
if (!phaseNum) {
|
||||
throw new GSDError(
|
||||
@@ -303,7 +303,7 @@ export const roadmapGetPhase: QueryHandler = async (args, projectDir) => {
|
||||
);
|
||||
}
|
||||
|
||||
const roadmapPath = planningPaths(projectDir).roadmap;
|
||||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||||
|
||||
let rawContent: string;
|
||||
try {
|
||||
@@ -312,7 +312,7 @@ export const roadmapGetPhase: QueryHandler = async (args, projectDir) => {
|
||||
return { data: { found: false, error: 'ROADMAP.md not found' } };
|
||||
}
|
||||
|
||||
const milestoneContent = await extractCurrentMilestone(rawContent, projectDir);
|
||||
const milestoneContent = await extractCurrentMilestone(rawContent, projectDir, workstream);
|
||||
const escapedPhase = escapeRegex(phaseNum);
|
||||
|
||||
// Search the current milestone slice first, then fall back to full roadmap.
|
||||
@@ -339,8 +339,8 @@ export const roadmapGetPhase: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with full roadmap analysis
|
||||
*/
|
||||
export const roadmapAnalyze: QueryHandler = async (_args, projectDir) => {
|
||||
const roadmapPath = planningPaths(projectDir).roadmap;
|
||||
export const roadmapAnalyze: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const roadmapPath = planningPaths(projectDir, workstream).roadmap;
|
||||
|
||||
let rawContent: string;
|
||||
try {
|
||||
@@ -349,8 +349,8 @@ export const roadmapAnalyze: QueryHandler = async (_args, projectDir) => {
|
||||
return { data: { error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null } };
|
||||
}
|
||||
|
||||
const content = await extractCurrentMilestone(rawContent, projectDir);
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const content = await extractCurrentMilestone(rawContent, projectDir, workstream);
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
|
||||
// IMPORTANT: Create regex INSIDE the function to avoid /g lastIndex persistence
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
||||
@@ -526,7 +526,7 @@ export const roadmapAnnotateDependencies: QueryHandler = async (args, projectDir
|
||||
* Mark requirement IDs complete in REQUIREMENTS.md (checkbox + traceability table).
|
||||
* Port of `cmdRequirementsMarkComplete` from milestone.cjs lines 11–87.
|
||||
*/
|
||||
export const requirementsMarkComplete: QueryHandler = async (args, projectDir) => {
|
||||
export const requirementsMarkComplete: QueryHandler = async (args, projectDir, workstream) => {
|
||||
if (args.length === 0) {
|
||||
throw new GSDError(
|
||||
'requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02',
|
||||
@@ -545,7 +545,7 @@ export const requirementsMarkComplete: QueryHandler = async (args, projectDir) =
|
||||
throw new GSDError('no valid requirement IDs found', ErrorClassification.Validation);
|
||||
}
|
||||
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
if (!existsSync(paths.requirements)) {
|
||||
return { data: { updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds } };
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ async function verificationPassed(phaseDirAbs: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export const routeNextAction: QueryHandler = async (_args, projectDir) => {
|
||||
const planning = planningPaths(projectDir).planning;
|
||||
export const routeNextAction: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const planning = planningPaths(projectDir, workstream).planning;
|
||||
const continueHere = existsSync(join(planning, '.continue-here.md'));
|
||||
|
||||
const sj = await stateJson([], projectDir);
|
||||
const sj = await stateJson([], projectDir, workstream);
|
||||
const sjd = sj.data as Record<string, unknown>;
|
||||
if (sjd.error) {
|
||||
return {
|
||||
@@ -85,11 +85,11 @@ export const routeNextAction: QueryHandler = async (_args, projectDir) => {
|
||||
|
||||
const consecutiveCalls = readConsecutiveCallCount(planning);
|
||||
|
||||
const ra = await roadmapAnalyze([], projectDir);
|
||||
const ra = await roadmapAnalyze([], projectDir, workstream);
|
||||
const raData = ra.data as { phases?: Array<Record<string, unknown>> };
|
||||
const phases = raData.phases ?? [];
|
||||
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
let dirCount = 0;
|
||||
try {
|
||||
dirCount = readdirSync(phasesDir, { withFileTypes: true }).filter(e => e.isDirectory()).length;
|
||||
@@ -97,7 +97,7 @@ export const routeNextAction: QueryHandler = async (_args, projectDir) => {
|
||||
|
||||
let unresolvedVerification = false;
|
||||
if (currentPhase) {
|
||||
const fp = await findPhase([currentPhase], projectDir);
|
||||
const fp = await findPhase([currentPhase], projectDir, workstream);
|
||||
const fd = fp.data as Record<string, unknown>;
|
||||
if (fd.found && fd.directory) {
|
||||
unresolvedVerification = await hasUnresolvedVerificationFails(
|
||||
@@ -126,7 +126,7 @@ export const routeNextAction: QueryHandler = async (_args, projectDir) => {
|
||||
uat_gaps: 0,
|
||||
};
|
||||
}
|
||||
const fp = await findPhase([cp], projectDir);
|
||||
const fp = await findPhase([cp], projectDir, workstream);
|
||||
const d = fp.data as Record<string, unknown>;
|
||||
const plans = (d.plans as string[]) ?? [];
|
||||
const summaries = (d.summaries as string[]) ?? [];
|
||||
@@ -212,7 +212,7 @@ export const routeNextAction: QueryHandler = async (_args, projectDir) => {
|
||||
};
|
||||
}
|
||||
|
||||
const fp = await findPhase([currentPhase], projectDir);
|
||||
const fp = await findPhase([currentPhase], projectDir, workstream);
|
||||
const pd = fp.data as Record<string, unknown>;
|
||||
const found = Boolean(pd.found);
|
||||
const cp = normalizePhaseName(currentPhase);
|
||||
|
||||
@@ -259,9 +259,10 @@ async function syncStateFrontmatter(content: string, projectDir: string): Promis
|
||||
*/
|
||||
async function readModifyWriteStateMd(
|
||||
projectDir: string,
|
||||
modifier: (content: string) => string | Promise<string>
|
||||
modifier: (content: string) => string | Promise<string>,
|
||||
workstream?: string,
|
||||
): Promise<string> {
|
||||
const statePath = planningPaths(projectDir).state;
|
||||
const statePath = planningPaths(projectDir, workstream).state;
|
||||
const lockPath = await acquireStateLock(statePath);
|
||||
try {
|
||||
let content: string;
|
||||
@@ -292,8 +293,9 @@ async function readModifyWriteStateMd(
|
||||
export async function readModifyWriteStateMdFull(
|
||||
projectDir: string,
|
||||
modifier: (content: string) => string | Promise<string>,
|
||||
workstream?: string,
|
||||
): Promise<void> {
|
||||
const statePath = planningPaths(projectDir).state;
|
||||
const statePath = planningPaths(projectDir, workstream).state;
|
||||
const lockPath = await acquireStateLock(statePath);
|
||||
try {
|
||||
let content = '';
|
||||
@@ -321,7 +323,7 @@ export async function readModifyWriteStateMdFull(
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { updated: true/false, field, value }
|
||||
*/
|
||||
export const stateUpdate: QueryHandler = async (args, projectDir) => {
|
||||
export const stateUpdate: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const field = args[0];
|
||||
const value = args[1];
|
||||
|
||||
@@ -337,7 +339,7 @@ export const stateUpdate: QueryHandler = async (args, projectDir) => {
|
||||
return result;
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
return { data: { updated, field, value: updated ? value : undefined } };
|
||||
};
|
||||
@@ -351,7 +353,7 @@ export const stateUpdate: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with `{ updated, failed }` matching `cmdStatePatch` in `state.cjs`
|
||||
*/
|
||||
export const statePatch: QueryHandler = async (args, projectDir) => {
|
||||
export const statePatch: QueryHandler = async (args, projectDir, workstream) => {
|
||||
let patches: Record<string, string>;
|
||||
|
||||
if (args.length >= 2 && args[0]?.startsWith('--')) {
|
||||
@@ -386,7 +388,7 @@ export const statePatch: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
return { data: { updated, failed } };
|
||||
};
|
||||
@@ -404,7 +406,7 @@ export const statePatch: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with phase metadata and `updated` field names (for raw parity)
|
||||
*/
|
||||
export const stateBeginPhase: QueryHandler = async (args, projectDir) => {
|
||||
export const stateBeginPhase: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const named = parseNamedArgs(args, ['phase', 'name', 'plans']);
|
||||
let phaseNumber = (named.phase as string | null) || '';
|
||||
let phaseName = (named.name as string | null) || '';
|
||||
@@ -527,7 +529,7 @@ export const stateBeginPhase: QueryHandler = async (args, projectDir) => {
|
||||
}
|
||||
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
return {
|
||||
data: {
|
||||
@@ -548,7 +550,7 @@ export const stateBeginPhase: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { advanced, current_plan, total_plans }
|
||||
*/
|
||||
export const stateAdvancePlan: QueryHandler = async (_args, projectDir) => {
|
||||
export const stateAdvancePlan: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
let result: Record<string, unknown> = { error: 'STATE.md not found' };
|
||||
|
||||
@@ -619,7 +621,7 @@ export const stateAdvancePlan: QueryHandler = async (_args, projectDir) => {
|
||||
});
|
||||
result = { advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans };
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
return { data: result };
|
||||
};
|
||||
@@ -633,7 +635,7 @@ export const stateAdvancePlan: QueryHandler = async (_args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { recorded: true/false }
|
||||
*/
|
||||
export const stateRecordMetric: QueryHandler = async (args, projectDir) => {
|
||||
export const stateRecordMetric: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const parsed = parseNamedArgs(args, ['phase', 'plan', 'duration', 'tasks', 'files']);
|
||||
const phase = parsed.phase as string | null;
|
||||
const plan = parsed.plan as string | null;
|
||||
@@ -664,7 +666,7 @@ export const stateRecordMetric: QueryHandler = async (args, projectDir) => {
|
||||
recorded = true;
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
if (recorded) {
|
||||
return { data: { recorded: true, phase, plan, duration } };
|
||||
@@ -681,13 +683,13 @@ export const stateRecordMetric: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { updated, percent, completed, total }
|
||||
*/
|
||||
export const stateUpdateProgress: QueryHandler = async (_args, projectDir) => {
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
export const stateUpdateProgress: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
let totalPlans = 0;
|
||||
let totalSummaries = 0;
|
||||
|
||||
try {
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir, workstream);
|
||||
const entries = await readdir(phasesDir, { withFileTypes: true });
|
||||
const phaseDirs = entries
|
||||
.filter(e => e.isDirectory())
|
||||
@@ -720,7 +722,7 @@ export const stateUpdateProgress: QueryHandler = async (_args, projectDir) => {
|
||||
return content.replace(plainProgressPattern, (_match, prefix: string) => `${prefix}${progressStr}`);
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
if (updated) {
|
||||
return { data: { updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr } };
|
||||
@@ -734,7 +736,7 @@ export const stateUpdateProgress: QueryHandler = async (_args, projectDir) => {
|
||||
* Appends a decision to the Decisions section. Removes placeholder text.
|
||||
* argv matches `gsd-tools.cjs`: `--phase`, `--summary`, `--rationale`, etc.
|
||||
*/
|
||||
export const stateAddDecision: QueryHandler = async (args, projectDir) => {
|
||||
export const stateAddDecision: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const parsed = parseNamedArgs(args, ['phase', 'summary', 'summary-file', 'rationale', 'rationale-file']);
|
||||
const phase = parsed.phase as string | null;
|
||||
let summaryText: string | null = null;
|
||||
@@ -777,7 +779,7 @@ export const stateAddDecision: QueryHandler = async (args, projectDir) => {
|
||||
added = true;
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
if (added) {
|
||||
return { data: { added: true, decision: entry } };
|
||||
@@ -789,7 +791,7 @@ export const stateAddDecision: QueryHandler = async (args, projectDir) => {
|
||||
* Query handler for state.add-blocker command.
|
||||
* argv: `--text`, `--text-file` (see `gsd-tools.cjs`).
|
||||
*/
|
||||
export const stateAddBlocker: QueryHandler = async (args, projectDir) => {
|
||||
export const stateAddBlocker: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const parsed = parseNamedArgs(args, ['text', 'text-file']);
|
||||
let blockerText: string | null = null;
|
||||
|
||||
@@ -824,7 +826,7 @@ export const stateAddBlocker: QueryHandler = async (args, projectDir) => {
|
||||
added = true;
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
if (added) {
|
||||
return { data: { added: true, blocker: blockerText } };
|
||||
@@ -836,7 +838,7 @@ export const stateAddBlocker: QueryHandler = async (args, projectDir) => {
|
||||
* Query handler for state.resolve-blocker command.
|
||||
* argv: `--text` (see `gsd-tools.cjs`).
|
||||
*/
|
||||
export const stateResolveBlocker: QueryHandler = async (args, projectDir) => {
|
||||
export const stateResolveBlocker: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const parsed = parseNamedArgs(args, ['text']);
|
||||
const searchText = parsed.text as string | null;
|
||||
if (!searchText) {
|
||||
@@ -873,7 +875,7 @@ export const stateResolveBlocker: QueryHandler = async (args, projectDir) => {
|
||||
content = content.replace(sectionPattern, (_match, header: string) => `${header}${newBody}`);
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
if (removedMatchingLine) {
|
||||
return { data: { resolved: true, blocker: searchText } };
|
||||
@@ -888,7 +890,7 @@ export const stateResolveBlocker: QueryHandler = async (args, projectDir) => {
|
||||
* Query handler for state.record-session command.
|
||||
* argv: `--stopped-at`, `--resume-file` (see `cmdStateRecordSession` in `state.cjs`).
|
||||
*/
|
||||
export const stateRecordSession: QueryHandler = async (args, projectDir) => {
|
||||
export const stateRecordSession: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const parsed = parseNamedArgs(args, ['stopped-at', 'resume-file']);
|
||||
const stoppedAt = parsed['stopped-at'] as string | null | undefined;
|
||||
const resumeFile = ((parsed['resume-file'] as string | null) ?? 'None');
|
||||
@@ -913,7 +915,7 @@ export const stateRecordSession: QueryHandler = async (args, projectDir) => {
|
||||
if (result) { content = result; updated.push('Resume File'); }
|
||||
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
if (updated.length > 0) {
|
||||
return { data: { recorded: true, updated } };
|
||||
@@ -924,7 +926,7 @@ export const stateRecordSession: QueryHandler = async (args, projectDir) => {
|
||||
/**
|
||||
* Query handler for state.planned-phase — port of `cmdStatePlannedPhase` from `state.cjs`.
|
||||
*/
|
||||
export const statePlannedPhase: QueryHandler = async (args, projectDir) => {
|
||||
export const statePlannedPhase: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const parsed = parseNamedArgs(args, ['phase', 'name', 'plans']);
|
||||
const phaseNumber = parsed.phase as string | null;
|
||||
const plansRaw = parsed.plans as string | null;
|
||||
@@ -943,7 +945,7 @@ export const statePlannedPhase: QueryHandler = async (args, projectDir) => {
|
||||
|
||||
const phaseLabel = String(phaseNumber).trim();
|
||||
|
||||
const statePath = planningPaths(projectDir).state;
|
||||
const statePath = planningPaths(projectDir, workstream).state;
|
||||
if (!existsSync(statePath)) {
|
||||
return { data: { error: 'STATE.md not found' } };
|
||||
}
|
||||
@@ -975,7 +977,7 @@ export const statePlannedPhase: QueryHandler = async (args, projectDir) => {
|
||||
lastActivity: `${today} -- Phase ${phaseLabel} planning complete`,
|
||||
});
|
||||
return content;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
return { data: { updated, phase: phaseNumber, plan_count: planCount } };
|
||||
};
|
||||
@@ -1009,7 +1011,7 @@ function parseNamedArgs(
|
||||
* Writes `WAITING.json` under both `.gsd/` and `.planning/` so readers that only
|
||||
* watch one location (e.g. init workflows) still observe the signal.
|
||||
*/
|
||||
export const stateSignalWaiting: QueryHandler = async (args, projectDir) => {
|
||||
export const stateSignalWaiting: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const parsed = parseNamedArgs(args, ['type', 'question', 'options', 'phase']);
|
||||
const type = (parsed.type as string | null) || 'decision_point';
|
||||
const question = (parsed.question as string | null) || null;
|
||||
@@ -1047,7 +1049,7 @@ export const stateSignalWaiting: QueryHandler = async (args, projectDir) => {
|
||||
/**
|
||||
* Port of `cmdSignalResume` from state.cjs.
|
||||
*/
|
||||
export const stateSignalResume: QueryHandler = async (_args, projectDir) => {
|
||||
export const stateSignalResume: QueryHandler = async (_args, projectDir, _workstream) => {
|
||||
const paths = [
|
||||
join(projectDir, '.gsd', 'WAITING.json'),
|
||||
join(projectDir, '.planning', 'WAITING.json'),
|
||||
@@ -1069,8 +1071,8 @@ export const stateSignalResume: QueryHandler = async (_args, projectDir) => {
|
||||
/**
|
||||
* Port of `cmdStateValidate` from state.cjs.
|
||||
*/
|
||||
export const stateValidate: QueryHandler = async (_args, projectDir) => {
|
||||
const paths = planningPaths(projectDir);
|
||||
export const stateValidate: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const statePath = paths.state;
|
||||
if (!existsSync(statePath)) {
|
||||
return { data: { error: 'STATE.md not found' } };
|
||||
@@ -1140,9 +1142,9 @@ export const stateValidate: QueryHandler = async (_args, projectDir) => {
|
||||
/**
|
||||
* Port of `cmdStateSync` from state.cjs. Supports `--verify` dry-run.
|
||||
*/
|
||||
export const stateSync: QueryHandler = async (args, projectDir) => {
|
||||
export const stateSync: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const verify = args.includes('--verify');
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const statePath = paths.state;
|
||||
if (!existsSync(statePath)) {
|
||||
return { data: { error: 'STATE.md not found' } };
|
||||
@@ -1230,7 +1232,7 @@ export const stateSync: QueryHandler = async (args, projectDir) => {
|
||||
return { data: { synced: false, changes, dry_run: true } };
|
||||
}
|
||||
|
||||
await readModifyWriteStateMd(projectDir, (body) => runModifier(body));
|
||||
await readModifyWriteStateMd(projectDir, (body) => runModifier(body), workstream);
|
||||
|
||||
return { data: { synced: true, changes, dry_run: false } };
|
||||
};
|
||||
@@ -1355,7 +1357,7 @@ function prunePass(content: string, cutoff: number): { newContent: string; archi
|
||||
* Port of `cmdStatePrune` from state.cjs.
|
||||
* Args: `--keep-recent N` (default 3), `--dry-run`, `--silent` (omit extra logging fields — no-op in SDK JSON).
|
||||
*/
|
||||
export const statePrune: QueryHandler = async (args, projectDir) => {
|
||||
export const statePrune: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const parsed = parseNamedArgs(args, ['keep-recent'], ['dry-run', 'silent']);
|
||||
const parsedKeepRecent = Number.parseInt(String(parsed['keep-recent'] ?? '3'), 10);
|
||||
if (!Number.isInteger(parsedKeepRecent) || parsedKeepRecent < 0) {
|
||||
@@ -1364,7 +1366,7 @@ export const statePrune: QueryHandler = async (args, projectDir) => {
|
||||
const keepRecent = parsedKeepRecent;
|
||||
const dryRun = parsed['dry-run'] === true;
|
||||
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const statePath = paths.state;
|
||||
if (!existsSync(statePath)) {
|
||||
return { data: { error: 'STATE.md not found' } };
|
||||
@@ -1411,7 +1413,7 @@ export const statePrune: QueryHandler = async (args, projectDir) => {
|
||||
const result = prunePass(b, cutoff);
|
||||
archived.push(...result.archivedSections);
|
||||
return result.newContent;
|
||||
});
|
||||
}, workstream);
|
||||
|
||||
const archivePath = join(paths.planning, 'STATE-ARCHIVE.md');
|
||||
const totalPruned = archived.reduce((sum, s) => sum + s.count, 0);
|
||||
|
||||
@@ -51,9 +51,9 @@ function loadConfigCjs(projectDir: string): Record<string, unknown> {
|
||||
*
|
||||
* Port of `cmdStateLoad` from `get-shit-done/bin/lib/state.cjs` lines 44–86.
|
||||
*/
|
||||
export const stateProjectLoad: QueryHandler = async (_args, projectDir) => {
|
||||
export const stateProjectLoad: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const config = loadConfigCjs(projectDir);
|
||||
const planDir = planningPaths(projectDir).planning;
|
||||
const planDir = planningPaths(projectDir, workstream).planning;
|
||||
|
||||
let stateRaw = '';
|
||||
try {
|
||||
|
||||
@@ -34,10 +34,10 @@ import type { QueryHandler } from './utils.js';
|
||||
*
|
||||
* Port of getMilestonePhaseFilter from core.cjs lines 1409-1442.
|
||||
*/
|
||||
export async function getMilestonePhaseFilter(projectDir: string): Promise<((dirName: string) => boolean) & { phaseCount: number }> {
|
||||
export async function getMilestonePhaseFilter(projectDir: string, workstream?: string): Promise<((dirName: string) => boolean) & { phaseCount: number }> {
|
||||
const milestonePhaseNums = new Set<string>();
|
||||
try {
|
||||
const roadmapContent = await readFile(planningPaths(projectDir).roadmap, 'utf-8');
|
||||
const roadmapContent = await readFile(planningPaths(projectDir, workstream).roadmap, 'utf-8');
|
||||
const roadmap = await extractCurrentMilestone(roadmapContent, projectDir);
|
||||
const phasePattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi;
|
||||
let m: RegExpExecArray | null;
|
||||
@@ -77,7 +77,7 @@ export async function getMilestonePhaseFilter(projectDir: string): Promise<((dir
|
||||
* Port of buildStateFrontmatter from state.cjs lines 650-760.
|
||||
* HIGH complexity: extracts fields, scans disk, computes progress.
|
||||
*/
|
||||
export async function buildStateFrontmatter(bodyContent: string, projectDir: string): Promise<Record<string, unknown>> {
|
||||
export async function buildStateFrontmatter(bodyContent: string, projectDir: string, workstream?: string): Promise<Record<string, unknown>> {
|
||||
const currentPhase = stateExtractField(bodyContent, 'Current Phase');
|
||||
const currentPhaseName = stateExtractField(bodyContent, 'Current Phase Name');
|
||||
const currentPlan = stateExtractField(bodyContent, 'Current Plan');
|
||||
@@ -103,8 +103,8 @@ export async function buildStateFrontmatter(bodyContent: string, projectDir: str
|
||||
let completedPlans: number | null = null;
|
||||
|
||||
try {
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir, workstream);
|
||||
const entries = await readdir(phasesDir, { withFileTypes: true });
|
||||
const phaseDirs = entries
|
||||
.filter(e => e.isDirectory())
|
||||
@@ -198,8 +198,8 @@ export async function buildStateFrontmatter(bodyContent: string, projectDir: str
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with rebuilt state frontmatter
|
||||
*/
|
||||
export const stateJson: QueryHandler = async (_args, projectDir) => {
|
||||
const statePath = planningPaths(projectDir).state;
|
||||
export const stateJson: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const statePath = planningPaths(projectDir, workstream).state;
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
@@ -212,7 +212,7 @@ export const stateJson: QueryHandler = async (_args, projectDir) => {
|
||||
const body = stripFrontmatter(content);
|
||||
|
||||
// Always rebuild from body + disk so progress reflects current state
|
||||
const built = await buildStateFrontmatter(body, projectDir);
|
||||
const built = await buildStateFrontmatter(body, projectDir, workstream);
|
||||
|
||||
// Preserve frontmatter-only fields that cannot be recovered from body
|
||||
if (existingFm && existingFm.stopped_at && !built.stopped_at) {
|
||||
@@ -242,8 +242,8 @@ export const stateJson: QueryHandler = async (_args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with field value or full content
|
||||
*/
|
||||
export const stateGet: QueryHandler = async (args, projectDir) => {
|
||||
const statePath = planningPaths(projectDir).state;
|
||||
export const stateGet: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const statePath = planningPaths(projectDir, workstream).state;
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
@@ -294,8 +294,8 @@ export const stateGet: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with structured snapshot
|
||||
*/
|
||||
export const stateSnapshot: QueryHandler = async (_args, projectDir) => {
|
||||
const statePath = planningPaths(projectDir).state;
|
||||
export const stateSnapshot: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const statePath = planningPaths(projectDir, workstream).state;
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
|
||||
@@ -163,8 +163,8 @@ export const summaryExtract: QueryHandler = async (args, projectDir) => {
|
||||
return { data: fullResult };
|
||||
};
|
||||
|
||||
export const historyDigest: QueryHandler = async (_args, projectDir) => {
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
export const historyDigest: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const digest: {
|
||||
phases: Record<
|
||||
string,
|
||||
|
||||
@@ -38,13 +38,13 @@ import type { QueryHandler } from './utils.js';
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { template: 'plan' | 'summary' | 'verification' }
|
||||
*/
|
||||
export const templateSelect: QueryHandler = async (args, projectDir) => {
|
||||
export const templateSelect: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phaseNum = args[0];
|
||||
if (!phaseNum) {
|
||||
return { data: { template: 'plan' } };
|
||||
}
|
||||
|
||||
const paths = planningPaths(projectDir);
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const normalized = normalizePhaseName(phaseNum);
|
||||
|
||||
// Find the phase directory
|
||||
|
||||
@@ -230,13 +230,13 @@ function parseVerificationItems(content: string, status: string): Record<string,
|
||||
/**
|
||||
* Cross-phase UAT / VERIFICATION audit — port of `cmdAuditUat` (`uat.cjs`).
|
||||
*/
|
||||
export const auditUat: QueryHandler = async (_args, projectDir) => {
|
||||
const paths = planningPaths(projectDir);
|
||||
export const auditUat: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
if (!existsSync(paths.phases)) {
|
||||
throw new GSDError('No phases directory found in planning directory', ErrorClassification.Blocked);
|
||||
}
|
||||
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir);
|
||||
const isDirInMilestone = await getMilestonePhaseFilter(projectDir, workstream);
|
||||
const results: Record<string, unknown>[] = [];
|
||||
|
||||
const dirs = readdirSync(paths.phases, { withFileTypes: true })
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface QueryResult {
|
||||
}
|
||||
|
||||
/** Signature for a query handler function. */
|
||||
export type QueryHandler = (args: string[], projectDir: string) => Promise<QueryResult>;
|
||||
export type QueryHandler = (args: string[], projectDir: string, workstream?: string) => Promise<QueryResult>;
|
||||
|
||||
// ─── generateSlug ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -186,8 +186,8 @@ export const verifyKeyLinks: QueryHandler = async (args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { passed, errors, warnings, warning_count }
|
||||
*/
|
||||
export const validateConsistency: QueryHandler = async (_args, projectDir) => {
|
||||
const paths = planningPaths(projectDir);
|
||||
export const validateConsistency: QueryHandler = async (_args, projectDir, workstream) => {
|
||||
const paths = planningPaths(projectDir, workstream);
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
@@ -344,7 +344,7 @@ export const validateConsistency: QueryHandler = async (_args, projectDir) => {
|
||||
* @param projectDir - Project root directory
|
||||
* @returns QueryResult with { status, errors, warnings, info, repairable_count, repairs_performed? }
|
||||
*/
|
||||
export const validateHealth: QueryHandler = async (args, projectDir) => {
|
||||
export const validateHealth: QueryHandler = async (args, projectDir, _workstream) => {
|
||||
const doRepair = args.includes('--repair');
|
||||
|
||||
// T-12-09: Home directory guard
|
||||
|
||||
@@ -133,13 +133,13 @@ export const verifyPlanStructure: QueryHandler = async (args, projectDir) => {
|
||||
* @returns QueryResult with { complete, phase, plan_count, summary_count, incomplete_plans, orphan_summaries, errors, warnings }
|
||||
* @throws GSDError with Validation classification if phase number missing
|
||||
*/
|
||||
export const verifyPhaseCompleteness: QueryHandler = async (args, projectDir) => {
|
||||
export const verifyPhaseCompleteness: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phase = args[0];
|
||||
if (!phase) {
|
||||
throw new GSDError('phase required', ErrorClassification.Validation);
|
||||
}
|
||||
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
const normalized = normalizePhaseName(phase);
|
||||
|
||||
// Find phase directory (mirror findPhase pattern from phase.ts)
|
||||
@@ -554,7 +554,7 @@ export const verifyPathExists: QueryHandler = async (args, projectDir) => {
|
||||
/**
|
||||
* Detect schema drift for a phase — port of `cmdVerifySchemaDrift` from verify.cjs lines 1013–1086.
|
||||
*/
|
||||
export const verifySchemaDrift: QueryHandler = async (args, projectDir) => {
|
||||
export const verifySchemaDrift: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phaseArg = args[0];
|
||||
const skipFlag = args.includes('--skip');
|
||||
|
||||
@@ -565,7 +565,7 @@ export const verifySchemaDrift: QueryHandler = async (args, projectDir) => {
|
||||
const { checkSchemaDrift } = await import('./schema-detect.js');
|
||||
const { execGit } = await import('./commit.js');
|
||||
|
||||
const phasesDir = planningPaths(projectDir).phases;
|
||||
const phasesDir = planningPaths(projectDir, workstream).phases;
|
||||
if (!existsSync(phasesDir)) {
|
||||
return {
|
||||
data: {
|
||||
|
||||
107
tests/bug-2524-sdk-query-ws-flag.test.cjs
Normal file
107
tests/bug-2524-sdk-query-ws-flag.test.cjs
Normal file
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Bug #2524: gsd-sdk query --ws <name> silently ignores the workstream flag.
|
||||
* Tests that --ws is forwarded through the call chain:
|
||||
* cli.ts -> registry.dispatch() -> planningPaths()
|
||||
*
|
||||
* Uses static source-file text assertions (no sdk/dist/ build required in CI).
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const helpersTs = fs.readFileSync(
|
||||
path.join(__dirname, '../sdk/src/query/helpers.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
const registryTs = fs.readFileSync(
|
||||
path.join(__dirname, '../sdk/src/query/registry.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
const cliTs = fs.readFileSync(
|
||||
path.join(__dirname, '../sdk/src/cli.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// ─── Layer 3: planningPaths() accepts workstream ───────────────────────────
|
||||
|
||||
describe('planningPaths() workstream support', () => {
|
||||
test('planningPaths signature includes optional workstream parameter', () => {
|
||||
assert.ok(
|
||||
helpersTs.includes('planningPaths(projectDir: string, workstream?: string)'),
|
||||
'planningPaths must accept an optional workstream parameter',
|
||||
);
|
||||
});
|
||||
|
||||
test('planningPaths uses relPlanningPath(workstream) to compute the base path', () => {
|
||||
assert.ok(
|
||||
helpersTs.includes('relPlanningPath(workstream)'),
|
||||
'planningPaths must call relPlanningPath(workstream) to scope the base path',
|
||||
);
|
||||
});
|
||||
|
||||
test('planningPaths imports relPlanningPath from workstream-utils', () => {
|
||||
assert.ok(
|
||||
helpersTs.includes('relPlanningPath'),
|
||||
'helpers.ts must import/use relPlanningPath from workstream-utils',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Layer 2: QueryRegistry.dispatch() accepts workstream ─────────────────
|
||||
|
||||
describe('QueryRegistry.dispatch() workstream threading', () => {
|
||||
test('dispatch method signature includes workstream parameter', () => {
|
||||
assert.ok(
|
||||
registryTs.includes('workstream?: string'),
|
||||
'dispatch() must accept an optional workstream parameter',
|
||||
);
|
||||
});
|
||||
|
||||
test('dispatch forwards workstream to the handler as third argument', () => {
|
||||
assert.ok(
|
||||
registryTs.includes('handler(args, projectDir, workstream)'),
|
||||
'dispatch() must pass workstream as the third argument to the handler',
|
||||
);
|
||||
});
|
||||
|
||||
test('QueryHandler type accepts a third workstream argument', () => {
|
||||
// QueryHandler type is defined in utils.ts, but registry.ts imports and uses it
|
||||
const utilsTs = fs.readFileSync(
|
||||
path.join(__dirname, '../sdk/src/query/utils.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
assert.ok(
|
||||
utilsTs.includes('workstream?: string') && utilsTs.includes('QueryHandler'),
|
||||
'QueryHandler type must include an optional workstream parameter',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Layer 1: CLI forwards args.ws to registry.dispatch() ─────────────────
|
||||
|
||||
describe('CLI forwards --ws to registry.dispatch()', () => {
|
||||
test('cli.ts passes args.ws as the workstream argument to registry.dispatch()', () => {
|
||||
assert.ok(
|
||||
cliTs.includes('registry.dispatch(matched.cmd, matched.args, args.projectDir, args.ws)'),
|
||||
'cli.ts must forward args.ws to registry.dispatch() as the workstream argument',
|
||||
);
|
||||
});
|
||||
|
||||
test('cli.ts defines a ws field in ParsedCliArgs', () => {
|
||||
assert.ok(
|
||||
cliTs.includes('ws: string | undefined'),
|
||||
'ParsedCliArgs must have a ws field typed as string | undefined',
|
||||
);
|
||||
});
|
||||
|
||||
test('cli.ts parses --ws flag from query argv', () => {
|
||||
assert.ok(
|
||||
cliTs.includes("if (a === '--ws' && argv[i + 1])"),
|
||||
'cli.ts query permissive parser must handle the --ws flag',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user