* fix(#2395): persist runtime identity into ~/.gsd/defaults.json for non-Claude installs Bug: finishInstall for non-Claude runtimes never persisted a 'runtime' key into ~/.gsd/defaults.json. resolveRuntime() precedence is GSD_RUNTIME env > config.runtime > 'claude', so both inputs being empty on a Cursor (or any non-Claude) install caused agent_runtime and every runtime-branded slash hint to silently fall through to 'claude'. Cursor users saw 'agent_runtime: "claude"' and Claude-formatted /gsd-* hints with no env or config hand-set. Fix mirrors the existing resolve_model_ids: 'omit' write site at the same call site (bin/install.js finishInstall, gated on !_hostBehaviors(runtime) .nativeModelAliases && !GSD_TEST_MODE). Writes runtime: <runtime> into ~/.gsd/defaults.json when absent/null/empty. Claude is the resolveRuntime() fallback so it needs no write; an explicit pre-existing runtime value is always preserved across installs of any runtime. Pattern parity with #1156 (default-to-omit intent) and #1569 (preserve explicit user value) — the new write is the third sibling on the same install-time persistence block. Regression tests in tests/install.test.cjs cover: - absent / null / empty-string runtime → populated to <install runtime> - explicit pre-existing runtime preserved (no clobber across runtimes) - parameterized across 5 non-Claude runtimes (cursor, codex, opencode, antigravity, windsurf) Out of scope (per triage): the resolveRuntime() precedence order itself (env > config > default) is unchanged. A separate follow-up noted in the issue (subagent rendering when runtime correctly identifies as 'cursor') is unrelated to branding and not addressed here. * chore(#2395): regenerate golden-install-parity fixtures for new runtime persistence The golden install-tree fixtures capture the post-install state, including ~/.gsd/defaults.json. For non-Claude runtimes, defaults.json now includes runtime: <runtime> — content hash updated for each affected runtime (17 golden-install-parity/*.json files). Claude's defaults.json is unchanged (no runtime key written for Claude — it's the resolveRuntime() fallback). * chore(#2395): drop product names from changeset fragment (product-name-purity gate) * test(#2395): move describe out of fix-1521 fold + add same-runtime idempotence test Code review (correctness subagent) flagged 2 Low test-quality issues: 1. The new Bug #2395 describe was inserted inside the folded:fix-1521-real-install-stamping IIFE callback, muddying test reporting and tracing the regression to the wrong epic. Moved it outside the IIFE close to be a top-level sibling. 2. No explicit same-runtime idempotence test — the suite covered cross-runtime preservation (cursor seed → opencode install) but not 'install cursor twice → second is a no-op'. Added: seeds fresh defaults, installs cursor, captures mtime, installs cursor again, asserts runtime unchanged AND defaults.json mtime unchanged (idempotent, no rewrite churn). * chore(changeset): backfill pr:2446 in .changeset/fierce-ravens-dance.md
This commit is contained in:
5
.changeset/fierce-ravens-dance.md
Normal file
5
.changeset/fierce-ravens-dance.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2446
|
||||
---
|
||||
**Non-Claude installs no longer brand all GSD output as Claude** — the installer never persisted `runtime: <id>` into `~/.gsd/defaults.json` for non-Claude runtimes, so `resolveRuntime()` (precedence: `GSD_RUNTIME` env > `config.runtime` > `'claude'`) fell through to the hard-coded `'claude'` default. A non-Claude install showed `agent_runtime: "claude"` and Claude-formatted `/gsd-*` slash hints with no env or config hand-set. The installer now persists `runtime: <runtime>` into `~/.gsd/defaults.json` for non-Claude runtimes, mirroring the existing `resolve_model_ids: "omit"` write at the same call site. Claude is the fallback so it needs no write; an explicit pre-existing `runtime` value is always preserved. (#2395)
|
||||
@@ -12266,6 +12266,19 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
||||
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
|
||||
}
|
||||
|
||||
// #2395: also persist `runtime: <runtime>` for non-Claude runtimes, so
|
||||
// resolveRuntime() (precedence: GSD_RUNTIME env > config.runtime > 'claude')
|
||||
// resolves to the install's actual runtime identity out of the box — without
|
||||
// this, agent_runtime and every runtime-branded slash hint falls through to
|
||||
// the hard-coded 'claude' default. Mirrors the resolve_model_ids write above:
|
||||
// honor an explicit pre-existing value (any string), only default-populating
|
||||
// when absent. Claude is the resolveRuntime() fallback, so it needs no write.
|
||||
if (defaults.runtime === undefined || defaults.runtime === null || defaults.runtime === '') {
|
||||
defaults.runtime = runtime;
|
||||
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
|
||||
console.log(` ${green}✓${reset} Set runtime: "${runtime}" in ~/.gsd/defaults.json`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "3f87862c1f6b7d30",
|
||||
"agents/gsd-advisor-researcher.md": "45af60c237748ee1",
|
||||
"agents/gsd-ai-researcher.md": "ba8898d9e739a501",
|
||||
"agents/gsd-assumptions-analyzer.md": "00a02fdcbaa8d833",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "de00bae0761140d7",
|
||||
"agents/gsd-advisor-researcher.md": "e5be2680b690c198",
|
||||
"agents/gsd-ai-researcher.md": "f3baf9025a5bc4b1",
|
||||
"agents/gsd-assumptions-analyzer.md": "b3219da6b194fdef",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
".clinerules/gsd.md": "d72c089c31c1fef3",
|
||||
".clinerules/hooks/PreToolUse": "7271c83ab8a80911",
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "267b26d00031ac1f",
|
||||
"agents/gsd-advisor-researcher.md": "e5be2680b690c198",
|
||||
"agents/gsd-ai-researcher.md": "49ebcbe6985b86c9",
|
||||
"agents/gsd-assumptions-analyzer.md": "b3219da6b194fdef",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "3abafed57651459c",
|
||||
"agents/gsd-advisor-researcher.md": "e5be2680b690c198",
|
||||
"agents/gsd-ai-researcher.md": "93ca6e24f4fd3681",
|
||||
"agents/gsd-assumptions-analyzer.md": "b3219da6b194fdef",
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
".agents/skills/gsd-workspace/SKILL.md": "06d6400d68318361",
|
||||
".agents/skills/gsd-workstreams/SKILL.md": "2f77bb94db1be1a4",
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "5ca58f5ff7ef0b92",
|
||||
"agents/gsd-advisor-researcher.md": "eea6d1604aaf305c",
|
||||
"agents/gsd-advisor-researcher.toml": "144f037c79aa9c26",
|
||||
"agents/gsd-ai-researcher.md": "303be751c91da4c8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "56eda2d6c407679d",
|
||||
"agents/gsd-advisor-researcher.agent.md": "1fb10242ff7fb1c9",
|
||||
"agents/gsd-ai-researcher.agent.md": "8e9c3a2ddd179bb1",
|
||||
"agents/gsd-assumptions-analyzer.agent.md": "cdabed4777a842fe",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "40c874a6a3dd0469",
|
||||
"agents/gsd-advisor-researcher.md": "e5be2680b690c198",
|
||||
"agents/gsd-ai-researcher.md": "abfc97706f16aaaf",
|
||||
"agents/gsd-assumptions-analyzer.md": "b3219da6b194fdef",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "c4a285ff34429381",
|
||||
"agents/gsd-advisor-researcher.md": "f1c4a47d3d8cea72",
|
||||
"agents/gsd-ai-researcher.md": "08847faf5494ff08",
|
||||
"agents/gsd-assumptions-analyzer.md": "f11a80795163bb5d",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "5a791a3c4ae9eb23",
|
||||
"agents/gsd-advisor-researcher.md": "3296fc4edef47f2e",
|
||||
"agents/gsd-ai-researcher.md": "9607993fbaafe9ba",
|
||||
"agents/gsd-assumptions-analyzer.md": "567d823ccd036b39",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "fea73a3ad95e3454",
|
||||
".kimi/hooks/gsd-check-update-worker.js": "6593f80914f4edee",
|
||||
".kimi/hooks/gsd-check-update.js": "aac7612ee04ff5fe",
|
||||
".kimi/hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "22e30e82b5758469",
|
||||
"agents/gsd-advisor-researcher.md": "6955bcd8ca353de0",
|
||||
"agents/gsd-ai-researcher.md": "28a05307dad84020",
|
||||
"agents/gsd-assumptions-analyzer.md": "45c01017e84ba193",
|
||||
|
||||
2
tests/fixtures/golden-install-parity/pi.json
vendored
2
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "615261cfd3ae1c96",
|
||||
"extensions/gsd.cjs": "619cec0af9cfdadf",
|
||||
"gsd-core/.gsd-runtime": "94e95f0bb38f8e0f",
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "91b2c6fa9472b9bb",
|
||||
"agents/gsd-advisor-researcher.md": "692fbf5ed5e233d5",
|
||||
"agents/gsd-ai-researcher.md": "f32dc6c6ed7e89fd",
|
||||
"agents/gsd-assumptions-analyzer.md": "6a2e38d685ba0fce",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "57cf028821734f24",
|
||||
"agents/gsd-advisor-researcher.md": "e5be2680b690c198",
|
||||
"agents/gsd-ai-researcher.md": "abfc97706f16aaaf",
|
||||
"agents/gsd-assumptions-analyzer.md": "b3219da6b194fdef",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "7a17effa731d8356",
|
||||
"agents/gsd-advisor-researcher.md": "e5be2680b690c198",
|
||||
"agents/gsd-ai-researcher.md": "abfc97706f16aaaf",
|
||||
"agents/gsd-assumptions-analyzer.md": "b3219da6b194fdef",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
".gsd/defaults.json": "98c870b9b696a3a5",
|
||||
"agents/gsd-advisor-researcher.md": "f1c4a47d3d8cea72",
|
||||
"agents/gsd-ai-researcher.md": "2f2da4b89d445860",
|
||||
"agents/gsd-assumptions-analyzer.md": "f11a80795163bb5d",
|
||||
|
||||
@@ -10390,5 +10390,132 @@ test('real install: claude-emitted execute-phase.md keeps claude default + workt
|
||||
'claude install must NOT have use_worktrees=false stamped',
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// Bug #2395 — finishInstall for non-Claude runtimes never persisted `runtime: <id>`
|
||||
// into ~/.gsd/defaults.json. resolveRuntime() precedence is GSD_RUNTIME > config.runtime
|
||||
// > 'claude', so both inputs being empty on a Cursor (or any non-Claude) install caused
|
||||
// every runtime-branded output (agent_runtime, formatGsdSlash hints, etc.) to silently
|
||||
// fall through to 'claude'. Fix mirrors the resolve_model_ids: "omit" write site that
|
||||
// already exists for non-Claude runtimes — writes runtime: <runtime> when absent.
|
||||
describe('Bug #2395: finishInstall persists runtime identity for non-Claude runtimes', () => {
|
||||
const LOCAL_ROOT = path.join(__dirname, '..');
|
||||
const LOCAL_FAKE_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2395-test-'));
|
||||
const LOCAL_GSD_DIR = path.join(LOCAL_FAKE_HOME, '.gsd');
|
||||
const LOCAL_DEFAULTS_PATH = path.join(LOCAL_GSD_DIR, 'defaults.json');
|
||||
const LOCAL_INSTALL_MODULE = require(path.join(LOCAL_ROOT, 'bin', 'install.js'));
|
||||
const LOCAL_SETTINGS_PATH = path.join(LOCAL_FAKE_HOME, `gsd-test-settings-${process.pid}-2395.json`);
|
||||
|
||||
const { before: __before, after: __after } = require('node:test');
|
||||
const __savedHome = process.env.HOME;
|
||||
const __savedUserProfile = process.env.USERPROFILE;
|
||||
__before(() => {
|
||||
process.env.HOME = LOCAL_FAKE_HOME;
|
||||
process.env.USERPROFILE = LOCAL_FAKE_HOME;
|
||||
});
|
||||
__after(() => {
|
||||
if (__savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = __savedHome;
|
||||
if (__savedUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = __savedUserProfile;
|
||||
try { cleanup(LOCAL_FAKE_HOME); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
function callFinishInstall(runtime) {
|
||||
const original = console.log;
|
||||
console.log = () => {};
|
||||
try {
|
||||
LOCAL_INSTALL_MODULE.finishInstall(
|
||||
LOCAL_SETTINGS_PATH,
|
||||
{}, null, false, runtime, true, null,
|
||||
);
|
||||
} finally {
|
||||
console.log = original;
|
||||
}
|
||||
}
|
||||
|
||||
function seedDefaults(obj) {
|
||||
fs.mkdirSync(LOCAL_GSD_DIR, { recursive: true });
|
||||
fs.writeFileSync(LOCAL_DEFAULTS_PATH, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function withUserPath(fn) {
|
||||
const saved = process.env.GSD_TEST_MODE;
|
||||
delete process.env.GSD_TEST_MODE;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
process.env.GSD_TEST_MODE = saved;
|
||||
}
|
||||
}
|
||||
|
||||
test('absent runtime → written to <install runtime> (cursor install surfaces cursor branding)', () => {
|
||||
withUserPath(() => {
|
||||
seedDefaults({ model_profile: 'balanced' });
|
||||
callFinishInstall('cursor');
|
||||
const after = JSON.parse(fs.readFileSync(LOCAL_DEFAULTS_PATH, 'utf8'));
|
||||
assert.equal(after.runtime, 'cursor', 'absent runtime must be populated with the install runtime identity');
|
||||
});
|
||||
});
|
||||
|
||||
test('null runtime → written to <install runtime> (treated as absent)', () => {
|
||||
withUserPath(() => {
|
||||
seedDefaults({ runtime: null, model_profile: 'balanced' });
|
||||
callFinishInstall('cursor');
|
||||
const after = JSON.parse(fs.readFileSync(LOCAL_DEFAULTS_PATH, 'utf8'));
|
||||
assert.equal(after.runtime, 'cursor', 'null runtime must be treated as absent and populated');
|
||||
});
|
||||
});
|
||||
|
||||
test('empty-string runtime → written to <install runtime> (treated as absent)', () => {
|
||||
withUserPath(() => {
|
||||
seedDefaults({ runtime: '', model_profile: 'balanced' });
|
||||
callFinishInstall('codex');
|
||||
const after = JSON.parse(fs.readFileSync(LOCAL_DEFAULTS_PATH, 'utf8'));
|
||||
assert.equal(after.runtime, 'codex', 'empty-string runtime must be treated as absent and populated');
|
||||
});
|
||||
});
|
||||
|
||||
test('explicit pre-existing runtime is preserved (no clobber across runtimes)', () => {
|
||||
withUserPath(() => {
|
||||
// User explicitly set runtime: 'cursor' — a subsequent opencode install must not clobber.
|
||||
seedDefaults({ runtime: 'cursor', resolve_model_ids: 'omit' });
|
||||
callFinishInstall('opencode');
|
||||
const after = JSON.parse(fs.readFileSync(LOCAL_DEFAULTS_PATH, 'utf8'));
|
||||
assert.equal(after.runtime, 'cursor', 'explicit user-set runtime must be preserved across install of a different runtime');
|
||||
});
|
||||
});
|
||||
|
||||
// The clobber guard is runtime-agnostic; parameterize across a representative slice of
|
||||
// non-Claude runtimes. Each should populate its own runtime identity when absent.
|
||||
for (const runtime of ['cursor', 'codex', 'opencode', 'antigravity', 'windsurf']) {
|
||||
test(`${runtime} install populates runtime: "${runtime}" when absent`, () => {
|
||||
withUserPath(() => {
|
||||
seedDefaults({ model_profile: 'balanced' });
|
||||
callFinishInstall(runtime);
|
||||
const after = JSON.parse(fs.readFileSync(LOCAL_DEFAULTS_PATH, 'utf8'));
|
||||
assert.equal(after.runtime, runtime, `${runtime} install must persist runtime: "${runtime}" so resolveRuntime() does not fall through to 'claude'`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('same-runtime idempotence: second cursor install is a no-op (runtime already set)', () => {
|
||||
withUserPath(() => {
|
||||
seedDefaults({ model_profile: 'balanced' });
|
||||
callFinishInstall('cursor');
|
||||
const after1 = JSON.parse(fs.readFileSync(LOCAL_DEFAULTS_PATH, 'utf8'));
|
||||
assert.equal(after1.runtime, 'cursor', 'first install must populate runtime: cursor');
|
||||
// Capture mtime; wait briefly so a no-op vs rewrite is distinguishable.
|
||||
const beforeMtime = fs.statSync(LOCAL_DEFAULTS_PATH).mtimeMs;
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 20) { /* spin briefly */ }
|
||||
callFinishInstall('cursor'); // second install — should be a no-op on runtime
|
||||
const after2 = JSON.parse(fs.readFileSync(LOCAL_DEFAULTS_PATH, 'utf8'));
|
||||
const afterMtime = fs.statSync(LOCAL_DEFAULTS_PATH).mtimeMs;
|
||||
assert.equal(after2.runtime, 'cursor', 'second install must leave runtime: cursor intact');
|
||||
assert.equal(afterMtime, beforeMtime, 'second install must NOT rewrite defaults.json (runtime already set, idempotent)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user