arbiterforge/post-merge-cleanup
Finish an already-merged branch. Proves the branch is an ancestor of the fetched default, classifies leftover artifacts as unique / redundant / superseded, returns to a clean --ff-only default checkout, and deletes the merged local branch — every discard confirmed per item. Routed to by {{CMD:cleanup}}.
npx skills add https://github.com/arbiterForge/codeArbiter --skill post-merge-cleanup
The walk back from a merged PR. Routed to by {{CMD:cleanup}}.
The work has landed on the default branch. What is left is local: the branch you
are standing on, and whatever the run left in the tree. This skill returns you to
a clean, fast-forwarded default checkout without losing anything you have not
explicitly agreed to lose.
It is ordinary lifecycle work and never needs {{CMD:override}}. When it
cannot proceed, it names the gate that stopped it. Issue #308 recorded the
alternative: with no owner for this transition, the routing loop reached for
{{CMD:chore}}, then for {{CMD:override}} — a bypass manufactured to cover a
coverage hole.
Read this, or STOP and surface the gap — never guess the default-branch name:
{{PROJECT_DIR}}/.codearbiter/CONTEXT.md — the default-branch name.If HEAD is already the default branch, there is no transition to make. Report the
dirty state read-only and exit; branch pruning across *other* branches belongs to
{{CMD:standup}}.
A branch that looks merged is not merged. Establish it against the network, not
against a stale local ref:
git fetch the remote holding the default branch. A fetch that fails STOPs —an unfetched comparison proves nothing.
(git merge-base --is-ancestor HEAD origin/<default>). This is the test that
matters, and it is deliberately not : gone] upstream state: a squash-merged
branch whose remote still exists is fully contained in the default branch and
is safe to delete, while a : gone] branch whose commits never landed is not.
HEAD is contained in it.
If HEAD is not an ancestor, STOP. Name the un-landed commits and route to
{{CMD:pr}}; nothing is deleted here.
Gate: the remote is fetched and HEAD is proven an ancestor of the fetched default
branch — or the skill has stopped.
List every dirty tracked change, every untracked file, and every stash reachable
from this branch. Classify each into exactly one of three, and say *why* for each:
regenerated build artifact whose generator is committed and rerunnable. Safe to
remove because removing it loses no information.
better shape. Name what supersedes it.
proven redundant or superseded IS unique. Uncertainty classifies as unique, not
as redundant.
Present the classification before touching anything. A file whose class you
cannot establish is reported as unique with the reason you could not classify it.
Gate: every dirty, untracked, and stashed artifact carries a class and a stated
reason, with unclassifiable items counted as unique.
Per item, in the order Phase 2 listed them. Never batch:
{{CMD:commit}} it on this branch before thetransition, move it aside, or leave it in place and stop the cleanup. It is
discarded only if the user explicitly confirms *that item by name*, with the
Phase 2 reasoning in view.
it is and why it is safe. Declining leaves it exactly where it is.
A stash is never dropped here. Stashes are reported with git stash show as the
suggested next step, the same report-and-route contract {{CMD:standup}} holds.
If anything the user chose to keep would block the checkout, STOP and say so
rather than removing it anyway. A blocked checkout is a fine outcome; a silent
discard is not.
Gate: every item is resolved by an explicit per-item decision, and the working
tree is clean enough to check out the default branch — or the skill has stopped
with the blocker named.
Only after Phase 3 leaves the tree safe:
(git branch --show-current). A checkout silently fails when another worktree
holds the branch, and every step after this one would otherwise run against
the wrong branch.
--ff-only. A divergence means the default branch moved ina way this skill will not reconcile: report it and stop. Never a merge commit,
never a rebase, never a reset.
Gate: HEAD is confirmed on the default branch by re-read, and the fast-forward
either succeeded or was reported as a refused divergence.
Offer deletion of the now-merged local branch, with the Phase 1 ancestry proof
restated. One confirmation, naming the branch.
git branch -d only. Never -D: the safety check is the point, and if -drefuses, the ancestry proof and the refusal disagree — report both and stop.
theirs to do or the platform's auto-delete-on-merge to do.
Declining leaves the branch in place. That is a normal outcome, not a failure.
Gate: the branch is deleted only after an explicit confirmation naming it, via
-d, with the remote untouched.
One short summary: what landed, what was removed, what was kept, and where HEAD
is now. State declines as declines — a cleanup the user stopped halfway is a
correct outcome reported plainly, not an error.
HEAD is an ancestor of the fetched default branch beforeany deletion. MUST NOT infer merge state from a : gone] upstream alone.
superseded as unique.
confirmation naming that item.
implied yes.
--ff-only, and MUST NOT merge, rebase, or reset to reach the defaultbranch.
git branch -d, never -D; MUST NOT delete a remote branch,force-push, or write to the default branch.
{{CMD:standup}} does.{{CMD:override}} when blocked. Name the gate instead.Take arbiterforge/post-merge-cleanup from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.