diff --git a/.changeset/nimble-yaks-climb.md b/.changeset/nimble-yaks-climb.md new file mode 100644 index 000000000..bf75a33b5 --- /dev/null +++ b/.changeset/nimble-yaks-climb.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2332 +--- +**Installing a non-Claude runtime no longer breaks Claude's model resolution in no-project sessions** — the installer writes `resolve_model_ids:"omit"` for non-alias runtimes into the machine-wide `~/.gsd/defaults.json`, which any runtime read back, so install order silently flipped Claude's adaptive tier aliases (executor→sonnet, planner→opus) to an empty model string. Resolution is now scoped to the runtime actually resolving, via a per-install `.gsd-runtime` marker: Claude ignores a global-defaults omit and keeps its tier aliases, non-alias runtimes still omit, and an explicit project-level `omit`/`true` is always honored. (#2297) diff --git a/bin/install.js b/bin/install.js index 41c0666c3..7180a9424 100755 --- a/bin/install.js +++ b/bin/install.js @@ -10880,6 +10880,22 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { failures.push('VERSION'); } + // #2297: write a per-install runtime marker co-located with VERSION at + // /gsd-core/.gsd-runtime. It gives resolveModelInternal a reliable + // "which runtime owns THIS install" signal in a no-project session (config.runtime + // is null and GSD_RUNTIME is not exported), so the shared ~/.gsd/defaults.json + // resolve_model_ids:"omit" policy (written below for non-alias runtimes only) + // applies ONLY when a non-alias runtime is actually resolving — a Claude session + // reads its own marker and keeps its tier aliases instead of inheriting another + // runtime's install-order-dependent "omit". See src/model-resolver.cts. + const runtimeMarkerDest = path.join(targetDir, 'gsd-core', '.gsd-runtime'); + fs.writeFileSync(runtimeMarkerDest, `${runtime}\n`); + if (verifyFileInstalled(runtimeMarkerDest, '.gsd-runtime')) { + console.log(` ${green}✓${reset} Wrote runtime marker (.gsd-runtime: ${runtime})`); + } else { + failures.push('.gsd-runtime'); + } + // Reusable: copy hooks/dist/ + hooks/lib/ into destRootDir, writing the // CommonJS package.json marker alongside them. Used below for the generic // configDir install path (guarded by hostBehaviors.skipSharedHooksInstall), diff --git a/src/model-resolver.cts b/src/model-resolver.cts index c475769f9..447a747be 100644 --- a/src/model-resolver.cts +++ b/src/model-resolver.cts @@ -10,7 +10,9 @@ * epic #1267; callers import resolvers from model-resolver.cjs directly. * * Dependencies (leaf modules only): - * - node:fs / node:path (stdlib, not currently needed — included for future use) + * - node:fs / node:path (read the per-install .gsd-runtime marker + project config for the #2297 omit gate) + * - ./runtime-name-policy.cjs (resolveRuntimeNameFromCandidates — canonicalize the active runtime) + * - ./planning-workspace.cjs (planningDir — workstream/project-aware project-config path) * - ./config-loader.cjs (loadConfig) * - ./configuration.cjs (CONFIG_DEFAULTS as CANONICAL_CONFIG_DEFAULTS) * - ./model-profiles.cjs (MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TIERS, nextTier) @@ -30,6 +32,98 @@ const { MODEL_PROFILES, AGENT_TO_PHASE_TYPE, AGENT_DEFAULT_TIERS, VALID_AGENT_TI import { MODEL_ALIAS_MAP, RUNTIME_PROFILE_MAP, PROVIDER_PRESETS } from './model-catalog.cjs'; +import fs from 'node:fs'; +import path from 'node:path'; +import { resolveRuntimeNameFromCandidates } from './runtime-name-policy.cjs'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import planningWorkspaceMod = require('./planning-workspace.cjs'); +const { planningDir } = planningWorkspaceMod; + +// ─── #2297: per-install runtime identity for the resolve_model_ids:"omit" gate ─ +// +// The installer writes `resolve_model_ids:"omit"` into the SHARED +// ~/.gsd/defaults.json for every runtime that lacks native model aliases (#1156). +// Because that file is machine-wide, a non-Claude install would otherwise poison +// a Claude no-project resolution into returning '' — silently defeating Claude's +// adaptive tier aliases. The "omit" must therefore apply only when a runtime that +// genuinely lacks native aliases is the one resolving. +// +// In a no-project session there is no `.planning/config.json` (so config.runtime +// is null) and GSD_RUNTIME is not exported by gsd-core, so the only reliable +// current-runtime signal is the per-install marker the installer co-locates next +// to VERSION at /gsd-core/.gsd-runtime (this file's dir is +// /gsd-core/bin/lib). Precedence for the gate: project config.runtime → +// GSD_RUNTIME env (manual/CI override + test seam) → install marker → 'claude'. +// +// `claude` is currently the ONLY runtime with nativeModelAliases:true; a +// registry-parity test guards this set so a future alias-capable runtime fails +// loudly here instead of silently omitting. +const RUNTIMES_WITH_NATIVE_ALIASES: ReadonlySet = new Set(['claude']); + +let _installMarkerCache: string | null | undefined; +function readInstallRuntimeMarker(): string | null { + if (_installMarkerCache !== undefined) return _installMarkerCache; + try { + const markerPath = path.join(__dirname, '..', '..', '.gsd-runtime'); + const raw = fs.readFileSync(markerPath, 'utf8').trim(); + _installMarkerCache = raw || null; + } catch { + // No marker: dev/source tree, or an install predating #2297. Fall through to + // the 'claude' default (keeps tier aliases — never worse than the bug). + _installMarkerCache = null; + } + return _installMarkerCache; +} + +// Test seams for the install-marker rung (the dev/source tree has no marker, so +// the file read always bottoms out at 'claude' — these let tests exercise the +// third precedence rung and reset the module-level cache between cases). +function _setInstallRuntimeMarkerForTests(value: string | null): void { + _installMarkerCache = value; +} +function _resetInstallRuntimeMarkerCacheForTests(): void { + _installMarkerCache = undefined; +} + +// The runtime whose install is actually resolving, canonicalized so an alias or +// case variant (e.g. "claude-code"/"Claude") cannot defeat the native-alias +// check below (#2297 review). Precedence mirrors resolveRuntime() +// (runtime-slash.cts): GSD_RUNTIME env → project config.runtime → per-install +// .gsd-runtime marker → 'claude'. +function resolveActiveRuntime(config: Record): string { + return resolveRuntimeNameFromCandidates( + process.env['GSD_RUNTIME'], + config['runtime'], + readInstallRuntimeMarker(), + ) || 'claude'; +} + +// Did the PROJECT's own config (root `.planning/config.json` or the active +// workstream/project override) explicitly set resolve_model_ids to "omit"? +// Project config takes precedence over the shared ~/.gsd/defaults.json (#2297 +// out-of-scope guard + #2517 finding #4): an explicit project "omit" is honored +// regardless of runtime, whereas an "omit" that came only from the global +// defaults is ignored by native-alias runtimes. Workstream/project-scope aware +// via planningDir (mirrors loadConfig's precedence: workstream value wins over +// root); a plain read avoids loadConfig's normalization side effects. +function projectExplicitlySetsOmit(cwd: string): boolean { + const wsDir = planningDir(cwd); + const rootDir = path.join(cwd, '.planning'); + const layers = wsDir === rootDir ? [rootDir] : [wsDir, rootDir]; // workstream > root + for (const dir of layers) { + try { + const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf8')) as Record; + const value = parsed?.['resolve_model_ids']; + // First layer that sets the key wins (matches loadConfig's deep-merge + // precedence). A layer that omits the key falls through to the next. + if (value !== undefined) return value === 'omit'; + } catch { + // Absent/unreadable layer — try the next. + } + } + return false; +} + // ─── Model alias resolution ─────────────────────────────────────────────────── interface TierEntryResolved { @@ -277,8 +371,18 @@ function resolveModelInternal(cwd: string, agentType: string): string { if (entry?.model) return entry.model; } - // 4. resolve_model_ids: "omit" - if (config['resolve_model_ids'] === 'omit') { + // 4. resolve_model_ids: "omit" — runtime-aware (#2297). Honor "omit" when the + // PROJECT explicitly set it (user intent — project config wins, #2517 finding + // #4) OR when the active runtime genuinely lacks native model aliases. Only a + // native-alias runtime (Claude) ignores an "omit" that came solely from the + // SHARED ~/.gsd/defaults.json — the #2297 poisoning fix — and falls through to + // its tier aliases below. Active runtime: GSD_RUNTIME → config.runtime → the + // per-install .gsd-runtime marker → 'claude' (canonicalized). + // NOTE: a non-Claude runtime that HAS a populated runtime-tier map already + // returned its own model id at step 3 above, before this gate — for those the + // explicit-project-omit honoring here is moot (step 3 wins, by #2517 design). + if (config['resolve_model_ids'] === 'omit' + && (projectExplicitlySetsOmit(cwd) || !RUNTIMES_WITH_NATIVE_ALIASES.has(resolveActiveRuntime(config)))) { return ''; } @@ -292,7 +396,11 @@ function resolveModelInternal(cwd: string, agentType: string): string { if (tier === 'inherit') return 'inherit'; const alias = tier; - if (config['resolve_model_ids']) { + // Only the explicit `true` opt-in materializes full model IDs (#1569). Guard + // against the loose-truthy check catching a "omit" that a native-alias runtime + // ignored above (#2297): "omit" must fall through to the tier ALIAS here, not + // be materialized into a full ID Claude's Agent tool cannot spawn. + if (config['resolve_model_ids'] === true) { return (MODEL_ALIAS_MAP as Record)[alias!] || alias!; } @@ -578,6 +686,8 @@ export = { resolveModelInternal, _resetModelPolicyWarningCacheForTests, _resetModelOverrideWarningCacheForTests, + _setInstallRuntimeMarkerForTests, + _resetInstallRuntimeMarkerCacheForTests, VALID_GRANULARITIES, resolveGranularityInternal, assertValidGranularityOverride, diff --git a/tests/fix-2297-resolve-model-ids-runtime-scoping.test.cjs b/tests/fix-2297-resolve-model-ids-runtime-scoping.test.cjs new file mode 100644 index 000000000..b50fd6e5a --- /dev/null +++ b/tests/fix-2297-resolve-model-ids-runtime-scoping.test.cjs @@ -0,0 +1,411 @@ +/** + * Bug #2297 — `resolve_model_ids:"omit"` must be scoped to the ACTIVE runtime, + * not applied blindly whenever it appears anywhere in the merged config. + * + * Root cause (pre-fix): the installer writes `resolve_model_ids:"omit"` into the + * SHARED `~/.gsd/defaults.json` for every runtime that lacks native model + * aliases (#1156). Because that file is machine-wide, installing a non-Claude + * runtime (e.g. codex) on a box that also runs Claude poisoned Claude's + * no-project resolution: Claude would see `resolve_model_ids:"omit"` in the + * merged global defaults and return `''` instead of its tier aliases + * (opus/sonnet/haiku), silently defeating Claude's adaptive tier distinction. + * + * Fix (`src/model-resolver.cts` `resolveModelInternal`): the `"omit"` branch + * now returns `''` ONLY when either + * (a) the PROJECT's own `.planning/config.json` explicitly sets + * `resolve_model_ids:"omit"` (user intent — #2517 finding #4, unchanged), OR + * (b) the ACTIVE runtime genuinely lacks native model aliases. + * A native-alias runtime (currently only `claude`) IGNORES an `"omit"` that + * came solely from the shared global defaults and falls through to its tier + * aliases. Active-runtime precedence: `process.env.GSD_RUNTIME` -> `config.runtime` + * -> per-install `.gsd-runtime` marker (absent in this dev/test tree, so the + * chain always bottoms out at `'claude'`) -> `'claude'` (all canonicalized). + * + * IMPORTANT (empirically verified — see dispatch report): the global-defaults + * merge path in `config-loader.cjs` (branch D: "no .planning/ at all") is ONLY + * exercised when the project directory has NO `.planning/` directory whatsoever. + * The moment a `.planning/` directory exists — even with an empty or absent + * `config.json` inside it — the loader takes a different branch that does NOT + * merge `~/.gsd/defaults.json` for these fields at all. So Group A below + * (which specifically exercises the global-defaults poisoning fix) uses BARE + * `fs.mkdtempSync` project dirs with no `.planning/` subdirectory. Group A #4, + * Group B, and Group C all need a real per-project config, so those DO create + * `.planning/config.json`. + */ + +'use strict'; + +const { describe, test, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { + resolveModelInternal, + _setInstallRuntimeMarkerForTests, + _resetInstallRuntimeMarkerCacheForTests, +} = require('../gsd-core/bin/lib/model-resolver.cjs'); + +// ─── HOME / GSD_HOME / GSD_RUNTIME isolation ──────────────────────────────── +// config-loader.cjs reads global defaults from +// path.join(process.env.GSD_HOME || os.homedir(), '.gsd', 'defaults.json'). +// Isolate both HOME and GSD_HOME to a fresh tmpdir per test (so a developer's +// real ~/.gsd/defaults.json never bleeds into assertions), and save/restore +// GSD_RUNTIME since several tests set it directly to drive the active-runtime +// chain (#2297's second precedence rung). Also save/restore GSD_WORKSTREAM and +// GSD_PROJECT (#2297 correctness-review hermeticity gap): planningDir() reads +// both directly from process.env when its ws/project params are omitted, so an +// ambient GSD_WORKSTREAM/GSD_PROJECT in a developer's shell could silently +// redirect projectExplicitlySetsOmit()'s config-file reads to the wrong layer. +let _origHome; +let _origUserProfile; +let _origGsdHome; +let _origGsdRuntime; +let _origGsdWorkstream; +let _origGsdProject; +let _isolatedHome; + +function isolateHome() { + _origHome = process.env.HOME; + _origUserProfile = process.env.USERPROFILE; + _origGsdHome = process.env.GSD_HOME; + _origGsdRuntime = process.env.GSD_RUNTIME; + _origGsdWorkstream = process.env.GSD_WORKSTREAM; + _origGsdProject = process.env.GSD_PROJECT; + _isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2297-home-')); + process.env.HOME = _isolatedHome; + // Windows resolves the home dir from USERPROFILE, not HOME — set both so the + // isolation holds cross-platform (local/require-userprofile-with-home). + process.env.USERPROFILE = _isolatedHome; + process.env.GSD_HOME = _isolatedHome; + delete process.env.GSD_RUNTIME; + delete process.env.GSD_WORKSTREAM; + delete process.env.GSD_PROJECT; +} + +function restoreHome() { + if (_origHome === undefined) delete process.env.HOME; else process.env.HOME = _origHome; + if (_origUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = _origUserProfile; + if (_origGsdHome === undefined) delete process.env.GSD_HOME; else process.env.GSD_HOME = _origGsdHome; + if (_origGsdRuntime === undefined) delete process.env.GSD_RUNTIME; else process.env.GSD_RUNTIME = _origGsdRuntime; + if (_origGsdWorkstream === undefined) delete process.env.GSD_WORKSTREAM; else process.env.GSD_WORKSTREAM = _origGsdWorkstream; + if (_origGsdProject === undefined) delete process.env.GSD_PROJECT; else process.env.GSD_PROJECT = _origGsdProject; + rmDir(_isolatedHome); + _isolatedHome = null; +} + +function rmDir(dir) { + if (typeof dir !== 'string' || dir.length === 0) return; + // eslint-disable-next-line local/no-raw-rmsync-in-tests -- carries the same maxRetries/retryDelay budget as helpers.cleanup; used for both the isolated-home and bare project temp dirs + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 }); +} + +function writeGlobalDefaults(obj) { + fs.mkdirSync(path.join(_isolatedHome, '.gsd'), { recursive: true }); + fs.writeFileSync(path.join(_isolatedHome, '.gsd', 'defaults.json'), JSON.stringify(obj, null, 2)); +} + +// Bare project dir with NO .planning/ subdirectory — needed to exercise the +// config-loader's global-defaults merge branch (see file header). +function mkProjNoPlanning() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2297-proj-noplan-')); +} + +// Project dir WITH a .planning/config.json — the normal "inside a project" path. +function mkProjWithConfig(obj) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2297-proj-')); + fs.mkdirSync(path.join(dir, '.planning'), { recursive: true }); + fs.writeFileSync(path.join(dir, '.planning', 'config.json'), JSON.stringify(obj, null, 2)); + return dir; +} + +// ─── Group A: GLOBAL-defaults "omit" is runtime-scoped (the #2297 fix) ───── +describe('#2297: global-defaults resolve_model_ids:"omit" is scoped to the active runtime', () => { + let projDir; + beforeEach(() => { isolateHome(); projDir = null; }); + afterEach(() => { rmDir(projDir); restoreHome(); }); + + test('no runtime signal defaults to claude: executor and planner get distinct non-empty tier aliases (acceptance #3)', () => { + // Global defaults poison the shared file with "omit" (simulating a + // non-Claude runtime having been installed on this machine). With no + // .planning/config.json (no project) and no GSD_RUNTIME, the active + // runtime falls back to 'claude', which has native aliases and must + // ignore the poisoned global "omit" — the adaptive tier distinction + // between executor (sonnet) and planner (opus) must survive. + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + + const executor = resolveModelInternal(projDir, 'gsd-executor'); + const planner = resolveModelInternal(projDir, 'gsd-planner'); + + assert.strictEqual(executor, 'sonnet'); + assert.strictEqual(planner, 'opus'); + assert.notStrictEqual(executor, ''); + assert.notStrictEqual(planner, ''); + assert.notStrictEqual(executor, planner); + }); + + test('GSD_RUNTIME="claude" explicitly: executor still resolves to "sonnet" (claude ignores global omit)', () => { + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + process.env.GSD_RUNTIME = 'claude'; + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-executor'), 'sonnet'); + }); + + test('GSD_RUNTIME="codex": a non-alias runtime still honors the global omit (acceptance #4)', () => { + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + process.env.GSD_RUNTIME = 'codex'; + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-executor'), ''); + }); + + // #2297 correctness-review BLOCKER: resolveActiveRuntime() must canonicalize + // its candidates via resolveRuntimeNameFromCandidates before checking + // RUNTIMES_WITH_NATIVE_ALIASES, or an alias/case variant of "claude" would + // fail the Set('claude').has() check and wrongly fall through to honoring the + // poisoned global omit. These would FAIL against a non-canonicalizing resolver. + test('GSD_RUNTIME="claude-code" (alias, not canonical "claude"): executor and planner still ignore the global omit', () => { + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + process.env.GSD_RUNTIME = 'claude-code'; + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-executor'), 'sonnet'); + assert.strictEqual(resolveModelInternal(projDir, 'gsd-planner'), 'opus'); + }); + + test('GSD_RUNTIME="Claude" (case variant): executor still resolves to "sonnet" (canonicalization is case-insensitive)', () => { + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + process.env.GSD_RUNTIME = 'Claude'; + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-executor'), 'sonnet'); + }); + + test('project config.runtime="codex" (no resolve_model_ids in project) takes precedence over GSD_RUNTIME/marker in the active-runtime chain', () => { + // config.runtime is checked before GSD_RUNTIME / the install marker. This + // scenario uses a REAL project (.planning/config.json present), so the + // config-loader does NOT merge ~/.gsd/defaults.json for resolve_model_ids + // at all here (see file header) — resolution instead reaches the #2517 + // runtime-tier path (step 3 in resolveModelInternal, which fires before + // the omit gate) and returns codex's native sonnet-tier model id directly, + // rather than the omit gate's ''. Verified empirically: the built resolver + // returns 'gpt-5.6-terra', not ''. Assert it is non-empty and NOT a claude + // alias, which is the property this test actually needs to guarantee + // (config.runtime, not GSD_RUNTIME/env, drove the resolution). + projDir = mkProjWithConfig({ runtime: 'codex' }); + writeGlobalDefaults({ resolve_model_ids: 'omit' }); // irrelevant: not merged when .planning/ exists + + const result = resolveModelInternal(projDir, 'gsd-executor'); + assert.notStrictEqual(result, ''); + assert.ok( + !['sonnet', 'opus', 'haiku'].includes(result), + `expected a non-claude-alias result for config.runtime="codex", got ${JSON.stringify(result)}` + ); + }); + + test('install-order independence (acceptance #1/#2): a global omit poisoned by a prior non-Claude install does not affect Claude resolution, and Claude retains its adaptive tier distinction', () => { + // Resolution depends on the RESOLVING runtime (active runtime at call + // time), not on install order — installing codex (or any non-alias + // runtime) before/after Claude must never change what Claude itself + // resolves to. Global omit present, no project, no runtime signal -> + // default 'claude' -> tier aliases survive. Distinct from the first Group A + // test above: this asserts install-order independence AND, specifically, + // that executor/planner remain DIFFERENT tiers under the poisoned global + // omit — i.e. install order never collapses Claude's adaptive tier + // distinction into a single omitted value. + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + + const executor = resolveModelInternal(projDir, 'gsd-executor'); + const planner = resolveModelInternal(projDir, 'gsd-planner'); + + assert.strictEqual(executor, 'sonnet'); + assert.strictEqual(planner, 'opus'); + assert.notStrictEqual(executor, planner, 'install-order poisoning must not collapse the adaptive tier distinction'); + }); +}); + +// ─── Group B: explicit PROJECT "omit" is still honored for EVERY runtime ─── +// (#2517 finding #4 — preserved, NOT changed by #2297.) +describe('#2297: explicit project-level resolve_model_ids:"omit" is honored regardless of runtime', () => { + let projDir; + beforeEach(() => { isolateHome(); projDir = null; }); + afterEach(() => { rmDir(projDir); restoreHome(); }); + + test('no runtime set, explicit project omit -> "" even though the default runtime is claude', () => { + projDir = mkProjWithConfig({ resolve_model_ids: 'omit' }); + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-planner'), ''); + }); + + test('runtime:"claude" + explicit project omit -> "" (mirrors #2517 finding #4)', () => { + projDir = mkProjWithConfig({ runtime: 'claude', resolve_model_ids: 'omit' }); + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-planner'), ''); + }); +}); + +// ─── Group B2: projectExplicitlySetsOmit is workstream-scope aware (#2297) ── +// The root .planning/config.json does NOT set resolve_model_ids, but the +// ACTIVE workstream's own config.json does — projectExplicitlySetsOmit() +// resolves via planningDir(cwd) (workstream layer wins over root, mirroring +// loadConfig's precedence), so the workstream's explicit "omit" must still be +// honored even though no global default and the default runtime (claude) would +// otherwise have returned a tier alias. +describe('#2297: explicit project-level "omit" is honored at the active-workstream config layer', () => { + let projDir; + let _origGsdWorkstreamForBlock; + beforeEach(() => { + isolateHome(); // clears GSD_WORKSTREAM/GSD_PROJECT as part of hermeticity + projDir = null; + _origGsdWorkstreamForBlock = process.env.GSD_WORKSTREAM; + }); + afterEach(() => { + if (_origGsdWorkstreamForBlock === undefined) delete process.env.GSD_WORKSTREAM; + else process.env.GSD_WORKSTREAM = _origGsdWorkstreamForBlock; + rmDir(projDir); + restoreHome(); + }); + + test('root config has no resolve_model_ids, but the active workstream config sets "omit" -> "" despite default runtime claude', () => { + const ws = 'ws-alpha'; + projDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2297-proj-ws-')); + fs.mkdirSync(path.join(projDir, '.planning'), { recursive: true }); + // Root config exists but does NOT set resolve_model_ids at all. + fs.writeFileSync( + path.join(projDir, '.planning', 'config.json'), + JSON.stringify({ model_profile: 'balanced' }, null, 2) + ); + // The active workstream's own config explicitly sets "omit". + const wsConfigDir = path.join(projDir, '.planning', 'workstreams', ws); + fs.mkdirSync(wsConfigDir, { recursive: true }); + fs.writeFileSync( + path.join(wsConfigDir, 'config.json'), + JSON.stringify({ resolve_model_ids: 'omit' }, null, 2) + ); + process.env.GSD_WORKSTREAM = ws; + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-planner'), ''); + }); +}); + +// ─── Group C: explicit `true` still materializes full model ids (acceptance #5) ── +describe('#2297: resolve_model_ids:true still materializes full Claude model ids', () => { + let projDir; + beforeEach(() => { isolateHome(); projDir = null; }); + afterEach(() => { rmDir(projDir); restoreHome(); }); + + test('resolve_model_ids:true + balanced profile -> full materialized claude-opus-4-8 id', () => { + projDir = mkProjWithConfig({ resolve_model_ids: true, model_profile: 'balanced' }); + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-planner'), 'claude-opus-4-8'); + }); +}); + +// ─── Group D: registry parity guard ───────────────────────────────────────── +describe('#2297: capability-registry nativeModelAliases parity guard', () => { + test('exactly the runtimes with hostBehaviors.nativeModelAliases:true match RUNTIMES_WITH_NATIVE_ALIASES ([\'claude\'])', () => { + // The model-resolver hardcodes RUNTIMES_WITH_NATIVE_ALIASES = new Set(['claude']) + // rather than reading the registry at runtime. This test keeps that + // hardcoded set honest against the generated registry's actual contract: + // registry.runtimes[id].runtime.hostBehaviors.nativeModelAliases. + // If a future runtime gains nativeModelAliases:true, this fails loudly so + // RUNTIMES_WITH_NATIVE_ALIASES in model-resolver.cts is updated in lockstep. + const registry = require('../gsd-core/bin/lib/capability-registry.cjs'); + + const nativeAliasRuntimes = Object.keys(registry.runtimes) + .filter((id) => registry.runtimes[id]?.runtime?.hostBehaviors?.nativeModelAliases === true) + .sort(); + + assert.deepStrictEqual(nativeAliasRuntimes, ['claude']); + }); +}); + +// ─── Group E: installer writes the per-install .gsd-runtime marker ───────── +describe('#2297: installer emits the gsd-core/.gsd-runtime marker (fixture parity)', () => { + test('claude and codex install-tree fixtures both list gsd-core/.gsd-runtime', () => { + // These fixtures are flat JSON arrays of install-relative paths, generated + // by running the real installer (tests/fixtures/install-tree/*.json). Their + // presence here proves the installer actually emits the per-install marker + // that resolveActiveRuntime()'s precedence chain falls back to. + const claudeFixturePath = path.join(__dirname, 'fixtures', 'install-tree', 'claude.json'); + const codexFixturePath = path.join(__dirname, 'fixtures', 'install-tree', 'codex.json'); + + const claudeFixture = JSON.parse(fs.readFileSync(claudeFixturePath, 'utf8')); + const codexFixture = JSON.parse(fs.readFileSync(codexFixturePath, 'utf8')); + + assert.ok(Array.isArray(claudeFixture), 'expected claude.json fixture to be a flat array of paths'); + assert.ok(Array.isArray(codexFixture), 'expected codex.json fixture to be a flat array of paths'); + + assert.ok( + claudeFixture.includes('gsd-core/.gsd-runtime'), + 'expected claude.json install-tree fixture to include gsd-core/.gsd-runtime' + ); + assert.ok( + codexFixture.includes('gsd-core/.gsd-runtime'), + 'expected codex.json install-tree fixture to include gsd-core/.gsd-runtime' + ); + }); +}); + +// ─── Group F: the install-marker precedence rung, driven directly (#2297) ── +// Previously untested: with no GSD_RUNTIME and no project config.runtime, the +// active runtime falls all the way through to the per-install .gsd-runtime +// marker (third precedence rung). The dev/source tree has no real marker file, +// so these tests drive that rung directly via the _setInstallRuntimeMarkerForTests +// / _resetInstallRuntimeMarkerCacheForTests seams exported specifically for this +// purpose (#2297 correctness-review gap). +describe('#2297: install-marker precedence rung (GSD_RUNTIME and config.runtime both absent)', () => { + let projDir; + beforeEach(() => { + isolateHome(); // also deletes GSD_RUNTIME + projDir = null; + // Belt-and-suspenders: the marker rung is only reached when GSD_RUNTIME and + // config.runtime are both absent; isolateHome() already deletes GSD_RUNTIME. + delete process.env.GSD_RUNTIME; + }); + afterEach(() => { + rmDir(projDir); + restoreHome(); + // CRITICAL: reset the module-level marker cache after every case in this + // block so a set value never leaks into a later case here, or into any + // OTHER describe block in this file (readInstallRuntimeMarker() otherwise + // memoizes the first value it sees for the lifetime of the process). + _resetInstallRuntimeMarkerCacheForTests(); + }); + + test('marker="codex" (non-alias runtime): honors the poisoned global omit -> ""', () => { + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + _setInstallRuntimeMarkerForTests('codex'); + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-executor'), ''); + }); + + test('marker="claude": ignores the poisoned global omit -> "sonnet"', () => { + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + _setInstallRuntimeMarkerForTests('claude'); + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-executor'), 'sonnet'); + }); + + test('marker="claude-code" (alias): canonicalized to "claude" and still ignores the poisoned global omit -> "sonnet"', () => { + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + _setInstallRuntimeMarkerForTests('claude-code'); + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-executor'), 'sonnet'); + }); + + test('marker unset (null): falls through to the "claude" default and ignores the poisoned global omit -> "sonnet"', () => { + writeGlobalDefaults({ resolve_model_ids: 'omit' }); + projDir = mkProjNoPlanning(); + _setInstallRuntimeMarkerForTests(null); + + assert.strictEqual(resolveModelInternal(projDir, 'gsd-executor'), 'sonnet'); + }); +}); diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index 9d437f1b6..f2794785a 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md": "5f86de1decbd16d2", "agents/gsd-user-profiler.md": "25d65f6458454764", "agents/gsd-verifier.md": "224e8df2d8fd1d95", + "gsd-core/.gsd-runtime": "54e851f5dd76ba94", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index fb72f7679..518579938 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -106,6 +106,7 @@ "commands/gsd-verify-work.md": "1cb62ea69b117acb", "commands/gsd-workspace.md": "dd1bc09d2b768e0b", "commands/gsd-workstreams.md": "52ab9c585d3a00f3", + "gsd-core/.gsd-runtime": "ba737a9959aad238", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index e38d67e4e..f275ccd65 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -105,6 +105,7 @@ "commands/gsd-verify-work.md": "246ac304c4dfde48", "commands/gsd-workspace.md": "5928e93b8ab475ac", "commands/gsd-workstreams.md": "52ab9c585d3a00f3", + "gsd-core/.gsd-runtime": "98038b25280788a4", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 9c3e777ca..191d27c58 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -34,6 +34,7 @@ "agents/gsd-ui-researcher.md": "9e3ac030767167e0", "agents/gsd-user-profiler.md": "003276f85792cfda", "agents/gsd-verifier.md": "2271174b5aa20e31", + "gsd-core/.gsd-runtime": "98038b25280788a4", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index 3ae2ffca5..76b57bc6c 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -38,6 +38,7 @@ "agents/gsd-ui-researcher.md": "1bb303f3a3c4dfc9", "agents/gsd-user-profiler.md": "622220df0654b6bf", "agents/gsd-verifier.md": "75912e9cadd83eb3", + "gsd-core/.gsd-runtime": "98d4891544955482", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index be00b5ba3..ea9b0e9fd 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -106,6 +106,7 @@ "commands/gsd-verify-work.md": "6edbbb82a1f2aea7", "commands/gsd-workspace.md": "d765ff60cde657a6", "commands/gsd-workstreams.md": "884b6c8d648422c7", + "gsd-core/.gsd-runtime": "04e9f0f6433d6d58", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index 2283faaf7..fa2feb266 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -141,6 +141,7 @@ "agents/gsd-verifier.md": "4ac4b860e2504374", "agents/gsd-verifier.toml": "8ed9fb961409e894", "config.toml": "b5f627b42f060910", + "gsd-core/.gsd-runtime": "243b0dc9b847e66c", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 6b55d69de..0fea999d6 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -36,6 +36,7 @@ "agents/gsd-user-profiler.agent.md": "ae16a248e18dd42b", "agents/gsd-verifier.agent.md": "87a8e3238e838a39", "copilot-instructions.md": "1fb04111759f1645", + "gsd-core/.gsd-runtime": "d0bcfb0a7f01f8c9", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index c325b7048..ce2d2bafe 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -106,6 +106,7 @@ "commands/gsd-verify-work.md": "86a42f859bc26dd6", "commands/gsd-workspace.md": "5c40114e6af87e3d", "commands/gsd-workstreams.md": "112660ceb663c750", + "gsd-core/.gsd-runtime": "0e49decc490cf91d", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index bfb688fae..6ecc73d01 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md": "8799b6013e06ae49", "agents/gsd-user-profiler.md": "ca3bf75581f211a0", "agents/gsd-verifier.md": "82d3e9015ed04017", + "gsd-core/.gsd-runtime": "d53ba9ce30ffd743", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index f6cf859d8..4718f00c2 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -106,6 +106,7 @@ "command/gsd-verify-work.md": "d07409c940a6ff00", "command/gsd-workspace.md": "9048133312f47fdc", "command/gsd-workstreams.md": "5e57eed1881c3891", + "gsd-core/.gsd-runtime": "687db6a1b110b149", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index b6bfa0e76..33062fbf3 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -99,6 +99,7 @@ "agents/subagents/gsd-user-profiler.yaml": "645826a29d079159", "agents/subagents/gsd-verifier.md": "699f62763424ac68", "agents/subagents/gsd-verifier.yaml": "2d2bd6b37626f382", + "gsd-core/.gsd-runtime": "cb556f5d5cee2779", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index 18c8e6ac7..def43406a 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -106,6 +106,7 @@ "command/gsd-verify-work.md": "f23fff8e6d71f704", "command/gsd-workspace.md": "1e581bdb33bc8f55", "command/gsd-workstreams.md": "5e57eed1881c3891", + "gsd-core/.gsd-runtime": "0d74cc0a7af2a416", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json index 5d466b6d3..f6f5f6a24 100644 --- a/tests/fixtures/golden-install-parity/pi.json +++ b/tests/fixtures/golden-install-parity/pi.json @@ -2,6 +2,7 @@ ".gsd-profile": "0e716a5fef4e6dc1", ".gsd/defaults.json": "560664b045e645cb", "extensions/gsd.cjs": "619cec0af9cfdadf", + "gsd-core/.gsd-runtime": "94e95f0bb38f8e0f", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index bc9d18d04..7442dbc44 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md": "e3768304c1c77753", "agents/gsd-user-profiler.md": "13388a80dc302a66", "agents/gsd-verifier.md": "113d8dcff860b595", + "gsd-core/.gsd-runtime": "2b626670f713516a", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index 03ebaca70..b8df3cf02 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md": "9e1a84a55a4cac99", "agents/gsd-user-profiler.md": "622220df0654b6bf", "agents/gsd-verifier.md": "99e95e235aacffbf", + "gsd-core/.gsd-runtime": "c96852d55ce78a68", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 6e44a10e3..cd049cdc3 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md": "c348aa3ff8412ecb", "agents/gsd-user-profiler.md": "622220df0654b6bf", "agents/gsd-verifier.md": "a07b00b9c5b7b338", + "gsd-core/.gsd-runtime": "fe100af8906532d6", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index 36c85cee3..4b6b848b1 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -106,6 +106,7 @@ "commands/gsd-verify-work.md": "71aad09ecfd3f844", "commands/gsd-workspace.md": "611002e4941d5654", "commands/gsd-workstreams.md": "57a8ef0af8c085d7", + "gsd-core/.gsd-runtime": "d0f601b915afd6bf", "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index 129debac2..3c57eb48e 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md", "agents/gsd-user-profiler.md", "agents/gsd-verifier.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index 1c5977a7c..8aa824a21 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -106,6 +106,7 @@ "commands/gsd-verify-work.md", "commands/gsd-workspace.md", "commands/gsd-workstreams.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index 46d4776ea..03c23b7b2 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -105,6 +105,7 @@ "commands/gsd-verify-work.md", "commands/gsd-workspace.md", "commands/gsd-workstreams.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index 656bf934b..5e75abf38 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -34,6 +34,7 @@ "agents/gsd-ui-researcher.md", "agents/gsd-user-profiler.md", "agents/gsd-verifier.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index 668e59098..cc3b39e3b 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -38,6 +38,7 @@ "agents/gsd-ui-researcher.md", "agents/gsd-user-profiler.md", "agents/gsd-verifier.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index dcc5e54bc..1b9122fe1 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -106,6 +106,7 @@ "commands/gsd-verify-work.md", "commands/gsd-workspace.md", "commands/gsd-workstreams.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index a5db2f467..38f06021b 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -141,6 +141,7 @@ "agents/gsd-verifier.md", "agents/gsd-verifier.toml", "config.toml", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index bf257edbf..1f091658e 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -36,6 +36,7 @@ "agents/gsd-user-profiler.agent.md", "agents/gsd-verifier.agent.md", "copilot-instructions.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index 373692429..ab33184c9 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -106,6 +106,7 @@ "commands/gsd-verify-work.md", "commands/gsd-workspace.md", "commands/gsd-workstreams.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index cb82f8eb8..3e24cc634 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md", "agents/gsd-user-profiler.md", "agents/gsd-verifier.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index 93ff07f73..13fd26098 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -106,6 +106,7 @@ "command/gsd-verify-work.md", "command/gsd-workspace.md", "command/gsd-workstreams.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 03d9428da..e2cd1609b 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -99,6 +99,7 @@ "agents/subagents/gsd-user-profiler.yaml", "agents/subagents/gsd-verifier.md", "agents/subagents/gsd-verifier.yaml", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index 0ac467bf3..bceedbaae 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -106,6 +106,7 @@ "command/gsd-verify-work.md", "command/gsd-workspace.md", "command/gsd-workstreams.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index 31f35703b..de7490c51 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -2,6 +2,7 @@ ".gsd-profile", ".gsd/defaults.json", "extensions/gsd.cjs", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index d1d1e8dc9..c6b6456b0 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md", "agents/gsd-user-profiler.md", "agents/gsd-verifier.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 401b9fa55..786427c7c 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md", "agents/gsd-user-profiler.md", "agents/gsd-verifier.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index eca97635e..7451e92e1 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -35,6 +35,7 @@ "agents/gsd-ui-researcher.md", "agents/gsd-user-profiler.md", "agents/gsd-verifier.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index fc10b6b55..ea0653367 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -106,6 +106,7 @@ "commands/gsd-verify-work.md", "commands/gsd-workspace.md", "commands/gsd-workstreams.md", + "gsd-core/.gsd-runtime", "gsd-core/VERSION", "gsd-core/bin/check-latest-version.cjs", "gsd-core/bin/ensure-runtime-build.cjs",