stacked-pr-merge
Skill13 starsChanged 11 months agoMITView on GitHub
A stack is a branch cut from another PR's branch instead of from main. Everything below was measured on shabaraba/vibing.nvim; each item is something that failed first. gh pr view / gh issue view have been observed exiting 0 with no output on this machine. Read PR state with gh api throughout, not with the porcelain commands. gh pr merge and PUT /repos/{owner}/{repo}/pulls/{n}/merge both come back 403 here. The one that works is the async endpoint: - **merge_method=merge, not squash.*…
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 44
Do not queue several merges.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Merging a Stack of PRs
A stack is a branch cut from another PR's branch instead of from main. Everything below was
measured on shabaraba/vibing.nvim; each item is something that failed first.
gh pr view / gh issue view have been observed exiting 0 with no output on this machine. Read
PR state with gh api throughout, not with the porcelain commands.
The merge call
gh pr merge and PUT /repos/{owner}/{repo}/pulls/{n}/merge both come back 403 here. The one
that works is the async endpoint:
OWNER=shabaraba REPO=vibing.nvim N=<pr number>
SHA="$(gh api "repos/$OWNER/$REPO/pulls/$N" --jq '.head.sha')"
gh api --method PUT "repos/$OWNER/$REPO/pulls/$N/merge-async" \
-f "sha=$SHA" -f 'merge_method=merge'merge_method=merge, notsquash. A stack depends on its lower PR's commits existing as themselves; squashing the bottom one rewrites the history every branch above it was cut from.- Take
shafrom the API. Expanding a short SHA by hand fails withPull request head branch was modified— the value has to be the full head SHA the API is reporting right now, not one you copied from a log. - It is async. The response is not the outcome. Confirm with
gh api "repos/$OWNER/$REPO/pulls/$N" --jq '.merged, .merge_commit_sha'before touching the next PR in the stack.
Merge bottom-up, one at a time
Merge the lowest PR, wait for it to land, confirm CI is green on the next one, then merge that. Do not queue several merges; each merge is what rewrites the branches above it.
GitHub rebases the stack at merge time, not at force-push time. So:
- Pushing additional commits to a lower PR (a fast-forward) leaves the branches above it alone — no upstream rebase needed.
- Rebasing a lower PR, or merging it, does rewrite what the branches above were cut from, and they need the treatment in the next section.
The stale-lineage trap
A branch cut from a lower PR's pre-merge tip is not repaired by git rebase origin/main. That
command replays every commit since the merge base — which includes the lower PR's old,
pre-rebase commits, now superseded on main — and the result is a pile of conflicts in files
you never touched. It is not a real conflict; it is the same change arriving twice.
Replay only your own commits instead:
git rebase --onto origin/main <old-fork-point> <your-branch><old-fork-point> is the commit your branch was cut from — the lower PR's head as it was before
it got rewritten. Used three times on the run this skill came from, with zero conflicts each time,
against a plain git rebase origin/main that conflicted every time.
Record the fork point before you merge anything. For each stacked branch:
git merge-base <your-branch> <lower-branch>That is the cheap half of this whole procedure, and it is only cheap while the lower branch still has its pre-merge commits. If you did not record it, the lower PR's old head is the second parent of its merge commit:
MC="$(gh api "repos/$OWNER/$REPO/pulls/$N" --jq '.merge_commit_sha')"
gh api "repos/$OWNER/$REPO/commits/$MC" --jq '.parents[1].sha'(That holds because merge_method=merge is what produced the merge commit. It does not hold for a
squash or rebase merge — another reason not to use them on a stack.)
Reading mergeability
gh api "repos/$OWNER/$REPO/pulls/$N" --jq '{mergeable, mergeable_state}'
gh api "repos/$OWNER/$REPO/commits/$SHA/check-runs" --jq '.check_runs[] | {name, status, conclusion}'mergeable_state: unstable is not a reason to stop. It goes unstable while any check is
pending, and on this repository that is routinely CodeRabbit's status and nothing else. Decide
from mergeable: true plus the conclusion of the checks you actually require; unstable with
every required check success is a merge you can make.
mergeable: null means GitHub has not computed it yet — re-read, do not act on it.
In this repository6 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- ci-gatesSkill
- github-flow-for-claude-on-webSkill
- remote-screenshotSkill
- self-testingSkill
- test-designSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

