* 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>
8.7 KiB
Worktree Path Safety
Guards for executor agents running inside Claude Code worktrees. The supplied-root pin (step 0p) runs in EVERY mode; the remaining checks run before any staging, Edit, or Write operation in worktree mode.
Supplied-root pin — step 0p (#4254, EVERY mode)
Sequential-mode dispatch (no isolation="worktree") gives the executor no
spawn-time cwd guarantee, and the worktree-only guards below do not apply — so
a sequential executor whose process cwd resolved to a different checkout of
the same repo would self-derive that checkout as its root and commit there,
silently. Step 0p closes that hole by comparing the executor's actual root
against a root the ORCHESTRATOR already validated — never against anything the
executor derives itself.
Runtime contract (executor): if your prompt contains a <project_root_pin>
block, run its guard script verbatim before your first Edit/Write and again
before every commit, in the same cwd as that write or commit. On FATAL, halt
and report — recovery (moving commits between checkouts) is an
orchestrator/human decision, never agent self-repair. If your prompt contains
NO <project_root_pin> block (worktree/isolated dispatch, or a legacy
orchestrator), emit one warning line and continue with steps 0a/0b below — do
not fail closed on dispatches that never carried a pin. Never bind
{PINNED_ROOT} yourself: if this template reaches you unbound it is
reference prose, not your pin — only the orchestrator's build-time
substitution produces a valid guard.
Composition contract (orchestrator — build time, NOT a sub-agent runtime
step): copy the guard below into the dispatched prompt inside a
<project_root_pin> block, substituting {PINNED_ROOT} with the literal value
of $ORCHESTRATOR_WT captured at execute_waves entry, shell-single-quoted:
wrap the path in '…' and escape any embedded ' as '\''. A path that
cannot be quoted this way must halt the phase (surface a blocker) rather than
ship a pin that could mis-parse. The comparison is git-vs-git on BOTH sides —
git -C resolves the pinned path to its repo's canonical toplevel in git's
own path representation, so symlink aliases, trailing slashes, /var vs
/private/var spellings, and Windows drive-letter forms — forward- or
backslash-separated, RUNNER~1-style short names included — compare equal by
construction (shell pwd -P normalization does NOT match git's emission on
Windows — do not re-introduce it).
Two portability rules baked into the guard below, learned from the #4254 CI
Windows legs: (1) a backslash comparator must be GENERATED at runtime
(printf '\134'), because a backslash written twice in the script text does
not survive the Windows command-line round-trip into bash — the doubled form
arrives halved, which silently rewrites any escape pattern that relies on it;
(2) every FATAL names its Guard stage and, where a git capture failed,
git's own stderr in a Diagnostic line, so a platform failure self-describes
instead of surfacing as a bare Actual root: <none>.
# gsd:guard=supplied-root-pin (#4254) — run before the first Edit/Write and before every commit.
PINNED_ROOT='{PINNED_ROOT}' # orchestrator build-time substitution — the only valid source of this value
PIN_STAGE=''
PIN_DIAG=''
gsd_pin_fail() {
echo "FATAL: executor root does not match the orchestrator-supplied PROJECT_ROOT pin (#4254)." >&2
echo " Pinned root: ${PINNED_ROOT:-<empty or unexpanded>}" >&2
echo " Actual root: ${ACTUAL_ROOT:-<none>}" >&2
echo " Guard stage: ${PIN_STAGE:-<unset>}" >&2
if [ -n "$PIN_DIAG" ]; then echo " Diagnostic: $PIN_DIAG" >&2; fi
echo " No writes or commits are permitted from this checkout. HALT and report; recovery is an" >&2
echo " orchestrator/human decision. Only the IMMEDIATE submodule of the pinned checkout is a" >&2
echo " legitimate other cwd — nested submodules must surface as a blocker, not self-route." >&2
exit 1
}
# Backslash comparator, generated at runtime: a backslash written twice in this
# script does not survive the Windows spawn path into bash (the command-line
# round-trip halves the doubled form), which rejected every C:\ pin at the form
# gate on the #4254 CI Windows legs. printf's octal escape is a lone backslash,
# which does survive; the quoted expansion below is literal in a case pattern.
BS=$(printf '\134')
# Fail closed if the comparator could not be generated: an empty BS would widen
# the drive-form arm below to drive-RELATIVE pins (C:foo) — the one fail-open
# seam in this construction, closed loudly rather than trusted to the shell.
if [ -z "$BS" ]; then
PIN_STAGE=form-gate
PIN_DIAG='backslash comparator generation failed (printf octal escape returned empty)'
gsd_pin_fail
fi
case "$PINNED_ROOT" in
''|'{PINNED_ROOT}') PIN_STAGE=pin-unbound; gsd_pin_fail ;; # empty or unexpanded pin — fail closed, never warn-and-proceed
/*) ;; # absolute POSIX form
[A-Za-z]:/*|[A-Za-z]:"$BS"*) ;; # Windows drive form, forward- or backslash-separated
*) PIN_STAGE=form-gate; gsd_pin_fail ;; # relative pin — never trustworthy across cwds
esac
ACTUAL_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -z "$ACTUAL_ROOT" ]; then
PIN_STAGE=actual-capture
PIN_DIAG="git rev-parse --show-toplevel from the cwd failed: $(git rev-parse --show-toplevel 2>&1 1>/dev/null)"
gsd_pin_fail
fi
PINNED_TL=$(git -C "$PINNED_ROOT" rev-parse --show-toplevel 2>/dev/null)
if [ -z "$PINNED_TL" ]; then
PIN_STAGE=pinned-capture
PIN_DIAG="git -C <pinned root> rev-parse --show-toplevel failed: $(git -C "$PINNED_ROOT" rev-parse --show-toplevel 2>&1 1>/dev/null)"
gsd_pin_fail
fi
if [ "$ACTUAL_ROOT" != "$PINNED_TL" ]; then
# Registered-submodule allowance: sub_repos plans legitimately commit inside an
# immediate submodule of the pinned checkout. The superproject working tree is
# git-emitted in the same representation as PINNED_TL, so the equality is
# representation-safe on every platform.
SUPER_TL=$(git rev-parse --show-superproject-working-tree 2>/dev/null)
if [ "$SUPER_TL" != "$PINNED_TL" ]; then
PIN_STAGE=root-mismatch
PIN_DIAG="actual=${ACTUAL_ROOT} pinned=${PINNED_TL} superproject=${SUPER_TL:-<none>}"
gsd_pin_fail
fi
fi
Worktree branch check (run once at spawn-time)
The spawn-time HEAD/base guard now lives in the canonical fragment
gsd-core/references/worktree-branch-check.md, which the orchestrator embeds directly
into your prompt at dispatch. Run that block FIRST, before any reset/checkout or staging.
If your prompt contains a <worktree_branch_check> embed instruction rather than the block itself, complete that read-and-embed step before any reset/checkout or staging.
cwd-drift sentinel — step 0a (#3097)
A prior Bash call may have cd'd out of the worktree into the main repo. When
that happens [ -f .git ] is false (main repo's .git is a directory), silently
skipping all worktree guards. The sentinel captures the spawn-time toplevel and
detects drift before every commit.
if [ -f .git ]; then # we are in a worktree
WT_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
case "$WT_GIT_DIR" in
*.git/worktrees/*)
SENTINEL="$WT_GIT_DIR/gsd-spawn-toplevel"
[ ! -f "$SENTINEL" ] && git rev-parse --show-toplevel > "$SENTINEL" 2>/dev/null
EXPECTED_TL=$(cat "$SENTINEL" 2>/dev/null)
ACTUAL_TL=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -n "$EXPECTED_TL" ] && [ "$ACTUAL_TL" != "$EXPECTED_TL" ]; then
echo "FATAL: cwd drifted from spawn-time worktree root (#3097)" >&2
echo " Spawn-time: $EXPECTED_TL" >&2
echo " Current: $ACTUAL_TL" >&2
echo "RECOVERY: cd \"$EXPECTED_TL\" before staging, then re-run this commit." >&2
exit 1
fi
;;
esac
fi
Absolute-path guard — step 0b (#3099)
Edit/Write calls using absolute paths constructed from the orchestrator's pwd
(main repo root) will resolve to the main repo, not the worktree. Writes land in
the wrong directory; git commit from the worktree sees a clean tree and the work
is silently lost.
Before any Edit or Write using an absolute path:
WT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
# Fail fast if ABS_PATH resolves outside the worktree
if [[ "$ABS_PATH" != "$WT_ROOT"* ]]; then
echo "WARNING: $ABS_PATH is outside the worktree ($WT_ROOT)" >&2
echo "Use a relative path or recompute the absolute path from WT_ROOT." >&2
fi
Prefer relative paths for all Edit/Write operations. When an absolute path is
unavoidable, always derive it from git rev-parse --show-toplevel run inside the
worktree — never from pwd captured in the orchestrator context.