* test(#4462): expose whitespace workstream scope * fix(#4462): normalize the workstream environment scope * docs(#4462): add changeset for #4509 * fix(#4462): normalize sibling planning scope readers * test(#4462): cover workstream normalization properties * fix(#4462): share normalized workstream resolution --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -683,7 +683,7 @@ function loadConfigResolved(cwd: string, options: Record<string, unknown> = {}):
|
||||
: (options['workstreamContext'] && Object.prototype.hasOwnProperty.call(options['workstreamContext'], 'ws'))
|
||||
? (options['workstreamContext'] as Record<string, unknown>)['ws']
|
||||
: (process.env['GSD_WORKSTREAM'] || null);
|
||||
const ws = typeof activeWorkstream === 'string' ? activeWorkstream : (activeWorkstream === null ? null : null);
|
||||
const ws = typeof activeWorkstream === 'string' ? activeWorkstream.trim() || null : null;
|
||||
// wsRequested: true when caller explicitly requested a non-empty workstream.
|
||||
// Used for source labeling (Fix 4) and early absent-dir intercept (Fix 2).
|
||||
const wsRequested = ws != null && ws !== '';
|
||||
|
||||
@@ -21,7 +21,7 @@ const { CONFIG_DEFAULTS } = configLoader;
|
||||
import { platformWriteSync, platformEnsureDir } from './shell-command-projection.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import planningWorkspace = require('./planning-workspace.cjs');
|
||||
const { planningDir, planningRoot, withPlanningLock } = planningWorkspace;
|
||||
const { planningDir, planningRoot, resolveEnvWorkstream, withPlanningLock } = planningWorkspace;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import modelProfiles = require('./model-profiles.cjs');
|
||||
const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTable } = modelProfiles;
|
||||
@@ -1269,7 +1269,7 @@ function resolveFromRootConfig(cwd: string, kp: string): { found: boolean; value
|
||||
// diverges from planningRoot without a workstream and loadConfigResolved does NOT
|
||||
// inherit root — matching the runtime's own `if (ws)` gate keeps the two surfaces
|
||||
// from diverging on the project-scoped (non-workstream) case.
|
||||
if (!process.env['GSD_WORKSTREAM']) return { found: false, value: undefined };
|
||||
if (!resolveEnvWorkstream()) return { found: false, value: undefined };
|
||||
const root = planningRoot(cwd);
|
||||
const rootConfigPath = path.join(root, 'config.json');
|
||||
let rootConfig: Record<string, unknown>;
|
||||
@@ -1388,7 +1388,7 @@ function cmdConfigPath(cwd: string, _raw: boolean, workstreamContext: Workstream
|
||||
* (caller uses `await` which is safe on a sync return value).
|
||||
*/
|
||||
function cmdMigrateConfig(cwd: string, raw: boolean): void {
|
||||
const ws = process.env['GSD_WORKSTREAM'] || null;
|
||||
const ws = resolveEnvWorkstream();
|
||||
// #3749: resolve the migration target through the project-aware resolver so
|
||||
// GSD_PROJECT scopes the write; migrateOnDisk itself cannot (see its
|
||||
// configPathOverride note).
|
||||
|
||||
@@ -107,6 +107,7 @@ const {
|
||||
todosDir,
|
||||
listAvailableWorkstreams,
|
||||
peekActiveWorkstream,
|
||||
resolveEnvWorkstream,
|
||||
diagnoseUnresolvedActiveWorkstream,
|
||||
describeUnresolvedWorkstreamReason,
|
||||
findContextMdIn,
|
||||
@@ -1552,7 +1553,7 @@ function cmdInitNewMilestone(cwd: string, raw: boolean, options: Record<string,
|
||||
// would otherwise silently delete a stale/invalid pointer as a side effect
|
||||
// of building a JSON report field, and (per #3579) could change what a
|
||||
// LATER resolution in the same process observes.
|
||||
const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
|
||||
const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
||||
const workstreamActive = !!resolvedWorkstream;
|
||||
const flatMode = !workstreamActive;
|
||||
|
||||
@@ -3370,7 +3371,7 @@ function cmdInitUpdate(cwd: string, raw: boolean, options: Record<string, unknow
|
||||
function cmdInitTransition(cwd: string, raw: boolean, options: Record<string, unknown> = {}): void {
|
||||
// #3579 root-cause fix: read-only informational field — peek, don't
|
||||
// self-heal (see cmdInitNewMilestone's identical rationale above).
|
||||
const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
|
||||
const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
||||
const workstreamActive = !!resolvedWorkstream;
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
@@ -3470,7 +3471,7 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
|
||||
// non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
|
||||
// here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
|
||||
// which would misreport a present-but-bad marker as no marker at all.
|
||||
const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
|
||||
const _resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
||||
if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
|
||||
// #3579: getActiveWorkstream now inherits a pointer-less session's read
|
||||
// from the shared .planning/active-workstream marker, so reaching this
|
||||
|
||||
@@ -100,7 +100,7 @@ const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSu
|
||||
const {
|
||||
planningDir, withPlanningLock, listAvailableWorkstreams,
|
||||
peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason,
|
||||
resolvePhaseIdConvention,
|
||||
resolveEnvWorkstream, resolvePhaseIdConvention,
|
||||
} = planningWorkspace;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- milestone-lock.cjs is an export= CommonJS module
|
||||
import milestoneLockMod = require('./milestone-lock.cjs');
|
||||
@@ -3358,7 +3358,7 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
// non-mutating peek so an unresolvable pointer isn't self-healed (cleared)
|
||||
// here and then found "absent" by diagnoseUnresolvedActiveWorkstream below,
|
||||
// which would misreport a present-but-bad marker as no marker at all.
|
||||
const resolvedWorkstream = process.env['GSD_WORKSTREAM'] || peekActiveWorkstream(cwd);
|
||||
const resolvedWorkstream = resolveEnvWorkstream() ?? peekActiveWorkstream(cwd);
|
||||
if (availableWorkstreams.length > 0 && !resolvedWorkstream) {
|
||||
// #3579: getActiveWorkstream now inherits a pointer-less session's read
|
||||
// from the shared .planning/active-workstream marker, so reaching this
|
||||
|
||||
@@ -139,12 +139,15 @@ type WorkstreamAdapterOpts = Record<string, unknown>;
|
||||
* two-readers-two-bases lesson).
|
||||
*/
|
||||
function resolveEnvWorkstream(): string | null {
|
||||
return process.env['GSD_WORKSTREAM'] ?? null;
|
||||
const value = process.env['GSD_WORKSTREAM']?.trim();
|
||||
return value || null;
|
||||
}
|
||||
|
||||
function planningDir(cwd: string, ws?: string | null, project?: string | null): string {
|
||||
if (project === undefined) project = process.env['GSD_PROJECT'] ?? null;
|
||||
if (project === undefined) project = process.env['GSD_PROJECT']?.trim() || null;
|
||||
else if (typeof project === 'string') project = project.trim() || null;
|
||||
if (ws === undefined) ws = resolveEnvWorkstream();
|
||||
else if (typeof ws === 'string') ws = ws.trim() || null;
|
||||
|
||||
// Reject path separators and traversal components in project/workstream names
|
||||
const BAD_SEGMENT = /[/\\]|\.\./;
|
||||
@@ -210,7 +213,7 @@ function worktreesOptedOutUnguarded(cwd: string): boolean {
|
||||
};
|
||||
const scoped = ownKey(readCfg(path.join(planningDir(cwd), 'config.json')));
|
||||
if (scoped.present) return scoped.value === false;
|
||||
if (process.env['GSD_WORKSTREAM']) {
|
||||
if (resolveEnvWorkstream() !== null) {
|
||||
const root = ownKey(readCfg(path.join(planningRoot(cwd), 'config.json')));
|
||||
if (root.present) return root.value === false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user