* test(#4254): sequential executor root pin — failing-first regression + matrix The new suite executes the shipped supplied-root-pin guard against real git fixtures (drifted primary-checkout cwd halts before the write and the FATAL names both roots; matching cwd permits it; unexpanded/empty pins halt; normalization forms; submodule and sibling boundaries; metacharacter quoting; drive-letter form gate) and locks the dispatch contract across execute-phase.md, its sequential-root-pin step fragment, and worktree-path-safety.md. The #2772 per-plan serialization assertion retargets to the fragment that now carries those rules (ADR-857 Phase 6 ceiling), plus the host-step wiring. * fix(#4254): pin sequential executor to the orchestrator's validated root Sequential-mode dispatch told the executor to self-derive PROJECT_ROOT from its own cwd; every existing guard is worktree-mode-only or self-referential, so an executor spawned with a drifted cwd committed onto the wrong checkout silently. - worktree-path-safety.md step 0p: mode-agnostic supplied-root pin guard, composed by the orchestrator at build time with the literal $ORCHESTRATOR_WT (git-vs-git comparison on both sides — representation-safe on Windows, the #4296 lesson), fail-closed on empty/unexpanded pins, registered-submodule allowance, warn-and-proceed only when the dispatch carries no pin block. - execute-phase.md sequential branch: build-time embed of the bound <project_root_pin> via the new execute-phase/steps/sequential-root-pin.md fragment (ADR-857 Phase 6 frozen ceiling — the host step cannot grow; the wave serialization rules move with the fragment, verbatim in substance) plus the per-write/commit pin instruction in <sequential_execution>. Worktree-mode dispatch untouched (its self-derived toplevel IS correct there). - INVENTORY rows (5 locales) + INVENTORY-MANIFEST + install-tree goldens regenerated for the new fragment; changeset added. * chore(#4254): backfill changeset PR number * fix(#4254): accept backslash-separated Windows drive pins CI on windows-latest showed every permit-path test failing with "Actual root: <none>": pins composed from Node's path.join arrive in the backslash drive form (C:\Users\RUNNER~1\...), which the guard's absolute-form gate rejected before the cwd-side root was ever computed — a legitimate matching pin could never pass. The gate now accepts either separator ([A-Za-z]:[\\/]); git -C resolves both forms (and 8.3 short names) to the same canonical toplevel, so the git-vs-git comparison is unaffected. Form-gate tests cover the emitted (C:/…) and produced (C:\…) spellings plus short names. * fix(#4254): portable drive-form gate for MSYS bash The bracket class [\\/] that accepted backslash drive pins parses inconsistently on MSYS bash (the Windows CI leg still rejected C:\ pins — every permit-path test red with "Actual root: <none>"). Replace it with standard pattern escaping outside brackets: [A-Za-z]:/*|[A-Za-z]:\\* — the escape form is version- and build-portable. Verified across all forms: both drive spellings accepted; bare "C:", relative, empty, and unexpanded rejected. * fix(#4254): runtime-generated backslash comparator + self-describing FATAL The Windows CI legs failed every #4254 permit-path row with 'Actual root: <none>' across two prior pattern spellings ([\\/] and \\*). Stage misattribution: <none> appears whenever the FATAL fires BEFORE the cwd-side capture assigns ACTUAL_ROOT — the absolute-form gate was what fired. Mechanism: the test harness spawns bash -c <script> through the Windows command-line boundary; that round-trip applies one extra shell-quoting pass with double-quote semantics — a backslash written twice in the script text arrives halved, while a lone backslash survives (the pin displays intact; row 9's pure-bash gate independently showed the halved pattern rejecting C:\ while C:/ still passed its surviving arm). On windows-latest every pin carries backslashes (os.tmpdir() is the 8.3 short form C:\Users\RUNNER~1\...), so the gate ate every pin before the actual root was ever computed. Fix, robust by construction: - the drive-form gate generates its backslash comparator at RUNTIME (BS=$(printf '\134'); match [A-Za-z]:"$BS"*) — the shipped guard now contains no doubled backslash anywhere, enforced by a regression assertion on the extracted guard text; - the FATAL self-describes: Guard stage (pin-unbound / form-gate / actual-capture / pinned-capture / root-mismatch) plus a Diagnostic line carrying git's own stderr for capture failures and both compared values for mismatches — future platform failures name their stage in the log; - row 9's hand-rolled duplicate case gate (transit-fragile copy, #4296 Minor 1 duplication smell) is replaced by driving the SHIPPED guard and asserting the stage; rows 2/4 pin the new stage machinery. Validated on darwin across drift/match/relative/unbound/empty/bare-drive/ forward-and-backslash drive forms, each also re-run under a simulated Windows transit (every doubled backslash halved) with identical outcomes. * fix(#4254): close the empty-comparator fail-open seam in the drive-form gate Self-review of the runtime-generated backslash comparator: if printf's octal escape ever returned empty, the drive arm [A-Za-z]:"$BS"* would widen to drive-RELATIVE pins (C:foo) — the construction's one theoretical fail-open path. Fail closed with a self-describing diagnostic instead of trusting the shell's printf. --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -238,3 +238,309 @@ describe('bug #3099: absolute-path safety guidance in gsd-executor.md', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// #4254 — sequential (non-isolated) executor dispatch had no hard pin to the
|
||||
// orchestrator's own worktree root: the dispatched prompt told the executor to
|
||||
// self-derive PROJECT_ROOT via `git rev-parse --show-toplevel`, and every
|
||||
// existing guard (steps 0a/0b worktree-only, step 0 branch-scoped) is
|
||||
// self-referential — so an executor spawned with a drifted cwd committed onto
|
||||
// the wrong checkout silently. The fix ships a mode-agnostic "supplied-root
|
||||
// pin" guard (step 0p) in worktree-path-safety.md, bound at dispatch time by
|
||||
// execute-phase.md's SEQUENTIAL branch only (worktree mode keeps its own,
|
||||
// intentionally different, self-derived root). These tests EXECUTE the shipped
|
||||
// guard against real git fixtures — no string-only vacuity (#4296 review
|
||||
// precedent, Blocker 2).
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
describe('bug #4254: sequential executor supplied-root pin', () => {
|
||||
// allow-test-rule: source-text-is-the-product (see #3097) — the reference
|
||||
// and the workflow markdown ARE the product under test. ROOT and
|
||||
// executePhaseSrc are re-declared here: the folded #3097/#3099 block above
|
||||
// declares its own copies inside a closure, out of this describe's scope.
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const executePhaseSrc = fs.readFileSync(
|
||||
path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md'), 'utf8',
|
||||
);
|
||||
const safetyRefPath = path.join(ROOT, 'gsd-core', 'references', 'worktree-path-safety.md');
|
||||
const pinStepPath = path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase', 'steps', 'sequential-root-pin.md');
|
||||
const safetySrc = fs.readFileSync(safetyRefPath, 'utf8');
|
||||
const pinStepSrc = fs.readFileSync(pinStepPath, 'utf8');
|
||||
const { createTempGitProject, cleanup } = require('./helpers.cjs');
|
||||
const { runHook } = require('./helpers/process-seam.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
const { HOOK_FANOUT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const PIN_MARKER = '# gsd:guard=supplied-root-pin';
|
||||
|
||||
// Extract the shipped guard — the exact ```bash block that carries the
|
||||
// marker — so the tests can never pass against a stale or hand-copied body.
|
||||
function extractPinGuard() {
|
||||
const body = safetySrc.split('```bash\n').find((b) => b.startsWith(PIN_MARKER));
|
||||
assert.ok(body, 'worktree-path-safety.md must ship the #4254 supplied-root-pin guard');
|
||||
return body.split('```')[0].trim();
|
||||
}
|
||||
|
||||
// The composition contract the orchestrator follows at dispatch: a
|
||||
// shell-single-quoted literal with `'\''` escaping for embedded quotes.
|
||||
const shellQuote = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
|
||||
|
||||
function composeGuard(pinQuotedLiteral) {
|
||||
return extractPinGuard().replace("PINNED_ROOT='{PINNED_ROOT}'", `PINNED_ROOT=${pinQuotedLiteral}`);
|
||||
}
|
||||
|
||||
// Run the composed guard in `cwd`; `probe` (optional bash line) runs only if
|
||||
// the guard passes — proving the write barrier, not just the exit code.
|
||||
function runPinGuard(cwd, pin, probe) {
|
||||
return runHook('-c', [composeGuard(shellQuote(pin)) + (probe ? `\n${probe}` : '')], {
|
||||
interpreter: 'bash',
|
||||
cwd,
|
||||
timeoutMs: HOOK_FANOUT_TIMEOUT_MS,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
});
|
||||
}
|
||||
|
||||
// Fixture: a primary checkout plus a linked worktree (the orchestrator's
|
||||
// lane). Sibling path — a worktree inside the primary's tree would show up
|
||||
// as an untracked directory and muddy the fixtures.
|
||||
function makeOrchestratorLane(prefix) {
|
||||
const primary = createTempGitProject(prefix);
|
||||
const lane = `${primary}-orchestrator-wt`;
|
||||
gitOrThrow(['worktree', 'add', '-q', '-b', 'phase/2-1', lane], { cwd: primary });
|
||||
return { primary, lane };
|
||||
}
|
||||
|
||||
test('#4254: reference ships the supplied-root pin section with composition + runtime contracts', () => {
|
||||
assert.match(safetySrc, /## Supplied-root pin — step 0p \(#4254, EVERY mode\)/);
|
||||
// Runtime contract: run before first Edit/Write and every commit; HALT on
|
||||
// FATAL; warn-and-proceed ONLY when the prompt carries no pin block.
|
||||
assert.match(safetySrc, /before your first Edit\/Write and again\s+before every commit/);
|
||||
assert.match(safetySrc, /NO `<project_root_pin>` block[\s\S]*?warning line and continue/);
|
||||
// The executor must never bind the placeholder itself — that is exactly
|
||||
// the #4254 failure mode re-armored as a "fix".
|
||||
assert.match(safetySrc, /Never bind\s+`\{PINNED_ROOT\}` yourself/);
|
||||
// Composition contract: build-time literal substitution, single-quoted,
|
||||
// git-vs-git comparison rationale (the #4296 Windows lesson).
|
||||
assert.match(safetySrc, /substituting[\s\S]*`\{PINNED_ROOT\}`[\s\S]*shell-single-quoted/);
|
||||
assert.match(safetySrc, /git-vs-git on BOTH sides/);
|
||||
});
|
||||
|
||||
test('#4254: RED regression — drifted-cwd executor halts before the wrong-checkout write', () => {
|
||||
const { primary, lane } = makeOrchestratorLane('gsd-4254-drift-');
|
||||
try {
|
||||
// The orchestrator pinned its own lane; the executor's process cwd
|
||||
// resolved to the PRIMARY checkout (the issue's exact shape).
|
||||
const marker = path.join(primary, 'write-marker');
|
||||
const res = runPinGuard(primary, lane, `printf 'W' > ${shellQuote(marker)}`);
|
||||
assert.equal(res.exitCode, 1, `mismatched root must halt, got:\n${res.stdout}\n${res.stderr}`);
|
||||
assert.equal(fs.existsSync(marker), false, 'no write may run after a root mismatch');
|
||||
assert.match(res.stderr, /FATAL[^\n]*#4254/);
|
||||
// Loud detection: the FATAL names BOTH roots — the pinned lane and the
|
||||
// actual (wrong) primary checkout. The primary's basename is unique to
|
||||
// it (the lane appends '-orchestrator-wt'), so matching it inside the
|
||||
// Actual-root line proves the right checkout is being named.
|
||||
assert.match(res.stderr, /Pinned root:[^\n]*orchestrator-wt/, 'FATAL must name the pinned root');
|
||||
const { escapeRegex } = require('../gsd-core/bin/lib/pattern.cjs');
|
||||
const primaryName = escapeRegex(path.basename(primary));
|
||||
assert.match(res.stderr, new RegExp(`Actual root:[^\\n]*${primaryName}`), 'FATAL must name the actual (wrong) root');
|
||||
assert.doesNotMatch(res.stderr, new RegExp(`Actual root:[^\\n]*orchestrator-wt`), 'the actual root must NOT be the pinned lane');
|
||||
// The FATAL self-describes its stage (and, on capture failures, git's own
|
||||
// stderr) — the discriminator whose absence let this class of failure read
|
||||
// as "capture returns empty" when the form gate was what fired on Windows.
|
||||
assert.match(res.stderr, /Guard stage: root-mismatch/, 'drifted cwd is a root mismatch, not a capture or form failure');
|
||||
assert.match(res.stderr, /Diagnostic: actual=.*pinned=.*superproject=<none>/, 'the mismatch diagnostic names both roots and the absent superproject');
|
||||
} finally {
|
||||
cleanup(primary);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4254: correct-cwd control — matching root permits the write', () => {
|
||||
const { primary, lane } = makeOrchestratorLane('gsd-4254-match-');
|
||||
try {
|
||||
const marker = path.join(lane, 'write-marker');
|
||||
const res = runPinGuard(lane, lane, `printf 'W' > ${shellQuote(marker)}`);
|
||||
assert.equal(res.exitCode, 0, `matching root must permit the write, got:\n${res.stderr}`);
|
||||
assert.equal(fs.readFileSync(marker, 'utf8'), 'W');
|
||||
} finally {
|
||||
cleanup(primary);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4254: unexpanded or empty pin fails closed (never warn-and-proceed inside a pin block)', () => {
|
||||
const { primary, lane } = makeOrchestratorLane('gsd-4254-unbound-');
|
||||
try {
|
||||
const marker = path.join(lane, 'write-marker');
|
||||
// Unexpanded: the orchestrator never performed the build-time substitution.
|
||||
const unexpanded = runHook('-c', [extractPinGuard() + `\nprintf 'W' > ${shellQuote(marker)}`], {
|
||||
interpreter: 'bash', cwd: lane, timeoutMs: HOOK_FANOUT_TIMEOUT_MS,
|
||||
});
|
||||
assert.equal(unexpanded.exitCode, 1, 'an unexpanded {PINNED_ROOT} must halt');
|
||||
assert.match(unexpanded.stderr, /Guard stage: pin-unbound/, 'an unexpanded pin halts at the pin-unbound stage');
|
||||
// Empty: substituted to nothing — indistinguishable from a forgotten transplant.
|
||||
const empty = runHook('-c', [composeGuard("''") + `\nprintf 'W' > ${shellQuote(marker)}`], {
|
||||
interpreter: 'bash', cwd: lane, timeoutMs: HOOK_FANOUT_TIMEOUT_MS,
|
||||
});
|
||||
assert.equal(empty.exitCode, 1, 'an empty pin must halt');
|
||||
assert.match(empty.stderr, /Guard stage: pin-unbound/, 'an empty pin halts at the pin-unbound stage');
|
||||
assert.equal(fs.existsSync(marker), false);
|
||||
} finally {
|
||||
cleanup(primary);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4254: normalization — trailing slash, symlink alias, subdirectory cwd, and unresolved temp-root spelling all match', () => {
|
||||
const { primary, lane } = makeOrchestratorLane('gsd-4254-norm-');
|
||||
try {
|
||||
const alias = `${primary}-alias`;
|
||||
fs.symlinkSync(lane, alias, 'junction');
|
||||
const subdir = path.join(lane, 'nested');
|
||||
fs.mkdirSync(subdir);
|
||||
// The unresolved spelling of the temp root (macOS: /var/... vs git's
|
||||
// resolved /private/var/...): both sides are git-emitted, so the
|
||||
// comparison is representation-safe by construction. On platforms
|
||||
// without the /var symlink this degenerates to the realpath form and
|
||||
// still asserts the matching property.
|
||||
const unresolvedLane = fs.realpathSync(lane).replace('/private/var/', '/var/');
|
||||
for (const pin of [lane, `${lane}/`, alias, unresolvedLane]) {
|
||||
const marker = path.join(lane, 'write-marker');
|
||||
const res = runPinGuard(subdir, pin, `printf 'W' > ${shellQuote(marker)}`);
|
||||
assert.equal(res.exitCode, 0, `pin form ${pin} must normalize to the same checkout:\n${res.stderr}`);
|
||||
fs.unlinkSync(marker);
|
||||
}
|
||||
} finally {
|
||||
cleanup(primary);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4254: boundary — registered submodule of the pinned checkout commits; unregistered and sibling checkouts halt', () => {
|
||||
const primary = createTempGitProject('gsd-4254-super-');
|
||||
const subSource = createTempGitProject('gsd-4254-subsource-');
|
||||
try {
|
||||
gitOrThrow(['-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', subSource, 'module'], { cwd: primary });
|
||||
gitOrThrow(['commit', '-qm', 'chore: register submodule'], { cwd: primary });
|
||||
const moduleRoot = path.join(primary, 'module');
|
||||
// Registered immediate submodule: legitimate sub_repos work, permitted.
|
||||
const inModule = path.join(moduleRoot, 'write-marker');
|
||||
assert.equal(runPinGuard(moduleRoot, primary, `printf 'W' > ${shellQuote(inModule)}`).exitCode, 0);
|
||||
fs.unlinkSync(inModule);
|
||||
// Unregistered nested clone inside the pinned checkout: halts.
|
||||
const rogue = path.join(primary, 'rogue-clone');
|
||||
gitOrThrow(['clone', '-q', subSource, rogue], { cwd: primary });
|
||||
const rogueMarker = path.join(rogue, 'write-marker');
|
||||
assert.equal(runPinGuard(rogue, primary, `printf 'W' > ${shellQuote(rogueMarker)}`).exitCode, 1);
|
||||
assert.equal(fs.existsSync(rogueMarker), false);
|
||||
// Sibling linked worktree of the SAME repo (right repo, wrong checkout —
|
||||
// the incident's exact shape): halts.
|
||||
const sibling = `${primary}-sibling-wt`;
|
||||
gitOrThrow(['worktree', 'add', '-q', '-b', 'phase/9-9', sibling], { cwd: primary });
|
||||
const siblingMarker = path.join(sibling, 'write-marker');
|
||||
assert.equal(runPinGuard(sibling, primary, `printf 'W' > ${shellQuote(siblingMarker)}`).exitCode, 1);
|
||||
assert.equal(fs.existsSync(siblingMarker), false);
|
||||
} finally {
|
||||
cleanup(primary);
|
||||
cleanup(subSource);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4254: pin outside any git repo, and cwd outside any git repo, both fail closed', () => {
|
||||
const { primary, lane } = makeOrchestratorLane('gsd-4254-norepo-');
|
||||
const outside = fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'gsd-4254-outside-'));
|
||||
try {
|
||||
assert.equal(runPinGuard(lane, outside).exitCode, 1, 'pin outside a repo must halt');
|
||||
assert.equal(runPinGuard(outside, lane).exitCode, 1, 'cwd outside a repo must halt');
|
||||
} finally {
|
||||
cleanup(outside);
|
||||
cleanup(primary);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4254: shell-metacharacter pin is safely quoted — no command substitution executes', () => {
|
||||
const { primary, lane } = makeOrchestratorLane('gsd-4254-meta-');
|
||||
try {
|
||||
const tricky = path.join(primary, `q' $HOME $(touch PWNED) x`);
|
||||
fs.symlinkSync(lane, tricky, 'junction');
|
||||
const marker = path.join(lane, 'write-marker');
|
||||
const res = runPinGuard(lane, tricky, `printf 'W' > ${shellQuote(marker)}`);
|
||||
assert.equal(res.exitCode, 0, `quoted metacharacter pin must match its checkout:\n${res.stderr}`);
|
||||
assert.equal(fs.existsSync(path.join(lane, 'PWNED')), false, 'command substitution in the pin must not execute');
|
||||
assert.equal(fs.existsSync(marker), true);
|
||||
} finally {
|
||||
cleanup(primary);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4254: relative pin is rejected; Windows drive-letter forms pass the form gate and fail only at the git lookup', () => {
|
||||
const { primary, lane } = makeOrchestratorLane('gsd-4254-forms-');
|
||||
try {
|
||||
// Relative pins are never trustworthy across cwds — rejected outright,
|
||||
// and the FATAL says so at the form-gate stage.
|
||||
const rel = runPinGuard(lane, 'relative/path');
|
||||
assert.equal(rel.exitCode, 1);
|
||||
assert.match(rel.stderr, /Guard stage: form-gate/, 'a relative pin must halt at the form gate');
|
||||
// The drive-letter forms Windows produces must be ACCEPTED by the shipped
|
||||
// guard's absolute-form gate and then fail only on the git lookup — never
|
||||
// on the form rejection: the forward-slash form git EMITS (C:/…) and the
|
||||
// backslash form Node's path.join and cmd.exe PRODUCE (C:\…, including
|
||||
// RUNNER~1-style short names). The Guard stage line is the discriminator:
|
||||
// pinned-capture means the form passed and `git -C` spoke (its stderr rides
|
||||
// the Diagnostic line), form-gate means the gate ate the pin — the exact
|
||||
// CI defect where every backslash pin died before the actual root was ever
|
||||
// computed. Driving the SHIPPED guard also removes the hand-rolled
|
||||
// duplicate `case` this row used to carry (the #4296 Minor 1 duplication
|
||||
// smell, and itself a transit-fragile copy of the broken pattern).
|
||||
for (const driveForm of ['C:/definitely/not/a/repo', 'C:\\definitely\\not\\a\\repo']) {
|
||||
const res = runPinGuard(lane, driveForm);
|
||||
assert.equal(res.exitCode, 1, `nonexistent drive-letter pin must fail closed (${driveForm})`);
|
||||
assert.match(
|
||||
res.stderr, /Guard stage: pinned-capture/,
|
||||
`drive form ${driveForm} must pass the form gate and halt at the pinned-capture stage, got:\n${res.stderr}`,
|
||||
);
|
||||
assert.match(res.stderr, /Diagnostic: git -C <pinned root> rev-parse --show-toplevel failed:/, 'the capture failure carries git stderr');
|
||||
}
|
||||
// Transit hardening, pinned on the shipped text itself: the guard must
|
||||
// generate its backslash comparator at RUNTIME (printf octal) and must not
|
||||
// contain a doubled backslash anywhere — a backslash written twice does
|
||||
// not survive the Windows command-line round-trip into bash (it arrives
|
||||
// halved, rewriting any escape pattern that relies on it). Two earlier
|
||||
// pattern spellings failed the Windows CI leg on exactly this.
|
||||
const guardBody = extractPinGuard();
|
||||
assert.doesNotMatch(guardBody, /\\\\/, 'the shipped guard must contain no doubled backslash (Windows transit halves it)');
|
||||
assert.match(guardBody, /printf '\\134'/, 'the drive-form gate must generate its backslash comparator at runtime');
|
||||
} finally {
|
||||
cleanup(primary);
|
||||
}
|
||||
});
|
||||
|
||||
test('#4254: execute-phase.md sequential branch pins the orchestrator root at build time', () => {
|
||||
const sequential = executePhaseSrc.split('**Sequential mode**')[1].split('4. **Wait for all agents')[0];
|
||||
assert.ok(sequential.length > 0, 'sequential-mode section not found');
|
||||
// The host step delegates the composition detail to the fragment (ADR-857
|
||||
// Phase 6 frozen ceiling — execute-phase.md cannot grow) and the prompt
|
||||
// gains the pin block plus the per-write/commit instruction.
|
||||
assert.match(sequential, /read and execute\s+`execute-phase\/steps\/sequential-root-pin\.md`/);
|
||||
assert.match(sequential, /<project_root_pin>/);
|
||||
assert.match(sequential, /Run the `<project_root_pin>` guard before your first Edit\/Write and before every commit/);
|
||||
// The fragment carries the full build-time embed contract.
|
||||
assert.match(pinStepSrc, /ORCHESTRATOR build-time embed/);
|
||||
assert.match(pinStepSrc, /\{PINNED_ROOT\}/);
|
||||
assert.match(pinStepSrc, /ORCHESTRATOR_WT/);
|
||||
assert.match(pinStepSrc, /do not pass this instruction through/i);
|
||||
// The self-derivation is replaced with the literal, preserving the
|
||||
// `PROJECT_ROOT=` binding the rest of <required_reading> depends on.
|
||||
assert.match(pinStepSrc, /replace the self-derivation line/);
|
||||
assert.match(pinStepSrc, /PROJECT_ROOT='<the same literal/);
|
||||
// Scoping: the fragment must say worktree mode keeps the self-derived form.
|
||||
assert.match(pinStepSrc, /sequential-mode\s+ONLY/i);
|
||||
// The wave serialization rules moved with it, verbatim in substance.
|
||||
assert.match(pinStepSrc, /two non-worktree plans in the same wave must serialize/);
|
||||
});
|
||||
|
||||
test('#4254: worktree-mode dispatch is untouched — keeps self-derivation, gains no pin', () => {
|
||||
const isolated = executePhaseSrc.slice(
|
||||
executePhaseSrc.indexOf('<parallel_execution>'),
|
||||
executePhaseSrc.indexOf('**Sequential mode**'),
|
||||
);
|
||||
assert.ok(isolated.length > 0, 'worktree-mode dispatch slice not found');
|
||||
assert.match(isolated, /PROJECT_ROOT=\$\(git rev-parse --show-toplevel/, 'isolated executor keeps its own (correct) root derivation');
|
||||
assert.doesNotMatch(isolated, /<project_root_pin>/, 'isolated prompt must not inherit the orchestrator root');
|
||||
});
|
||||
});
|
||||
|
||||
1
tests/fixtures/install-tree/antigravity.json
vendored
1
tests/fixtures/install-tree/antigravity.json
vendored
@@ -394,6 +394,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/augment.json
vendored
1
tests/fixtures/install-tree/augment.json
vendored
@@ -466,6 +466,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
@@ -359,6 +359,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/claude.json
vendored
1
tests/fixtures/install-tree/claude.json
vendored
@@ -394,6 +394,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/cline.json
vendored
1
tests/fixtures/install-tree/cline.json
vendored
@@ -396,6 +396,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/codebuddy.json
vendored
1
tests/fixtures/install-tree/codebuddy.json
vendored
@@ -466,6 +466,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/codex.json
vendored
1
tests/fixtures/install-tree/codex.json
vendored
@@ -430,6 +430,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/copilot.json
vendored
1
tests/fixtures/install-tree/copilot.json
vendored
@@ -395,6 +395,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/cursor.json
vendored
1
tests/fixtures/install-tree/cursor.json
vendored
@@ -394,6 +394,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/hermes.json
vendored
1
tests/fixtures/install-tree/hermes.json
vendored
@@ -394,6 +394,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/kilo.json
vendored
1
tests/fixtures/install-tree/kilo.json
vendored
@@ -466,6 +466,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/kimi-code.json
vendored
1
tests/fixtures/install-tree/kimi-code.json
vendored
@@ -395,6 +395,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/kimi.json
vendored
1
tests/fixtures/install-tree/kimi.json
vendored
@@ -431,6 +431,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/opencode.json
vendored
1
tests/fixtures/install-tree/opencode.json
vendored
@@ -466,6 +466,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/pi.json
vendored
1
tests/fixtures/install-tree/pi.json
vendored
@@ -254,6 +254,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/qwen.json
vendored
1
tests/fixtures/install-tree/qwen.json
vendored
@@ -394,6 +394,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/trae.json
vendored
1
tests/fixtures/install-tree/trae.json
vendored
@@ -394,6 +394,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/windsurf.json
vendored
1
tests/fixtures/install-tree/windsurf.json
vendored
@@ -322,6 +322,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
1
tests/fixtures/install-tree/zcode.json
vendored
1
tests/fixtures/install-tree/zcode.json
vendored
@@ -466,6 +466,7 @@
|
||||
"gsd-core/workflows/execute-phase/steps/protected-branch.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
|
||||
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
|
||||
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
|
||||
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
|
||||
|
||||
@@ -6502,11 +6502,30 @@ describe('execute-phase.md dispatch wires USE_WORKTREES_FOR_PLAN (#2772)', () =>
|
||||
});
|
||||
|
||||
test('"Worktrees disabled" sequential rule is documented per-plan, not project-level', () => {
|
||||
// #4254 moved the wave-serialization rules into the sequential-root-pin step
|
||||
// fragment (ADR-857 Phase 6 frozen ceiling — the host step cannot grow), so the
|
||||
// rule is asserted where it now lives AND that the host step still wires the
|
||||
// fragment in at the Sequential mode branch.
|
||||
const pinFragmentPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'gsd-core',
|
||||
'workflows',
|
||||
'execute-phase',
|
||||
'steps',
|
||||
'sequential-root-pin.md'
|
||||
);
|
||||
const frag = fs.readFileSync(pinFragmentPath, 'utf-8');
|
||||
assert.match(
|
||||
frag,
|
||||
/worktrees are disabled for a plan/i,
|
||||
'sequential-execution rule must be expressed per-plan (in the sequential-root-pin fragment)'
|
||||
);
|
||||
const md = fs.readFileSync(workflowPath, 'utf-8');
|
||||
assert.match(
|
||||
md,
|
||||
/worktrees are disabled for a plan/i,
|
||||
'sequential-execution rule must be expressed per-plan'
|
||||
/execute-phase\/steps\/sequential-root-pin\.md/,
|
||||
'execute-phase.md must wire the sequential-root-pin fragment at the Sequential mode branch'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user