From 3a623b1117ac9523a7e674d7680a41c7fd5806a9 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 26 Apr 2026 10:53:17 -0400 Subject: [PATCH] fix(query): thread workstream through all query handlers All 18+ query handlers accepted _workstream but never forwarded it to planningPaths/loadConfig/getMilestoneInfo. Remove _ prefix and pass workstream to all internal helper calls so --ws flag actually scopes path resolution. Affected handlers: initNewProject, initProgress, initManager, configGet, configPath, configSet, configSetModelProfile, configNewProject, configEnsureSection, validateHealth, commit, checkCommit, commitToSubrepo. Also fixes validate.ts to use paths.* fields from planningPaths instead of hardcoded join(projectDir, '.planning') paths. Closes #2731 --- sdk/src/query/commit.ts | 12 ++++++------ sdk/src/query/config-mutation.ts | 16 ++++++++-------- sdk/src/query/config-query.ts | 8 ++++---- sdk/src/query/init-complex.ts | 24 ++++++++++++------------ sdk/src/query/validate.ts | 14 +++++++------- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/sdk/src/query/commit.ts b/sdk/src/query/commit.ts index e477fe116..6a714cbb8 100644 --- a/sdk/src/query/commit.ts +++ b/sdk/src/query/commit.ts @@ -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, _workstream) => { +export const commit: QueryHandler = async (args, projectDir, workstream) => { const allArgs = [...args]; // Extract flags @@ -117,7 +117,7 @@ export const commit: QueryHandler = async (args, projectDir, _workstream) => { // Check commit_docs config unless --force if (!hasForce) { - const paths = planningPaths(projectDir); + const paths = planningPaths(projectDir, workstream); try { const raw = await readFile(paths.config, 'utf-8'); const config = JSON.parse(raw) as Record; @@ -180,8 +180,8 @@ export const commit: QueryHandler = async (args, projectDir, _workstream) => { * @param projectDir - Project root directory * @returns QueryResult with { can_commit, reason, commit_docs, staged_files } */ -export const checkCommit: QueryHandler = async (_args, projectDir, _workstream) => { - const paths = planningPaths(projectDir); +export const checkCommit: QueryHandler = async (_args, projectDir, workstream) => { + const paths = planningPaths(projectDir, workstream); let commitDocs = true; try { @@ -227,7 +227,7 @@ export const checkCommit: QueryHandler = async (_args, projectDir, _workstream) // ─── commitToSubrepo ───────────────────────────────────────────────────── -export const commitToSubrepo: QueryHandler = async (args, projectDir, _workstream) => { +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']); @@ -239,7 +239,7 @@ export const commitToSubrepo: QueryHandler = async (args, projectDir, _workstrea return { data: { committed: false, reason: 'commit message required' } }; } - const paths = planningPaths(projectDir); + const paths = planningPaths(projectDir, workstream); let config: Record = {}; try { const raw = await readFile(paths.config, 'utf-8'); diff --git a/sdk/src/query/config-mutation.ts b/sdk/src/query/config-mutation.ts index 010f2de70..5dcbb3ab6 100644 --- a/sdk/src/query/config-mutation.ts +++ b/sdk/src/query/config-mutation.ts @@ -186,7 +186,7 @@ function setConfigValue(obj: Record, 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, _workstream) => { +export const configSet: QueryHandler = async (args, projectDir, workstream) => { const keyPath = args[0]; const rawValue = args[1]; if (!keyPath) { @@ -214,7 +214,7 @@ export const configSet: QueryHandler = async (args, projectDir, _workstream) => } // D6: Lock protection for read-modify-write (match CJS config.cjs:296) - const paths = planningPaths(projectDir); + const paths = planningPaths(projectDir, workstream); const lockPath = await acquireStateLock(paths.config); let previousValue: unknown; try { @@ -255,7 +255,7 @@ export const configSet: QueryHandler = async (args, projectDir, _workstream) => * @returns QueryResult with { set: true, profile, agents } * @throws GSDError with Validation if profile is invalid */ -export const configSetModelProfile: QueryHandler = async (args, projectDir, _workstream) => { +export const configSetModelProfile: QueryHandler = async (args, projectDir, workstream) => { const profileName = args[0]; if (!profileName) { throw new GSDError( @@ -273,7 +273,7 @@ export const configSetModelProfile: QueryHandler = async (args, projectDir, _wor } // D6: Lock protection for read-modify-write - const paths = planningPaths(projectDir); + const paths = planningPaths(projectDir, workstream); const lockPath = await acquireStateLock(paths.config); let previousProfile = 'balanced'; try { @@ -317,8 +317,8 @@ export const configSetModelProfile: QueryHandler = async (args, projectDir, _wor * @param projectDir - Project root directory * @returns QueryResult with { created: true, path } or { created: false, reason } */ -export const configNewProject: QueryHandler = async (args, projectDir, _workstream) => { - const paths = planningPaths(projectDir); +export const configNewProject: QueryHandler = async (args, projectDir, workstream) => { + const paths = planningPaths(projectDir, workstream); // Idempotent: don't overwrite existing config if (existsSync(paths.config)) { @@ -443,13 +443,13 @@ export const configNewProject: QueryHandler = async (args, projectDir, _workstre * @param projectDir - Project root directory * @returns QueryResult with { ensured: true, section } */ -export const configEnsureSection: QueryHandler = async (args, projectDir, _workstream) => { +export const configEnsureSection: QueryHandler = async (args, projectDir, workstream) => { const sectionName = args[0]; if (!sectionName) { throw new GSDError('Usage: config-ensure-section
', ErrorClassification.Validation); } - const paths = planningPaths(projectDir); + const paths = planningPaths(projectDir, workstream); let config: Record = {}; try { const raw = await readFile(paths.config, 'utf-8'); diff --git a/sdk/src/query/config-query.ts b/sdk/src/query/config-query.ts index da2866d5c..c949eb06d 100644 --- a/sdk/src/query/config-query.ts +++ b/sdk/src/query/config-query.ts @@ -79,13 +79,13 @@ 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, _workstream) => { +export const configGet: QueryHandler = async (args, projectDir, workstream) => { const keyPath = args[0]; if (!keyPath) { throw new GSDError('Usage: config-get ', ErrorClassification.Validation); } - const paths = planningPaths(projectDir); + const paths = planningPaths(projectDir, workstream); let raw: string; try { raw = await readFile(paths.config, 'utf-8'); @@ -129,8 +129,8 @@ export const configGet: QueryHandler = async (args, projectDir, _workstream) => * @param projectDir - Project root directory * @returns QueryResult with `{ path: string }` absolute or project-relative resolution via planningPaths */ -export const configPath: QueryHandler = async (_args, projectDir, _workstream) => { - const paths = planningPaths(projectDir); +export const configPath: QueryHandler = async (_args, projectDir, workstream) => { + const paths = planningPaths(projectDir, workstream); return { data: { path: paths.config } }; }; diff --git a/sdk/src/query/init-complex.ts b/sdk/src/query/init-complex.ts index f3c2a869a..d85e19a03 100644 --- a/sdk/src/query/init-complex.ts +++ b/sdk/src/query/init-complex.ts @@ -93,8 +93,8 @@ function deriveStatusFromCheckbox( * * Port of cmdInitNewProject from init.cjs lines 296-399. */ -export const initNewProject: QueryHandler = async (_args, projectDir, _workstream) => { - const config = await loadConfig(projectDir); +export const initNewProject: QueryHandler = async (_args, projectDir, workstream) => { + const config = await loadConfig(projectDir, workstream); // Detect search API key availability from env vars and ~/.gsd/ files const gsdHome = join(homedir(), '.gsd'); @@ -209,10 +209,10 @@ export const initNewProject: QueryHandler = async (_args, projectDir, _workstrea * * Port of cmdInitProgress from init.cjs lines 1139-1284. */ -export const initProgress: QueryHandler = async (_args, projectDir, _workstream) => { - const config = await loadConfig(projectDir); - const milestone = await getMilestoneInfo(projectDir); - const paths = planningPaths(projectDir); +export const initProgress: QueryHandler = async (_args, projectDir, workstream) => { + const config = await loadConfig(projectDir, workstream); + const milestone = await getMilestoneInfo(projectDir, workstream); + const paths = planningPaths(projectDir, workstream); const phases: Record[] = []; let currentPhase: Record | null = null; @@ -225,7 +225,7 @@ export const initProgress: QueryHandler = async (_args, projectDir, _workstream) try { const rawRoadmap = await readFile(paths.roadmap, 'utf-8'); - const roadmapContent = await extractCurrentMilestone(rawRoadmap, projectDir); + const roadmapContent = await extractCurrentMilestone(rawRoadmap, projectDir, workstream); const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi; let hm: RegExpExecArray | null; while ((hm = headingPattern.exec(roadmapContent)) !== null) { @@ -372,10 +372,10 @@ export const initProgress: QueryHandler = async (_args, projectDir, _workstream) * * Port of cmdInitManager from init.cjs lines 854-1137. */ -export const initManager: QueryHandler = async (_args, projectDir, _workstream) => { - const config = await loadConfig(projectDir); - const milestone = await getMilestoneInfo(projectDir); - const paths = planningPaths(projectDir); +export const initManager: QueryHandler = async (_args, projectDir, workstream) => { + const config = await loadConfig(projectDir, workstream); + const milestone = await getMilestoneInfo(projectDir, workstream); + const paths = planningPaths(projectDir, workstream); let rawContent: string; try { @@ -384,7 +384,7 @@ export const initManager: QueryHandler = async (_args, projectDir, _workstream) return { data: { error: 'No ROADMAP.md found. Run /gsd-new-milestone first.' } }; } - const content = await extractCurrentMilestone(rawContent, projectDir); + const content = await extractCurrentMilestone(rawContent, projectDir, workstream); // Pre-compute directory listing once let phaseDirEntries: string[] = []; diff --git a/sdk/src/query/validate.ts b/sdk/src/query/validate.ts index c770dff97..1c6e35de6 100644 --- a/sdk/src/query/validate.ts +++ b/sdk/src/query/validate.ts @@ -344,7 +344,7 @@ export const validateConsistency: QueryHandler = async (_args, projectDir, works * @param projectDir - Project root directory * @returns QueryResult with { status, errors, warnings, info, repairable_count, repairs_performed? } */ -export const validateHealth: QueryHandler = async (args, projectDir, _workstream) => { +export const validateHealth: QueryHandler = async (args, projectDir, workstream) => { const doRepair = args.includes('--repair'); // T-12-09: Home directory guard @@ -365,13 +365,13 @@ export const validateHealth: QueryHandler = async (args, projectDir, _workstream }; } - const paths = planningPaths(projectDir); - const planBase = join(projectDir, '.planning'); + const paths = planningPaths(projectDir, workstream); + const planBase = paths.planning; const projectPath = join(planBase, 'PROJECT.md'); - const roadmapPath = join(planBase, 'ROADMAP.md'); - const statePath = join(planBase, 'STATE.md'); - const configPath = join(planBase, 'config.json'); - const phasesDir = join(planBase, 'phases'); + const roadmapPath = paths.roadmap; + const statePath = paths.state; + const configPath = paths.config; + const phasesDir = paths.phases; interface Issue { code: string;