merge-it — merge a ready PR, then wrap up automatically
The active counterpart to post-merge. When I say “merge it”, I mean: do the merge now (the PR is ready), then run the whole post-merge wrap-up (verify → tidy → UMS) on your own, without asking. Asking “want me to run UMS?” after merging is the exact gap this skill closes — the answer is a standing yes (see preferences.md).
When this fires
- “merge it”, “merge this”, “merge the PR”, “go ahead and merge” (as an explicit merge directive). Deliberately excludes vague approval like “ship it” / “lgtm” — merging is irreversible, so require an explicit merge verb.
- Distinct from
merge-main/sync-pr-branch(those mergemaininto a branch to sync it — they do NOT merge the PR). - If the PR is already merged, skip steps 2–3 and go straight to step 4 (
post-merge).
Procedure
1. Identify the PR and confirm it’s ready — never assume
- Resolve which PR is meant (the one from the current session; if ambiguous, ask which number).
- Then record that PR’s
headRefOidandbaseRefNamebefore the readiness check, so its result is tied to one head and one target, and require both live values to equal them immediately before every direct merge, on the initially current path as much as after a recovery. - Confirm it is fully clean before merging (the ARDI terminal state — see
shared/workflow/fully-clean.md): every CI workflow and check run — not just required ones — is green and completed (never still queued or in progress) AND every reviewer’s latest verdict is clean. Verify with a fresh query, not a cached verdict:mcp__github__pull_request_read(getformergeable_state,get_check_runsfor CI) — orgh pr view <N>/gh pr checks <N>in a local session.get_check_runs/gh pr checksonly cover check runs (plus legacy statuses), not every raw Actions workflow run — seefully-clean.md’saction_required-with-zero-jobs gotcha if something looks off despite an all-clear checks view. - If the PR changes a rendered document (Word, PDF, slides, a manuscript), confirm the page-by-page review of the render at the current head has been done and its evidence posted on the PR, along with an independent referee read of that render (
review-rendered-documents). If it has not, do it now or stop; never merge on green CI alone. - Check
mergeStateStatusin addition tomergeable. A PR can be"MERGEABLE"but"BLOCKED"when branch protection requires at least one approving review and only bot/comment reviews exist. Fix: requestthe repository owneras reviewer (gh pr edit <N> --add-reviewer <reviewer>—EDIT_PR) and leave a note that the PR is clean and ready. Don’t attempt to force-merge. Except inLacaedemon/sparta, which never requeststhe repository owner— seerequest-pr-review’s Scope section. There, leave the ready-for-merge note and surface the block to the user instead. This step names the rawgh pr editcommand rather than deferring to that skill, so it does not inherit the exception on its own. - If any CI workflow or check run is red or still in progress/queued/pending, or the review still has open findings, do not merge. Report what’s blocking instead. (Only merge a not-clean PR if the user explicitly says to anyway.)
For a GitLab MR, use python3 scripts/check-mr-fully-clean.py <iid> --project <id-or-path> --quorum <number-of-reachable-providers> instead of the GitHub checker. It must print the current full head SHA and a clean verdict. Before the merge call, re-read the MR and confirm its sha and target_branch are unchanged. Use glab api --method PUT "projects/<project>/merge_requests/<iid>/merge" -f "sha=<pinned-sha>" -f "auto_merge=false" or the equivalent API client; GitLab rejects the request when the source head has moved.
2. Merge
Before the merge command, run the base-currency check that
fully-clean.mdstates in its stale-base rule (the Do bullets beginning “for a direct merge”), by merge mode. In a local session, fetch the PR’s configured base andrefs/pull/<N>/headfrom the-Rrepository and confirm the merge-base is the base tip. In a remote session withoutgit, readgh api "repos/<owner>/<repo>/compare/<base-encoded>...<head-sha>"(the base name encoded as one path segment,jq -rn --arg b "<base>" '$b|@uri') and requirebehind_byof 0, recordingbase_commit.shaas<pinned-tip>for the pre-merge recheck of the same endpoint, which requiresbehind_byof 0 again andbase_commit.shaequal to that pin. Where neither is available, do not merge until #2982 supplies the tool. On a base that requires a merge queue, stop and report: the queue form of this gate is #3030 and is out of scope until it lands. It is a manual step until #2982 wires it intocheck-pr-fully-clean.pyfor GitHub; the GitLab path is instrumented bycheck-mr-fully-clean.py, and on a repository that does not require an up-to-date branch GitHub would otherwise permit the stale merge, which is why the manual check stays required there. When it fails on a direct merge, update the branch pinned to the recorded head. Locally:gh api -X PUT "repos/<owner>/<repo>/pulls/<N>/update-branch" -f expected_head_sha="<pinned-sha>". Remotely:update_pull_request_branchwithexpectedHeadSha. A422whose message names an expected-head mismatch is the another-writer signal only when the liveheadRefOidhas actually changed from<pinned-sha>— re-read it and compare before settling ownership, since a correctly-lengthed but wrong-content SHA (guessed or padded from an abbreviation instead of read in full) produces the byte-identical message with no other writer involved. Match on the substringexpected head sha, since the live text carries a curly apostrophe and a trailing period that this ASCII rendering cannot show. Any other422is a failed update to stop on. Then wait untilheadRefOidchanges (the update answers202 Acceptedbefore the merge lands), with a deadline of a few minutes, treating expiry as a failed update to stop on and report, record that SHA, rerun the base-currency check on it, and then rerun the whole clean gate pinned to it. Immediately before the merge command, check that the live head is still that SHA, thatbaseRefNameis unchanged, and that the live base tip still equals the<pinned-tip>the currency check printed (recorded before the gate reran, since a shell variable does not survive into a later tool call), and repeat the cycle if any moved during the gate (a concurrent push can pass a currency-only recheck while the gate covered the earlier head). Then pass the pin to the merge itself,--match-head-commit "<pinned-sha>"(orexpectedHeadShaon the MCP merge tool), so a push after the read is refused rather than merged. That closes the head side only. The base can still advance between the read and the merge, and where that must not happen the repository needs a merge queue or an up-to-date-branch requirement with every clean-gate check required, perfully-clean.md. A repeat names the moving ref, not the remedy. When the base moved twice it outruns the gate: merge under strict up-to-date protection instead (or through a merge queue once #3030 lands), perfully-clean.md. When the head moved, another writer is on the branch: settle ownership perclaim-prbefore rerunning, since no queue or protection setting stabilizes a head someone else pushes to.For GitLab, if the currency check fails, call
glab api --method PUT "projects/<project>/merge_requests/<iid>/rebase", poll the MR withinclude_rebase_in_progress=trueuntilrebase_in_progressis false, and reruncheck-mr-fully-clean.pyfrom the beginning on the new SHA. Do not useauto_merge=trueas a substitute for the pinned clean gate; it is the GitLab analogue of deferred auto-merge and does not pin the reviewed head.Default to squash for a feature branch with many small iteration commits (and/or a merge-of-main commit) — it gives
mainone clean commit. Use a plain merge commit only if the user asks or the repo clearly prefers it; don’t stop to ask for a method on a routine feature PR.Give the squash an accurate commit title and body when the PR body has gone stale across the review loop — pass
commit_title/commit_messagerather than letting GitHub paste the outdated description. KeepCloses #Nin the message so the linked issue auto-closes.If
gh pr mergefails withHead branch is out of date, the base advanced after the pre-merge read: go back through the stale-base recovery above (the pinned update, the bounded wait, the currency check, and the whole clean gate on the new head) rather than syncing by hand. The pinned update resolves the PR’s configured base itself (stacked and release PRs target a branch other thanmain, and the endpoint reads that from the PR), and the pin refuses to update over a concurrent push, which a hand merge would not. The update creates a new head SHA, so the clean verdict that authorized the first attempt no longer applies, and a repository that does not make every workflow and review a required check would otherwise merge an unreviewed head, which is why that cycle reruns the whole gate before its own pinned merge attempt. If the merge still fails, don’t compare againstoriginblindly — for a cross-fork PR,originis the base repo, not necessarily where the head branch lives, sogit ls-remote origin refs/heads/<branch>can silently read a missing ref or an unrelated same-named branch in the base repo. Get the actual head repo and ref from the PR API first (gh pr view <N> --json headRepositoryOwner,headRepository,headRefName), then query that repo’s ref (gh api repos/<head-owner>/<head-repo>/git/refs/heads/<head-ref> --jq .object.sha— verified this endpoint works) and compare it against the PR API’s own.head.sha(gh api repos/<owner>/<repo>/pulls/<N> --jq .head.sha) — the PR object can lag the branch ref briefly, so the correct response is to wait until the two agree, not to keep retrying blindly. Only use--adminas a last resort when the user has separately and explicitly authorized the branch-protection bypass itself — ordinary merge authorization does not cover it (seepreferences.md).
# MERGE_PR — remote/web (GitHub MCP):
# mcp__github__merge_pull_request merge_method=squash expectedHeadSha=<pinned-sha> commit_title=… commit_message=…
# local (the pin is the headRefOid recorded before the readiness check):
gh pr merge "<N>" -R "<owner>/<repo>" --squash --match-head-commit "<pinned-sha>" --subject "<title>" --body "<accurate summary; Closes #N>"The -R is load-bearing, not tidiness — a bare gh pr merge <N> refuses even from inside the repo it would merge into. Two PreToolUse guards gate the command, and with no session grant in play the bare form trips both. hooks/require-gh-repo-flag.py blocks any mutating repo-scoped gh command without -R regardless of any grant, and hooks/no-unauthorized-merge.py reads the merge’s target repository off the command text only, never off the working directory — so a merge naming no repo has no derivable target for a standing per-repository grant (Morrison-Lab/ai-config, the macros repo) to attach to. Naming the repo is what makes that grant applicable, and it is the whole difference between the two commands.
An active /mwc is the exception to the second guard and not to the first: the session grant is keyed on a marker file in the current checkout rather than on the command’s target, so it clears no-unauthorized-merge.py for a bare merge — and require-gh-repo-flag.py refuses it anyway. Write the -R either way.
See mwc, “Three things the standing grant deliberately does not cover”, for the guard’s own reasoning.
In remote/web sessions, load the merge tool’s schema with ToolSearch (select:mcp__github__merge_pull_request) before the first call to confirm the exact name and parameters – tool-mappings.md is the canonical gh→MCP reference (per CLAUDE.md’s “Skills that call gh/glab” rule); Morrison-Lab/gha’s own CLAUDE.md carries an equivalent table for that repo.
3. Verify the merge landed — never assume
Confirm merged == true (the merge tool’s result) and re-check the PR state and that the linked issue auto-closed. A base that requires a merge queue never reaches this step from an agent path: #3030 carries the queue form (enrollment is asynchronous and needs its own state machine), and until it lands the procedure stops before the merge command there. If the merge didn’t land (conflict, branch protection, not mergeable), stop and report — don’t tidy or run UMS.
4. Chain into post-merge — automatically
Run the post-merge skill (invoke it by name) for the rest: tidy the local branch (checkout main, pull, git branch -d, remove any worktree), confirm deferred items are tracked, and run UMS to bank what the PR’s review lifecycle taught. Do this without a separate prompt — opening the UMS follow-up branch + PR is a standing yes (preferences.md).
The merge is not done at step 3. post-merge’s own step 1.1 (verify-merge-commit-ci) checks that every workflow triggered on the merge commit itself — not only the PR’s own CI — actually passed, since a repo can run a workflow on push (a full/PDF render, a deploy) that never ran on the PR at all. Don’t report the merge finished between steps 3 and 4; step 4’s chain into post-merge is what actually runs that check.
Relationship to other skills
post-merge— step 4 delegates to it.post-mergeassumes the PR is already merged (verify → tidy → UMS);merge-itadds the actual merge in front of it for the “it’s clean, merge it” case.merge-main/sync-pr-branch— mergemainINTO a PR branch to sync; unrelated to merging the PR itself. Don’t confuse the trigger words.ardi/iterate— the loop that gets a PR to fully-clean;merge-itis what you run once it’s there and the user says go.ums— the learnings steppost-mergeruns at the end.wrap-up/merged— session-level bookend;merge-itis per-PR.
Anti-patterns
- ❌ Asking “want me to run UMS / wrap up?” after merging — it’s automatic.
- ❌ Reporting the merge as done and moving on (including to other things the user said while the merge was in flight) without actually running step 4 — a busy, multi-threaded conversation makes this easy to drop, but the chain isn’t optional follow-up; it’s part of the merge action itself. See
mwc’s own anti-pattern entry for the concrete case this happened in. - ❌ Reporting the merge done on the strength of the PR’s own green CI, without
post-merge’s step 1.1 confirming the merge commit’s own workflow runs also passed — a push-only render or deploy never ran on the PR (Morrison-Lab/mds#19). - ❌ Merging a PR that isn’t fully clean (red or still-in-progress CI, or open findings) without the user explicitly saying so.
- ❌ Letting the squash commit inherit a stale PR description — pass an accurate title/body when the body no longer matches the final diff.
- ❌ Confusing “merge it” (merge the PR) with “merge main” (sync the branch).
- ❌
git branch -D(force) in the tidy without checking why-drefused. - ❌ Stopping at a post-merge summary table without emitting an explicit stopping-point statement (stating whether or not a clean stopping point was reached) per
wrap-up’s closing checklist.