-
Check working tree state
git status --short
If output is non-empty (uncommitted, staged, or untracked changes), surface what's there and ask the user before continuing. Do not silently stash or discard.
-
Note the starting branch and verify its PR status
git branch --show-current
-
If on master: skip the verification step and proceed to step 3.
-
If on any other branch: check whether its PR is merged before this skill considers the branch safe to delete later.
gh pr list --state merged --head <branch-name> --json number,mergedAt,title --limit 1
If the PR is unmerged or there is no PR for the branch, treat the branch as active work — switch to master for the sync but do not propose deleting it.
-
Switch to master and fetch with prune
git checkout master
git fetch --prune
--prune removes remote-tracking refs (origin/branch-foo) for branches deleted on GitHub after merge. On this repo that clears the origin/dependabot/* refs left behind by merged dependency bumps.
-
Pull latest master (fast-forward only)
git pull --ff-only
git rev-list --left-right --count origin/master...master
--ff-only aborts instead of creating a merge commit when the branch can't be fast-forwarded; if it refuses, stop and surface — plain git pull would silently merge here, defeating this skill's purpose. But --ff-only is a no-op in the ahead-only case — if local master has a stray commit not on origin/master and the remote has nothing newer, the pull succeeds silently and that stray commit becomes the baseline for every future branch. The rev-list check makes it explicit: the second number is how many commits local master is ahead of the remote; anything but 0 0 means stop and surface — master should never be ahead of origin/master. (Mirrors the same check in /ftf-new-branch step 2.)
-
Identify safe-to-delete merged local branches
Don't gather candidates from git branch --merged master alone — GitHub's squash-merge default rewrites history, so a squash-merged branch's tip is never reachable from master and --merged silently excludes it (see the squash-merge note in step 6). Relying on --merged would mean squash-merged branches are never even considered for cleanup — and on a repo with Dependabot, most merges are squashes. --merged also over-includes: a tip can be reachable without its PR ever being merged (cherry-picks, manual rebases, never-PR'd branches). So reachability is neither necessary nor sufficient.
Treat "has a merged PR" as the real signal. Enumerate every local branch except master and the current branch, and check each:
git branch --format='%(refname:short)' | grep -vx master
gh pr list --state merged --head <branch-name> --json number,mergedAt,headRefOid --limit 1
- Merged PR exists → candidate for deletion; carry it to step 6 (which picks
-d vs the squash-merge -D path via the SHA check).
- No merged PR (open PR, no PR, or closed-without-merge) → leave alone. Surface separately as "unverified PR status — not proposing deletion." This applies to the starting branch from step 2 as well.
-
Confirm before deleting stale branches
List the stale branches to the user with short context (last commit subject + age) and ask for confirmation before deleting anything. Default to a single confirm-all prompt; only ask per-branch if the list is suspicious or the user has previously asked for granular control.
On confirmation:
git branch -d <branch-name>
Use -d (safe), not -D. -d succeeds for true-merged branches. If -d refuses ("not fully merged") on a candidate that step 5 confirmed has a merged PR, the branch was squash-merged (its tip isn't reachable from master) — don't force-delete blindly; drop to the SHA check below.
Squash-merge exception: GitHub's squash-merge default produces branches whose tip is NOT reachable from master even after the PR is confirmed merged. In that case git branch --merged master won't list the branch (which is why step 5 gathers candidates by merged-PR state, not --merged) and git branch -d will refuse to delete it ("not fully merged"). When gh pr list --state merged --head <branch> returns a merged PR but -d refuses, a SHA check is required before offering -D — --head filters by branch name only, so a reused name can match an unrelated older merged PR and the force-delete could destroy unmerged work.
pr_head_oid=$(gh pr list --state merged --head <branch> --json headRefOid --limit 1 | jq -r '.[0].headRefOid')
branch_tip=$(git rev-parse <branch>)
if [ "$pr_head_oid" = "$branch_tip" ]; then
else
fi
Only proceed with -D on explicit user confirmation AND a verified SHA match.
-
Report final state
See Output below.