Branch: Stack
Read, link, extend, sync and tidy a GitHub stack without opening or landing a PR: status, link an existing chain, add a layer, sync after trunk or a lower layer moved, tidy stale local tracking
SKILL.mdskills/branch-stack/SKILL.md
GitHubBranch: Stack
Everything that changes a stack's shape or keeps it current, and nothing that opens or lands a pull request. pr-create opens a layer's PR (with base <parent>), pr-land lands one (gh stack merge <pr#> --merge); this skill hands over to them by name and stops. The facts behind every command here are in ~/.claude/library/references/stacked-prs.md; read its "Three sources of truth" and "Worktrees: one per stack" sections before the first action.
Every action starts with the script. ~/.claude/library/scripts/stack-status.py reads GitHub's stack field, the PR bases and the local gh-stack tracking, and prints one verdict per branch: linked, chained-unlinked, independent, no-pr. No action guesses from a base branch.
Step 0: Parse the arguments
action is required and is one of status, link, add, sync, tidy.
| Call | Meaning |
|---|---|
status [branch|pr ...] |
the verdict table for the targets (default: the current branch) and every tracked stack |
link <pr|branch> ... |
link PRs that already exist into a GitHub stack, bottom to top; a stack number first appends the rest to that stack |
add <branch> |
a new branch on top of the current stack, ready for commits and pr-create |
sync |
trunk moved: cascade-rebase every layer and push |
sync upstack |
this layer changed after review: rebase the layers above it and push |
tidy |
drop stale stacks from local tracking |
Hard stop on invalid input: print the one-line reason and the usage block, run nothing.
| Invalid call | Reason to print |
|---|---|
| no action | branch-stack needs an action |
link with fewer than two targets |
link needs at least two PRs or branches, bottom to top |
add with no branch |
add needs a branch name |
sync with anything but upstack |
sync takes nothing, or upstack |
tidy followed by anything |
tidy takes no arguments |
Usage: /branch-stack status [branch|pr ...]
/branch-stack link <pr|branch> <pr|branch> ... (bottom to top; a stack number first appends)
/branch-stack add <branch>
/branch-stack sync [upstack]
/branch-stack tidy
Action: status
~/.claude/library/scripts/stack-status.py <targets>(no targets: the current branch). Relay its three sections as they come: the table, the tracked stacks, the next steps.- Stop. The map is the output; a "Next" line that names a command is an offer, not an instruction, and the user picks the action.
Action: link
For PRs that already exist and are chained by base (chained-unlinked), or an unlinked PR to append to a stack.
stack-status.py <every target>. Stop and say so when any target isno-pr(open its PR first:pr-create base <parent>;gh stack linkcould push a bare branch and open the PR itself, but with an auto-generated title and none ofpr-create's approved body), or islinkedto a stack other than the one named by a leading stack number (one PR sits in one stack;tidyorgh stack unstackfirst is the user's call).- Order check: each target's base must be the previous target's branch (the first target's base is the trunk, or the top of the named stack). Out of order: print the chain the bases describe and stop. This check is the protection, since
linkcorrects the base of an existing PR that sits in the wrong place (GitHub'sgh-stackdocs say so; v0.1.0's--helpis silent, so it is treated as true) and would retarget a live PR from a mis-ordered list. The same goes for the bottom target's base:linkdefaults it to the repo's default branch and this skill passes no--base, so a bottom PR based on any other trunk stops here with that said. - Show the command and ask:
gh stack link <targets>. The approval covers one command. - Run it, then
stack-status.py <targets>again and relay the table.linkwrites no local tracking, so the rows say "not tracked here"; that is expected, andgh stack checkout <pr#>is the step before anysync.
Action: add
A new layer on top of the stack the current branch belongs to.
stack-status.py. The current branch must belinkedand tracked here with its branch present; otherwise print the row's "Next" line (gh stack checkout <pr#>, orlinkfirst) and stop.git status --porcelainmust be empty, and~/.claude/library/scripts/checkout-occupied.shmust exit 0 (another live session in this worktree means stop and say so).gh stack top(the new layer goes on the top, never mid-stack), thengh stack add <branch>. A branch name keeps the command out of an editor.-mcreates a commit, so on the clean tree step 2 guarantees it dies withnothing to commit;add -A/-uwithout-mopens an editor and is never run.- Close in two lines: the branch exists on top of stack #N and is checked out; commits go here, and
pr-create base <top-layer-branch>opens its PR when ready.
Action: sync
sync is a cascade-rebase and a --force-with-lease push of every layer. It rewrites SHAs, which is the expected mechanism for a stack (pr-handle_review's carve-out), and it is the one command that must never run blind.
stack-status.py. The current branch must belinked, tracked here, branch present. Missing or untracked: offergh stack checkout <pr#>(it checks branches out, so it is asked, not assumed) and rerun the status after it.git status --porcelainempty,checkout-occupied.shexit 0. A dirty tree or another session: stop and say which.- What will move. For each open layer, bottom to top,
git rev-list --left-right --count origin/<branch>...<branch>andgit log --oneline --merges origin/<trunk>..<branch>. Print one table: layer, PR, local vs remote, merge commits. A merge commit on a layer is flattened by the rebase, which is correct (see the reference's "Trunk moved" flow) but must be said before it happens. - Ask. The approval names the layers and that they will be force-pushed. Then one of:
sync:gh stack syncsync upstack:gh stack rebase --upstack, thengh stack push
- Outcomes. Read the output as well as the exit code. Exit 0 with
Sync abortedin the output: local and remote had diverged and nothing moved; say so, print the two chains it shows and stop, since the user decides which side wins. Exit 0 endingStack syncedorBranches synced:stack-status.py --openand relay the table (Branches syncedmeans the branches were pushed but GitHub holds no stack object for them). Exit 3 (rebase conflict):gh stackhas restored every branch; print the files it names and the two ways on,gh stack rebasethen resolve,git add,gh stack rebase --continue, orgh stack rebase --abort; do not resolve conflicts here without being asked. Thebelongs to multiple stacksmessage (exit 6 fromview, exit 2 fromsyncon v0.1.0, so match the text): the checked-out branch is usually the trunk of several stacks;gh stack checkout <a branch unique to this stack>and run again, and offertidyonly when one of them is stale. Exit 8 (stack locked): another process is mid-command; wait, never delete the lock.
Action: tidy
stack-status.py --open. The "Local Tracking" section marks a stack stale when GitHub reports every PR in itMERGED(the tracking file's own flag lags until async, so the script asks GitHub). A stack with a closed PR, a layer with no PR yet or a state GitHub would not return is not stale, and neither is one whose branches are all missing locally but whose PRs are open (its branches come back withgh stack checkout).- No stale stack: say so and stop.
- One AskUserQuestion (multi-select) over the stale stacks, each option naming the stack number and its branches.
- For each approved:
gh stack unstack <n> --local.--localis not optional here: without it the command also unstacks the merged PRs on GitHub, which changes nothing useful and asks GitHub to rewrite closed history. stack-status.py --openagain; the stale rows are gone.
Red flags
Never: gh stack merge, gh pr merge, gh pr create or gh stack submit from this skill (the first two are pr-land's, the others pr-create's); gh stack init on branches whose PRs already exist (that is link); git merge main or git merge <parent> into a layer (sync is the whole point); a sync with a dirty tree, with another session in the worktree, or without the "what will move" table; gh stack modify or gh stack switch (interactive editors that hang); gh stack add -A/-u without -m; gh stack unstack without --local from tidy; git push --force by hand (the lease is what makes the rewrite safe); resolving a rebase conflict without being asked; a second worktree for a layer of a stack this session already has a worktree for.