Skip to content
Back to skills

Land Stack

ASecurity

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.

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
ai-agentspythongogitapi

Works with

  • api

Security analysis

A100/100

Pro scans all 9 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add EdbertChan/catstack --skill land-stack --agent claude-code

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.

Security grade badge for Land Stack
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/edbertchan-land-stack/badge)](https://www.skillsdirectory.com/skills/edbertchan-land-stack)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Files in this skill

  • SKILL.md6.3 KB
  • scripts/verify_stack.py9.8 KB
  • tests/fires_example.md444 B
  • tests/fixtures/catstack_queue_state_2026-09-11.json3.6 KB
  • tests/fixtures/invoker_open_prs_2026-09-01.json13.7 KB
  • tests/stays_silent_example.md353 B
  • tests/test_queue_state.py2.6 KB
  • tests/test_superseded_gate_step.py1.8 KB
  • tests/test_verify_stack.py4.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.