Land (queue/merge) a stacked pull request safely, on any repo using a PR-stacking workflow (Mergify, Graphite, or similar). Trigger when asked to land, merge, ship, or queue a PR or PR stack. Enforces that you act only on SHA-verified PR numbers — never a PR found by branch name.
Installs into .claude/skills of the current project.
Are you the author of Land Stack?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/edbertchan-land-stack)
---
name: land-stack
description: >
Land (queue/merge) a stacked pull request safely, on any repo using a
PR-stacking workflow (Mergify, Graphite, or similar). Trigger when asked to
land, merge, ship, or queue a PR or PR stack. Enforces that you act only on
SHA-verified PR numbers — never a PR found by branch name.
---
# land-stack
Use this skill whenever the user asks to **land / merge / ship / queue** a PR or a PR stack.
Generalized from a real incident in a repo using Mergify Stacks — the specifics below (label names, `gh` commands) assume that tooling, but the discipline transfers to any stacking workflow: adapt the discovery/merge commands to whatever queue tool the repo actually uses.
## Hard rule
**Never identify the PR to land by branch name.** Two different PRs can share a
branch name (an auto-generated workflow branch PR and the intended stack
PR). You must land by **SHA-verified PR number**, and every PR must pass a
guard before any write (label, thread-resolve, queue, merge).
## Steps
1. **Resolve PR numbers, bottom of stack first.** If the user gives numbers or
URLs, use those. If they do not, make a best-effort read-only discovery pass
and suggest the numbers yourself:
- Enumerate open PRs broadly, e.g. `gh pr list --state open --json number,baseRefName,headRefName,headRefOid,title --limit 100`.
- Filter to the repo's actual stack-branch naming convention (e.g. `stack/`).
- Prefer candidates whose `headRefOid` exists in the local clone, so the code
is actually available for review.
- Order the stack by base/head links: the bottom PR targets the trunk; each
later PR targets the previous PR's head branch.
- Detect whether two or more open candidates share the same `headRefName`.
If they do, present the exact bottom-up PR numbers and ask the user to
confirm them before landing. This is the only discovery case that requires
confirmation.
- If every candidate `headRefName` is unique, run the guard on the suggested
sequence and, when it passes, land it without an additional confirmation.
Never discover by branch name. Do not run `gh pr list --head <branch>` to
decide what to land — that is the unsafe path this skill exists to prevent.
2. **Verify with a guard before any write.** Check, for each PR: head SHA exists
in the local clone (it is the code you reviewed), head branch matches the
repo's real stack-branch convention (rejects raw workflow/auto branches),
the PRs form a proper stack (each base is the previous head; the bottom's
base is the trunk), and all are OPEN. If any check fails, stop and resolve
the mismatch with a fresh discovery pass — do not work around it. The check
is `scripts/verify_stack.py` in this skill (see below); run it and paste
its output rather than reasoning through the four checks by hand.
```sh
python3 scripts/verify_stack.py --repo <owner/name> --trunk <trunk> --git-dir <local clone> <bottom> <next> ...
python3 scripts/verify_stack.py --repo <owner/name> --discover # suggest stacks; exit 3 = confirm with the user
```
Exit 0 means every check passed for that exact order. Exit 1 lists the failing
check per PR. The script never calls `gh pr list --head`.
Then check each PR is not superseded before rebasing it or resolving its
conflicts. A branch behind its base still lists differing files; that
listing does not say which way they differ. Fetch the trunk, then run the
catstack gate `scripts/ci/check_branch_not_superseded.py` (the skill
directory links into the catstack checkout, so it sits three levels up
from the resolved skill path) and paste its output:
```sh
git fetch origin
python3 "$(realpath ~/.claude/skills/land-stack)/../../../scripts/ci/check_branch_not_superseded.py" '#<number>' --base origin/<trunk>
```
- Exit 0, LIVE: the PR carries work the trunk lacks; rebase it if needed
and go on.
- Exit 3, SUPERSEDED: do not rebase or resolve conflicts, since that would
revert landed work. Close the PR with a comment naming the PRs that
superseded it (find them in `git log --oneline <merge-base>..origin/<trunk>`
over the files it touches).
- Exit 2 or any other code, UNCHECKED: the check did not run. Fix what it
names (fetch, unshallow, correct the ref) and rerun; never treat it as
LIVE.
3. **Land bottom-up.** Merge the bottom PR, wait for it to actually merge, then
retarget the next PR's base onto the trunk before merging it. Repeat up the
stack. A base change can report an unsettled/unknown mergeability state
immediately after — wait briefly and re-check before merging, don't merge
on a stale read.
After every queue write (a label, a `@mergifyio queue` comment, a merge
command), read the queue's own state before saying anything is queued:
```sh
python3 scripts/verify_stack.py --repo <owner/name> --queue-state <n> ...
```
Exit 0 means each PR is merged or queued. Exit 1 names a PR the queue
never took. Exit 2 means its state could not be read, which is not a pass.
A queue label satisfying a rule's conditions is not the same as queued:
Mergify reports `Merge queue is ready` until a `@mergifyio queue` comment
(or the dashboard) actually queues the PR.
4. **Never batch merges without checking each result.** A merge command can
look silent on both success and some failure paths; a silent-looking run is
not proof of a merge.
## Do not
- Do not bypass the guard by hand-adding a bypass label or merging directly to
skip a broken check — if the queue is unhealthy, that's a different, riskier
operation that needs its own explicit authorization, not this skill.
- Do not resolve review threads to unblock a merge by default. Decision tree:
- **Automated review thread under an explicit babysit-until-merged job**
(for example CodeRabbit): after the current head addresses the thread
(or the thread is outdated), resolve it yourself via the GitHub
review-thread API. Do not bounce that to the user.
- **Deferral required for human reviewer threads:** resolve only when the
user has decided to defer those findings; record the deferral on the PR.
This rule alone never authorizes resolving a human thread.
- **Otherwise leave the thread open:** when there is no babysit-until-merged
job, or the head does not address the thread, leave it open.
- Do not act on a PR whose head SHA is not in your local clone.
## Prove state before reporting it
Before telling the user a PR is merged, queued, blocked, or failing CI, re-run
the exact status query in that same turn — do not repeat a status you checked
earlier in the conversation. "Merging" is not "merged"; a queued PR can still
fail a re-run of the full suite.
## Why this exists
A raw workflow-branch PR shared a branch name with the intended stack PR.
Landing "the PR on this branch" by name queued the wrong PR. The guard makes
that mistake fail closed instead of merging silently.