Imported from wan-huiyan/agent-traffic-control (
plugins/agent-traffic-control/skills/subagent-driven-branch-ref-froze-stranded-commits/SKILL.md). Install upstream withnpx skills add wan-huiyan/agent-traffic-control --skill subagent-driven-branch-ref-froze-stranded-commits. Copyright stays with the author.
Subagent-driven session left commits stranded — branch ref froze while worktree HEAD advanced
Problem
You ran a long superpowers:subagent-driven-development (or similar fresh-
subagent-per-task) session in a worktree. Each subagent did its task, committed
its work, the controlling session ran spec + code-quality review per task, all
green. At the end you pushed the worktree's branch to origin, opened a PR, CI
went green, you squash-merged.
Hours or one session later, somebody runs pytest on freshly-pulled main and
gets a ModuleNotFoundError, a missing-file assertion, or a sql_path missing
error pointing at code that was definitely committed during the session.
git log in your old worktree still shows all the commits at the top of the
branch. But on main, half of them are gone.
The squash commit on main contains only the early portion of your work —
typically the first N tasks before some specific transition point. Tasks
after that point exist as commits in the worktree (git log shows them)
but did not reach origin, did not reach the PR, did not reach the merge.
Context / Trigger Conditions
Strong signal after the merge:
gh pr view <N>body claims a file count (e.g. "55 files changed") that doesn't match the actual squash-commit file count (git diff-tree --no-commit-id --name-only -r <merge-sha> | wc -l).- A test that passed locally during the session now fails on main because a file the test depends on isn't there.
- Receiving session's first
pip installorpytestsurfacesModuleNotFoundError,sql_path missing, or "no such file" referencing work the previous session reported as DONE.
Strong signal during the session (catch it before pushing):
git rev-parse HEAD≠git rev-parse <feature-branch>in the worktree. The branch ref is behind.git reflog show <feature-branch>has a reflog gap — the most recentworktree-...@{0}entry is older than the most recent commit shown bygit log. The commits between branch-ref's frozen point and current HEAD are stranded.git statusin the worktree may or may not reportHEAD detached at <sha>— sometimes the detach happened mid-stream and a later subagent created the symbolic-ref linkage on its own, leaving an inconsistent state.
Strong signal across sessions:
- The pattern is specific to subagent-driven sessions because each
subagent's tool calls run in an isolated process tree. A detached-HEAD
transition introduced by one subagent (e.g. for a quick
git showor post-stash recovery) propagates silently to every later subagent. A single human running the same workflow would notice the prompt change to(HEAD detached at <sha>).
Solution
Prevention (the cheap path)
Add an explicit pre-merge verification step before any gh pr merge:
# 1. All three of these should produce the SAME sha:
git rev-parse HEAD
git rev-parse <feature-branch>
git rev-parse origin/<feature-branch>
# 2. This should print NOTHING (zero commits ahead of remote):
git log origin/<feature-branch>..HEAD --oneline
# 3. PR body file-count canary:
# "N files changed" in the PR body should match
gh pr diff <N> --repo <owner>/<repo> --name-only | wc -l
# 4. Reflog must extend to current HEAD:
# Compare the topmost entry from each — they should agree.
git reflog show <feature-branch> | head -1
git log -1 --oneline
If any one of those is misaligned, STOP. Do not merge. Recover with the hotfix path below before pushing the merge button.
Recovery (post-merge)
If the merge already happened and you're seeing the symptom:
# 0. Confirm the diagnosis.
git -C <worktree-path> rev-parse HEAD # the "true" tip
git -C <worktree-path> rev-parse <branch> # the frozen ref
# If these differ, you have stranded commits.
# 1. List exactly which commits are stranded (in topological order).
git -C <worktree-path> log <branch>..HEAD --oneline
# 2. Branch a recovery branch from current origin/main.
git fetch origin
git checkout -B recover/<short-name> origin/main
# 3. Cherry-pick the stranded commits in order.
git cherry-pick <oldest-stranded-sha>..<newest-stranded-sha>
# (or list them individually if the range is non-linear)
# 4. If the cherry-picks reference paths that have moved on main since
# the original work was authored (common when other PRs landed
# concurrently and reorganized files), add a follow-up commit that
# fixes the path references. Don't try to fix them inside the
# cherry-picks — that breaks the audit trail.
# 5. Run tests against the merged result of recovery + main.
<test command>
# 6. Push, PR, merge.
git push -u origin recover/<short-name>
gh pr create --base main --title "fix: recover <N> commits stranded by PR #<original> squash" \
--body "<post-mortem-ish explanation; cite the original PR's merge commit>"
The recovery PR should bundle BOTH the cherry-picks AND any path fixes required for the merged tree to function. Splitting them produces a half-broken main between the two merges.
Forensic verification
You can confirm the diagnosis precisely:
# The reflog gap tells you WHERE the freeze happened.
git -C <worktree-path> reflog show <branch> | head -1
# vs
git -C <worktree-path> log --oneline -1
# The gap is everything between those two.
# Pre-squash file count vs post-squash file count.
gh pr diff <N> --repo <owner>/<repo> --name-only | wc -l # what GitHub saw
git -C <main-repo> diff-tree --no-commit-id --name-only -r <merge-sha> | wc -l
# These should agree. If both are smaller than your mental model, the
# strand is the gap to your mental model.
Verification
After the recovery PR lands:
git diff origin/main origin/recover/<name>(BEFORE the recovery merge) should show only the stranded commits + the path fix.- After merge, the receiving session's pytest should be green.
git -C <main-repo> ls-tree -r --name-only main | grep <previously-missing-file>should return the file.- The original symptom (e.g.
sql_path missing,ModuleNotFoundError) is gone.
Example
the-handover-repo project, session 6 (2026-05-18). Plan B's 18 TDD tasks executed
via superpowers:subagent-driven-development in a worktree at
.claude/worktrees/plan-b-brief-runner on branch
worktree-plan-b-brief-runner.
Each implementer subagent reported DONE with a commit SHA. The controlling
session's per-task reviewers ran pytest from inside the worktree and saw
all green. At the end, the controlling session ran:
git push -u origin worktree-plan-b-brief-runner
gh pr create --title "feat(plan-b): brief-runner core backend" ...
# CI green; squash-merge.
PR #9 merged at
d84ceee7032f73a60dc0bb2af57939aa3f987625. The PR body claimed "55 files
changed, 2,295 insertions" — but the merge commit's file list had only 42.
Diagnosis post-merge:
$ git rev-parse HEAD # in worktree
b31d37349c8a1316883014d50108f90eb52f93f8 # Task 18 ✓
$ git rev-parse worktree-plan-b-brief-runner # in worktree
e9bb471acdac9d14c2114097f3230e00a11bc493 # Task 11 ← FROZEN
$ git reflog show worktree-plan-b-brief-runner | head -3
e9bb471 worktree-plan-b-brief-runner@{0}: commit: feat(brief-runner): CUSTOM_PARAMETER ...
2dbed0e worktree-plan-b-brief-runner@{1}: commit: feat(brief-runner): inline SQL parametrizer ...
# No reflog entries for Tasks 12-18, but they exist as commits!
The seven commits between e9bb471 (Task 11) and b31d373 (Task 18) — sql_hash, Run Pack generator, all four route handlers, Dockerfile + cloudbuild + .gcloudignore + deploy runbook — were stranded.
Recovery via PR #14
(squash d808859):
git checkout -B recover/s5-plan-b-stranded-commits origin/main
git cherry-pick bc9748b 947315a 77ff3ce 0a66933 29ff572 5972d34 b31d373
# All cherry-picks applied cleanly.
# Then a follow-up commit to repoint analysis/SQL/*.registry.toml
# sql_path values to analysis/SQL/handover/<file>.sql, because PR #11
# had moved the SQL files under us during the original session.
pytest -q # 166 passed, 1 skipped
git push -u origin recover/s5-plan-b-stranded-commits
gh pr create ...
gh pr merge --squash --delete-branch
Time from discovery to recovered-on-main: ~25 minutes. Full post-mortem with
forensics in
docs/analysis/2026-05-18-pr-9-stranded-commits.md of the the-handover-repo repo.
Notes
-
The reflog gap is the single most reliable forensic.
git logwalks from HEAD and shows everything.git reflog show <branch>shows only what the branch ref actually saw — and a detached-HEAD commit doesn't update the branch ref. The asymmetry is your tell. -
gh pr mergewill not warn you. GitHub's merge button operates on the remote branch state, which is exactly what you pushed. The mismatch between your local worktree HEAD and the pushed ref is invisible to GitHub. -
The PR-body file count is the second canary. Many session-handoff-style PR bodies summarize "M files changed, N insertions" from the controlling session's mental model. If that number is higher than
gh pr diff <N> --name-only | wc -l, something is wrong. -
A controlling session's
git logis a false positive for "everything is fine". Without--branches=<branch>it walks from HEAD. To validate the push will ship what you think, always compare againstorigin/<branch>explicitly. -
Subagents may detach HEAD for innocuous reasons. A subagent that runs
git show <sha>followed by an interruptedgit checkout(or relies on a tool that does so internally) can leave HEAD detached. Subsequent subagents will commit on the detached HEAD and never restore the branch ref. Addinggit symbolic-ref HEAD || echo DETACHEDto each implementer subagent's opening diagnostic catches this before the first commit. -
Concurrent PRs amplify the damage. If main moved while your worktree was open, the squash-merge silently reconciles your truncated tree against the new main — sometimes producing functioning but partial output, sometimes producing broken-reference output. See the example above where PR #11 moved SQL files under PR #9 while it was open; both the strand AND the path drift bit at once.
References
- the-handover-repo PR #9 (the truncated original) and PR #14 (the cherry-pick recovery).
- the-handover-repo post-mortem at
docs/analysis/2026-05-18-pr-9-stranded-commits.md(full forensic timeline + remediation recipe). - Sister skills:
working-tree-edits-stranded-on-squash-merge(uncommitted Edit() — different mechanism),pr-followup-commit-stranded-after-squash(commits pushed after merge — different timing),pr-hijack-via-stale-worktree-branch-ref(overwriting someone else's branch upstream — different problem entirely). superpowers:subagent-driven-developmentskill — where this pattern emerges. Worth adding the pre-merge verification snippet to that skill's "Common Mistakes" section.- Pro Git §3.5 — Remote Branches
on why
git logandgit reflog show <branch>can disagree.