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

@@ -4,18 +4,18 @@
// Per CONTRIBUTING.md exception matrix.
/**
* GSD Tools Tests - autonomous.md and complete-milestone.md workstream-scoped
* MSD Tools Tests - autonomous.md and complete-milestone.md workstream-scoped
* STATE/ROADMAP/archive paths (#4455)
*
* Both workflows used to read/write hardcoded literal `.planning/STATE.md` /
* `.planning/ROADMAP.md` / `.planning/milestones/...` paths in their shell
* fences — bypassing workstream scoping entirely. When GSD_WORKSTREAM is set,
* fences — bypassing workstream scoping entirely. When MSD_WORKSTREAM is set,
* `init.manager`/`init.complete-milestone` resolve `state_path`/`roadmap_path`/
* `archive_dir` into the workstream, but the literal reads/writes still hit
* root `.planning/` (or silently returned empty).
*
* These tests extract the real bash fences from the workflow files and
* execute them (with a stubbed `gsd_run`) rather than string-matching the
* execute them (with a stubbed `msd_run`) rather than string-matching the
* markdown — the same idiom tests/new-milestone-clear-phases.test.cjs uses.
*
* One file (not two) per scripts/lint-test-file-count.cjs's per-module cap —
@@ -31,7 +31,7 @@ const path = require('path');
const os = require('os');
const { runHook: runHookSeam } = require('./helpers/process-seam.cjs');
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
const { scanFencedBlocks } = require('../gsd-core/bin/lib/markdown-sectionizer.cjs');
const { scanFencedBlocks } = require('../msd-core/bin/lib/markdown-sectionizer.cjs');
const { cleanup } = require('./helpers.cjs');
/** Return the raw text of every ```bash fenced block in `text`. */
@@ -73,7 +73,7 @@ function extractFenceContaining(markdown, startMarker, endMarker, marker) {
}
describe('autonomous.md workstream-scoped paths (#4455)', () => {
const WORKFLOW_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'autonomous.md');
const WORKFLOW_PATH = path.join(__dirname, '..', 'msd-core', 'workflows', 'autonomous.md');
const content = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const discoverPhasesFence = extractFenceBetween(content, '## 2. Discover Phases', '## 3. Execute Phase');
@@ -83,7 +83,7 @@ describe('autonomous.md workstream-scoped paths (#4455)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-autonomous-ws-'));
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-autonomous-ws-'));
});
afterEach(() => {
@@ -91,43 +91,43 @@ describe('autonomous.md workstream-scoped paths (#4455)', () => {
});
/**
* Write a canned init.manager-shaped JSON payload and a gsd_run stub that
* Write a canned init.manager-shaped JSON payload and a msd_run stub that
* returns it verbatim. Each call also appends one byte to a call-log file
* — NOT a shell variable increment, because `INIT_MANAGER=$(gsd_run ...)`
* runs gsd_run inside the command-substitution SUBSHELL, so a variable
* — NOT a shell variable increment, because `INIT_MANAGER=$(msd_run ...)`
* runs msd_run inside the command-substitution SUBSHELL, so a variable
* mutated there never survives back into the caller's shell.
*/
function stubGsdRun(jsonPayload) {
function stubMsdRun(jsonPayload) {
const jsonPath = path.join(tmpDir, 'init-manager.json');
const callLogPath = path.join(tmpDir, 'gsd-run-calls.log');
const callLogPath = path.join(tmpDir, 'msd-run-calls.log');
fs.writeFileSync(jsonPath, JSON.stringify(jsonPayload));
fs.writeFileSync(callLogPath, '');
return `gsd_run() { printf 'x' >> "${callLogPath}"; cat "${jsonPath}"; }\n`;
return `msd_run() { printf 'x' >> "${callLogPath}"; cat "${jsonPath}"; }\n`;
}
/** Number of gsd_run invocations recorded by the most recent stubGsdRun-backed script. */
function gsdRunCallCount() {
return fs.readFileSync(path.join(tmpDir, 'gsd-run-calls.log'), 'utf8').length;
/** Number of msd_run invocations recorded by the most recent stubMsdRun-backed script. */
function msdRunCallCount() {
return fs.readFileSync(path.join(tmpDir, 'msd-run-calls.log'), 'utf8').length;
}
describe('discover_phases step: reads STATE.md from init.manager state_path, not a hardcoded literal', () => {
test('flat mode: no GSD_WORKSTREAM — resolves the root STATE.md path (regression guard)', () => {
test('flat mode: no MSD_WORKSTREAM — resolves the root STATE.md path (regression guard)', () => {
const statePath = path.join(tmpDir, 'STATE-root.md');
fs.writeFileSync(statePath, '# Root State\n');
const script = `${stubGsdRun({ state_path: statePath })}${discoverPhasesFence}\nprintf 'STATE_CONTENT=[%s]\\n' "$STATE_CONTENT"`;
const script = `${stubMsdRun({ state_path: statePath })}${discoverPhasesFence}\nprintf 'STATE_CONTENT=[%s]\\n' "$STATE_CONTENT"`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <discover_phases fence>');
assert.ok(r.stdout.includes('STATE_CONTENT=[# Root State'),
`expected root STATE.md content, got: ${r.stdout}`);
});
test('GSD_WORKSTREAM=alpha: reads the workstream-scoped STATE.md, not the root one (#4455 regression)', () => {
test('MSD_WORKSTREAM=alpha: reads the workstream-scoped STATE.md, not the root one (#4455 regression)', () => {
const rootStatePath = path.join(tmpDir, 'STATE-root.md');
fs.writeFileSync(rootStatePath, '# Root State — must not be read\n');
const wsStatePath = path.join(tmpDir, 'STATE-alpha.md');
fs.writeFileSync(wsStatePath, '# Workstream Alpha State\n');
const script = `${stubGsdRun({ state_path: wsStatePath })}${discoverPhasesFence}\nprintf 'STATE_CONTENT=[%s]\\n' "$STATE_CONTENT"`;
const script = `${stubMsdRun({ state_path: wsStatePath })}${discoverPhasesFence}\nprintf 'STATE_CONTENT=[%s]\\n' "$STATE_CONTENT"`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <discover_phases fence>');
assert.ok(r.stdout.includes('STATE_CONTENT=[# Workstream Alpha State'),
@@ -141,20 +141,20 @@ describe('autonomous.md workstream-scoped paths (#4455)', () => {
test('flat mode: resolves the root STATE.md path (regression guard)', () => {
const statePath = path.join(tmpDir, 'STATE-root.md');
fs.writeFileSync(statePath, '# Root State\n');
const script = `${stubGsdRun({ state_path: statePath })}${iterateFence}`;
const script = `${stubMsdRun({ state_path: statePath })}${iterateFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <iterate fence>');
// The fence's own `cat "$STATE_PATH"` line prints the raw re-read to stdout.
assert.ok(r.stdout.includes('# Root State'), `expected root STATE.md content, got: ${r.stdout}`);
});
test('GSD_WORKSTREAM=alpha: re-reads the workstream STATE.md, not root (#4455 regression)', () => {
test('MSD_WORKSTREAM=alpha: re-reads the workstream STATE.md, not root (#4455 regression)', () => {
const rootStatePath = path.join(tmpDir, 'STATE-root.md');
fs.writeFileSync(rootStatePath, '# Root State — must not be read\n');
const wsStatePath = path.join(tmpDir, 'STATE-alpha.md');
fs.writeFileSync(wsStatePath, '# Workstream Alpha State\n');
const script = `${stubGsdRun({ state_path: wsStatePath })}${iterateFence}`;
const script = `${stubMsdRun({ state_path: wsStatePath })}${iterateFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <iterate fence>');
assert.ok(r.stdout.includes('# Workstream Alpha State'),
@@ -166,11 +166,11 @@ describe('autonomous.md workstream-scoped paths (#4455)', () => {
test('does not double-fetch init.manager within the iterate fence (no-double-fetch requirement)', () => {
const statePath = path.join(tmpDir, 'STATE-root.md');
fs.writeFileSync(statePath, '# Root State\n');
const script = `${stubGsdRun({ state_path: statePath })}${iterateFence}`;
const script = `${stubMsdRun({ state_path: statePath })}${iterateFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <iterate fence>');
assert.strictEqual(gsdRunCallCount(), 1,
`iterate fence must call gsd_run exactly once (no double-fetch), got ${gsdRunCallCount()}; stdout: ${r.stdout}`);
assert.strictEqual(msdRunCallCount(), 1,
`iterate fence must call msd_run exactly once (no double-fetch), got ${msdRunCallCount()}; stdout: ${r.stdout}`);
});
});
@@ -180,7 +180,7 @@ describe('autonomous.md workstream-scoped paths (#4455)', () => {
fs.mkdirSync(archiveDir, { recursive: true });
fs.writeFileSync(path.join(archiveDir, 'v1.0-ROADMAP.md'), '# Archived Roadmap\n');
const script = `milestone_version="1.0"\n${stubGsdRun({ archive_dir: archiveDir })}${lifecycle5bFence}`;
const script = `milestone_version="1.0"\n${stubMsdRun({ archive_dir: archiveDir })}${lifecycle5bFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <lifecycle 5b fence>');
// The fence concatenates with a literal bash `/` ("${ARCHIVE_DIR}/v...-ROADMAP.md"),
@@ -191,7 +191,7 @@ describe('autonomous.md workstream-scoped paths (#4455)', () => {
`expected ls to find the root archive file, got: ${r.stdout}`);
});
test('GSD_WORKSTREAM=alpha: checks the workstream-scoped archive dir, not root (#4455 regression)', () => {
test('MSD_WORKSTREAM=alpha: checks the workstream-scoped archive dir, not root (#4455 regression)', () => {
const rootArchiveDir = path.join(tmpDir, 'milestones-root');
fs.mkdirSync(rootArchiveDir, { recursive: true });
fs.writeFileSync(path.join(rootArchiveDir, 'v1.0-ROADMAP.md'), '# Root Archived Roadmap — must not be found\n');
@@ -200,7 +200,7 @@ describe('autonomous.md workstream-scoped paths (#4455)', () => {
fs.mkdirSync(wsArchiveDir, { recursive: true });
fs.writeFileSync(path.join(wsArchiveDir, 'v1.0-ROADMAP.md'), '# Workstream Archived Roadmap\n');
const script = `milestone_version="1.0"\n${stubGsdRun({ archive_dir: wsArchiveDir })}${lifecycle5bFence}`;
const script = `milestone_version="1.0"\n${stubMsdRun({ archive_dir: wsArchiveDir })}${lifecycle5bFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <lifecycle 5b fence>');
// See the flat-mode test above for why this is a literal `/` join, not path.join.
@@ -213,21 +213,21 @@ describe('autonomous.md workstream-scoped paths (#4455)', () => {
});
describe('complete-milestone.md workstream-scoped paths (#4455)', () => {
const WORKFLOW_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'complete-milestone.md');
const WORKFLOW_PATH = path.join(__dirname, '..', 'msd-core', 'workflows', 'complete-milestone.md');
const content = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const STEP_START = '<step name="reorganize_roadmap_and_delete_originals">';
const STEP_END = '<step name="write_retrospective">';
const backlogFence = extractFenceContaining(content, STEP_START, STEP_END, 'BACKLOG_SECTION');
const sentinelFence = extractFenceContaining(content, STEP_START, STEP_END, '.gsd-allow-shrink');
const commitFilesFence = extractFenceContaining(content, STEP_START, STEP_END, 'gsd_run query commit');
const sentinelFence = extractFenceContaining(content, STEP_START, STEP_END, '.msd-allow-shrink');
const commitFilesFence = extractFenceContaining(content, STEP_START, STEP_END, 'msd_run query commit');
const requirementsRmFence = extractFenceContaining(content, STEP_START, STEP_END, 'git rm');
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-complete-milestone-ws-'));
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-complete-milestone-ws-'));
});
afterEach(() => {
@@ -235,36 +235,36 @@ describe('complete-milestone.md workstream-scoped paths (#4455)', () => {
});
/**
* Write a canned init.complete-milestone-shaped JSON payload and a gsd_run
* Write a canned init.complete-milestone-shaped JSON payload and a msd_run
* stub that returns it for the `query init.complete-milestone` fetch, and
* echoes `gsd_run_call:<argv>` for every OTHER call (e.g. `query commit
* echoes `msd_run_call:<argv>` for every OTHER call (e.g. `query commit
* ...`) — the same recording-stub idiom tests/new-milestone-clear-phases.
* test.cjs uses, so the commit `--files` list can be asserted on directly.
*/
function stubGsdRun(jsonPayload) {
function stubMsdRun(jsonPayload) {
const jsonPath = path.join(tmpDir, 'init-cm.json');
fs.writeFileSync(jsonPath, JSON.stringify(jsonPayload));
return `gsd_run() { if [ "$1" = "query" ] && [ "$2" = "init.complete-milestone" ]; then cat "${jsonPath}"; else printf 'gsd_run_call:%s\\n' "$*"; fi; }\n`;
return `msd_run() { if [ "$1" = "query" ] && [ "$2" = "init.complete-milestone" ]; then cat "${jsonPath}"; else printf 'msd_run_call:%s\\n' "$*"; fi; }\n`;
}
describe('backlog extraction: reads ROADMAP.md from init.complete-milestone roadmap_path', () => {
test('flat mode: no GSD_WORKSTREAM — resolves the root ROADMAP.md path (regression guard)', () => {
test('flat mode: no MSD_WORKSTREAM — resolves the root ROADMAP.md path (regression guard)', () => {
const roadmapPath = path.join(tmpDir, 'ROADMAP-root.md');
fs.writeFileSync(roadmapPath, '# Roadmap\n\n## Backlog\n\n- 999.1 Root backlog item\n');
const script = `${stubGsdRun({ roadmap_path: roadmapPath })}${backlogFence}\nprintf 'BACKLOG=[%s]\\n' "$BACKLOG_SECTION"`;
const script = `${stubMsdRun({ roadmap_path: roadmapPath })}${backlogFence}\nprintf 'BACKLOG=[%s]\\n' "$BACKLOG_SECTION"`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <backlog fence>');
assert.ok(r.stdout.includes('Root backlog item'),
`expected root ROADMAP.md backlog content, got: ${r.stdout}`);
});
test('GSD_WORKSTREAM=alpha: reads the workstream-scoped ROADMAP.md, not root (#4455 regression)', () => {
test('MSD_WORKSTREAM=alpha: reads the workstream-scoped ROADMAP.md, not root (#4455 regression)', () => {
const rootRoadmapPath = path.join(tmpDir, 'ROADMAP-root.md');
fs.writeFileSync(rootRoadmapPath, '# Roadmap\n\n## Backlog\n\n- 999.1 Root backlog item — must not be read\n');
const wsRoadmapPath = path.join(tmpDir, 'ROADMAP-alpha.md');
fs.writeFileSync(wsRoadmapPath, '# Roadmap\n\n## Backlog\n\n- 999.1 Workstream Alpha backlog item\n');
const script = `${stubGsdRun({ roadmap_path: wsRoadmapPath })}${backlogFence}\nprintf 'BACKLOG=[%s]\\n' "$BACKLOG_SECTION"`;
const script = `${stubMsdRun({ roadmap_path: wsRoadmapPath })}${backlogFence}\nprintf 'BACKLOG=[%s]\\n' "$BACKLOG_SECTION"`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <backlog fence>');
assert.ok(r.stdout.includes('Workstream Alpha backlog item'),
@@ -278,20 +278,20 @@ describe('complete-milestone.md workstream-scoped paths (#4455)', () => {
test('flat mode: sentinel names the root ROADMAP.md path (regression guard)', () => {
const roadmapPath = path.join(tmpDir, 'ROADMAP-root.md');
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
const script = `${stubGsdRun({ roadmap_path: roadmapPath })}${sentinelFence}`;
const script = `${stubMsdRun({ roadmap_path: roadmapPath })}${sentinelFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <sentinel fence>');
const sentinelContent = fs.readFileSync(path.join(tmpDir, '.planning', '.gsd-allow-shrink'), 'utf8').trim();
const sentinelContent = fs.readFileSync(path.join(tmpDir, '.planning', '.msd-allow-shrink'), 'utf8').trim();
assert.strictEqual(sentinelContent, roadmapPath);
});
test('GSD_WORKSTREAM=alpha: sentinel names the workstream ROADMAP.md, not root (#4455 regression)', () => {
test('MSD_WORKSTREAM=alpha: sentinel names the workstream ROADMAP.md, not root (#4455 regression)', () => {
const wsRoadmapPath = path.join(tmpDir, 'ROADMAP-alpha.md');
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
const script = `${stubGsdRun({ roadmap_path: wsRoadmapPath })}${sentinelFence}`;
const script = `${stubMsdRun({ roadmap_path: wsRoadmapPath })}${sentinelFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <sentinel fence>');
const sentinelContent = fs.readFileSync(path.join(tmpDir, '.planning', '.gsd-allow-shrink'), 'utf8').trim();
const sentinelContent = fs.readFileSync(path.join(tmpDir, '.planning', '.msd-allow-shrink'), 'utf8').trim();
assert.strictEqual(sentinelContent, wsRoadmapPath);
assert.notStrictEqual(sentinelContent, path.join(tmpDir, '.planning', 'ROADMAP.md'));
});
@@ -299,7 +299,7 @@ describe('complete-milestone.md workstream-scoped paths (#4455)', () => {
describe('safety commit --files list: STATE/ROADMAP/archive/MILESTONES/PROJECT paths all scoped together', () => {
function runCommitFence(cmJson) {
const script = `${stubGsdRun(cmJson)}${commitFilesFence}`;
const script = `${stubMsdRun(cmJson)}${commitFilesFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <commit --files fence>');
return r.stdout;
@@ -316,7 +316,7 @@ describe('complete-milestone.md workstream-scoped paths (#4455)', () => {
milestones_path: milestonesPath, project_path: projectPath,
});
assert.ok(out.includes('gsd_run_call:query commit'), `expected the commit call to be recorded, got: ${out}`);
assert.ok(out.includes('msd_run_call:query commit'), `expected the commit call to be recorded, got: ${out}`);
assert.ok(out.includes(statePath), `expected root STATE.md in --files, got: ${out}`);
assert.ok(out.includes(roadmapPath), `expected root ROADMAP.md in --files, got: ${out}`);
assert.ok(out.includes(`${archiveDir}/v[X.Y]-ROADMAP.md`), `expected root archive ROADMAP in --files, got: ${out}`);
@@ -324,17 +324,17 @@ describe('complete-milestone.md workstream-scoped paths (#4455)', () => {
assert.ok(out.includes(projectPath), `expected root PROJECT.md in --files, got: ${out}`);
});
test('GSD_WORKSTREAM=alpha: --files lists workstream-scoped STATE/ROADMAP/archive/MILESTONES paths, but PROJECT.md STAYS root (#4455 regression)', () => {
test('MSD_WORKSTREAM=alpha: --files lists workstream-scoped STATE/ROADMAP/archive/MILESTONES paths, but PROJECT.md STAYS root (#4455 regression)', () => {
const wsStatePath = path.join(tmpDir, 'STATE-alpha.md');
const wsRoadmapPath = path.join(tmpDir, 'ROADMAP-alpha.md');
const wsArchiveDir = path.join(tmpDir, 'milestones-alpha');
const wsMilestonesPath = path.join(tmpDir, 'MILESTONES-alpha.md');
// PROJECT.md is genuinely SHARED across workstreams — never cloned per
// workstream (gsd-core/references/workstream-flag.md marks it
// workstream (msd-core/references/workstream-flag.md marks it
// `# Shared`; new-milestone.md states it outright; cmdWorkstreamCreate
// never creates one under a workstream dir). The real
// cmdInitCompleteMilestone therefore ALWAYS returns the root path for
// project_path regardless of GSD_WORKSTREAM — reflected here by
// project_path regardless of MSD_WORKSTREAM — reflected here by
// passing the SAME root-shaped value the flat-mode test above uses,
// not a workstream-shaped one. An earlier version of this fix wrongly
// pinned MILESTONES.md and PROJECT.md as identically workstream-scoped
@@ -368,17 +368,17 @@ describe('complete-milestone.md workstream-scoped paths (#4455)', () => {
const requirementsPath = path.join(tmpDir, 'REQUIREMENTS-root.md');
fs.writeFileSync(requirementsPath, '# Requirements\n');
fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true }); // git rm needs a repo; the stub below intercepts it
const script = `git() { printf 'git_call:%s\\n' "$*"; }\n${stubGsdRun({ requirements_path: requirementsPath })}${requirementsRmFence}`;
const script = `git() { printf 'git_call:%s\\n' "$*"; }\n${stubMsdRun({ requirements_path: requirementsPath })}${requirementsRmFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <requirements rm fence>');
assert.ok(r.stdout.includes(`git_call:rm ${requirementsPath}`),
`expected git rm to target the root REQUIREMENTS.md, got: ${r.stdout}`);
});
test('GSD_WORKSTREAM=alpha: removes the workstream-scoped REQUIREMENTS.md, not root (#4455 follow-up regression)', () => {
test('MSD_WORKSTREAM=alpha: removes the workstream-scoped REQUIREMENTS.md, not root (#4455 follow-up regression)', () => {
const wsRequirementsPath = path.join(tmpDir, 'REQUIREMENTS-alpha.md');
fs.writeFileSync(wsRequirementsPath, '# Requirements\n');
const script = `git() { printf 'git_call:%s\\n' "$*"; }\n${stubGsdRun({ requirements_path: wsRequirementsPath })}${requirementsRmFence}`;
const script = `git() { printf 'git_call:%s\\n' "$*"; }\n${stubMsdRun({ requirements_path: wsRequirementsPath })}${requirementsRmFence}`;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: tmpDir });
throwIfFailed(r, 'bash <requirements rm fence>');
assert.ok(r.stdout.includes(`git_call:rm ${wsRequirementsPath}`),