mcpbeat

Post Merge Cleanup

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}}.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
138
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/arbiterForge/codeArbiter --skill post-merge-cleanup

The instruction itself

9 sections, as written by the author

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.

Pre-flight

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}}.

Phase 1 — Prove the merge · gate: BLOCK

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.

  • Confirm the current branch is an ancestor of the fetched default

(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.

  • Report the proof as a fact — the default branch, the fetched SHA, and that

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.

Phase 2 — Classify the residue · gate: BLOCK

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:

  • Redundant — byte-identical to content already on the default branch, or a

regenerated build artifact whose generator is committed and rerunnable. Safe to

remove because removing it loses no information.

  • Superseded — an earlier form of something the merged PR already landed in a

better shape. Name what supersedes it.

  • Unique — anything else. This is the default: an artifact that cannot be

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.

Phase 3 — Resolve the residue · gate: STOP

Per item, in the order Phase 2 listed them. Never batch:

  • Unique — offer to keep it: {{CMD:commit}} it on this branch before the

transition, 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.

  • Redundant / superseded — offer removal, one confirmation each, stating what

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.

Phase 4 — Transition · gate: BLOCK

Only after Phase 3 leaves the tree safe:

  • Check out the default branch, then verify the checkout actually happened

(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.

  • Fast-forward with --ff-only. A divergence means the default branch moved in

a 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.

Phase 5 — Delete the merged local branch · gate: STOP

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 -d

refuses, the ancestry proof and the refusal disagree — report both and stop.

  • The remote branch is never touched. If the user wants it gone, that is

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.

Phase 6 — Receipt

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.

Hard rules

  • MUST fetch and prove HEAD is an ancestor of the fetched default branch before

any deletion. MUST NOT infer merge state from a : gone] upstream alone.

  • MUST classify every artifact, and MUST treat anything not provably redundant or

superseded as unique.

  • MUST NOT discard a unique or unclassifiable artifact without an explicit

confirmation naming that item.

  • MUST confirm every removal and the branch deletion individually — no batched or

implied yes.

  • MUST re-read the current branch after checkout before acting on it.
  • MUST use --ff-only, and MUST NOT merge, rebase, or reset to reach the default

branch.

  • MUST use git branch -d, never -D; MUST NOT delete a remote branch,

force-push, or write to the default branch.

  • MUST NOT drop a stash — report and route, as {{CMD:standup}} does.
  • MUST NOT route to {{CMD:override}} when blocked. Name the gate instead.

How to use it

Copy the folder

Take arbiterforge/post-merge-cleanup from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

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.