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

@@ -13,7 +13,7 @@
// either scope widens.
/**
* #4465 — /gsd:undo commit selection must be milestone-bounded and HEAD-reachable.
* #4465 — /msd:undo commit selection must be milestone-bounded and HEAD-reachable.
*
* The defect: `--phase` documented a primary path reading `.planning/.phase-manifest.json`,
* a file nothing in the repository writes, so the documented fallback was the only real
@@ -26,7 +26,7 @@
*
* Two halves. The first pins the SHAPE of undo.md's fences (substring assertions over
* the deployed prose — each half's own read site carries the marker). The second
* EXECUTES those fences against a real git fixture and the real `gsd-tools.cjs`, so
* EXECUTES those fences against a real git fixture and the real `msd-tools.cjs`, so
* a fence that matches the expected text but does not do the expected thing still fails.
*/
@@ -34,9 +34,9 @@ const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { scanFencedBlocks } = require('../gsd-core/bin/lib/markdown-sectionizer.cjs');
const { scanFencedBlocks } = require('../msd-core/bin/lib/markdown-sectionizer.cjs');
const UNDO_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'undo.md');
const UNDO_PATH = path.join(__dirname, '..', 'msd-core', 'workflows', 'undo.md');
/** Return the raw text of every ```bash fenced block in `content`. */
function extractBashBlocks(content) {
@@ -87,12 +87,12 @@ describe('#4465: undo commit selection is bounded', () => {
});
test('O: every PHASE_DIR-scoped git call runs from the project root find-phase answers against', () => {
// find-phase prints a path relative to the PROJECT ROOT (gsd-tools resolves it before
// find-phase prints a path relative to the PROJECT ROOT (msd-tools resolves it before
// dispatch), while the workflow's shell stays wherever the user invoked it. A pathspec is
// read relative to git's cwd, so from a subdirectory a bare `git log -- "${PHASE_DIR}"`
// matches nothing: the anchor comes back empty and a legitimate undo is refused, and the
// collision check comes back empty and never fires (review round 5, self-found).
const roots = (bash.match(/PROJECT_ROOT=\$\(gsd_run query planning inspect --pick generated_from\.cwd --raw/g) || []).length;
const roots = (bash.match(/PROJECT_ROOT=\$\(msd_run query planning inspect --pick generated_from\.cwd --raw/g) || []).length;
assert.equal(roots, 2, 'both modes must take PROJECT_ROOT from the same owner as find-phase (#4465)');
const code = bash.split('\n').filter((l) => !/^\s*#/.test(l));
// And from nowhere else: a rooted call is only as good as the root it is handed, so a later
@@ -117,7 +117,7 @@ describe('#4465: undo commit selection is bounded', () => {
const code = extractBashBlocks(content)
.map((b) => b.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n'));
// The COMMON git directory is the repository's identity: a linked worktree has its own per-worktree
// git dir but shares the object database, and gsd-tools maps its planning to the MAIN worktree, so a
// git dir but shares the object database, and msd-tools maps its planning to the MAIN worktree, so a
// per-worktree comparison refuses a legitimate undo there (review round 5, third pass).
const gates = code.filter((b) => /PHASE_DIR_FOREIGN=""/.test(b)
&& /_gd_here=\$\(_d=\$\(git rev-parse --git-common-dir 2>\/dev\/null\)/.test(b)
@@ -136,7 +136,7 @@ describe('#4465: undo commit selection is bounded', () => {
// find-phase resolves through planningDir, which roots an active workstream at
// .planning/workstreams/<ws>/ — a hardcoded .planning/phases/ would read the
// root's same-numbered phase instead.
const uses = (bash.match(/gsd_run query find-phase/g) || []).length;
const uses = (bash.match(/msd_run query find-phase/g) || []).length;
assert.equal(uses, 2, 'both modes must resolve PHASE_DIR via find-phase (#4465)');
});
@@ -221,7 +221,7 @@ describe('#4465: undo commit selection is bounded', () => {
test('H: dependency_check reads the workstream-resolved planning root', () => {
assert.ok(
/PLANNING_DIR=\$\(gsd_run query planning inspect --pick generated_from\.planning_root/.test(bash),
/PLANNING_DIR=\$\(msd_run query planning inspect --pick generated_from\.planning_root/.test(bash),
'dependency_check must resolve the planning root rather than hardcoding .planning/ (#4465)',
);
assert.ok(
@@ -311,7 +311,7 @@ describe('#4465: undo commit selection is bounded', () => {
// EXECUTES them: the exact ```bash fences the runtime runs are sliced out of
// undo.md by content anchor, glued behind the inputs the workflow would have
// set, and run with `bash -c` inside a real git fixture against the real
// `gsd-tools.cjs` — the same createTempGitProject + fence-execution shape
// `msd-tools.cjs` — the same createTempGitProject + fence-execution shape
// new-milestone-clear-phases.test.cjs (#2308) and
// code-review-pipeline-regression.test.cjs (#2352) use. A substring match cannot
// tell a live invocation from a dead one; a run can.
@@ -326,7 +326,7 @@ const { createTempGitProject, cleanup, readFileNormalized } = require('./helpers
const { createFixture, seedPhase, seedWorkstream } = require('./fixtures/index.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const GSD_TOOLS_BIN = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
const MSD_TOOLS_BIN = path.join(__dirname, '..', 'msd-core', 'bin', 'msd-tools.cjs');
const SKIP_WIN32 = process.platform === 'win32'
? 'POSIX bash fence execution over a git fixture (see #2352 precedent)'
: false;
@@ -357,7 +357,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// gather_commits, MODE=phase: resolve → anchor → select
const phaseResolve = fenceWhere(bodies, 'phase resolve',
(b) => b.includes('PHASE_DIR=$(gsd_run query find-phase "${TARGET_PHASE}"'));
(b) => b.includes('PHASE_DIR=$(msd_run query find-phase "${TARGET_PHASE}"'));
const phaseArchivedGuard = fenceWhere(bodies, 'phase archived guard',
(b) => b.includes('PHASE_DIR_ARCHIVED=""') && !b.includes('PLAN_PHASE'));
// Located loosely on purpose: shape test C pins the anchor's spelling, and a locator that
@@ -373,25 +373,25 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
(b) => b.includes('grep -E "\\(${TARGET_PLAN}\\):"'));
// dependency_check: planning-root resolution
const planningRoot = fenceWhere(bodies, 'planning root',
(b) => b.includes('PLANNING_DIR=$(gsd_run query planning inspect'));
(b) => b.includes('PLANNING_DIR=$(msd_run query planning inspect'));
// The composed script replays the fences in order in ONE shell, seeded with
// what the workflow sets (TARGET_*), plus the real gsd_run over the real
// what the workflow sets (TARGET_*), plus the real msd_run over the real
// binary. That is deliberately the workflow's own data flow — `PHASE_DIR`,
// `PHASE_START`, `UNDO_RANGE` are carried from fence to fence by the runtime
// that executes undo.md — and it is what these tests prove: the selection
// logic, not the runtime's variable transport between blocks.
const GSD_RUN = 'gsd_run() { node "$GSD_TOOLS_BIN" "$@"; }';
const MSD_RUN = 'msd_run() { node "$MSD_TOOLS_BIN" "$@"; }';
// `runIn` is the directory the shell starts in, when it is not the fixture root: the user
// can invoke /gsd:undo from anywhere inside the project.
// can invoke /msd:undo from anywhere inside the project.
function runFences(cwd, seed, fences, tail = '', extraEnv = {}, runIn = cwd) {
const script = [seed, GSD_RUN, ...fences, tail].join('\n');
const env = { ...process.env, GSD_TOOLS_BIN, HOME: cwd };
const script = [seed, MSD_RUN, ...fences, tail].join('\n');
const env = { ...process.env, MSD_TOOLS_BIN, HOME: cwd };
// A developer's active workstream must not leak into the fixture; a test that wants
// one names it through `extraEnv`, applied after the scrub.
delete env.GSD_WORKSTREAM;
delete env.GSD_PROJECT;
delete env.MSD_WORKSTREAM;
delete env.MSD_PROJECT;
Object.assign(env, extraEnv);
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd: runIn, env, timeoutMs: PROBE_TIMEOUT_MS });
throwIfFailed(r, 'bash <undo.md fences>');
@@ -413,7 +413,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// The reported repro (#4465): milestone 1 ships phase 03 and is archived;
// milestone 2 reuses the number. A dead branch carries a matching scope too.
function multiMilestoneFixture() {
const cwd = createTempGitProject('gsd-4465-mm-');
const cwd = createTempGitProject('msd-4465-mm-');
const mainBranch = gitOrThrow(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }).trim();
seedPhase(cwd, '03-auth', { '03-01-PLAN.md': '# auth\n' });
commitFile(cwd, 'src/auth.js', 'auth\n', 'feat(03-01): implement auth endpoint');
@@ -474,7 +474,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// writes; find-phase never searches it. The tests below pin what find-phase actually
// returns for BOTH layouts, so a change to either reader shows up here.
function twoArchivedMilestonesFixture() {
const cwd = createTempGitProject('gsd-4465-arch-');
const cwd = createTempGitProject('msd-4465-arch-');
seedPhase(cwd, '03-auth', { '03-01-PLAN.md': '# auth\n' });
gitOrThrow(['add', '-A'], { cwd });
gitOrThrow(['commit', '-q', '-m', 'docs(03-01): v1.0 phase plan'], { cwd });
@@ -557,7 +557,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// live directory -- so the collision check asks history whether this exact path was ever
// vacated (round 4: it used to look for an archived twin by layout, and missed ws-*).
function reusedSlugFixture() {
const cwd = createTempGitProject('gsd-4465-slug-');
const cwd = createTempGitProject('msd-4465-slug-');
seedPhase(cwd, '03-auth', { '03-01-PLAN.md': '# v1\n' });
gitOrThrow(['add', '-A'], { cwd });
gitOrThrow(['commit', '-q', '-m', 'docs(03-01): v1 plan'], { cwd });
@@ -629,7 +629,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// passes. What it catches is the opposite failure -- a check that refuses on a NAME it
// finds under milestones/ rather than on this path's history, which is the layout-keyed
// shape round 4 replaced. Round 2 pinned it for a stray FILE against the old glob.
const cwd = createTempGitProject('gsd-4465-file-twin-');
const cwd = createTempGitProject('msd-4465-file-twin-');
t.after(() => cleanup(cwd));
seedPhase(cwd, '06-live', { '06-01-PLAN.md': '# live\n' });
fs.mkdirSync(path.join(cwd, '.planning', 'milestones', 'v1.0-phases'), { recursive: true });
@@ -652,7 +652,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// A CONTROL, like the one above: it passes with the collision check deleted, and fails only
// if the check starts refusing on a directory name. `vnondigit-phases` is not a milestone
// either reader of the archive tree admits, and this live path was never vacated.
const cwd = createTempGitProject('gsd-4465-malformed-');
const cwd = createTempGitProject('msd-4465-malformed-');
t.after(() => cleanup(cwd));
seedPhase(cwd, '05-live', { '05-01-PLAN.md': '# live\n' });
fs.mkdirSync(path.join(cwd, '.planning', 'milestones', 'vnondigit-phases', '05-live'), { recursive: true });
@@ -690,7 +690,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// .planning/workstreams/milestones/phases/NN-x as archived and refuses a phase that
// is live and revertible — a fail-closed bug, but a bug. Only the archive LAYOUTS
// (`v<X.Y>-phases/<phase>` and `ws-<name>-<date>/phases/<phase>`) separate the two.
const cwd = createTempGitProject('gsd-4465-wsname-');
const cwd = createTempGitProject('msd-4465-wsname-');
t.after(() => cleanup(cwd));
const dir = path.join(cwd, '.planning', 'workstreams', 'milestones', 'phases', '03-live');
fs.mkdirSync(dir, { recursive: true });
@@ -700,11 +700,11 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
commitFile(cwd, 'src/live.js', 'l\n', 'feat(03-01): workstream work');
// Activate by env, the same route find-phase honours for an active workstream.
const script = [
'TARGET_PHASE=03', GSD_RUN, phaseResolve, phaseArchivedGuard, phaseAnchor, phaseSelect,
'TARGET_PHASE=03', MSD_RUN, phaseResolve, phaseArchivedGuard, phaseAnchor, phaseSelect,
'echo "ARCHIVED=[${PHASE_DIR_ARCHIVED}]"',
].join('\n');
const env = { ...process.env, GSD_TOOLS_BIN, HOME: cwd, GSD_WORKSTREAM: 'milestones' };
delete env.GSD_PROJECT;
const env = { ...process.env, MSD_TOOLS_BIN, HOME: cwd, MSD_WORKSTREAM: 'milestones' };
delete env.MSD_PROJECT;
const r = runHookSeam('-c', [script], { interpreter: 'bash', cwd, env, timeoutMs: PROBE_TIMEOUT_MS });
throwIfFailed(r, 'bash <undo.md fences>');
assert.ok(r.stdout.includes('ARCHIVED=[]'),
@@ -720,7 +720,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// archived phase's former path is `.planning/workstreams/<name>/phases/<phase>`. Re-creating
// the workstream under the same name with the same phase slug re-creates that exact path.
function workstreamArchiveFixture({ recreate }) {
const cwd = createTempGitProject('gsd-4465-ws-');
const cwd = createTempGitProject('msd-4465-ws-');
commitFile(cwd, '.planning/workstreams/feat/phases/03-auth/03-01-PLAN.md', '# gen 1\n',
'docs(03-01): feat generation-1 plan');
commitFile(cwd, 'src/gen1.js', 'g1\n', 'feat(03-01): feat generation-1 work');
@@ -737,7 +737,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
}
return cwd;
}
const WS_FEAT = { GSD_WORKSTREAM: 'feat' };
const WS_FEAT = { MSD_WORKSTREAM: 'feat' };
test('negative control: WITHOUT the collision guard, a re-created workstream selects its archived generation too', (t) => {
const cwd = workstreamArchiveFixture({ recreate: true });
@@ -802,7 +802,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// would run forward from it -- the exact contamination the refusal exists for.
const cwd = workstreamArchiveFixture({ recreate: false });
t.after(() => cleanup(cwd));
const stub = 'gsd_run() { echo ".planning/milestones/ws-feat-2026-09-01/phases/03-auth"; }';
const stub = 'msd_run() { echo ".planning/milestones/ws-feat-2026-09-01/phases/03-auth"; }';
const report = 'printf "ARCHIVED=[%s]\\nPHASE_DIR=[%s]\\nUNDO_RANGE=[%s]\\n" "$PHASE_DIR_ARCHIVED" "$PHASE_DIR" "$UNDO_RANGE"';
for (const [seed, fences] of [
['TARGET_PHASE=03', [stub, phaseResolve, phaseArchivedGuard, phaseAnchor, phaseSelect]],
@@ -820,7 +820,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
test('self-found: a phase REMOVED and re-added under the same slug is refused too', (t) => {
// No archive anywhere -- the path was simply deleted and re-created. A layout glob has no
// entry to find; the history check sees the vacancy. Same anchor defect, same refusal.
const cwd = createTempGitProject('gsd-4465-readd-');
const cwd = createTempGitProject('msd-4465-readd-');
t.after(() => cleanup(cwd));
commitFile(cwd, '.planning/phases/03-auth/03-01-PLAN.md', '# first\n', 'docs(03-01): first plan');
commitFile(cwd, 'src/first.js', 'f\n', 'feat(03-01): first attempt');
@@ -835,7 +835,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
});
function droppedPlanFixture() {
const cwd = createTempGitProject('gsd-4465-dropplan-');
const cwd = createTempGitProject('msd-4465-dropplan-');
commitFile(cwd, '.planning/phases/03-auth/03-01-PLAN.md', '# one\n', 'docs(03-01): plan one');
commitFile(cwd, '.planning/phases/03-auth/03-02-PLAN.md', '# two\n', 'docs(03-02): plan two');
gitOrThrow(['rm', '-q', '.planning/phases/03-auth/03-02-PLAN.md'], { cwd });
@@ -863,7 +863,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
'the whole phase must still be selectable');
});
// Review round 5, self-found: the user can run /gsd:undo from anywhere inside the project.
// Review round 5, self-found: the user can run /msd:undo from anywhere inside the project.
// find-phase answers relative to the PROJECT ROOT; a pathspec is read relative to git's cwd.
// Every test above runs from the fixture root, where the two coincide and the mismatch is
// invisible. These run the same fences from `sub/dir`.
@@ -936,7 +936,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// the code in child repositories. From a child, findProjectRoot returns the parent, so the project
// root and the repository the commits live in are different repositories.
function subReposFixture(t) {
const parent = createFixture({ prefix: 'gsd-4465-subrepos-', planning: false, git: true, projectDoc: false });
const parent = createFixture({ prefix: 'msd-4465-subrepos-', planning: false, git: true, projectDoc: false });
t.after(() => cleanup(parent));
fs.writeFileSync(path.join(parent, '.gitignore'), 'child/\n');
fs.mkdirSync(path.join(parent, '.planning'), { recursive: true });
@@ -950,7 +950,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
commitFile(child, 'a.js', 'a\n', 'feat(03-01): old child work');
commitFile(child, 'b.js', 'b\n', 'feat(03-01): current child work');
// HOME off the parent, so the sub_repos branch of findProjectRoot is the one that resolves.
const home = createFixture({ prefix: 'gsd-4465-home-', planning: false, git: false });
const home = createFixture({ prefix: 'msd-4465-home-', planning: false, git: false });
t.after(() => cleanup(home));
return { parent, child, env: { HOME: home } };
}
@@ -997,14 +997,14 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
assert.deepEqual(subjects(out.replace(/(PHASE_START|UNDO_RANGE)=.*\n?/g, '')), []);
});
// Review round 5, third pass: a LINKED worktree. gsd-tools maps a linked worktree with no
// Review round 5, third pass: a LINKED worktree. msd-tools maps a linked worktree with no
// .planning/ of its own to the MAIN worktree (resolveMainWorktreeCwd), so PROJECT_ROOT is the main
// checkout while the shell sits in the linked one: two worktrees, one repository. The anchor then
// comes from the main branch's history, which the linked HEAD may or may not contain.
function linkedWorktreeFixture(t, { branchAfterPhase }) {
const main = createTempGitProject('gsd-4465-wt-main-');
const main = createTempGitProject('msd-4465-wt-main-');
t.after(() => cleanup(main));
const linked = createFixture({ prefix: 'gsd-4465-wt-linked-', planning: false, git: false });
const linked = createFixture({ prefix: 'msd-4465-wt-linked-', planning: false, git: false });
t.after(() => cleanup(linked));
const phase = () => {
seedPhase(main, '03-auth', { '03-01-PLAN.md': '# plan\n' });
@@ -1015,7 +1015,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
if (branchAfterPhase) phase();
gitOrThrow(['worktree', 'add', '-q', '-b', 'feature', linked], { cwd: main });
if (!branchAfterPhase) phase();
// No .planning/ in the linked checkout: that is what sends gsd-tools to the main worktree.
// No .planning/ in the linked checkout: that is what sends msd-tools to the main worktree.
gitOrThrow(['rm', '-rq', '.planning'], { cwd: linked });
gitOrThrow(['commit', '-q', '-m', 'chore: executor worktree without planning'], { cwd: linked });
commitFile(linked, 'src/b.js', 'b\n', 'feat(03-01): linked work');
@@ -1065,7 +1065,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
// directory holding .planning/, so `git rev-parse --show-toplevel` would be the wrong -C
// here; generated_from.cwd is the directory find-phase itself resolved against. Run from the
// project root, so this pins the choice of root, not the subdirectory case above.
const repo = createFixture({ prefix: 'gsd-4465-nested-', planning: false, git: true, projectDoc: false });
const repo = createFixture({ prefix: 'msd-4465-nested-', planning: false, git: true, projectDoc: false });
t.after(() => cleanup(repo));
const project = path.join(repo, 'app');
commitFile(repo, 'app/.planning/phases/03-auth/03-01-PLAN.md', '# nested\n', 'docs(03-01): nested plan');
@@ -1081,7 +1081,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
});
test('single-milestone selection is unchanged: every phase commit, none from a later phase', (t) => {
const cwd = createTempGitProject('gsd-4465-single-');
const cwd = createTempGitProject('msd-4465-single-');
t.after(() => cleanup(cwd));
seedPhase(cwd, '03-auth', { '03-01-PLAN.md': '# auth\n' });
commitFile(cwd, 'src/a.js', 'a\n', 'feat(03-01): implement auth endpoint');
@@ -1100,7 +1100,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
});
test('limit-1: a matching commit one before PHASE_START is excluded; PHASE_START itself is included', (t) => {
const cwd = createTempGitProject('gsd-4465-limit-');
const cwd = createTempGitProject('msd-4465-limit-');
t.after(() => cleanup(cwd));
// Matching scope, committed BEFORE the phase directory exists: outside the window.
commitFile(cwd, 'src/pre.js', 'pre\n', 'feat(03-01): stray pre-phase commit');
@@ -1118,7 +1118,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
test('root commit: a phase whose first commit is the repository root is fully selected', (t) => {
// createTempGitProject seeds an initial commit, so build the root by hand.
const cwd = createFixture({ prefix: 'gsd-4465-root-', planning: true, git: false });
const cwd = createFixture({ prefix: 'msd-4465-root-', planning: true, git: false });
t.after(() => cleanup(cwd));
const g = (args) => gitOrThrow(args, { cwd });
g(['init', '-q']);
@@ -1157,7 +1157,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
});
test('workstream: --phase resolves the ACTIVE workstream\'s phase directory, not the root\'s', (t) => {
const cwd = createTempGitProject('gsd-4465-ws-');
const cwd = createTempGitProject('msd-4465-ws-');
t.after(() => cleanup(cwd));
// Root scope: phase 03 exists and has a matching commit.
seedPhase(cwd, '03-root', { '03-01-PLAN.md': '# root\n' });
@@ -1180,7 +1180,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
});
test('dependency_check: PLANNING_DIR resolves the active workstream root, and falls back to .planning without one', (t) => {
const cwd = createTempGitProject('gsd-4465-pd-');
const cwd = createTempGitProject('msd-4465-pd-');
t.after(() => cleanup(cwd));
const tail = 'echo "PLANNING_DIR=${PLANNING_DIR}"';
const flat = runFences(cwd, '', [planningRoot], tail);
@@ -1191,7 +1191,7 @@ describe('#4465: undo commit selection — executed against a git fixture', { sk
`expected the active workstream's root; got:\n${ws}`);
// And the fallback is real, not the only path: with no .planning at all the
// pick yields '' (planning_root is null) and the fence lands on the literal.
const bare = createFixture({ prefix: 'gsd-4465-noplan-', planning: false, git: true, projectDoc: false });
const bare = createFixture({ prefix: 'msd-4465-noplan-', planning: false, git: true, projectDoc: false });
t.after(() => cleanup(bare));
const none = runFences(bare, '', [planningRoot], tail);
assert.ok(none.includes('PLANNING_DIR=.planning'), `expected the literal fallback; got:\n${none}`);