fix(workflows): assert HEAD on per-agent branch before worktree commits (#2924) (#2941)

* fix(workflows): assert HEAD on per-agent branch before worktree commits

Worktree-mode setup could leave HEAD attached to a protected branch (master),
causing agent commits to land there. The previous response was a destructive
self-recovery via 'git update-ref refs/heads/master <sha>', which silently
rewinds the protected branch and destroys concurrent commits in multi-active
scenarios (parallel agents, user committing while agent runs).

- Reorder <worktree_branch_check> in execute-phase.md and quick.md to assert
  HEAD via 'git symbolic-ref' BEFORE any 'git reset --hard'. HALT with a
  blocker if HEAD is on main/master/develop/trunk/release/* or detached.
- Add a per-commit HEAD assertion (step 0) to gsd-executor.md
  <task_commit_protocol>; HEAD attachment can drift after 'git checkout <sha>'.
- Forbid 'git update-ref refs/heads/<protected>' in
  <destructive_git_prohibition>; surface the blocker rather than self-heal.
- Remove '--no-verify' as the worktree-mode default in execute-phase.md,
  execute-plan.md, quick.md, and references/git-integration.md. Hooks now
  run on every executor commit; opt out only via workflow.worktree_skip_hooks.
- Add regression test that parses the worktree_branch_check blocks structurally
  and asserts the symbolic-ref check precedes the reset --hard, no workflow
  performs update-ref on a protected ref, and --no-verify is no longer the
  default in any parallel-execution prompt.

* fix(#2924): address CodeRabbit review findings on worktree HEAD PR

- Add positive worktree-agent-* allow-list to <task_commit_protocol> step 0
  in gsd-executor.md and to <worktree_branch_check> in execute-phase.md and
  quick.md. The deny-list (main|master|develop|trunk|release/*) silently
  allowed feature/* and other arbitrary branches outside the agent namespace.
- Register workflow.worktree_skip_hooks in both config schemas
  (sdk/src/query/config-schema.ts and get-shit-done/bin/lib/config-schema.cjs)
  and document it in docs/CONFIGURATION.md so config-set accepts it.
- Fix stash lifecycle in execute-phase.md post-wave hook validation: stash
  under a named ref and pop after the hook run; warn on pop failure.
- Pre-dispatch PLAN.md commit in quick.md: gate on git diff --cached --quiet
  for idempotency and exit 1 with a clear error on commit failure (both the
  --no-verify and the normal branches) — no more swallowing real errors.
- Test fixes (tests/bug-2924-worktree-head-attachment.test.cjs):
  - Parse the protected-branch alternation structurally and require
    main, master, develop, trunk, release/.* (release/* was previously
    skipped by the \\b...\\b regex).
  - Use fs.readdirSync(dir, { recursive: true }) so workflows in nested
    subdirectories are also asserted against the update-ref ban.
  - Add allow-list assertions for execute-phase.md, quick.md, and
    gsd-executor.md to lock in the new positive namespace check.

* test(#2924): assert sub-section end marker exists before slicing

* test(#2924): use section boundary instead of fixed window for parallel-agents slice
This commit is contained in:
Tom Boucher
2026-05-01 09:23:02 -04:00
committed by GitHub
parent 2cc8796265
commit 8de8acee46
9 changed files with 583 additions and 43 deletions

View File

@@ -506,40 +506,37 @@ increases monotonically across waves. `{status}` is `complete` (success),
</objective>
<worktree_branch_check>
FIRST ACTION before any other work: verify this worktree's branch is based on the correct commit.
Run:
FIRST ACTION: HEAD assertion MUST run before any reset/checkout. Worktrees
spawned by Claude Code's `isolation="worktree"` use the `worktree-agent-<id>`
namespace. If HEAD is on a protected ref (main/master/develop/trunk/release/*)
or detached, HALT — do NOT self-recover by force-rewinding via `git update-ref`,
that destroys concurrent commits in multi-active scenarios (#2924). Only after
Step 1 passes is `git reset --hard` safe (#2015 — affects all platforms).
```bash
ACTUAL_BASE=$(git merge-base HEAD {EXPECTED_BASE})
```
If `ACTUAL_BASE` != `{EXPECTED_BASE}` (i.e. the worktree branch was created from an older
base such as `main` instead of the feature branch HEAD), hard-reset to the correct base:
```bash
# Safe: this runs before any agent work, so no uncommitted changes to lose
git reset --hard {EXPECTED_BASE}
# Verify correction succeeded
if [ "$(git rev-parse HEAD)" != "{EXPECTED_BASE}" ]; then
echo "ERROR: Could not correct worktree base — aborting to prevent data loss"
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 worktree-agent-*); refusing to self-recover via 'git update-ref' (#2924)." >&2
exit 1
fi
if ! echo "$ACTUAL_BRANCH" | grep -Eq '^worktree-agent-[A-Za-z0-9._/-]+$'; then
echo "FATAL: worktree HEAD '$ACTUAL_BRANCH' is not in the worktree-agent-* namespace; refusing to commit (#2924)." >&2
exit 1
fi
ACTUAL_BASE=$(git merge-base HEAD {EXPECTED_BASE})
if [ "$ACTUAL_BASE" != "{EXPECTED_BASE}" ]; then
git reset --hard {EXPECTED_BASE}
[ "$(git rev-parse HEAD)" != "{EXPECTED_BASE}" ] && { echo "ERROR: could not correct worktree base"; exit 1; }
fi
```
`reset --hard` is safe here because this is a fresh worktree with no user changes. It
resets both the HEAD pointer AND the working tree to the correct base commit (#2015).
If `ACTUAL_BASE` == `{EXPECTED_BASE}`: the branch base is correct, proceed immediately.
This check fixes a known issue where `EnterWorktree` creates branches from
`main` instead of the current feature branch HEAD (affects all platforms).
Per-commit HEAD assertion lives in `agents/gsd-executor.md` `<task_commit_protocol>` step 0.
</worktree_branch_check>
<parallel_execution>
You are running as a PARALLEL executor agent in a git worktree.
Use --no-verify on all git commits to avoid pre-commit hook contention
with other agents. The orchestrator validates hooks once after all agents complete.
For `gsd-sdk query commit` (or legacy `gsd-tools.cjs` commit): add --no-verify flag when needed.
For direct git commits: use git commit --no-verify -m "..."
Run `git commit` normally — hooks run by default. Do NOT pass `--no-verify`
unless the orchestrator surfaces `workflow.worktree_skip_hooks=true` in this
prompt; silent bypass violates project CLAUDE.md guidance (#2924).
IMPORTANT: Do NOT modify STATE.md or ROADMAP.md. execute-plan.md
auto-detects worktree mode (`.git` is a file, not a directory) and skips
@@ -656,13 +653,16 @@ increases monotonically across waves. `{status}` is `complete` (success),
**This fallback applies automatically to all runtimes.** Claude Code's Task() normally
returns synchronously, but the fallback ensures resilience if it doesn't.
5. **Post-wave hook validation (parallel mode only):**
When agents committed with `--no-verify`, run pre-commit hooks once after the wave:
5. **Post-wave hook validation (parallel mode only):** Hooks run on every executor commit by default (#2924); this post-wave run only fires when `workflow.worktree_skip_hooks=true` opted out of per-commit hooks:
```bash
# Run project's pre-commit hooks on the current state
git diff --cached --quiet || git stash # stash any unstaged changes
git hook run pre-commit 2>&1 || echo "⚠ Pre-commit hooks failed — review before continuing"
SKIP_HOOKS=$(gsd-sdk query config-get workflow.worktree_skip_hooks 2>/dev/null || echo "false")
if [ "$SKIP_HOOKS" = "true" ]; then
# Stash uncommitted changes under a named ref so we always pop (bare `git stash` strands them on hook/script failure).
STASHED=false
if (! git diff --quiet || ! git diff --cached --quiet) && git stash push -u -m "gsd-post-wave-hook-$$" >/dev/null 2>&1; then STASHED=true; fi
git hook run pre-commit 2>&1 || echo "⚠ Pre-commit hooks failed — review before continuing"
[ "$STASHED" = "true" ] && (git stash pop >/dev/null 2>&1 || echo "⚠ Could not pop gsd-post-wave-hook stash — recover manually")
fi
```
If hooks fail: report the failure and ask "Fix hook issues now?" or "Continue to next wave?"