refactor: hard-fork GSD -> MSD (Make Software Done)

Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
Jakub Zych
2026-10-06 01:47:40 +02:00
parent fe069b2a56
commit a9a7a328e6
2763 changed files with 78465 additions and 78434 deletions

View File

@@ -27,13 +27,13 @@ const fc = require('fast-check');
// ─── module under test ────────────────────────────────────────────────────────
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
const { loadConfig, loadConfigResolved, _resetRuntimeWarningCacheForTests, _deepMergeConfig } = configLoader;
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeTempProject(prefix = 'gsd-cfg-loader-test-') {
function makeTempProject(prefix = 'msd-cfg-loader-test-') {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
return tmpDir;
@@ -218,25 +218,25 @@ describe('loadConfig — unknown-key warning dedup', () => {
});
test('unknown key produces a warning mentioning the key name', () => {
writeConfig(tmpDir, { __gsd_unknown_sentinel__: true });
writeConfig(tmpDir, { __msd_unknown_sentinel__: true });
loadConfig(tmpDir);
const warnings = stderrLines.filter(l => l.includes('__gsd_unknown_sentinel__'));
const warnings = stderrLines.filter(l => l.includes('__msd_unknown_sentinel__'));
assert.ok(warnings.length >= 1, 'should warn about unknown key');
});
test('calling loadConfig twice does not double-emit the same unknown-key warning', () => {
writeConfig(tmpDir, { __gsd_dedup_test__: true });
writeConfig(tmpDir, { __msd_dedup_test__: true });
loadConfig(tmpDir);
loadConfig(tmpDir);
const warnings = stderrLines.filter(l => l.includes('__gsd_dedup_test__'));
const warnings = stderrLines.filter(l => l.includes('__msd_dedup_test__'));
// Should appear at most once
assert.ok(warnings.length <= 1, `warning emitted more than once: ${warnings.length} times`);
});
test('agent_tools is accepted without an unknown-key warning (#4032)', () => {
writeConfig(tmpDir, { agent_tools: { 'gsd-executor': ['WebFetch'] } });
writeConfig(tmpDir, { agent_tools: { 'msd-executor': ['WebFetch'] } });
const config = loadConfig(tmpDir);
assert.deepEqual(config.agent_tools, { 'gsd-executor': ['WebFetch'] });
assert.deepEqual(config.agent_tools, { 'msd-executor': ['WebFetch'] });
assert.equal(
stderrLines.some((line) => line.includes('agent_tools')),
false,
@@ -254,7 +254,7 @@ describe('loadConfig — unknown-key warning dedup', () => {
// Asserts on the exported Set rather than stderr prose (CONTRIBUTING.md —
// Prohibited: Raw Text Matching on Test Outputs).
test('_resetRuntimeWarningCacheForTests clears the unknown-key dedup set', () => {
writeConfig(tmpDir, { __gsd_reset_probe__: true });
writeConfig(tmpDir, { __msd_reset_probe__: true });
loadConfig(tmpDir);
assert.ok(
configLoader._warnedUnknownConfigKeys.size > 0,
@@ -417,15 +417,15 @@ describe('loadConfigResolved — provenance', () => {
assert.ok('model_profile' in result.config);
});
test('source is "global-defaults" when no .planning exists but ~/.gsd/defaults.json readable', () => {
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-home-test-'));
const origGsdHome = process.env['GSD_HOME'];
test('source is "global-defaults" when no .planning exists but ~/.msd/defaults.json readable', () => {
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-home-test-'));
const origMsdHome = process.env['MSD_HOME'];
try {
const gsdDir = path.join(homeTmp, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({ model_profile: 'home-defaults' }), 'utf-8');
process.env['GSD_HOME'] = homeTmp;
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-noplanning-'));
const msdDir = path.join(homeTmp, '.msd');
fs.mkdirSync(msdDir, { recursive: true });
fs.writeFileSync(path.join(msdDir, 'defaults.json'), JSON.stringify({ model_profile: 'home-defaults' }), 'utf-8');
process.env['MSD_HOME'] = homeTmp;
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-noplanning-'));
try {
const result = loadConfigResolved(noPlanning);
assert.equal(result.source, 'global-defaults');
@@ -435,26 +435,26 @@ describe('loadConfigResolved — provenance', () => {
cleanup(noPlanning);
}
} finally {
if (origGsdHome === undefined) delete process.env['GSD_HOME'];
else process.env['GSD_HOME'] = origGsdHome;
if (origMsdHome === undefined) delete process.env['MSD_HOME'];
else process.env['MSD_HOME'] = origMsdHome;
cleanup(homeTmp);
}
});
test('source is "builtin-defaults" when no .planning and no global defaults', () => {
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-noplanning2-'));
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-nohome-'));
const origGsdHome = process.env['GSD_HOME'];
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-noplanning2-'));
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-nohome-'));
const origMsdHome = process.env['MSD_HOME'];
try {
// Point GSD_HOME to a directory with no .gsd/defaults.json
process.env['GSD_HOME'] = homeTmp;
// Point MSD_HOME to a directory with no .msd/defaults.json
process.env['MSD_HOME'] = homeTmp;
const result = loadConfigResolved(noPlanning);
assert.equal(result.source, 'builtin-defaults');
assert.equal(result.degraded, false);
assert.ok('model_profile' in result.config);
} finally {
if (origGsdHome === undefined) delete process.env['GSD_HOME'];
else process.env['GSD_HOME'] = origGsdHome;
if (origMsdHome === undefined) delete process.env['MSD_HOME'];
else process.env['MSD_HOME'] = origMsdHome;
cleanup(noPlanning);
cleanup(homeTmp);
}
@@ -501,32 +501,32 @@ describe('loadConfigResolved — provenance', () => {
assert.equal(explicit.source, 'root');
assert.equal(explicit.degraded, false);
const original = process.env.GSD_WORKSTREAM;
const original = process.env.MSD_WORKSTREAM;
try {
process.env.GSD_WORKSTREAM = workstream;
process.env.MSD_WORKSTREAM = workstream;
const ambient = loadConfigResolved(tmpDir);
assert.equal(ambient.source, 'root');
assert.equal(ambient.degraded, false);
} finally {
if (original === undefined) delete process.env.GSD_WORKSTREAM;
else process.env.GSD_WORKSTREAM = original;
if (original === undefined) delete process.env.MSD_WORKSTREAM;
else process.env.MSD_WORKSTREAM = original;
}
}
});
test('Fix 2a: GSD_WORKSTREAM set to nonexistent workstream (dir absent) → source:"root", degraded:true', () => {
test('Fix 2a: MSD_WORKSTREAM set to nonexistent workstream (dir absent) → source:"root", degraded:true', () => {
writeConfig(tmpDir, { model_profile: 'root-value' });
const origWs = process.env['GSD_WORKSTREAM'];
const origWs = process.env['MSD_WORKSTREAM'];
try {
process.env['GSD_WORKSTREAM'] = 'nonexistent-ws';
process.env['MSD_WORKSTREAM'] = 'nonexistent-ws';
// Do NOT create the workstream directory
const result = loadConfigResolved(tmpDir);
assert.equal(result.source, 'root', 'nonexistent workstream should fall back to source:"root"');
assert.equal(result.degraded, true, 'should be degraded when workstream dir is absent');
assert.equal(result.config.model_profile, 'root-value', 'config should equal root config');
} finally {
if (origWs === undefined) delete process.env['GSD_WORKSTREAM'];
else process.env['GSD_WORKSTREAM'] = origWs;
if (origWs === undefined) delete process.env['MSD_WORKSTREAM'];
else process.env['MSD_WORKSTREAM'] = origWs;
}
});
@@ -603,7 +603,7 @@ const path = require('path');
const { createTempProject, cleanup } = require('./helpers.cjs');
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
const { loadConfig } = require('../gsd-core/bin/lib/config-loader.cjs');
const { loadConfig } = require('../msd-core/bin/lib/config-loader.cjs');
function makeSubRepo(parent, name) {
const dir = path.join(parent, name);
@@ -776,17 +776,17 @@ const { runNode } = require('./helpers/process-seam.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
/**
* Run gsd-tools and return { stdout, stderr, status }.
* Run msd-tools and return { stdout, stderr, status }.
* Always captures stderr even when exit code is 0.
*/
function runWithStderr(args, cwd, env = {}) {
const result = runNode([TOOLS_PATH, ...args], {
cwd,
// #3532: pin GSD_HOME to an empty sandbox so a developer's real
// ~/.gsd/defaults.json cannot leak shadow-key warnings into children that
// #3532: pin MSD_HOME to an empty sandbox so a developer's real
// ~/.msd/defaults.json cannot leak shadow-key warnings into children that
// these suites assert are stderr-clean (TEST_ENV_BASE only BLANKS the
// var; an empty string falls through to the real homedir).
env: { ...process.env, ...TEST_ENV_BASE, GSD_HOME: installSpawnHome(), ...env },
env: { ...process.env, ...TEST_ENV_BASE, MSD_HOME: installSpawnHome(), ...env },
timeoutMs: PROBE_TIMEOUT_MS,
});
return {
@@ -807,7 +807,7 @@ describe('bug-3523 — no warning for legacy top-level branching_strategy', () =
});
test('loadConfig emits no stderr when config.json has top-level branching_strategy', () => {
tmpDir = createTempProject('gsd-3523-warn-');
tmpDir = createTempProject('msd-3523-warn-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(
configPath,
@@ -829,7 +829,7 @@ describe('bug-3523 — no warning for legacy top-level branching_strategy', () =
});
test('branching_strategy value is still surfaced after loadConfig on legacy shape', () => {
tmpDir = createTempProject('gsd-3523-value-');
tmpDir = createTempProject('msd-3523-value-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(
configPath,
@@ -885,13 +885,13 @@ describe('bug-3523 — double-emission reduced to single-emission', () => {
// fixed) to verify the deduplication guard works for other keys too.
// We verify that the count of warning lines for a single unknown key is
// exactly once — not zero and not two — even if loadConfig is invoked twice internally.
tmpDir = createTempProject('gsd-3523-dedup-');
tmpDir = createTempProject('msd-3523-dedup-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(
configPath,
JSON.stringify({
// intentionally_unknown_key_for_dedup_test: a key that can never be valid
__gsd3523_dedup_sentinel__: true,
__msd3523_dedup_sentinel__: true,
}, null, 2),
'utf-8'
);
@@ -906,7 +906,7 @@ describe('bug-3523 — double-emission reduced to single-emission', () => {
// Count how many times the sentinel key appears in warnings
const warningLines = result.stderr
.split('\n')
.filter(l => l.includes('__gsd3523_dedup_sentinel__'));
.filter(l => l.includes('__msd3523_dedup_sentinel__'));
assert.equal(
warningLines.length,
@@ -928,7 +928,7 @@ describe('bug-3523 — option 3 on-disk migration of branching_strategy', () =>
});
test('after loadConfig, on-disk config.json has branching_strategy under git.*', () => {
tmpDir = createTempProject('gsd-3523-writeback-');
tmpDir = createTempProject('msd-3523-writeback-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(
configPath,
@@ -959,7 +959,7 @@ describe('bug-3523 — option 3 on-disk migration of branching_strategy', () =>
test('migration does not clobber existing git.branching_strategy', () => {
// If git.branching_strategy is already set, the top-level value should
// not overwrite it (nested wins, matching SDK mergeDefaults precedence).
tmpDir = createTempProject('gsd-3523-no-clobber-');
tmpDir = createTempProject('msd-3523-no-clobber-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(
configPath,
@@ -991,7 +991,7 @@ describe('bug-3523 — option 3 on-disk migration of branching_strategy', () =>
});
test('workstream load also self-heals legacy root branching_strategy', () => {
tmpDir = createTempProject('gsd-3523-workstream-root-');
tmpDir = createTempProject('msd-3523-workstream-root-');
const rootConfigPath = path.join(tmpDir, '.planning', 'config.json');
const workstreamDir = path.join(tmpDir, '.planning', 'workstreams', 'alpha');
fs.mkdirSync(workstreamDir, { recursive: true });
@@ -1010,7 +1010,7 @@ describe('bug-3523 — option 3 on-disk migration of branching_strategy', () =>
);
const triggerResult = runWithStderr(['resolve-model', 'planner'], tmpDir, {
GSD_WORKSTREAM: 'alpha',
MSD_WORKSTREAM: 'alpha',
});
assert.equal(
@@ -1071,7 +1071,7 @@ describe('bug-3523 — CJS↔SDK contract: both agree on legacy branching_strate
});
test('CJS loadConfig surfaces branching_strategy matching SDK mergeDefaults behavior', () => {
tmpDir = createTempProject('gsd-3523-parity-');
tmpDir = createTempProject('msd-3523-parity-');
const configPath = path.join(tmpDir, '.planning', 'config.json');
// The fixture that the SDK's mergeDefaults handles correctly (PR #3116).
fs.writeFileSync(
@@ -1286,7 +1286,7 @@ describe('#2997: phase_id_convention is not silently dropped on a clean read', (
const cfgPath = (dir) => path.join(dir, '.planning', 'config.json');
test('setting phase_id_convention in config.json survives into the resolved config', () => {
const tmpDir = createTempDir('gsd-2997-');
const tmpDir = createTempDir('msd-2997-');
try {
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ phase_id_convention: 'milestone-prefixed' }), 'utf-8');
@@ -1298,7 +1298,7 @@ describe('#2997: phase_id_convention is not silently dropped on a clean read', (
});
test('phase_id_convention set to null round-trips correctly', () => {
const tmpDir = createTempDir('gsd-2997-null-');
const tmpDir = createTempDir('msd-2997-null-');
try {
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ phase_id_convention: null }), 'utf-8');
@@ -1309,7 +1309,7 @@ describe('#2997: phase_id_convention is not silently dropped on a clean read', (
});
test('phase_id_convention absent → null in resolved config (no false default)', () => {
const tmpDir = createTempDir('gsd-2997-absent-');
const tmpDir = createTempDir('msd-2997-absent-');
try {
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(cfgPath(tmpDir), JSON.stringify({ commit_docs: true }), 'utf-8');
@@ -1326,7 +1326,7 @@ describe('#2997: phase_id_convention is not silently dropped on a clean read', (
// exists. Under a project .planning/config.json (Branch A — every real project)
// the global file is never opened, so each of these set globally is silently
// inert. `effort` is deliberately absent: the install-time effort sync
// (readGsdEffectiveEffortConfig) DOES merge the global file, so warning on it
// (readMsdEffectiveEffortConfig) DOES merge the global file, so warning on it
// would be false for the channel users control via effort sync.
const GLOBAL_KEYS_SHADOWED_UNDER_PROJECT = [
'model_profile', 'commit_docs', 'research', 'plan_checker', 'verifier',
@@ -1339,42 +1339,42 @@ const GLOBAL_KEYS_SHADOWED_UNDER_PROJECT = [
describe('#3532 shadowed global-defaults warning', () => {
let tmpDir;
let gsdHome;
let msdHome;
let stderrLines;
let originalStderrWrite;
let originalGsdHome;
let originalMsdHome;
beforeEach(() => {
tmpDir = makeTempProject('gsd-3532-shadow-');
gsdHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3532-home-'));
tmpDir = makeTempProject('msd-3532-shadow-');
msdHome = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3532-home-'));
stderrLines = [];
originalStderrWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = (chunk) => { stderrLines.push(String(chunk)); return true; };
originalGsdHome = process.env.GSD_HOME;
process.env.GSD_HOME = gsdHome;
originalMsdHome = process.env.MSD_HOME;
process.env.MSD_HOME = msdHome;
if (_resetRuntimeWarningCacheForTests) _resetRuntimeWarningCacheForTests();
});
afterEach(() => {
process.stderr.write = originalStderrWrite;
if (originalGsdHome === undefined) delete process.env.GSD_HOME;
else process.env.GSD_HOME = originalGsdHome;
if (originalMsdHome === undefined) delete process.env.MSD_HOME;
else process.env.MSD_HOME = originalMsdHome;
if (tmpDir) cleanup(tmpDir);
if (gsdHome) cleanup(gsdHome);
tmpDir = gsdHome = null;
if (msdHome) cleanup(msdHome);
tmpDir = msdHome = null;
});
function writeGlobalDefaults(obj) {
fs.mkdirSync(path.join(gsdHome, '.gsd'), { recursive: true });
fs.mkdirSync(path.join(msdHome, '.msd'), { recursive: true });
fs.writeFileSync(
path.join(gsdHome, '.gsd', 'defaults.json'),
path.join(msdHome, '.msd', 'defaults.json'),
JSON.stringify(obj, null, 2),
);
}
test('project config + global model keys -> one warning naming both keys', () => {
writeConfig(tmpDir, { model_profile: 'balanced' });
writeGlobalDefaults({ model_overrides: { 'gsd-executor': 'haiku' }, model_profile: 'quality' });
writeGlobalDefaults({ model_overrides: { 'msd-executor': 'haiku' }, model_profile: 'quality' });
loadConfigResolved(tmpDir);
const warnings = stderrLines.filter(l => l.includes('model_overrides') && l.includes('model_profile'));
assert.equal(warnings.length, 1, `expected exactly one shadowed-keys warning, got: ${stderrLines.join('')}`);
@@ -1404,7 +1404,7 @@ describe('#3532 shadowed global-defaults warning', () => {
});
test('bare dir without .planning honors global defaults without warning (Branch D)', () => {
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3532-bare-'));
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3532-bare-'));
try {
writeGlobalDefaults({ model_profile: 'quality' });
const resolution = loadConfigResolved(bare);
@@ -1418,8 +1418,8 @@ describe('#3532 shadowed global-defaults warning', () => {
test('unparseable global defaults skip the shadow warning', () => {
writeConfig(tmpDir, { model_profile: 'balanced' });
fs.mkdirSync(path.join(gsdHome, '.gsd'), { recursive: true });
fs.writeFileSync(path.join(gsdHome, '.gsd', 'defaults.json'), '{not json');
fs.mkdirSync(path.join(msdHome, '.msd'), { recursive: true });
fs.writeFileSync(path.join(msdHome, '.msd', 'defaults.json'), '{not json');
loadConfigResolved(tmpDir);
assert.equal(stderrLines.filter(l => l.includes('shadowed')).length, 0);
});
@@ -1434,9 +1434,9 @@ describe('#3532 shadowed global-defaults warning', () => {
test('non-resolution global keys do not warn from this check', () => {
writeConfig(tmpDir, { model_profile: 'balanced' });
writeGlobalDefaults({ __gsd_3532_arbitrary__: true });
writeGlobalDefaults({ __msd_3532_arbitrary__: true });
loadConfigResolved(tmpDir);
assert.equal(stderrLines.filter(l => l.includes('__gsd_3532_arbitrary__') && l.includes('shadowed')).length, 0);
assert.equal(stderrLines.filter(l => l.includes('__msd_3532_arbitrary__') && l.includes('shadowed')).length, 0);
});
// Typed-IR parity canary (CONTRIBUTING: assert the exported dedup Set, not
@@ -1508,7 +1508,7 @@ describe('regressions — #3760 loader never expands or persists a non-object se
let tmpDir;
beforeEach(() => {
tmpDir = makeTempProject('gsd-3760-loader-');
tmpDir = makeTempProject('msd-3760-loader-');
_resetRuntimeWarningCacheForTests();
});
@@ -1597,7 +1597,7 @@ describe('regressions — #3760 loader never expands or persists a non-object se
const {
_resetUnusableInputWarningsForTests,
_unusableInputEmissionCountForTests,
} = require('../gsd-core/bin/lib/unusable-input.cjs');
} = require('../msd-core/bin/lib/unusable-input.cjs');
function emissionsDuring(fn) {
const before = _unusableInputEmissionCountForTests();
@@ -1636,19 +1636,19 @@ describe('regressions — #3760 loader never expands or persists a non-object se
// ─── #3894: workflow.research_before_questions resolves from global defaults ──
describe('#3894 research_before_questions global-defaults forwarding', () => {
test('nested workflow.research_before_questions in ~/.gsd/defaults.json resolves', () => {
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894-home-'));
const origGsdHome = process.env['GSD_HOME'];
test('nested workflow.research_before_questions in ~/.msd/defaults.json resolves', () => {
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3894-home-'));
const origMsdHome = process.env['MSD_HOME'];
try {
const gsdDir = path.join(homeTmp, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
const msdDir = path.join(homeTmp, '.msd');
fs.mkdirSync(msdDir, { recursive: true });
// The reporter's exact shape: same nesting as the forwarded
// workflow.post_planning_gaps, one resolves and one did not.
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({
fs.writeFileSync(path.join(msdDir, 'defaults.json'), JSON.stringify({
workflow: { post_planning_gaps: true, research_before_questions: true },
}), 'utf-8');
process.env['GSD_HOME'] = homeTmp;
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894-noplanning-'));
process.env['MSD_HOME'] = homeTmp;
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3894-noplanning-'));
try {
const result = loadConfigResolved(noPlanning);
assert.equal(result.config.post_planning_gaps, true, 'control: the already-forwarded key resolves');
@@ -1660,21 +1660,21 @@ describe('#3894 research_before_questions global-defaults forwarding', () => {
cleanup(noPlanning);
}
} finally {
if (origGsdHome === undefined) delete process.env['GSD_HOME'];
else process.env['GSD_HOME'] = origGsdHome;
if (origMsdHome === undefined) delete process.env['MSD_HOME'];
else process.env['MSD_HOME'] = origMsdHome;
cleanup(homeTmp);
}
});
test('flat top-level research_before_questions in ~/.gsd/defaults.json also resolves (Branch D alias parity)', () => {
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894b-home-'));
const origGsdHome = process.env['GSD_HOME'];
test('flat top-level research_before_questions in ~/.msd/defaults.json also resolves (Branch D alias parity)', () => {
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3894b-home-'));
const origMsdHome = process.env['MSD_HOME'];
try {
const gsdDir = path.join(homeTmp, '.gsd');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'defaults.json'), JSON.stringify({ research_before_questions: true }), 'utf-8');
process.env['GSD_HOME'] = homeTmp;
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894b-noplanning-'));
const msdDir = path.join(homeTmp, '.msd');
fs.mkdirSync(msdDir, { recursive: true });
fs.writeFileSync(path.join(msdDir, 'defaults.json'), JSON.stringify({ research_before_questions: true }), 'utf-8');
process.env['MSD_HOME'] = homeTmp;
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3894b-noplanning-'));
try {
const result = loadConfigResolved(noPlanning);
assert.equal(result.config.research_before_questions, true);
@@ -1682,56 +1682,56 @@ describe('#3894 research_before_questions global-defaults forwarding', () => {
cleanup(noPlanning);
}
} finally {
if (origGsdHome === undefined) delete process.env['GSD_HOME'];
else process.env['GSD_HOME'] = origGsdHome;
if (origMsdHome === undefined) delete process.env['MSD_HOME'];
else process.env['MSD_HOME'] = origMsdHome;
cleanup(homeTmp);
}
});
test('unset resolves to the documented default (false), never undefined', () => {
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894c-'));
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3894c-home-'));
const origGsdHome = process.env['GSD_HOME'];
const noPlanning = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3894c-'));
const homeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3894c-home-'));
const origMsdHome = process.env['MSD_HOME'];
try {
process.env['GSD_HOME'] = homeTmp; // no .gsd/defaults.json — builtin defaults
process.env['MSD_HOME'] = homeTmp; // no .msd/defaults.json — builtin defaults
const result = loadConfigResolved(noPlanning);
assert.equal(result.config.research_before_questions, false, 'CANONICAL_CONFIG_DEFAULTS.workflow.research_before_questions is false');
} finally {
if (origGsdHome === undefined) delete process.env['GSD_HOME'];
else process.env['GSD_HOME'] = origGsdHome;
if (origMsdHome === undefined) delete process.env['MSD_HOME'];
else process.env['MSD_HOME'] = origMsdHome;
cleanup(homeTmp);
cleanup(noPlanning);
}
});
});
// ── #4717 — an empty config.runtime is filled from GSD_RUNTIME / the marker ──
// ── #4717 — an empty config.runtime is filled from MSD_RUNTIME / the marker ──
describe('loadConfigResolved — runtime identity fill (#4717)', () => {
const slash = require('../gsd-core/bin/lib/runtime-slash.cjs');
const slash = require('../msd-core/bin/lib/runtime-slash.cjs');
const fsx = require('node:fs');
const pathx = require('node:path');
const { createTempDir: mkTmp4717, cleanup: cleanup4717 } = require('./helpers.cjs');
let tmpCodexHome;
let originalCodexHome;
let originalGsdRuntime;
let originalMsdRuntime;
let originalGsdHome;
let originalMsdHome;
let originalHome;
let originalUserProfile;
beforeEach(() => {
originalCodexHome = process.env.CODEX_HOME;
originalGsdRuntime = process.env.GSD_RUNTIME;
originalGsdHome = process.env.GSD_HOME;
originalMsdRuntime = process.env.MSD_RUNTIME;
originalMsdHome = process.env.MSD_HOME;
originalHome = process.env.HOME;
originalUserProfile = process.env.USERPROFILE;
delete process.env.GSD_RUNTIME;
tmpCodexHome = mkTmp4717('gsd-4717-');
delete process.env.MSD_RUNTIME;
tmpCodexHome = mkTmp4717('msd-4717-');
process.env.CODEX_HOME = tmpCodexHome;
// Isolate the SHARED defaults file too: loadConfigResolved's global-defaults
// branch reads ~/.gsd/defaults.json from GSD_HOME || homedir, and a real
// branch reads ~/.msd/defaults.json from MSD_HOME || homedir, and a real
// machine's stamped defaults would bleed into these rows (#4717 review).
process.env.GSD_HOME = tmpCodexHome;
process.env.MSD_HOME = tmpCodexHome;
process.env.HOME = tmpCodexHome;
process.env.USERPROFILE = tmpCodexHome;
slash._setInstallRuntimeMarkerForTests('codex');
@@ -1741,10 +1741,10 @@ describe('loadConfigResolved — runtime identity fill (#4717)', () => {
slash._resetInstallRuntimeMarkerCacheForTests();
if (originalCodexHome === undefined) delete process.env.CODEX_HOME;
else process.env.CODEX_HOME = originalCodexHome;
if (originalGsdRuntime === undefined) delete process.env.GSD_RUNTIME;
else process.env.GSD_RUNTIME = originalGsdRuntime;
if (originalGsdHome === undefined) delete process.env.GSD_HOME;
else process.env.GSD_HOME = originalGsdHome;
if (originalMsdRuntime === undefined) delete process.env.MSD_RUNTIME;
else process.env.MSD_RUNTIME = originalMsdRuntime;
if (originalMsdHome === undefined) delete process.env.MSD_HOME;
else process.env.MSD_HOME = originalMsdHome;
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
@@ -1753,17 +1753,17 @@ describe('loadConfigResolved — runtime identity fill (#4717)', () => {
});
test('an empty config.runtime is filled from the install marker (#4717)', (t) => {
const projDir = mkTmp4717('gsd-4717-proj-');
const projDir = mkTmp4717('msd-4717-proj-');
t.after(() => cleanup4717(projDir));
const resolved = loadConfigResolved(projDir, { persist: false });
assert.equal(resolved.config.runtime, 'codex',
'the marker-owned runtime fills an empty config.runtime (copy-on-write)');
});
test('GSD_RUNTIME outranks the marker in the identity fill (#4717)', (t) => {
const projDir = mkTmp4717('gsd-4717-proj-');
test('MSD_RUNTIME outranks the marker in the identity fill (#4717)', (t) => {
const projDir = mkTmp4717('msd-4717-proj-');
t.after(() => cleanup4717(projDir));
process.env.GSD_RUNTIME = 'kimi';
process.env.MSD_RUNTIME = 'kimi';
const resolved = loadConfigResolved(projDir, { persist: false });
assert.equal(resolved.config.runtime, 'kimi');
});
@@ -1778,17 +1778,17 @@ describe('loadConfigResolved — runtime identity fill (#4717)', () => {
test('#4717 stamped-defaults leg: a shared defaults runtime does not leak past THIS install\'s marker', (t) => {
// The issue's second failure shape: the first non-Claude install stamped
// `runtime` into the SHARED ~/.gsd/defaults.json; a Claude install on the
// `runtime` into the SHARED ~/.msd/defaults.json; a Claude install on the
// same machine must not inherit that identity. Branch D forwards the
// stamped value; the fill corrects it to the marker's own.
fsx.mkdirSync(pathx.join(tmpCodexHome, '.gsd'), { recursive: true });
fsx.mkdirSync(pathx.join(tmpCodexHome, '.msd'), { recursive: true });
fsx.writeFileSync(
pathx.join(tmpCodexHome, '.gsd', 'defaults.json'),
pathx.join(tmpCodexHome, '.msd', 'defaults.json'),
JSON.stringify({ runtime: 'codex' }),
);
slash._setInstallRuntimeMarkerForTests('claude');
const projDir = bareProjDir(t, 'gsd-4717-proj-stamped-');
const projDir = bareProjDir(t, 'msd-4717-proj-stamped-');
const resolved = loadConfigResolved(projDir);
assert.equal(resolved.source, 'global-defaults', 'fixture: the shared-defaults branch must fire');
@@ -1800,14 +1800,14 @@ describe('loadConfigResolved — runtime identity fill (#4717)', () => {
});
test('#4717 stamped-defaults leg: with no marker of its own, the stamped value still stands (status quo preserved)', (t) => {
fsx.mkdirSync(pathx.join(tmpCodexHome, '.gsd'), { recursive: true });
fsx.mkdirSync(pathx.join(tmpCodexHome, '.msd'), { recursive: true });
fsx.writeFileSync(
pathx.join(tmpCodexHome, '.gsd', 'defaults.json'),
pathx.join(tmpCodexHome, '.msd', 'defaults.json'),
JSON.stringify({ runtime: 'codex' }),
);
slash._setInstallRuntimeMarkerForTests(null);
const projDir = bareProjDir(t, 'gsd-4717-proj-stamped-nomarker-');
const projDir = bareProjDir(t, 'msd-4717-proj-stamped-nomarker-');
const resolved = loadConfigResolved(projDir);
assert.equal(resolved.config.runtime, 'codex', 'no own identity — the stamped value is all we know');
@@ -1816,7 +1816,7 @@ describe('loadConfigResolved — runtime identity fill (#4717)', () => {
test('#4717 fail-safe: a garbage marker does not fill the runtime (#4717 review)', (_t) => {
slash._setInstallRuntimeMarkerForTests(' not-a-runtime ');
const projDir = bareProjDir(_t, 'gsd-4717-proj-garbage-');
const projDir = bareProjDir(_t, 'msd-4717-proj-garbage-');
const resolved = loadConfigResolved(projDir);
assert.equal(
@@ -1827,7 +1827,7 @@ describe('loadConfigResolved — runtime identity fill (#4717)', () => {
});
test('an explicit config.runtime is preserved — the fill never overrides it (#4717)', (t) => {
const projDir = mkTmp4717('gsd-4717-proj-');
const projDir = mkTmp4717('msd-4717-proj-');
t.after(() => cleanup4717(projDir));
fsx.mkdirSync(pathx.join(projDir, '.planning'), { recursive: true });
fsx.writeFileSync(
@@ -1840,7 +1840,7 @@ describe('loadConfigResolved — runtime identity fill (#4717)', () => {
});
test('copy-on-write: the shared builtin-defaults object is never mutated (#4717)', (t) => {
const projDir = mkTmp4717('gsd-4717-proj-');
const projDir = mkTmp4717('msd-4717-proj-');
t.after(() => cleanup4717(projDir));
const resolved = loadConfigResolved(projDir, { persist: false });
assert.equal(resolved.config.runtime, 'codex');