post-merge — wrap up a merged PR (verify, tidy, then UMS)
The per-PR bookend to a piece of work. Once a PR/MR merges: confirm it landed, clean up the local branch, make sure nothing was left dangling, and — the point of the skill — run UMS to learn from how the PR went while the review lifecycle is still fresh in context.
When this fires
- A PR/MR you were working on just merged.
- “post-merge”, “wrap up the merged PR”, “clean up after the merge”, “the PR merged — now what?”
- “merge it” / “merge this” route to
merge-it, not here — that skill performs the merge first, then chains into this one. Only handle those phrases here when the PR is already merged (no merge left to do). - Distinct from
wrap-up(session-level, may span several PRs/issues) —post-mergeis the single-PR version, run each time a PR lands.
Procedure
1. Verify the merge — never assume
gh pr view <N> --json number,title,state,mergedAt,mergeCommit,headRefName # VIEW_PR
# GitLab
glab mr view <N>Confirm state == MERGED and mergedAt is set. If it isn’t actually merged, stop and report — don’t tidy a branch whose work hasn’t landed. (The standing never assume; always verify rule applied to closing out a PR.)
1.5. Cascade conflict scan
In an ultracode/coordinator session, delegate this whole step to a subagent rather than running the scan-and-resolve loop in the main thread — it’s exactly the kind of investigation-plus-fix work the coordinator should hand off (see memories/preferences.md’s coordinator-mode bullet). Brief the subagent with the merged PR’s number/branch and the steps below; have it report back which PRs it found conflicting, what it did about each, and any it skipped (already claimed, conflict it couldn’t understand). Do the scan inline only for a solo (non-orchestrated) session.
If any OTHER agents already own a claimed branch (an active, resumable Agent-tool session, not a one-shot Workflow-internal agent() call), message each one directly right after the merge, instead of relying solely on a separate scan to find and fix their conflict after the fact: “main just advanced (PR #N merged) — fetch and merge origin/main into your branch now, resolve any conflict yourself (you have the context on your own change), then continue.” This is faster and higher-context than a scanning subagent guessing at the resolution from outside: the branch’s own owning agent already knows why its code looks the way it does.
This depends on the coordinator finding out about a merge in the first place — so brief every delegated agent, up front, to report back the instant its OWN tracked PR merges, not just when its ARDI work is done. A PR sitting “ready for merge” isn’t the end of that agent’s watch: keep polling until the merge actually happens — by a human, since the agent itself must not self-merge — then notify the coordinator immediately. This is what lets the coordinator fan out the “merge main now” nudge above to every OTHER live agent right when it matters, instead of the coordinator having to separately poll every open PR’s merge state itself to notice. Fold this into the standard delegation brief (see gia/gii’s per-issue agent prompts) alongside the no-self-merge instruction, rather than treating it as a one-off ask.
Reserve the scan-and-fix subagent above for branches with NO active owning agent (e.g. a completed Workflow run’s one-shot agent that already returned).
A squash-merge on main can knock previously-mergeable open PRs into conflict. Scan right after the merge is confirmed:
gh pr list --state open \
--json number,title,headRefName,mergeable,mergeStateStatus,comments # LIST_PRSFor each PR where mergeable == "CONFLICTING" or "UNKNOWN" (GitHub can take minutes to finish computing mergeability after a push — a genuinely conflicting PR can sit in UNKNOWN and get missed if you filter for CONFLICTING alone):
Verify before claiming — don’t trust the flag alone. See
resolve-conflicts, “Verify before you act”:git merge-tree --write-tree origin/main origin/<branch>gives ground truth without a worktree (git ≥ 2.38). Skip if it comes back clean.Check claim status. Read the most recent comment. If it says “Working on this — paws off” (or equivalent), skip it — another session owns it.
Claim it.
gh pr comment <N> --body "Working on this — paws off until I'm done." # COMMENT_PRCreate an isolated worktree, fetch the latest
main(the squash-merge commit that caused the conflict), and merge:git fetch origin main <branch> # FETCH — fetch both: we need the new main tip git worktree add .claude/worktrees/pr-<N> origin/<branch> cd .claude/worktrees/pr-<N> git checkout -b <branch> # or --track origin/<branch> if the name is free git merge origin/main # MERGE_BRANCH — picks up the new squash-merge commitResolve conflicts using the
resolve-conflictsskill (consolidate both sides’ intent; do not blindly pick one side wholesale).Run the repo’s pre-commit checks,
git fetchthe branch again, then push. The claim comment isn’t an atomic lock — a repo’s own automated bot (e.g. an@claudeCI agent triggered independently by the same merge event) can pick up and resolve the identical cascade conflict in parallel even when no claim comment was posted. If the fetch shows the remote has moved with an equivalent fix already pushed, adopt it (verify withgit merge-tree --write-tree origin/main origin/<branch>[git ≥ 2.38] — no remaining conflict — plus a content diff against what you were about to push) instead of force-pushing a duplicate merge commit. Only push your own resolution if the remote is still where you left it.git fetch origin <branch> # FETCH # If origin/<branch> already carries an equivalent fix, stop here — don't push. git diff HEAD origin/<branch> # empty/equivalent means the bot beat you to it git push origin <branch> # PUSH — only if the remote hasn't moved cd - git worktree remove .claude/worktrees/pr-<N>Unclaim with a brief resolution summary:
gh pr comment <N> --body "Conflict resolved — branch is now mergeable. <one-line summary of what conflicted and how it was resolved>" # COMMENT_PR
Resolve PRs one at a time — not because worktrees race each other (each worktree is an independent checkout), but because the same human or bot may be actively working a PR between your claim and your push. One-at-a-time keeps the blast radius small. Skip any PR whose conflict is in a file you can’t understand without more context — comment asking for clarification instead.
2. Tidy the local branch
if [ "$(git branch --show-current)" != "main" ]; then
git checkout main
fi
git fetch origin main
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then
git pull --ff-only origin main
fi
if git show-ref --verify --quiet "refs/heads/<merged-branch>"; then
git branch -d <merged-branch> # -d, NOT -D
fiUse git branch -d (not -D): -d refuses to delete a branch with commits that aren’t merged. If it refuses, the branch has unmerged work — investigate before forcing anything.
These guards avoid a common no-op/error sequence after gh pr merge --delete-branch: that command may already switch this checkout to main, fast-forward it, and delete the local merged branch.
If the PR was built in a git worktree (agent isolation or session-lock), remove the worktree as part of the tidy — a worktree pins its branch, so git branch -d refuses while the worktree still holds it (“branch is checked out at
git worktree list # find the merged branch's worktree path
git worktree remove <path> # refuses on a dirty tree — don't blindly --force
git branch -d <merged-branch> # now succeedsWorktree containing a submodule: git worktree remove <path> refuses with fatal: working trees containing submodules cannot be moved or removed even when the tree is perfectly clean (git status --short empty) — this is a different refusal than the dirty-tree one above, triggered by the mere presence of a submodule, not by uncommitted state. Confirm clean with git status --short first (as always), then git worktree remove --force <path> is the correct move here, not a sign of misclassification.
Running from within the worktree: if post-merge fires while the shell is inside the worktree being tidied, git worktree remove <path> fails (“cannot remove the currently checked out worktree”) and git checkout main is blocked (worktrees are locked to their branch). Use the repo root instead:
REPO="$(git rev-parse --git-common-dir)/.." # main checkout root
git -C "$REPO" worktree remove "$(git rev-parse --show-toplevel)" # worktree root
git -C "$REPO" branch -d <merged-branch>Diverged main checkout: git pull --ff-only fails when the main checkout has local commits from a concurrent session that hasn’t been pushed. Don’t force-merge or reset their work — skip the pull and delete the branch only. The branch deletion is what matters; another session will pull main when it’s ready. If git branch -d refuses because local main doesn’t yet include the merge (diverged HEAD, remote branch already deleted), use git branch -D — step 1 already confirmed the PR is merged, so the force-delete is safe here.
For a repo-wide sweep of all dead worktrees (not just this PR’s), run clean-worktrees (cw).
If other local branches were stacked on this one, offer to rebase them onto the new main rather than deleting silently (see cb / clean-branches).
3. Confirm deferred items are tracked
If the PR’s review loop deferred or acknowledged anything, make sure each has a follow-up issue (preferences: never leave deferred items untracked). List them, linked. File any that slipped through.
4. Run UMS — learn from the PR’s lifecycle
Run the full ums procedure (invoke the ums skill by name), focused on what this PR taught:
- Recurring review findings — anything the reviewer flagged across rounds → encode the fix so the next PR avoids it from the start.
- Corrections / guidance the user gave mid-PR → preference + skill update (per “update BOTH skills AND preferences”).
- Tool / CI quirks hit during the loop → the matching topical memory file (
github.md,github-actions.md,git.md,r-quarto.md,claude-code.md) ordebugging.md; seememories/MEMORY.md. - A multi-step pattern that emerged → run
spot-skill-opportunitiesto judge whether it’s genuinely recurring, then hand off toskill-builder.
This is the “learn from mistakes and guidance along the way” step — a merge is the natural checkpoint to bank those lessons before the context is gone. If nothing durable emerged, say so explicitly rather than manufacturing edits. (UMS commits its own changes via a branch + PR.)
Guard against recursion: skip this step when the merged PR was itself a UMS/learnings PR — one whose diff is entirely memory/skill edits capturing a previous PR’s lessons — and no new lessons emerged from its own review loop. Re-running UMS there is redundant — the lessons are already encoded in the PR that just merged — and spawns an endless UMS-on-UMS chain (each UMS PR merges → triggers post-merge → triggers another UMS PR). Still do steps 1–3 and 5; just don’t manufacture a fresh UMS PR. (If the UMS PR’s own review surfaced a genuinely new, separate lesson, capture that — but not a restatement of what the PR already banked. Concretely: a reviewer approving with no comments means nothing new, so skip; a reviewer flagging a missing anti-pattern that isn’t already in the UMS diff is a new lesson worth a follow-up.)
5. Report
A linked summary: the merged PR, the auto-closed issue, any deferred follow-up issues, what UMS updated, and a Pacific-time timestamp (TZ=America/Los_Angeles date "+%Y-%m-%d %H:%M %Z"; the explicit TZ enforces PT on a machine set to any other zone).
Relationship to other skills
wrap-up— session-level bookend; also embeds UMS.post-mergeis the per-PR version: run it each time a PR lands; runwrap-uponce at session end. They share the verify-then-UMS shape.ums— step 4 invokes it.spot-skill-opportunities— step 4’s “multi-step pattern that emerged” bullet routes here to judge recurrence before handing off toskill-builder.cb/clean-branches— for stacked or stale sibling branches.clean-worktrees/cw— if the PR was built in a git worktree, remove it during the tidy (step 2); a leftover worktree pins its branch and blocksgit branch -d.st/gi— the front of the lifecycle thatpost-mergecloses.
Anti-patterns
- ❌ Deleting the branch before confirming the merge actually landed.
- ❌ Reaching for
git branch -D(force) without checking why-drefused. - ❌ Force-pulling or resetting a diverged main checkout — the divergence may be another session’s in-progress work. Skip the pull; don’t clobber it.
- ❌ Skipping UMS on a normal PR — the just-merged PR is exactly when the lessons are freshest.
- ❌ Recursing UMS on a UMS PR — running UMS again when the just-merged PR was itself the learnings PR, restating lessons it already banked (see step 4’s guard). The chain has to terminate somewhere.
- ❌ Leaving deferred/acknowledged items without follow-up issues.
- ❌ Reporting “all cleaned up” while a stacked sibling branch dangles unmentioned.