Files
msd-core/gsd-core/references/worktree-branch-check.md
Tom Boucher 77bf21b3a6 fix(#1995): widen worktree branch regex to accept agent-<id> namespace (#2548)
* test(#1995): regression test for agent-<id> branch namespace

Add failing-first tests proving that normalizeCleanupManifestEntry and
planWorktreeRecordAgent reject Claude Code's current agent-<id> isolation
branches (only worktree-agent-<id> is accepted). Boundary tests cover both
namespaces plus rejection cases.

* fix(#1995): widen worktree branch regex to accept agent-<id> namespace

Claude Code's isolation="worktree" branch naming changed from
worktree-agent-<id> to agent-<id>. Widen the regex in all 7 locations
from ^worktree-agent-[A-Za-z0-9._/-]+$ to ^(worktree-)?agent-[A-Za-z0-9._/-]+$
so both namespaces are accepted. Introduce a shared WORKTREE_AGENT_BRANCH_RE
constant in src/worktree-safety.cts to prevent future drift.

Closes #1995

* fix(#1995): update workflow guards, test assertions, and baselines

Widen the branch-check regex in execute-phase.md and execute-plan.md.
Update all test assertions that checked for ^worktree-agent- to expect
the widened ^(worktree-)?agent- pattern. Regenerate golden-install-parity
fixtures, agent-size-baseline, and workflow-size-baseline.

Closes #1995

* fix(#1995): update extractCwdGuardBash sanity check for widened regex

The e2e test's sanity check verified the extracted bash block contained
'worktree-agent-'. After widening to '(worktree-)?agent-', update the
check to match the new pattern.

* fix(#1995): widen missed workflow-guard branch check + changeset + lint fixes

- hooks/gsd-workflow-guard.js: widen startsWith('worktree-agent-') to
  /^(worktree-)?agent-/ regex — same defect class, was missed in prior commit
- tests/worktree.test.cjs: fix indentation regression from prior edit
- Add .changeset/1995-worktree-agent-branch-namespace.md (pr:0 placeholder)

Found by orthogonal code review (Step 4).

* fix(#1995): regenerate golden + size baselines for workflow-guard change

* docs(#1995): backfill changeset PR number (2548)
2026-07-23 07:36:53 -04:00

2.8 KiB

Worktree branch check (spawn-time guard)

Canonical, fail-closed, verify-only guard embedded into every worktree sub-agent prompt at dispatch. This is the single source of truth for the worktree_branch_check block — do not inline a copy elsewhere. History of coordinated edits: #2924, #2015, #3174, #48.

Contract for orchestrators: before dispatch, capture EXPECTED_BASE=$(git rev-parse HEAD), then embed the block below into the sub-agent prompt verbatim, substituting {EXPECTED_BASE} with that captured SHA. Orchestrators that intentionally create a docs-only pre-dispatch plan commit may also substitute {EXPECTED_BASE_ALTERNATE} with that commit's immediate parent so runtimes that fork from either side of the docs-only commit pass the same fail-closed guard (#1265). Otherwise substitute {EXPECTED_BASE_ALTERNATE} with an empty string. The sub-agent only verifies and fails closed; the orchestrator (the worktree lifecycle owner) performs any base recovery — the sub-agent never rewrites a worktree it did not create (#48).

<worktree_branch_check> FIRST ACTION: HEAD assertion MUST run before anything else, and this block is VERIFY-ONLY. Worktrees spawned by Claude Code's isolation="worktree" use the agent-<id> namespace (previously worktree-agent-<id>; both are accepted). The orchestrator owns this worktree's lifecycle; a sub-agent MUST NOT hold state-correction primitives (hard-reset, update-ref, force-move, index-discard) on a worktree it did not create (#48, #2924). If ANY assertion below fails, HALT immediately — print the FATAL line, exit 42, and let the orchestrator (the lifecycle owner) decide recovery. Do NOT self-recover, do NOT commit.

HEAD_REF=$(git symbolic-ref --quiet HEAD || echo "DETACHED")
ACTUAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$HEAD_REF" = "DETACHED" ] || echo "$ACTUAL_BRANCH" | grep -Eq '^(main|master|develop|trunk|release/.*)$'; then
  echo "FATAL: worktree HEAD on '$ACTUAL_BRANCH' (expected agent-* or worktree-agent-*); refusing to commit or self-recover via 'git update-ref' (#2924)." >&2
  exit 42
fi
if ! echo "$ACTUAL_BRANCH" | grep -Eq '^(worktree-)?agent-[A-Za-z0-9._/-]+$'; then
  echo "FATAL: worktree HEAD '$ACTUAL_BRANCH' is not in the agent-* / worktree-agent-* namespace; refusing to commit (#2924)." >&2
  exit 42
fi
ACTUAL_BASE=$(git rev-parse HEAD)
EXPECTED_BASE_ALTERNATE="{EXPECTED_BASE_ALTERNATE}"
if [ "$ACTUAL_BASE" != "{EXPECTED_BASE}" ] && { [ -z "$EXPECTED_BASE_ALTERNATE" ] || [ "$ACTUAL_BASE" != "$EXPECTED_BASE_ALTERNATE" ]; }; then
  echo "FATAL: worktree base mismatch — HEAD is $ACTUAL_BASE, expected {EXPECTED_BASE}${EXPECTED_BASE_ALTERNATE:+ or $EXPECTED_BASE_ALTERNATE}. Orchestrator owns recovery; sub-agent refuses to rewrite the worktree (#48)." >&2
  exit 42
fi

</worktree_branch_check>