refactor(#299): deepen active workstream resolution context (#300)

This commit is contained in:
Tom Boucher
2026-05-26 12:25:32 -04:00
committed by GitHub
parent 1f2ef131c5
commit aec61416d8
4 changed files with 29 additions and 11 deletions

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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) ──────────────────