diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 46b577681..48609e959 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -174,7 +174,7 @@ const path = require('path'); const core = require('./lib/core.cjs'); const { error, findProjectRoot, ERROR_REASON } = core; const { getActiveWorkstream } = require('./lib/planning-workspace.cjs'); -const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs'); +const { resolveActiveWorkstreamContext, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs'); const state = require('./lib/state.cjs'); const phase = require('./lib/phase.cjs'); const roadmap = require('./lib/roadmap.cjs'); @@ -293,14 +293,15 @@ async function main() { // Optional workstream override for parallel milestone work. // Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null. let ws = null; + let workstreamContext = null; try { - const wsResolution = resolveActiveWorkstream(cwd, args, process.env, { + workstreamContext = resolveActiveWorkstreamContext(cwd, args, process.env, { getStored: getActiveWorkstream, }); - ws = wsResolution.ws; - args = wsResolution.args; + ws = workstreamContext.ws; + args = workstreamContext.args; // Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths. - applyResolvedWorkstreamEnv(wsResolution, process.env); + applyResolvedWorkstreamEnv(workstreamContext, process.env); } catch (err) { error(err.message || String(err)); } @@ -431,7 +432,7 @@ async function main() { // When --pick is active, capture stdout and extract the requested field. if (pickField) { const captured = await captureStdoutSyncWrites(async () => { - await runCommand(command, args, cwd, raw, defaultValue, originalCommand); + await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext); }); const resolved = resolveAtFileOutput(captured); try { @@ -451,7 +452,7 @@ async function main() { // every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check // that breaks on PowerShell and other non-bash shells. const captured = await captureStdoutSyncWrites(async () => { - await runCommand(command, args, cwd, raw, defaultValue, originalCommand); + await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext); }); fs.writeSync(1, resolveAtFileOutput(captured)); } @@ -518,7 +519,7 @@ function extractField(obj, fieldPath) { return current; } -async function runCommand(command, args, cwd, raw, defaultValue, originalCommand) { +async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) { switch (command) { case 'state': { routeStateCommand({ @@ -801,7 +802,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand // The SDK handler (configPath) also exists but requires a projectDir that // is already resolved. Both produce identical output; keeping CJS here is // simpler and avoids sync-bridge overhead for a trivial path lookup. - config.cmdConfigPath(cwd, raw); + config.cmdConfigPath(cwd, raw, workstreamContext); break; } diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 73bd72a58..87bf5ca9e 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -651,10 +651,12 @@ function getCmdConfigSetModelProfileResultMessage( * Print the resolved config.json path (workstream-aware). Used by settings.md * so the workflow writes/reads the correct file when a workstream is active (#2282). */ -function cmdConfigPath(cwd) { +function cmdConfigPath(cwd, _raw, workstreamContext = null) { // Always emit as plain text — a file path is used via shell substitution, // never consumed as JSON. Passing raw=true forces plain-text output. - const configPath = path.join(planningDir(cwd), 'config.json'); + const configPath = workstreamContext && workstreamContext.configPath + ? workstreamContext.configPath + : path.join(planningDir(cwd), 'config.json'); output(configPath, true, configPath); } diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 9fd362d98..bfd2f5c70 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -280,6 +280,8 @@ function _deepMergeConfig(base, overlay) { function loadConfig(cwd, options = {}) { const activeWorkstream = Object.prototype.hasOwnProperty.call(options, 'workstream') ? options.workstream + : (options.workstreamContext && Object.prototype.hasOwnProperty.call(options.workstreamContext, 'ws')) + ? options.workstreamContext.ws : (process.env.GSD_WORKSTREAM || null); // When GSD_WORKSTREAM is set, load root config first so workstream config // can inherit from it. This prevents users from duplicating model_overrides, diff --git a/tests/core.test.cjs b/tests/core.test.cjs index 699fdfce3..3a4ecc1d8 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -298,6 +298,19 @@ describe('loadConfig workstream config inheritance (#2714)', () => { assert.strictEqual(config.model_profile, 'quality'); assert.strictEqual(process.env.GSD_WORKSTREAM, 'feature-f'); }); + + test('loadConfig accepts workstreamContext.ws without requiring env mutation', () => { + writeRootConfig({ model_profile: 'balanced' }); + writeWorkstreamConfig('feature-g', { model_profile: 'quality' }); + delete process.env.GSD_WORKSTREAM; + + const config = loadConfig(tmpDir, { + workstreamContext: { ws: 'feature-g' }, + }); + + assert.strictEqual(config.model_profile, 'quality'); + assert.strictEqual(process.env.GSD_WORKSTREAM, undefined); + }); }); // ─── loadConfig commit_docs gitignore auto-detection (#1250) ──────────────────