fix(#2003): add --runtime override to capability state + loop render-hooks
resolveCapabilityRuntimeState derived the config dir from resolveRuntime(cwd) (GSD_RUNTIME -> config.runtime -> 'claude') when no --config-dir was passed, so a repo with persisted runtime:'codex' resolved the config dir to ~/.codex where the Claude skill isn't installed -> surfaced:false / hooks silently no-op when the operator drove from Claude Code. capability state and loop render-hooks parsed only --config-dir, never --runtime, so there was no way to assert the actually-active runtime. Add a runtimeOverride param to resolveCapabilityRuntimeState (canonicalized via runtime-name-policy so aliases like codex-app work); when present it short-circuits the persisted-runtime fallback and resolves getGlobalConfigDir for the explicit runtime. Thread --runtime through cmdCapabilityState and cmdLoopRenderHooks, and parse it in gsd-tools.cjs for both commands (dual --runtime X / --runtime=X form, mirroring --config-dir and the existing capability-set --runtime precedent). Help text updated. Without the override, behavior is byte-identical to today (regression-guarded).
This commit is contained in:
@@ -179,14 +179,23 @@
|
||||
*
|
||||
* Loop Extension Point Queries (ADR-857 phase 3c):
|
||||
* loop render-hooks <point> Resolve + render active Capability hooks at a loop point
|
||||
* [--config-dir <path>] [--runtime <r>] [--active-cap <capId>]
|
||||
* Returns JSON envelope { point, activeHooks, rendered }
|
||||
* Valid points: discuss:pre/post, plan:pre/post,
|
||||
* execute:pre/wave:pre/wave:post/post, verify:pre/post, ship:pre/post
|
||||
* --runtime: override the auto-detected runtime (#2003) so the config
|
||||
* dir resolves to that runtime's home even when
|
||||
* .planning/config.json persists a different runtime.
|
||||
*
|
||||
* Capability State (ADR-857 phase 4b):
|
||||
* capability state [--config-dir <path>] Resolve per-capability install/surface/hook-activation state
|
||||
* capability state [--config-dir <path>] [--runtime <r>] Resolve per-capability install/surface/hook-activation state
|
||||
* Returns JSON envelope { runtimeConfigDir, capabilities[] }
|
||||
* --config-dir: runtime config dir (default: auto-detect current runtime)
|
||||
* --runtime: override the auto-detected runtime (#2003); bypasses the
|
||||
* GSD_RUNTIME → config.runtime → 'claude' precedence so a
|
||||
* repo with a persisted runtime can still resolve another
|
||||
* runtime's config dir (e.g. driving Claude Code from a
|
||||
* repo that persists runtime:"codex").
|
||||
*
|
||||
* GSD-2 Migration:
|
||||
* from-gsd2 [--path <dir>] [--force] [--dry-run]
|
||||
@@ -1598,9 +1607,28 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
}
|
||||
loopActiveCap = value;
|
||||
}
|
||||
// --runtime <r> (#2003): explicit runtime override so the config-dir
|
||||
// resolution bypasses the persisted-runtime fallback (GSD_RUNTIME →
|
||||
// config.runtime). Mirrors the --config-dir dual-form (--runtime X /
|
||||
// --runtime=X) and the capability-set --runtime precedent.
|
||||
let loopRuntime = undefined;
|
||||
const runtimeEqArg = args.find(arg => arg.startsWith('--runtime='));
|
||||
const runtimeIdx = args.indexOf('--runtime');
|
||||
if (runtimeEqArg) {
|
||||
const value = runtimeEqArg.slice('--runtime='.length).trim();
|
||||
if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
||||
loopRuntime = value;
|
||||
} else if (runtimeIdx !== -1) {
|
||||
const value = args[runtimeIdx + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
loopRuntime = value;
|
||||
}
|
||||
loopResolver.cmdLoopRenderHooks(cwd, args[2], raw, {
|
||||
configDir: loopConfigDir ? path.resolve(loopConfigDir) : undefined,
|
||||
activeCap: loopActiveCap,
|
||||
runtime: loopRuntime,
|
||||
});
|
||||
} else {
|
||||
error(
|
||||
@@ -1752,7 +1780,24 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
configDir = configDirVal;
|
||||
}
|
||||
const resolvedConfigDir = configDir ? path.resolve(configDir) : null;
|
||||
capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, {});
|
||||
// --runtime <r> (#2003): explicit runtime override so the config-dir
|
||||
// resolution bypasses the persisted-runtime fallback. Dual-form like
|
||||
// --config-dir (--runtime X / --runtime=X).
|
||||
let stateRuntime = undefined;
|
||||
const stateRuntimeEqArg = args.find(arg => arg.startsWith('--runtime='));
|
||||
const stateRuntimeIdx = args.indexOf('--runtime');
|
||||
if (stateRuntimeEqArg) {
|
||||
const value = stateRuntimeEqArg.slice('--runtime='.length).trim();
|
||||
if (!value) error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
||||
stateRuntime = value;
|
||||
} else if (stateRuntimeIdx !== -1) {
|
||||
const value = args[stateRuntimeIdx + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
error('Missing value for --runtime', ERROR_REASON ? ERROR_REASON.USAGE : undefined);
|
||||
}
|
||||
stateRuntime = value;
|
||||
}
|
||||
capabilityState.cmdCapabilityState(cwd, resolvedConfigDir, raw, { runtime: stateRuntime });
|
||||
} else if (capSubcommand === 'set') {
|
||||
// capability set <id> [--on|--off|--enable|--disable] [--gate <key>=<bool>]... [--config-dir <dir>] [--runtime <r>] [--scope <s>]
|
||||
const capId = args[2];
|
||||
|
||||
@@ -441,6 +441,7 @@ function resolveCapabilityRuntimeState(
|
||||
cwd: string,
|
||||
runtimeConfigDir: string | undefined | null,
|
||||
configOverride?: Record<string, unknown>,
|
||||
runtimeOverride?: string,
|
||||
): ResolveCapabilityRuntimeStateResult {
|
||||
const warnings: string[] = [];
|
||||
|
||||
@@ -460,15 +461,35 @@ function resolveCapabilityRuntimeState(
|
||||
const runtimeHomes = require('./runtime-homes.cjs') as {
|
||||
getGlobalConfigDir: (runtime: string) => string;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const runtimeSlash = require('./runtime-slash.cjs') as {
|
||||
resolveRuntime: (projectDir: string | null | undefined) => string;
|
||||
};
|
||||
// Detect the active runtime via GSD_RUNTIME → config.runtime → 'claude'.
|
||||
// resolveRuntime reads config.json directly (no side effects) and returns
|
||||
// a lowercased canonical runtime name.
|
||||
const detectedRuntime = runtimeSlash.resolveRuntime(cwd);
|
||||
resolvedConfigDir = runtimeHomes.getGlobalConfigDir(detectedRuntime);
|
||||
// #2003: an explicit --runtime override bypasses the persisted-runtime
|
||||
// fallback (GSD_RUNTIME → config.runtime → 'claude') so, e.g., a repo with
|
||||
// persisted runtime:"codex" resolves the Claude config dir when the operator
|
||||
// is driving from Claude Code. Canonicalize via runtime-name-policy (handles
|
||||
// aliases like codex-app → codex); if canonicalization yields nothing, fall
|
||||
// through to the persisted-runtime resolution below. Mirrors the update-
|
||||
// context / effort sync precedent (read/diagnostic paths accepting both
|
||||
// --config-dir and --runtime).
|
||||
if (typeof runtimeOverride === 'string' && runtimeOverride.trim() !== '') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const runtimeNamePolicy = require('./runtime-name-policy.cjs') as {
|
||||
canonicalizeRuntimeName: (value: unknown) => string | null;
|
||||
};
|
||||
const canonical = runtimeNamePolicy.canonicalizeRuntimeName(runtimeOverride);
|
||||
if (canonical) {
|
||||
resolvedConfigDir = runtimeHomes.getGlobalConfigDir(canonical);
|
||||
}
|
||||
}
|
||||
if (!resolvedConfigDir) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const runtimeSlash = require('./runtime-slash.cjs') as {
|
||||
resolveRuntime: (projectDir: string | null | undefined) => string;
|
||||
};
|
||||
// Detect the active runtime via GSD_RUNTIME → config.runtime → 'claude'.
|
||||
// resolveRuntime reads config.json directly (no side effects) and returns
|
||||
// a lowercased canonical runtime name.
|
||||
const detectedRuntime = runtimeSlash.resolveRuntime(cwd);
|
||||
resolvedConfigDir = runtimeHomes.getGlobalConfigDir(detectedRuntime);
|
||||
}
|
||||
} catch {
|
||||
// Defensive fallback: use ~/.claude if the canonical resolver throws.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
@@ -569,9 +590,12 @@ function cmdCapabilityState(
|
||||
cwd: string,
|
||||
runtimeConfigDir: string | undefined | null,
|
||||
raw: boolean,
|
||||
_options: Record<string, unknown> = {},
|
||||
options: Record<string, unknown> = {},
|
||||
): void {
|
||||
const result = resolveCapabilityRuntimeState(cwd, runtimeConfigDir);
|
||||
// #2003: thread an explicit --runtime override so the config-dir resolution
|
||||
// bypasses the persisted-runtime fallback (GSD_RUNTIME → config.runtime).
|
||||
const runtimeOverride = typeof options['runtime'] === 'string' ? options['runtime'] : undefined;
|
||||
const result = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, undefined, runtimeOverride);
|
||||
for (const warning of result.warnings) {
|
||||
coreError(`capability state: ${warning}`);
|
||||
}
|
||||
|
||||
@@ -475,6 +475,12 @@ function cmdLoopRenderHooks(
|
||||
const runtimeConfigDir = typeof options['configDir'] === 'string'
|
||||
? options['configDir']
|
||||
: undefined;
|
||||
// #2003: thread an explicit --runtime override into the capability-state
|
||||
// resolver so the config-dir resolution bypasses the persisted-runtime
|
||||
// fallback (GSD_RUNTIME → config.runtime). Without this, a repo with persisted
|
||||
// runtime:"codex" resolves the config dir to ~/.codex and execute:post /
|
||||
// verify:post hooks silently no-op when the operator drives from Claude Code.
|
||||
const runtimeOverride = typeof options['runtime'] === 'string' ? options['runtime'] : undefined;
|
||||
// Load the config snapshot ONCE and share it with both the capability-state
|
||||
// resolver (via configOverride) and loop-hook resolution, so federated keys
|
||||
// present in loadConfig resolve identically for `active` and for hook when/
|
||||
@@ -488,7 +494,7 @@ function cmdLoopRenderHooks(
|
||||
} catch {
|
||||
config = {};
|
||||
}
|
||||
const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, config) as {
|
||||
const state = resolveCapabilityRuntimeState(cwd, runtimeConfigDir, config, runtimeOverride) as {
|
||||
warnings?: string[];
|
||||
capabilities: Array<{ id: string; enabled?: boolean; active: boolean }>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user