Publishing as GitHub stacked PRs
When the reviewer says a stack is ready, the agent pushes its branches, opens one PR per commit with the titles and bodies from the plan, links them into a GitHub stack and records each PR back in Local Review. Local Review itself never talks to GitHub.
Only when the reviewer asks, one stack at a time:
- Fix every warning, run the plan for one stack, and show it to the reviewer. Skip follow-on stacks unless the reviewer names one. If it still has draft PRs, say so: the reviewer marks them ready (or tells you to go ahead anyway).
- Wait for an explicit go-ahead.
- Publish that one stack (below).
- Record what you created, and report back with the PR URLs.
- Stop. The next stack waits for the next go-ahead.
This uses the gh stack extension (gh extension install github/gh-stack).
See About stacked PRs, the
CLI reference and the extension’s
docs (FAQ, REST API). gh stack has no
title or body flags: submit opens an interactive editor, and --auto uses generated text. So create each PR
with gh pr create, which takes the exact title and body, then link the PRs into a stack with gh stack link,
which is meant for branches managed by other tools.
Each repo gets one GitHub stack, grown one Local Review stack at a time: the first go-ahead creates it, and each later one appends to its top by stack number. (Stack numbers share the repo’s numberspace with PRs and issues, so they never equal a PR number.)
cd ~/code/my-servicelocal-review plan $P --repo $R --stack 1 --format json > /tmp/plan.json
# 0. stacked PRs available? Any 200 (even `[]`) means yes; 404 means not enabled, or no access to the repogh api "repos/{owner}/{repo}/stacks?per_page=1" >/dev/null
# 1. push the stack's branches (bottom to top)git push --force-with-lease origin $(jq -r '.prs[].branch.name' /tmp/plan.json)
# 2. one PR per branch, bottom to top, with the plan's base, title and full bodyjq -c '.prs[]' /tmp/plan.json | while read -r pr; do jq -r .body <<<"$pr" > /tmp/pr-body.md gh pr create --draft --head "$(jq -r .branch.name <<<"$pr")" --base "$(jq -r .base <<<"$pr")" \ --title "$(jq -r .title <<<"$pr")" --body-file /tmp/pr-body.mddone
# 3a. first stack in this repo: create the GitHub stack, bottom to top. Always pass --base: without it, link# retargets the bottom PR to the repo's default branch.gh stack link --remote origin --base "$(jq -r '.prs[0].base' /tmp/plan.json)" $(jq -r '.prs[].branch.name' /tmp/plan.json)# 3b. later stack: append only the new PRs to the existing GitHub stack, by its number (--base is ignored here)N=$(gh api "repos/{owner}/{repo}/stacks?pull_request=<any earlier PR number>" --jq '.[0].number')gh stack link --remote origin "$N" $(jq -r '.prs[].branch.name' /tmp/plan.json)
# check: the stack, bottom to top (gh stack view can't see it; see below)gh api "repos/{owner}/{repo}/stacks?pull_request=$(gh pr view "$(jq -r '.prs[0].branch.name' /tmp/plan.json)" --json number --jq .number)" \ --jq '.[0] | "stack #\(.number) on \(.base.ref): \([.pull_requests[] | "#\(.number) \(.head.ref)"] | join(" <- "))"'
# 4. record each PR in its prs file (this also records the branch)for b in $(jq -r '.prs[].branch.name' /tmp/plan.json); do sha=$(git rev-parse "$b") gh pr view "$b" --json number,url,headRefName,baseRefName \ | jq '{github: {number, url, branch: .headRefName, base: .baseRefName}}' \ | curl -s -X PUT -H "$J" -d @- http://localhost:5622/api/projects/$P/repos/$R/commits/$sha/pr >/dev/nulldoneChecked against the docs and the extension’s source (October 2026):
- No setup: stacked PRs are GA on all github.com plans (6 Oct 2026), with no repo or org setting. Step 0 is
the check.
gh stackexits9for “not enabled for this repository” (a 404 from the Stacks API). linkkeeps no local state, sogh stack view,push,rebaseandsyncdon’t know about the stack (exit2, “not part of a stack”). Read it withgh apiinstead, as above.linkpushes branch arguments without force, so push rebased branches with--force-with-leasefirst (step 1). It reuses open PRs for those branches and retargets any whose base breaks the chain. It only adds, and only to the top. A PR can be in only one stack, and a stack holds 2–100 PRs, all in the same repo (no forks). Don’t pass--open: it marks every PR in the command ready for review. Removing or reordering meansgh stack unstack <n>and relinking, which is the reviewer’s call.- A fully merged stack can’t be extended. The next Local Review stack then becomes a new GitHub stack (3a,
with the plan’s base, which is
mainby then). - Force-pushing rebased branches keeps the stack: membership belongs to the PRs, not the commits. Merging needs a
linear history, so each branch must contain the one below it, which
--update-refskeeps true. - CI and branch protection treat every PR as if it targeted the stack’s base, so workflows on
pull_requesttomainrun for each PR in the stack. To merge a PR, every PR below it must pass its checks and reviews too.
Not verified (needs a live test):
- splitting one repo’s chain into several GitHub stacks, each based on the previous one’s top branch. GitHub says
stacks can branch off one another, but we haven’t tried
link --base <previous top branch>. Use one stack per repo; - whether approvals survive a local force-push. GitHub says its own Rebase stack button keeps them, but after a local force-push the repo’s “dismiss stale approvals” rule probably applies.
Check gh stack link --help before you run it, and if anything differs from the above, tell the reviewer rather than guess.
Images
Section titled “Images”GitHub has no API for attaching an image to a PR body: its upload exists only in the web UI. So an assets/<name>
reference in a plan body would show as a broken image on GitHub. For each image the plan warns about, either:
- record a URL the PR’s readers can reach, with
local-review asset url <project> <name> <url>(for example an image committed to a repo they can read, or one already uploaded to an earlier PR or issue), then re-run the plan: the body now has that URL; or - leave it to the reviewer: publish as usual, and tell them which PRs have images to drag in by hand (the plan’s
images[].fileis where each one is). Dragging an image into the PR on GitHub uploads it; they then replace theassets/<name>reference with the link GitHub inserts.
Don’t upload images anywhere public on your own initiative, and ask before committing one to a repo.