bundles/github/skills/release-cleanup/SKILL.md
Verify a release branch is provably merged into the trunk (default branch) via the squash-aware GitHub PR merge oracle, then prune merged local and remote feature branches and stale git worktrees. Squash-merge aware — uses GitHub PR merge state as the merge oracle, not commit ancestry. Use when the user asks to clean up branches after a deploy, prune worktrees, remove merged branches, or confirm nothing stale was left behind before pruning.
npx skillsauth add shipshitdev/library release-cleanupInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Confirm a release branch's work has reached the trunk (default branch), then prune the feature branches and git worktrees that are no longer needed. Verification is a hard gate: never prune until each branch's work is proven to have reached the trunk and no in-flight work is stranded.
This skill is standalone and manually triggerable. It does not promote code (use
release-pr-gates for that) and does not deploy (use deploy). It runs after a
promotion has landed and tidies up.
Commit ancestry is NOT a reliable merge signal. GitHub's default merge mode is
squash, which collapses a branch into a single new commit on the base. After a
squash merge the branch tip is not an ancestor of the base, so git branch --merged / --no-merged and A..B ranges all report a fully-merged branch as
unmerged. Rebase-merges have the same property.
Consequence if you trust ancestry on a squash repo:
git branch -d refuses every local merged branch.Therefore this skill's merge oracle is GitHub PR state first, ancestry second:
A branch's work is IN the production branch iff EITHER
MERGED and that PR's mergeCommit is an ancestor of the
production branch (covers squash, rebase, and merge-commit), OROnly branches that satisfy this are prunable. Everything else is reported, never deleted.
Inputs:
gh repo view --json defaultBranchRef if not suppliedverify (gate only), dry-run (default, plan only), or prune (execute after confirmation)Outputs:
Creates/Modifies:
git worktree prunegit remote prune)External Side Effects:
git push origin --deleteConfirmation Required:
Delegates To:
release-pr-gates when a branch is NOT yet merged into the trunk and the user wants to open or land the PR firstgh-fix-ci when a PR targeting the trunk is still open with failing checksgit-safety when a branch about to be pruned may contain secrets in history worth scrubbing firstDo not use this skill to promote code or to delete unmerged work. It only removes what is provably in the production branch.
Protected branches are never deleted:
master main (trunk / default branch) + the currently checked-out branch + HEAD
Hard rules:
git branch --merged alone. A branch is prunable only when its
work is proven to be in the production branch.dry-run: print the exact plan and stop. Deletion only
happens in prune mode after the user confirms the printed plan.git branch -D (force local delete) is used ONLY for a local branch the oracle
has proven is in the production branch — squash/rebase merges legitimately
require it because -d cannot see them. For any branch NOT proven-in-prod,
force flags are never used; report it instead.git worktree remove --force and deleting a remote branch the oracle has NOT
proven-in-prod are never done automatically.gh auth status -h github.com
git status -sb
git remote -v
git fetch --all --prune
gh repo view --json nameWithOwner,defaultBranchRef --jq '.defaultBranchRef.name'
Determine the trunk from the repo metadata:
gh repo view --json defaultBranchRef --jq .defaultBranchRef.name. Never hardcode master or main.Snapshot every PR once — this is the data the Merge Oracle runs against:
gh pr list --state all --limit 1000 \
--json number,headRefName,baseRefName,state,mergedAt,mergeCommit \
> /tmp/rc_prs.json
Raise --limit if the repo has more open+closed PRs than that.
For each candidate branch (feature branches targeted for cleanup), verify that its work has reached the trunk. Ancestry is the first signal, but squash merges require corroboration against the latest merged PR for that branch before declaring work missing.
TRUNK=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
# Show commits on a candidate branch not yet in the trunk
git log --oneline origin/${TRUNK}..origin/<branch>
# Check the PR that merged this branch into the trunk
gh pr list --base ${TRUNK} --head <branch> --state merged --limit 1 \
--json number,mergedAt,mergeCommit
Interpreting a non-empty result:
Run the Merge Oracle over every non-protected remote branch. Do NOT use
git branch -r --no-merged for this — it lies on squash repos.
TRUNK=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
PROD="origin/${TRUNK}"
classify_branch() { # arg: branch name without origin/
local b="$1" rec st mc base num
rec=$(jq -c --arg b "$b" \
'[.[]|select(.headRefName==$b)]|sort_by(.number)|last' /tmp/rc_prs.json)
if [ -z "$rec" ] || [ "$rec" = "null" ]; then
git merge-base --is-ancestor "refs/remotes/origin/$b" "$PROD" 2>/dev/null \
&& { echo "PRUNABLE_NO_PR_FF"; return; }
echo "STRANDED_NO_PR"
return
fi
st=$(jq -r '.state' <<<"$rec")
mc=$(jq -r '.mergeCommit.oid // empty' <<<"$rec")
base=$(jq -r '.baseRefName' <<<"$rec")
num=$(jq -r '.number' <<<"$rec")
case "$st" in
OPEN) echo "IN_FLIGHT_OPEN_PR(#$num->$base)";;
CLOSED) git merge-base --is-ancestor "refs/remotes/origin/$b" "$PROD" 2>/dev/null \
&& echo "PRUNABLE_CLOSED_PR_IN_PROD(#$num)" \
|| echo "STRANDED_CLOSED_UNMERGED(#$num)";;
MERGED)
if [ -n "$mc" ] && git merge-base --is-ancestor "$mc" "$PROD" 2>/dev/null; then
echo "PRUNABLE_IN_TRUNK(#$num)"
elif git merge-base --is-ancestor "refs/remotes/origin/$b" "$PROD" 2>/dev/null; then
echo "PRUNABLE_IN_TRUNK(#$num)"
else
echo "MERGED_NOT_YET_IN_TRUNK(#$num->$base)"
fi;;
esac
}
# Drive it over all non-protected remote branches:
git branch -r --format '%(refname:short)' \
| grep -v -- '->' \
| sed 's#^origin/##' \
| grep -vxE "origin|${TRUNK}|HEAD" \
| while read -r b; do printf '%-50s %s\n' "$b" "$(classify_branch "$b")"; done
Buckets and what they mean:
PRUNABLE_* — work is in the trunk. Safe to prune.MERGED_NOT_YET_IN_TRUNK — PR was merged into an intermediate branch that has not
yet been merged into the trunk. NOT prunable yet; this is a real "not yet in trunk"
signal for that branch. Report it.IN_FLIGHT_OPEN_PR — open PR. In progress. Skip, never prune.STRANDED_* — no merged PR and not in the trunk. Genuinely forgotten work.
Report loudly, never prune.Gate outcome:
release-pr-gates.STRANDED_* branch => report as a warning; the user decides whether it was
meant to ship. This is the "nothing is stale" guarantee.The prunable remote set is exactly the branches the oracle tagged PRUNABLE_* in
Phase 2b. Now compute the local and worktree sets the same way.
TRUNK=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)
CURRENT="$(git symbolic-ref --quiet --short HEAD || echo)"
PROTECT="${TRUNK}|${CURRENT:-__none__}"
# Local branches — classify each with the SAME oracle (reuse classify_branch,
# but test the LOCAL ref, not origin/, for the ancestry fallback).
git branch --format '%(refname:short)' \
| grep -vxE "$PROTECT" \
| while read -r b; do
rec=$(jq -c --arg b "$b" \
'[.[]|select(.headRefName==$b)]|sort_by(.number)|last' /tmp/rc_prs.json)
mc=$(jq -r '.mergeCommit.oid // empty' <<<"${rec:-null}")
st=$(jq -r '.state // empty' <<<"${rec:-null}")
if { [ "$st" = "MERGED" ] && [ -n "$mc" ] \
&& git merge-base --is-ancestor "$mc" "$PROD" 2>/dev/null; } \
|| git merge-base --is-ancestor "$b" "$PROD" 2>/dev/null; then
echo "PRUNABLE_LOCAL $b (needs -D if squash-merged)"
else
echo "KEEP_LOCAL $b ($st)"
fi
done
# Worktrees
git worktree list --porcelain
For each worktree other than the main checkout, classify it:
git -C <path> status --porcelain empty => safe to remove.Print the plan as three explicit lists — local branches, remote branches,
worktree paths — each annotated with the oracle verdict and PR number, plus a
skipped list with reasons (MERGED_NOT_YET_IN_TRUNK, IN_FLIGHT_OPEN_PR,
STRANDED_*, dirty worktree). Then stop and ask for confirmation. In dry-run
(default) and verify modes, end here.
prune Mode, After Confirmation)Only after the user confirms the printed plan:
# Local branches proven-in-prod. Try -d first; fall back to -D ONLY when the
# oracle proved the branch is in prod (squash/rebase merges require it).
for b in <prunable-local-branches>; do
git branch -d "$b" 2>/dev/null || git branch -D "$b"
done
# Remote branches proven-in-prod
git push origin --delete <branch> ...
# Worktrees flagged safe
git worktree remove <path> ... # never --force; refuses on dirty
git worktree prune
# Drop stale remote-tracking refs
git remote prune origin
git fetch --all --prune
Rules during execution:
-D is permitted ONLY for branches the Phase-3 oracle tagged PRUNABLE_LOCAL.
Never blind-force a branch that is not proven-in-prod.git worktree remove refuses (dirty/locked), do not --force. Report and skip.release-cleanup verify — Phase 1 + 2 only. Report trunk verification status and the branch classification. No plan, no deletion.release-cleanup or release-cleanup dry-run — Phases 1-3. Verify, then print the prune plan. No deletion. (Default.)release-cleanup prune — Phases 1-4. Verify, print plan, confirm, then delete.If the user explicitly scopes the cleanup ("only worktrees", "local branches only", "skip remote"), honor it: still run verification, but restrict the plan and execution to the requested resource types.
Report:
STRANDED_*), if anyMERGED_NOT_YET_IN_TRUNK), if any-D was needed)development
TypeScript refactoring and modernization guidelines from a principal specialist perspective. This skill should be used when refactoring, reviewing, or modernizing TypeScript code to ensure type safety, compiler performance, and idiomatic patterns. Triggers on tasks involving TypeScript type architecture, narrowing, generics, error handling, or migration to modern TypeScript features.
tools
Resolves TypeScript and JavaScript problems across type-level programming, performance, monorepo management, migration, and modern tooling. Invoke when diagnosing "type instantiation excessively deep" errors, migrating JS to TS, configuring strict tsconfig, debugging module resolution, or choosing between Biome/ESLint/Turborepo/Nx.
tools
Turborepo monorepo build system guidance. Triggers on: `turbo.json`, task pipelines, `dependsOn`, caching, remote cache, the `turbo` CLI, `--filter`, `--affected`, CI optimization, environment variables, internal packages, monorepo structure, and package boundaries. Use when the user configures tasks or workflows, creates packages, sets up a monorepo, shares code between apps, runs changed packages, debugs cache behavior, or works in an `apps/` plus `packages/` workspace.
tools
Provides Tailwind CSS v4 performance optimization and best practices guidelines. Triggers when writing, reviewing, or refactoring Tailwind CSS v4 code; when working with Tailwind configuration, @theme directive, utility classes, responsive design, dark mode, container queries, or CSS generation optimization.