tk-merge
Open the merge stage -- per PR in dependency order, fast-forward master onto the reviewed green head, push it, retarget stacked children, delete the branch, close the issue and delete its plan. Invoke
SKILL.md.claude/skills/tk-merge/SKILL.md
GitHub/tk-merge -- the merge stage
Run /tk-workflow first. An argument, when the owner gave one, is:
The PRs to merge are those of the issues the argument names (an issue number or a PR number), else those the build-stage message of this session names. With neither, stop and ask.
Entry check
Current branch: !git branch --show-current
Working tree (empty means clean):
!git status --short
Master vs origin, commits ahead / behind (0 0 means in sync):
!git fetch -q origin 2>/dev/null; git rev-list --left-right --count master...origin/master
Plans on this machine:
!grep -H -E -r --include='*-plan.md' '^(Issue|Branch|Base|Thread|Status):' .plans 2>/dev/null || echo none
Open pull requests, with the head SHA and the state of each check on it:
!gh pr list --state open --json number,headRefName,baseRefName,headRefOid,statusCheckRollup --jq '.[] | "#\(.number) \(.headRefName) -> \(.baseRefName) \(.headRefOid[0:12]) \([.statusCheckRollup[]? | "\(.name)=\(.conclusion // .status)"] | join(" "))"' 2>&1 || true
CI workflows in the tree (empty means no CI):
!ls .github/workflows/ 2>/dev/null || true
The tree is clean, master is in sync with origin, and for each PR to merge:
it is open; its head SHA on GitHub is the local branch's head (git rev-parse <slug>); and with CI in the tree, every check on that head is
SUCCESS, SKIPPED or NEUTRAL. The checks are read from the list above
or from gh-watch.sh <sha> (exit 0), never from the conversation: a
build-stage message that says green is a claim, and GitHub is the evidence.
Anything else (a dirty tree, a head that differs, a check IN_PROGRESS,
QUEUED or FAILURE, a PR whose base is a branch not being merged in this
run) is a stop for that PR and for every PR stacked on it; say which, and
the owner decides. The others proceed.
Step by step
For each PR, in landing order (a PR after the PR its base branch belongs to):
Make it a fast-forward. When master moved since the branch was cut (an independent PR merged before it),
git merge-base --is-ancestor master <slug>fails: rebase it onto master (git rebase master <slug>), thengit range-diff master <old head> <slug>must show every commit unchanged in content. A conflict, or a commit whose content changed, is a stop: the reviewed code is no longer the code that would merge. Otherwisegit push --force-with-lease, and with CI wait for the new head withgh-watch.sh(exit 0; exit 3 means rerun with the same SHA) before going on. A stacked PR whose parent was fast-forwarded in this run is already a fast-forward.Fast-forward and publish, with the github skill's "Fast-forward merge":
git checkout master git merge --ff-only <slug> git push origin masterNo merge commit, ever. A rejected push (origin moved, a ruleset that wants checks this head lacks) is a stop: report it; never force.
Verify:
gh pr view <n> --json state -q .stateisMERGED. When it is not, report it and leave the PR, the branch and the issue as they are.Retarget every open PR whose base is this branch, before the branch goes:
gh pr edit <child> --base master.Delete the branch locally and on origin:
git branch -d <slug> && git push origin --delete <slug>.Close the issue:
gh issue close <issue> -c "Landed in #<n>".Delete the plan:
rm .plans/<slug>-plan.md. Every reason it carried is in the code or the documents; the build stage's check made sure of that.
Close with one line per PR: <slug>: #<pr> merged, <n> commits, <first sha>..<last sha>, issue #<issue> closed, plus "rebased first" when step 1
rebased, and one line per PR that stopped, with the reason.
Correct: logging (#20) and metrics (#21, stacked on logging) are green and
approved. You fast-forward master to logging's head, push, see #20 MERGED,
retarget #21 to master, delete server-logging, close #12, delete its
plan; metrics is now a fast-forward of master, so you merge it the same
way.
Incorrect: you delete server-logging before retargeting #21, and GitHub
closes #21. Or: the build-stage message said green, the list shows a check
IN_PROGRESS, and the merge runs on the message. Or: the push is rejected
and you retry with --force.