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

@@ -12,7 +12,7 @@
* F. No origin/HEAD, no remote show, no local branches → returns "main" (last resort)
* G. Anti-regression guard: five affected workflows must NOT contain the
* duplicated bare `:-main` / `:-master` fallback pattern that was the root cause.
* They must call `gsd_run query git.base-branch` instead.
* They must call `msd_run query git.base-branch` instead.
* (see the source-text-is-the-product exemption declared below this docblock —
* the workflow .md content IS the runtime surface; the absence of the bad
* pattern is what ships to agents.)
@@ -29,8 +29,8 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { runGsdTools, cleanup, readFileNormalized } = require('./helpers.cjs');
const { ExitError } = require('../gsd-core/bin/lib/cli-exit.cjs');
const { runMsdTools, cleanup, readFileNormalized } = require('./helpers.cjs');
const { ExitError } = require('../msd-core/bin/lib/cli-exit.cjs');
const { makeFaultyGit } = require('./helpers/faulty-deps.cjs');
const { gitOrThrow, throwIfFailed } = require('./helpers/git-fixture.cjs');
const { runHook } = require('./helpers/process-seam.cjs');
@@ -40,7 +40,7 @@ const { GIT_TIMEOUT_MS, HOOK_FANOUT_TIMEOUT_MS } = require('./helpers/timeouts.c
/**
* The #3819 executor pre-commit protected-branch guard test harness below
* invokes a bash script with fake `git`/`gsd_run` shell functions defined
* invokes a bash script with fake `git`/`msd_run` shell functions defined
* INLINE — no real subprocess fan-out happens, so this is lighter than
* `HOOK_FANOUT_TIMEOUT_MS` (60000ms, sized for a hook that fans out to
* nested shell spawns). Pre-existing value, unchanged by this migration.
@@ -72,7 +72,7 @@ const WORKTREE_INFO_TIMEOUT_MS = 5000;
* and local branches.
*/
function createGitRepo(opts = {}) {
const { prefix = 'gsd-1146-', defaultBranch = 'master' } = opts;
const { prefix = 'msd-1146-', defaultBranch = 'master' } = opts;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
gitOrThrow(['init', '-b', defaultBranch], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
gitOrThrow(['config', 'user.email', 'test@test.com'], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
@@ -86,7 +86,7 @@ function createGitRepo(opts = {}) {
}
/**
* Create a .planning dir so gsd-tools resolveProjectRoot doesn't bail.
* Create a .planning dir so msd-tools resolveProjectRoot doesn't bail.
*/
function addPlanning(dir) {
fs.mkdirSync(path.join(dir, '.planning', 'phases'), { recursive: true });
@@ -108,9 +108,9 @@ function snapshotPlanningTree(dir) {
}
/**
* Write a gsd config.json with git.base_branch set.
* Write a msd config.json with git.base_branch set.
*/
function setGsdConfig(dir, key, value) {
function setMsdConfig(dir, key, value) {
const cfgDir = path.join(dir, '.planning');
fs.mkdirSync(cfgDir, { recursive: true });
const cfgPath = path.join(cfgDir, 'config.json');
@@ -125,21 +125,21 @@ function setGsdConfig(dir, key, value) {
for (let i = 0; i < parts.length - 1; i++) {
const k = parts[i];
if (k === '__proto__' || k === 'prototype' || k === 'constructor') {
throw new Error(`setGsdConfig: unsafe config key segment '${k}'`);
throw new Error(`setMsdConfig: unsafe config key segment '${k}'`);
}
if (typeof obj[k] !== 'object' || obj[k] === null) obj[k] = {};
obj = obj[k];
}
const lastKey = parts[parts.length - 1];
if (lastKey === '__proto__' || lastKey === 'prototype' || lastKey === 'constructor') {
throw new Error(`setGsdConfig: unsafe config key segment '${lastKey}'`);
throw new Error(`setMsdConfig: unsafe config key segment '${lastKey}'`);
}
obj[lastKey] = value;
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
}
// Paths to the five affected workflow files
const WORKFLOW_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
const WORKFLOW_DIR = path.join(__dirname, '..', 'msd-core', 'workflows');
const AFFECTED_WORKFLOWS = [
path.join(WORKFLOW_DIR, 'execute-phase.md'),
path.join(WORKFLOW_DIR, 'quick.md'),
@@ -153,12 +153,12 @@ const AFFECTED_WORKFLOWS = [
describe('#1146: git.base-branch resolver', () => {
test('A. config override git.base_branch → returned immediately', (t) => {
const dir = createGitRepo({ prefix: 'gsd-1146-a-', defaultBranch: 'master' });
const dir = createGitRepo({ prefix: 'msd-1146-a-', defaultBranch: 'master' });
t.after(() => cleanup(dir));
addPlanning(dir);
setGsdConfig(dir, 'git.base_branch', 'develop');
setMsdConfig(dir, 'git.base_branch', 'develop');
const result = runGsdTools(['query', 'git.base-branch'], dir);
const result = runMsdTools(['query', 'git.base-branch'], dir);
assert.ok(result.success, `git.base-branch with config override failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'develop',
@@ -167,8 +167,8 @@ describe('#1146: git.base-branch resolver', () => {
test('B. origin/HEAD symref resolves → returned', (t) => {
// Create an "origin" bare repo with main branch
const originDir = createGitRepo({ prefix: 'gsd-1146-b-origin-', defaultBranch: 'main' });
const worktreeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1146-b-wt-'));
const originDir = createGitRepo({ prefix: 'msd-1146-b-origin-', defaultBranch: 'main' });
const worktreeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1146-b-wt-'));
t.after(() => { cleanup(originDir); cleanup(worktreeDir); });
// Clone from origin — this sets origin/HEAD
@@ -181,7 +181,7 @@ describe('#1146: git.base-branch resolver', () => {
const symref = gitOrThrow(['symbolic-ref', 'refs/remotes/origin/HEAD'], { cwd: worktreeDir, timeoutMs: GIT_TIMEOUT_MS }).trim();
assert.ok(symref.includes('origin/main'), `Expected origin/HEAD→origin/main, got: ${symref}`);
const result = runGsdTools(['query', 'git.base-branch'], worktreeDir);
const result = runMsdTools(['query', 'git.base-branch'], worktreeDir);
assert.ok(result.success, `git.base-branch symref test failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'main',
@@ -191,8 +191,8 @@ describe('#1146: git.base-branch resolver', () => {
test('C. KEY REGRESSION — master repo, origin/HEAD unset → returns "master" not "main"', (t) => {
// This is the bug: git init + remote add without git remote set-head → no origin/HEAD
// Current code falls back to :-main → wrong. Fixed code uses `git remote show origin`.
const originDir = createGitRepo({ prefix: 'gsd-1146-c-origin-', defaultBranch: 'master' });
const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1146-c-clone-'));
const originDir = createGitRepo({ prefix: 'msd-1146-c-origin-', defaultBranch: 'master' });
const cloneDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1146-c-clone-'));
t.after(() => { cleanup(originDir); cleanup(cloneDir); });
// Manually add remote WITHOUT cloning (so origin/HEAD is never set)
@@ -217,7 +217,7 @@ describe('#1146: git.base-branch resolver', () => {
}
assert.strictEqual(hasSymref, false, 'Test setup: origin/HEAD must be unset for this test case');
const result = runGsdTools(['query', 'git.base-branch'], cloneDir);
const result = runMsdTools(['query', 'git.base-branch'], cloneDir);
assert.ok(result.success, `git.base-branch regression test failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'master',
@@ -225,12 +225,12 @@ describe('#1146: git.base-branch resolver', () => {
});
test('D. No remote, local branch "master" present, "main" absent → returns "master"', (t) => {
const dir = createGitRepo({ prefix: 'gsd-1146-d-', defaultBranch: 'master' });
const dir = createGitRepo({ prefix: 'msd-1146-d-', defaultBranch: 'master' });
t.after(() => cleanup(dir));
addPlanning(dir);
// No remote configured — falls through to local branch detection
const result = runGsdTools(['query', 'git.base-branch'], dir);
const result = runMsdTools(['query', 'git.base-branch'], dir);
assert.ok(result.success, `git.base-branch local branch test failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'master',
@@ -238,11 +238,11 @@ describe('#1146: git.base-branch resolver', () => {
});
test('E. No remote, local branch "main" present → returns "main"', (t) => {
const dir = createGitRepo({ prefix: 'gsd-1146-e-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-1146-e-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
const result = runGsdTools(['query', 'git.base-branch'], dir);
const result = runMsdTools(['query', 'git.base-branch'], dir);
assert.ok(result.success, `git.base-branch main branch test failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'main',
@@ -250,12 +250,12 @@ describe('#1146: git.base-branch resolver', () => {
});
test('F. No remote, no main/master local branch → returns "main" (last resort default)', (t) => {
const dir = createGitRepo({ prefix: 'gsd-1146-f-', defaultBranch: 'develop' });
const dir = createGitRepo({ prefix: 'msd-1146-f-', defaultBranch: 'develop' });
t.after(() => cleanup(dir));
addPlanning(dir);
// Branch named "develop" — neither main nor master
const result = runGsdTools(['query', 'git.base-branch'], dir);
const result = runMsdTools(['query', 'git.base-branch'], dir);
assert.ok(result.success, `git.base-branch default fallback test failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'main',
@@ -263,14 +263,14 @@ describe('#1146: git.base-branch resolver', () => {
});
test('A2. config override with flat base_branch key (legacy form) → returned immediately', (t) => {
const dir = createGitRepo({ prefix: 'gsd-1146-a2-', defaultBranch: 'master' });
const dir = createGitRepo({ prefix: 'msd-1146-a2-', defaultBranch: 'master' });
t.after(() => cleanup(dir));
addPlanning(dir);
// Write flat base_branch directly to config root (legacy form, not nested under "git")
const cfgPath = require('node:path').join(dir, '.planning', 'config.json');
require('node:fs').writeFileSync(cfgPath, JSON.stringify({ base_branch: 'release' }, null, 2) + '\n');
const result = runGsdTools(['query', 'git.base-branch'], dir);
const result = runMsdTools(['query', 'git.base-branch'], dir);
assert.ok(result.success, `git.base-branch with flat config key failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'release',
@@ -285,7 +285,7 @@ describe('#1146: git.base-branch resolver', () => {
// value back. `normalizeLegacyKeys` must hoist/resolve `base_branch` the
// same way it already does `branching_strategy` — canonical nested wins,
// stale flat top-level is dropped.
const dir = createGitRepo({ prefix: 'gsd-3648-a3-', defaultBranch: 'master' });
const dir = createGitRepo({ prefix: 'msd-3648-a3-', defaultBranch: 'master' });
t.after(() => cleanup(dir));
addPlanning(dir);
const cfgPath = require('node:path').join(dir, '.planning', 'config.json');
@@ -294,7 +294,7 @@ describe('#1146: git.base-branch resolver', () => {
JSON.stringify({ base_branch: 'stale-flat', git: { base_branch: 'canonical-nested' } }, null, 2) + '\n',
);
const result = runGsdTools(['query', 'git.base-branch'], dir);
const result = runMsdTools(['query', 'git.base-branch'], dir);
assert.ok(result.success, `git.base-branch with both config keys failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'canonical-nested',
@@ -304,25 +304,25 @@ describe('#1146: git.base-branch resolver', () => {
test('H. No remote, both "main" and "master" local branches exist → returns "main" (main wins tie-break)', (t) => {
// Tier-4 tie-break: when both main and master exist locally and no remote info is available,
// "main" wins (documented in tryLocalBranch JSDoc — modern default).
const dir = createGitRepo({ prefix: 'gsd-1146-h-', defaultBranch: 'master' });
const dir = createGitRepo({ prefix: 'msd-1146-h-', defaultBranch: 'master' });
t.after(() => cleanup(dir));
addPlanning(dir);
// Create a "main" branch alongside the existing "master"
gitOrThrow(['branch', 'main'], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
// No remote configured — falls to tier-4 (local branch existence)
const result = runGsdTools(['query', 'git.base-branch'], dir);
const result = runMsdTools(['query', 'git.base-branch'], dir);
assert.ok(result.success, `git.base-branch both-branches test failed:\n${result.error}`);
const branch = result.output.trim();
assert.strictEqual(branch, 'main',
`Expected 'main' to win when both main and master exist locally, got: '${branch}'`);
});
test('G. Anti-regression: all five affected workflows use gsd_run query git.base-branch, not bare :-main / :-master', () => {
test('G. Anti-regression: all five affected workflows use msd_run query git.base-branch, not bare :-main / :-master', () => {
// The root-cause pattern: DEFAULT_BRANCH=${DEFAULT_BRANCH:-main} or BASE_BRANCH="${BASE_BRANCH:-main}"
// After fix: workflows call gsd_run query git.base-branch and remove the bare fallback.
// After fix: workflows call msd_run query git.base-branch and remove the bare fallback.
const BAD_PATTERN = /\$\{(?:DEFAULT_BRANCH|BASE_BRANCH):-(?:main|master)\}/;
const RESOLVER_CALL = /gsd_run query git\.base-branch/;
const RESOLVER_CALL = /msd_run query git\.base-branch/;
for (const wfPath of AFFECTED_WORKFLOWS) {
const name = path.basename(wfPath);
@@ -331,12 +331,12 @@ describe('#1146: git.base-branch resolver', () => {
assert.ok(
!BAD_PATTERN.test(content),
`${name} still contains the bare :-main/:-master fallback pattern. ` +
'Must be replaced with gsd_run query git.base-branch (Issue #1146).',
'Must be replaced with msd_run query git.base-branch (Issue #1146).',
);
assert.ok(
RESOLVER_CALL.test(content),
`${name} does not call \`gsd_run query git.base-branch\`. ` +
`${name} does not call \`msd_run query git.base-branch\`. ` +
'All five affected workflows must delegate to the single resolver (Issue #1146).',
);
}
@@ -345,12 +345,12 @@ describe('#1146: git.base-branch resolver', () => {
// ─── gitWorktreeInfoInternal: behaviour (#1268 T0, T1 #1277) ─────────────────
const gitBaseBranch = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'git-base-branch.cjs'));
const gitBaseBranch = require(path.join(__dirname, '..', 'msd-core', 'bin', 'lib', 'git-base-branch.cjs'));
const { createTempGitProject, createTempDir } = require('./helpers.cjs');
describe('#1268 gitWorktreeInfoInternal: relocation to git-base-branch', () => {
test('gitWorktreeInfoInternal(createTempGitProject()) returns {inside:true, worktreeRoot:<non-empty string>}', (t) => {
const dir = createTempGitProject('gsd-wt-info-');
const dir = createTempGitProject('msd-wt-info-');
t.after(() => cleanup(dir));
const result = gitBaseBranch.gitWorktreeInfoInternal(dir);
// `git rev-parse --show-toplevel` reports the resolved (symlink-free) path,
@@ -366,7 +366,7 @@ describe('#1268 gitWorktreeInfoInternal: relocation to git-base-branch', () => {
// realpath, or the assertion just encodes the separator convention of
// whatever platform it was written on. This is separators only — POSIX's
// `replace` is a no-op there, so the assertion keeps its full strength on
// POSIX. The remote gsd-test matrix is Linux-only and cannot exercise this
// POSIX. The remote msd-test matrix is Linux-only and cannot exercise this
// path; it only surfaced on the Windows GitHub Actions shard.
assert.strictEqual(result.inside, true, 'inside must be true for a git project dir');
assert.strictEqual(result.worktreeRoot, fs.realpathSync.native(dir).replace(/\\/g, '/'),
@@ -374,7 +374,7 @@ describe('#1268 gitWorktreeInfoInternal: relocation to git-base-branch', () => {
});
test('gitWorktreeInfoInternal(createTempDir()) returns {inside:false, worktreeRoot:null} for a non-git dir', (t) => {
const dir = createTempDir('gsd-wt-info-nongit-');
const dir = createTempDir('msd-wt-info-nongit-');
t.after(() => cleanup(dir));
const result = gitBaseBranch.gitWorktreeInfoInternal(dir);
assert.strictEqual(result.inside, false, 'inside must be false for a non-git dir');
@@ -401,7 +401,7 @@ describe('#1268 gitWorktreeInfoInternal: relocation to git-base-branch', () => {
describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last-resort default', () => {
test('every tier-2/3/4 git query TIMES OUT → last-resort "main" is UNVERIFIED', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3057-b4-fault-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3057-b4-fault-'));
t.after(() => cleanup(dir));
// No .planning/config.json in this dir → the config-override tier is
// skipped naturally (loadConfig's real-fs read misses cleanly).
@@ -414,7 +414,7 @@ describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last
});
test('every tier-2/3/4 git query cleanly reports no candidate → last-resort "main" is VERIFIED', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3057-b4-clean-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3057-b4-clean-'));
t.after(() => cleanup(dir));
// Default passthrough: exitCode 0, empty stdout for every call — a real,
// completed "no answer" from git, not a failure (timedOut:false, error:null).
@@ -427,7 +427,7 @@ describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last
});
test('resolveBaseBranch (string-returning) is unaffected — both cases still return "main"', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3057-b4-compat-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3057-b4-compat-'));
t.after(() => cleanup(dir));
assert.strictEqual(
gitBaseBranch.resolveBaseBranch(dir, { execGit: makeFaultyGit({ faults: [{ kind: 'timeout' }] }) }),
@@ -440,7 +440,7 @@ describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last
});
test('cmdGitBaseBranch writes an unverified-fallback diagnostic to stderr ONLY when unverified', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3057-b4-cmd-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3057-b4-cmd-'));
t.after(() => cleanup(dir));
let stdoutText = '';
@@ -474,7 +474,7 @@ describe('#3057 B4: resolveBaseBranchDiagnostics — verified vs unverified last
// failure) silently answered `false` — the wrong failure direction for a
// protection guard. An unverified guess must not let a caller conclude
// "definitely not protected".
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3648-major-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-3648-major-'));
t.after(() => cleanup(dir));
let stdoutText = '';
@@ -558,15 +558,15 @@ function extractProtectedBranchWarningBash(workflowFile, stepName) {
if (refInBash) refBuffer.push(line);
}
block = refBlocks.find((candidate) => candidate.includes('--is-protected'));
// #3648: sync-runtime-launcher.cjs adds the canonical gsd_run resolver
// #3648: sync-runtime-launcher.cjs adds the canonical msd_run resolver
// preamble to this standalone step file (runtime-launcher-parity's
// one-preamble-per-file rule). That preamble defines its own gsd_run(),
// one-preamble-per-file rule). That preamble defines its own msd_run(),
// which would shadow this harness's injected mock and reach the real
// gsd-tools.cjs on the machine running the test. The preamble's own
// msd-tools.cjs on the machine running the test. The preamble's own
// correctness is covered by tests/runtime-launcher-parity.test.cjs; this
// harness only needs the #3552 warning logic, so strip it here.
if (block) {
block = block.split('\n').filter((l) => !l.trimStart().startsWith('_GSD_SHIM_NAME=')).join('\n');
block = block.split('\n').filter((l) => !l.trimStart().startsWith('_MSD_SHIM_NAME=')).join('\n');
}
}
}
@@ -589,7 +589,7 @@ function writeProtectedBranchWarningScript(prefix, bash) {
' fi',
' printf "%s\\n" "$CURRENT_BRANCH_VALUE"',
'}',
'gsd_run() {',
'msd_run() {',
' if [ "$#" -ne 4 ] || [ "$1" != query ] || [ "$2" != git.base-branch ] ||',
' [ "$3" != --is-protected ] || [ "$4" != "$CURRENT_BRANCH_VALUE" ]; then',
' printf "unexpected protected-branch query\\n"',
@@ -599,7 +599,7 @@ function writeProtectedBranchWarningScript(prefix, bash) {
// explanations to stderr. Emitting one here is what makes a swallowed
// `2>/dev/null` at the call site visible to a test.
' if [ -n "${DIAGNOSTIC_TEXT:-}" ]; then printf "%s\\n" "$DIAGNOSTIC_TEXT" >&2; fi',
// QUERY_EXIT lets a test make the query FAIL (gsd-tools missing, a crash, a
// QUERY_EXIT lets a test make the query FAIL (msd-tools missing, a crash, a
// non-zero exit). Defaults to 0, so every pre-existing caller of this
// harness is unaffected. Nothing is printed on stdout in that case —
// matching a real failed command substitution.
@@ -772,18 +772,18 @@ describe('#3552: configured protected branches', () => {
});
test('#3552 active workstream CLI uses configured list and excludes root-only names', (t) => {
const dir = createGitRepo({ prefix: 'gsd-3552-cli-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3552-cli-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
setGsdConfig(dir, 'git.protected_branches', ['root-only']);
setMsdConfig(dir, 'git.protected_branches', ['root-only']);
fs.mkdirSync(path.join(dir, '.planning', 'workstreams', 'alpha'), { recursive: true });
// Both HOME and USERPROFILE must be redirected — Node's os.homedir() (and
// anything relying on it) consults USERPROFILE first on Windows, where
// setting HOME alone leaves the real home directory in effect and makes
// this isolation silently vacuous on that platform.
const workstreamEnv = { GSD_WORKSTREAM: 'alpha', HOME: dir, USERPROFILE: dir };
const workstreamEnv = { MSD_WORKSTREAM: 'alpha', HOME: dir, USERPROFILE: dir };
const setResult = runGsdTools(
const setResult = runMsdTools(
['config-set', 'git.protected_branches', '["develop","next"]'],
dir,
workstreamEnv,
@@ -795,13 +795,13 @@ describe('#3552: configured protected branches', () => {
));
assert.deepStrictEqual(workstreamConfig.git.protected_branches, ['develop', 'next']);
const match = runGsdTools(
const match = runMsdTools(
['query', 'git.base-branch', '--is-protected', 'develop'], dir, workstreamEnv,
);
const control = runGsdTools(
const control = runMsdTools(
['query', 'git.base-branch', '--is-protected', 'topic/3552'], dir, workstreamEnv,
);
const rootOnly = runGsdTools(
const rootOnly = runMsdTools(
['query', 'git.base-branch', '--is-protected', 'root-only'], dir, workstreamEnv,
);
@@ -828,7 +828,7 @@ describe('#3552: configured protected branches', () => {
test('#3552 execute-phase warns on true, stays silent on false, and continues both', (t) => {
const bash = extractProtectedBranchWarningBash('execute-phase.md', 'handle_branching');
const { scriptDir, scriptPath } = writeProtectedBranchWarningScript(
'gsd-3552-execute-',
'msd-3552-execute-',
bash,
);
t.after(() => cleanup(scriptDir));
@@ -856,7 +856,7 @@ describe('#3552: configured protected branches', () => {
test('#3552 ship warns on true, stays silent on false, and keeps the none-strategy offer', (t) => {
const bash = extractProtectedBranchWarningBash('ship.md', 'preflight_checks');
const { scriptDir, scriptPath } = writeProtectedBranchWarningScript(
'gsd-3552-ship-',
'msd-3552-ship-',
bash,
);
t.after(() => cleanup(scriptDir));
@@ -941,11 +941,11 @@ describe('#3552: configured protected branches', () => {
});
test('#3648 cmdGitBaseBranch usage errors emit structured ERROR_REASON.USAGE and do not print stack trace', (t) => {
const dir = createGitRepo({ prefix: 'gsd-3648-usage-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3648-usage-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
const unknownFlag = runGsdTools(['query', 'git.base-branch', '--unknown-flag', '--json-errors'], dir);
const unknownFlag = runMsdTools(['query', 'git.base-branch', '--unknown-flag', '--json-errors'], dir);
assert.strictEqual(unknownFlag.success, false);
assert.strictEqual(unknownFlag.exitCode, 1);
assert.doesNotMatch(unknownFlag.error, /^\s*at\s+/m, 'usage errors must not print stack traces');
@@ -953,7 +953,7 @@ describe('#3552: configured protected branches', () => {
try { parsed = JSON.parse(unknownFlag.error); } catch {}
assert.strictEqual(parsed?.reason, 'usage');
const surplusPositional = runGsdTools(['query', 'git.base-branch', '--is-protected', 'main', 'extra', '--json-errors'], dir);
const surplusPositional = runMsdTools(['query', 'git.base-branch', '--is-protected', 'main', 'extra', '--json-errors'], dir);
assert.strictEqual(surplusPositional.success, false);
assert.strictEqual(surplusPositional.exitCode, 1);
assert.doesNotMatch(surplusPositional.error, /^\s*at\s+/m);
@@ -963,7 +963,7 @@ describe('#3552: configured protected branches', () => {
});
test('#3648 Minor F-1: nested and flat predicate reads stay aligned with the loader', () => {
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
const cases = [
{ git: { base_branch: 'nested', protected_branches: ['develop'] }, base_branch: 'flat', protected_branches: ['bogus'] },
{ git: { base_branch: 'nested' }, base_branch: 'flat' },
@@ -992,8 +992,8 @@ describe('#3552: configured protected branches', () => {
// warning on a branch that is not protected. The diagnostic was exercised
// only by its unit test — dead in production.
const cases = [
['execute-phase.md', 'handle_branching', 'gsd-3648-b4-execute-'],
['ship.md', 'preflight_checks', 'gsd-3648-b4-ship-'],
['execute-phase.md', 'handle_branching', 'msd-3648-b4-execute-'],
['ship.md', 'preflight_checks', 'msd-3648-b4-ship-'],
];
for (const [workflow, step, prefix] of cases) {
@@ -1029,8 +1029,8 @@ describe('#3552: configured protected branches', () => {
});
test('#3648 round-4: a FAILED query degrades visibly, and does not abort under set -e', (t) => {
// Found by the round-4 external review. `IS_PROTECTED=$(gsd_run ...)` had
// two problems when the query itself failed (gsd-tools absent, a crash, any
// Found by the round-4 external review. `IS_PROTECTED=$(msd_run ...)` had
// two problems when the query itself failed (msd-tools absent, a crash, any
// non-zero exit): the substitution yielded an empty string, so `[ "$X" = true ]`
// was simply false and the workflow continued with NO warning and no trace —
// a silent fail-open in the guard whose whole purpose is to warn; and the
@@ -1039,10 +1039,10 @@ describe('#3552: configured protected branches', () => {
//
// The contract is neither fail-open nor fail-closed: it degrades VISIBLY.
// Claiming "protected" on no evidence would warn on every branch whenever
// gsd-tools is unavailable; claiming "not protected" is the silent hole.
// msd-tools is unavailable; claiming "not protected" is the silent hole.
const cases = [
['execute-phase.md', 'handle_branching', 'gsd-3648-r4-execute-'],
['ship.md', 'preflight_checks', 'gsd-3648-r4-ship-'],
['execute-phase.md', 'handle_branching', 'msd-3648-r4-execute-'],
['ship.md', 'preflight_checks', 'msd-3648-r4-ship-'],
];
for (const [workflow, step, prefix] of cases) {
@@ -1094,7 +1094,7 @@ describe('#3552: configured protected branches', () => {
// state from warning text in tool output; the comparison it replaced was
// directly evaluable.
const bash = extractProtectedBranchWarningBash('ship.md', 'preflight_checks');
const { scriptDir, scriptPath } = writeProtectedBranchWarningScript('gsd-3648-bind-', bash);
const { scriptDir, scriptPath } = writeProtectedBranchWarningScript('msd-3648-bind-', bash);
t.after(() => cleanup(scriptDir));
const baseEnv = { ...process.env, CURRENT_BRANCH_VALUE: 'develop' };
@@ -1166,12 +1166,12 @@ describe('#3819: allow_default_branch_commits escape hatch', () => {
});
test('#3819 CLI: git.allow_default_branch_commits:true makes --is-protected report false on the base branch', (t) => {
const dir = createGitRepo({ prefix: 'gsd-3819-cli-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3819-cli-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
setGsdConfig(dir, 'git.allow_default_branch_commits', true);
setMsdConfig(dir, 'git.allow_default_branch_commits', true);
const escaped = runGsdTools(['query', 'git.base-branch', '--is-protected', 'main'], dir);
const escaped = runMsdTools(['query', 'git.base-branch', '--is-protected', 'main'], dir);
assert.ok(escaped.success, escaped.error);
assert.strictEqual(escaped.output, 'false',
'the escape hatch must make the CLI report the base branch as not protected');
@@ -1179,10 +1179,10 @@ describe('#3819: allow_default_branch_commits escape hatch', () => {
// Negative control: a sibling fixture without the config key must still
// report the base branch as protected — proves the flag, not some other
// difference between the two fixtures, drives the result above.
const controlDir = createGitRepo({ prefix: 'gsd-3819-cli-control-', defaultBranch: 'main' });
const controlDir = createGitRepo({ prefix: 'msd-3819-cli-control-', defaultBranch: 'main' });
t.after(() => cleanup(controlDir));
addPlanning(controlDir);
const control = runGsdTools(['query', 'git.base-branch', '--is-protected', 'main'], controlDir);
const control = runMsdTools(['query', 'git.base-branch', '--is-protected', 'main'], controlDir);
assert.ok(control.success, control.error);
assert.strictEqual(control.output, 'true');
assert.notStrictEqual(escaped.output, control.output,
@@ -1190,17 +1190,17 @@ describe('#3819: allow_default_branch_commits escape hatch', () => {
});
});
// ─── #3819: gsd-executor.md pre-commit protected-branch guard ────────────────
// ─── #3819: msd-executor.md pre-commit protected-branch guard ────────────────
/**
* Extract the FIRST ```bash fenced block that follows the "Pre-commit
* protected-branch safety assertion" heading in agents/gsd-executor.md.
* protected-branch safety assertion" heading in agents/msd-executor.md.
* Keyed off the heading text (not a `<step name>` tag — this step has no
* such wrapper) so a future rewording of the heading fails the test loudly
* instead of silently extracting nothing.
*/
function extractExecutorPreCommitBash() {
const executorPath = path.join(__dirname, '..', 'agents', 'gsd-executor.md');
const executorPath = path.join(__dirname, '..', 'agents', 'msd-executor.md');
const content = readFileNormalized(executorPath);
const lines = content.split('\n');
const headingIndex = lines.findIndex(
@@ -1208,7 +1208,7 @@ function extractExecutorPreCommitBash() {
);
if (headingIndex === -1) {
throw new Error(
'agents/gsd-executor.md: could not find the "Pre-commit HEAD safety ' +
'agents/msd-executor.md: could not find the "Pre-commit HEAD safety ' +
'assertion" heading — has it been reworded?',
);
}
@@ -1226,13 +1226,13 @@ function extractExecutorPreCommitBash() {
if (inBash) buffer.push(line);
}
throw new Error(
'agents/gsd-executor.md: no ```bash block found after the pre-commit protected-branch ' +
'agents/msd-executor.md: no ```bash block found after the pre-commit protected-branch ' +
'heading — has the step been restructured?',
);
}
/**
* Write a standalone script that mocks `git` and `gsd_run`, then runs the
* Write a standalone script that mocks `git` and `msd_run`, then runs the
* extracted pre-commit guard bash verbatim. Deliberately does NOT `set -e`:
* the guard's fatal paths call `exit 1` explicitly, and this script must run
* to completion on the non-fatal path to print the GUARD_PASSED sentinel.
@@ -1266,10 +1266,10 @@ function writeExecutorGuardScript(prefix, bash, { branch, headRef, isWorktree, q
' return 97',
' fi',
'}',
'gsd_run() {',
'msd_run() {',
` if [ "$#" -ne 4 ] || [ "$1" != query ] || [ "$2" != git.base-branch ] || ` +
`[ "$3" != --is-protected ] || [ "$4" != "${branch}" ]; then`,
' printf "unexpected gsd_run invocation: %s\\n" "$*" >&2',
' printf "unexpected msd_run invocation: %s\\n" "$*" >&2',
' return 0',
' fi',
queryExit
@@ -1282,11 +1282,11 @@ function writeExecutorGuardScript(prefix, bash, { branch, headRef, isWorktree, q
return { scriptDir, scriptPath };
}
describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
describe('#3819: msd-executor.md pre-commit protected-branch guard', () => {
const bash = extractExecutorPreCommitBash();
test('#3819 non-worktree, branch is protected (query says true) → HALT with exit 1, no GUARD_PASSED', (t) => {
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-nonwt-protected-', bash, {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-nonwt-protected-', bash, {
branch: 'main',
headRef: 'main',
isWorktree: false,
@@ -1302,7 +1302,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
});
test('#3819 non-worktree, branch is not protected (query says false) → continues, GUARD_PASSED', (t) => {
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-nonwt-clean-', bash, {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-nonwt-clean-', bash, {
branch: 'feature-x',
headRef: 'feature-x',
isWorktree: false,
@@ -1317,7 +1317,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
});
test('#3819 worktree, branch is protected → HALT with exit 1 (protected check fires before the allow-list check)', (t) => {
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-wt-protected-', bash, {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-wt-protected-', bash, {
branch: 'main',
headRef: 'main',
isWorktree: true,
@@ -1332,7 +1332,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
});
test('#3819 worktree, branch not protected but outside the agent-*/worktree-agent-*/worktree-wf_* namespace → HALT', (t) => {
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-wt-outside-namespace-', bash, {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-wt-outside-namespace-', bash, {
branch: 'some-random-branch',
headRef: 'some-random-branch',
isWorktree: true,
@@ -1347,7 +1347,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
});
test('#3819 worktree, branch not protected AND in the agent-* namespace → continues, GUARD_PASSED', (t) => {
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-wt-clean-', bash, {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-wt-clean-', bash, {
branch: 'agent-42',
headRef: 'agent-42',
isWorktree: true,
@@ -1362,7 +1362,7 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
});
test('#3819 detached HEAD (non-worktree) → HALT with exit 1 before any protected-branch query', (t) => {
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-detached-', bash, {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-detached-', bash, {
branch: 'HEAD',
headRef: 'DETACHED',
isWorktree: false,
@@ -1376,24 +1376,24 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
assert.match(result.stderr, /detached/);
});
test('#3819 HOSTILE: gsd_run RUNS but reports the branch as protected (resolver-internal fail-closed) → HALT', (t) => {
test('#3819 HOSTILE: msd_run RUNS but reports the branch as protected (resolver-internal fail-closed) → HALT', (t) => {
// This is the scenario that must stay fail-closed unconditionally:
// gsd_run itself succeeds (exit 0) — e.g. because cmdGitBaseBranch's own
// msd_run itself succeeds (exit 0) — e.g. because cmdGitBaseBranch's own
// pre-existing #3057 B4 logic already answered "true" when it could not
// VERIFY the real default branch — and the bash guard must never
// second-guess that answer. It is deliberately indistinguishable, at
// this bash layer, from an ordinary "yes this branch is protected"
// answer (see the "non-worktree, branch is protected" test above) —
// that IS the fail-closed contract: the guard trusts what gsd_run says
// when gsd_run actually says something.
// that IS the fail-closed contract: the guard trusts what msd_run says
// when msd_run actually says something.
//
// This is a DIFFERENT failure mode from "gsd_run could not be invoked at
// This is a DIFFERENT failure mode from "msd_run could not be invoked at
// all" (nonzero exit / no output), which is covered by the two
// "gsd_run itself is unavailable" tests below and — per the #3819
// "msd_run itself is unavailable" tests below and — per the #3819
// approval's item 2 ("keep the existing names as a fallback where the
// default cannot be resolved") — deliberately does NOT fail closed for
// every branch; it falls back to the pre-#3819 five-name list instead.
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-resolver-unverified-', bash, {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-resolver-unverified-', bash, {
branch: 'feature-x',
headRef: 'feature-x',
isWorktree: false,
@@ -1408,8 +1408,8 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
assert.doesNotMatch(result.stdout, /GUARD_PASSED/);
});
test('#3819 gsd_run itself is unavailable (query cannot run) + branch matches the old five-name list → still HALTs via the fallback', (t) => {
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-unavailable-listed-', bash, {
test('#3819 msd_run itself is unavailable (query cannot run) + branch matches the old five-name list → still HALTs via the fallback', (t) => {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-unavailable-listed-', bash, {
branch: 'develop',
headRef: 'develop',
isWorktree: false,
@@ -1424,13 +1424,13 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
assert.match(result.stderr, /protected\/default branch/);
});
test('#3819 gsd_run itself is unavailable (query cannot run) + branch does NOT match the old five-name list → falls back to allowing it', (t) => {
// Deliberate #3819-approved tradeoff: when gsd-tools itself cannot even be
test('#3819 msd_run itself is unavailable (query cannot run) + branch does NOT match the old five-name list → falls back to allowing it', (t) => {
// Deliberate #3819-approved tradeoff: when msd-tools itself cannot even be
// invoked, an unlisted branch is allowed through rather than blocking every
// branch outright — unlike the "HOSTILE: the protected-branch query itself
// fails" test above (gsd-tools runs fine but the resolver's own internal
// fails" test above (msd-tools runs fine but the resolver's own internal
// verification fails), which stays fail-closed.
const { scriptDir, scriptPath } = writeExecutorGuardScript('gsd-3819-guard-unavailable-unlisted-', bash, {
const { scriptDir, scriptPath } = writeExecutorGuardScript('msd-3819-guard-unavailable-unlisted-', bash, {
branch: 'my-feature-branch',
headRef: 'my-feature-branch',
isWorktree: false,
@@ -1450,10 +1450,10 @@ describe('#3819: gsd-executor.md pre-commit protected-branch guard', () => {
// <final_commit> block itself — the workflow .md IS the product surface here,
// same rationale as Guard G (see the top-of-file exemption above).
test('#3819 <final_commit> block instructs the executor to re-run the Step 0 guard before the final commit', () => {
const executorPath = path.join(__dirname, '..', 'agents', 'gsd-executor.md');
const executorPath = path.join(__dirname, '..', 'agents', 'msd-executor.md');
const content = readFileNormalized(executorPath);
const startIndex = content.indexOf('<final_commit>');
assert.notStrictEqual(startIndex, -1, 'agents/gsd-executor.md: could not find <final_commit> tag — has it been renamed?');
assert.notStrictEqual(startIndex, -1, 'agents/msd-executor.md: could not find <final_commit> tag — has it been renamed?');
const fenceIndex = content.indexOf('```bash', startIndex);
const excerpt = fenceIndex === -1
? content.slice(startIndex, startIndex + 2000)
@@ -1507,7 +1507,7 @@ describe('#3648 property: config-set validation and resolver filtering must agre
// structural keeps the two definitions of "usable branch name" in step —
// only this property, which asks both about the same values.
const fc = require('./helpers/fast-check-setup.cjs');
const { isValidProtectedBranches } = require('../gsd-core/bin/lib/config.cjs');
const { isValidProtectedBranches } = require('../msd-core/bin/lib/config.cjs');
/** A value generator weighted towards the boundary cases both surfaces care about. */
const entry = () => fc.oneof(
@@ -1564,7 +1564,7 @@ describe('#3648 property: config-set validation and resolver filtering must agre
// BRAND-NEW key through it invents an undocumented top-level spelling that
// silently outranks `git.protected_branches`. `protected_branches` has no
// legacy form, so it resolves nested-only — in production and in the seam.
const dir = createGitRepo({ prefix: 'gsd-3648-flat-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3648-flat-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
fs.writeFileSync(
@@ -1572,7 +1572,7 @@ describe('#3648 property: config-set validation and resolver filtering must agre
JSON.stringify({ protected_branches: ['bogus'], git: { protected_branches: ['develop'] } }),
);
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
assert.deepStrictEqual(
configLoader.loadConfig(dir, { persist: false })['protected_branches'], ['develop'],
'the canonical nested key must win outright');
@@ -1645,7 +1645,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
}
test('a legacy-key config survives --is-protected byte-for-byte, and is still honoured', (t) => {
const dir = createGitRepo({ prefix: 'gsd-3648-b1-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3648-b1-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
const cfgPath = writeLegacyConfig(dir);
@@ -1653,8 +1653,8 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
const before = fs.readFileSync(cfgPath);
const planningBefore = snapshotPlanningTree(dir);
const match = runGsdTools(['query', 'git.base-branch', '--is-protected', 'develop'], dir);
const control = runGsdTools(['query', 'git.base-branch', '--is-protected', 'topic/3648'], dir);
const match = runMsdTools(['query', 'git.base-branch', '--is-protected', 'develop'], dir);
const control = runMsdTools(['query', 'git.base-branch', '--is-protected', 'topic/3648'], dir);
assert.ok(match.success, match.error);
assert.ok(control.success, control.error);
@@ -1680,13 +1680,13 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
// Without this the test above cannot fail for the right reason — a fixture
// that never triggered a normalization would keep its bytes no matter what
// `persist` did. This proves the fixture is live.
const dir = createGitRepo({ prefix: 'gsd-3648-b1-neg-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3648-b1-neg-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
const cfgPath = writeLegacyConfig(dir);
const before = fs.readFileSync(cfgPath);
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
const persisted = configLoader.loadConfig(dir);
assert.notDeepStrictEqual(
@@ -1698,7 +1698,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
});
test('negative control: a planted .planning sibling changes the tree snapshot', (t) => {
const dir = createGitRepo({ prefix: 'gsd-3648-b1-stray-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3648-b1-stray-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
const before = snapshotPlanningTree(dir);
@@ -1712,13 +1712,13 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
});
test('persist:false changes only the side effect, not the resolved config', (t) => {
const dir = createGitRepo({ prefix: 'gsd-3648-b1-parity-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3648-b1-parity-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
const cfgPath = writeLegacyConfig(dir);
const before = fs.readFileSync(cfgPath);
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
const quiet = configLoader.loadConfig(dir, { persist: false });
assert.deepStrictEqual(fs.readFileSync(cfgPath), before,
'persist:false must not write');
@@ -1737,7 +1737,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
// persistence and rewrote the root config — the predicate's `persist:false`
// was silently discarded for exactly the projects that use workstreams.
// Found by the round-4 external review.
const dir = createGitRepo({ prefix: 'gsd-3648-b1-ws-', defaultBranch: 'main' });
const dir = createGitRepo({ prefix: 'msd-3648-b1-ws-', defaultBranch: 'main' });
t.after(() => cleanup(dir));
addPlanning(dir);
// The workstream directory exists but carries no config.json — the shape
@@ -1747,7 +1747,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
const cfgPath = writeLegacyConfig(dir);
const before = fs.readFileSync(cfgPath);
const configLoader = require('../gsd-core/bin/lib/config-loader.cjs');
const configLoader = require('../msd-core/bin/lib/config-loader.cjs');
const quiet = configLoader.loadConfig(dir, { persist: false, workstream: 'alpha' });
assert.strictEqual(quiet['base_branch'], 'develop',
@@ -1764,7 +1764,7 @@ describe('#3648 Blocker 1: --is-protected is a QUERY and must not rewrite config
});
describe('#3648 Major 1: readEffectiveGitConfig readFile seam — config present but unusable', () => {
const CWD = path.join(path.sep, 'gsd-3648-seam');
const CWD = path.join(path.sep, 'msd-3648-seam');
/** Drive the seam with raw config text, recording the paths requested. */
function readWith(raw, seenPaths) {
@@ -2144,7 +2144,7 @@ describe('#3057 W3: gitWorktreeInfoInternal — no work tree, and git failing mi
// `git rev-parse --is-inside-work-tree` exits 0 in a bare repo and prints
// "false" — the exitCode guard does NOT fire, so this is the stdout check,
// and it is reachable without any injection.
const dir = createTempDir('gsd-3057-w3-bare-');
const dir = createTempDir('msd-3057-w3-bare-');
t.after(() => cleanup(dir));
gitOrThrow(['init', '--bare'], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS });
@@ -2253,38 +2253,38 @@ describe('#3057 W3: cmdGitBaseBranch — the DEFAULT diagnostic sink', () => {
});
});
// ─── setGsdConfig prototype-pollution guard (#1406) ───────────────────────────
// ─── setMsdConfig prototype-pollution guard (#1406) ───────────────────────────
describe('#1406: setGsdConfig prototype-pollution guard', () => {
describe('#1406: setMsdConfig prototype-pollution guard', () => {
test('rejects __proto__ as a key segment', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1406-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1406-'));
t.after(() => cleanup(dir));
assert.throws(() => setGsdConfig(dir, '__proto__', 'x'), /unsafe config key segment/);
assert.throws(() => setGsdConfig(dir, '__proto__.polluted', true), /unsafe config key segment/);
assert.throws(() => setMsdConfig(dir, '__proto__', 'x'), /unsafe config key segment/);
assert.throws(() => setMsdConfig(dir, '__proto__.polluted', true), /unsafe config key segment/);
});
test('rejects constructor / prototype chain segments', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1406-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1406-'));
t.after(() => cleanup(dir));
assert.throws(() => setGsdConfig(dir, 'constructor.prototype.polluted', true), /unsafe config key segment/);
assert.throws(() => setGsdConfig(dir, 'safe.__proto__', true), /unsafe config key segment/);
assert.throws(() => setGsdConfig(dir, 'a.prototype.b', true), /unsafe config key segment/);
assert.throws(() => setMsdConfig(dir, 'constructor.prototype.polluted', true), /unsafe config key segment/);
assert.throws(() => setMsdConfig(dir, 'safe.__proto__', true), /unsafe config key segment/);
assert.throws(() => setMsdConfig(dir, 'a.prototype.b', true), /unsafe config key segment/);
});
test('does not pollute Object.prototype after rejected attempts', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1406-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1406-'));
t.after(() => cleanup(dir));
try { setGsdConfig(dir, '__proto__.polluted', true); } catch (_) { /* expected */ }
try { setGsdConfig(dir, 'constructor.prototype.polluted', true); } catch (_) { /* expected */ }
try { setGsdConfig(dir, 'a.__proto__.polluted', true); } catch (_) { /* expected */ }
try { setMsdConfig(dir, '__proto__.polluted', true); } catch (_) { /* expected */ }
try { setMsdConfig(dir, 'constructor.prototype.polluted', true); } catch (_) { /* expected */ }
try { setMsdConfig(dir, 'a.__proto__.polluted', true); } catch (_) { /* expected */ }
assert.strictEqual(({}).polluted, undefined);
assert.strictEqual(Object.prototype.polluted, undefined);
});
test('still writes a normal nested key', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1406-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-1406-'));
t.after(() => cleanup(dir));
setGsdConfig(dir, 'git.base_branch', 'develop');
setMsdConfig(dir, 'git.base_branch', 'develop');
const cfgPath = path.join(dir, '.planning', 'config.json');
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
assert.strictEqual(cfg.git.base_branch, 'develop');
@@ -2301,7 +2301,7 @@ describe('#1406: setGsdConfig prototype-pollution guard', () => {
/**
* Regression tests for bug #2004
*
* /gsd-pr-branch must not exclude milestone archive and structural planning
* /msd-pr-branch must not exclude milestone archive and structural planning
* commits. The previous implementation filtered ALL .planning/-only commits,
* including STATE.md, ROADMAP.md, MILESTONES.md, and milestones/** updates
* that are needed to preserve repository planning state after a merge.
@@ -2322,7 +2322,7 @@ const fs = require('node:fs');
const path = require('node:path');
const workflowPath = path.resolve(
__dirname, '..', 'gsd-core', 'workflows', 'pr-branch.md'
__dirname, '..', 'msd-core', 'workflows', 'pr-branch.md'
);
describe('bug #2004: pr-branch preserves structural planning commits', () => {
@@ -2427,7 +2427,7 @@ const { cleanup } = require('./helpers.cjs');
const EXECUTE_PHASE_PATH = path.join(
__dirname,
'..',
'gsd-core',
'msd-core',
'workflows',
'execute-phase.md'
);
@@ -2510,7 +2510,7 @@ function extractHandleBranchingBash() {
* same treatment in 80f14cac; this test deserves the same coverage).
*/
function setupFixture(defaultBranch = 'main') {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2916-'));
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2916-'));
const seedPath = path.join(root, 'seed');
const originPath = path.join(root, 'origin.git');
const clonePath = path.join(root, 'clone');
@@ -2544,7 +2544,7 @@ function runHandleBranchingStep(bash, cwd, branchName) {
// Write the script to a sibling tempdir, not inside the repo — putting it in
// `cwd` would create an untracked file that trips `git status --porcelain`
// and steers the step into its dirty-tree fallback path.
const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2916-step-'));
const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2916-step-'));
const scriptPath = path.join(scriptDir, 'handle-branching.sh');
const script = `#!/usr/bin/env bash\nset -uo pipefail\nBRANCH_NAME="${branchName}"\n${bash}\n`;
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
@@ -2713,7 +2713,7 @@ describe('#3679 — pr-branch pre-existing planning content + verify deletion ga
});
test('verify fails when the PR diff deletes target-tracked planning files', (t) => {
const repo = createTempDir('gsd-3679-verify-fail-');
const repo = createTempDir('msd-3679-verify-fail-');
t.after(() => cleanup(repo));
const g = (args) => gitOrThrow(args, { cwd: repo });
g(['init', '-q', '-b', 'main']);
@@ -2755,7 +2755,7 @@ describe('#3679 — pr-branch pre-existing planning content + verify deletion ga
});
test('verify passes a clean diff with zero deletions', (t) => {
const repo = createTempDir('gsd-3679-verify-clean-');
const repo = createTempDir('msd-3679-verify-clean-');
t.after(() => cleanup(repo));
const g = (args) => gitOrThrow(args, { cwd: repo });
g(['init', '-q', '-b', 'main']);
@@ -2793,7 +2793,7 @@ describe('#3679 — pr-branch pre-existing planning content + verify deletion ga
});
test('create loop preserves pre-existing transient content (#3720 guarantee pinned)', (t) => {
const repo = createTempDir('gsd-3679-create-mixed-');
const repo = createTempDir('msd-3679-create-mixed-');
t.after(() => cleanup(repo));
const g = (args) => gitOrThrow(args, { cwd: repo });
g(['init', '-q', '-b', 'main']);
@@ -2847,7 +2847,7 @@ describe('#3679 — pr-branch pre-existing planning content + verify deletion ga
});
test('create loop preserves planning content on pure-code commits', (t) => {
const repo = createTempDir('gsd-3679-create-pure-');
const repo = createTempDir('msd-3679-create-pure-');
t.after(() => cleanup(repo));
const g = (args) => gitOrThrow(args, { cwd: repo });
g(['init', '-q', '-b', 'main']);