Skip to content

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:

  1. 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).
  2. Wait for an explicit go-ahead.
  3. Publish that one stack (below).
  4. Record what you created, and report back with the PR URLs.
  5. 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.)

Terminal window
cd ~/code/my-service
local-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 repo
gh 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 body
jq -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.md
done
# 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/null
done

Checked 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 stack exits 9 for “not enabled for this repository” (a 404 from the Stacks API).
  • link keeps no local state, so gh stack view, push, rebase and sync don’t know about the stack (exit 2, “not part of a stack”). Read it with gh api instead, as above.
  • link pushes branch arguments without force, so push rebased branches with --force-with-lease first (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 means gh 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 main by 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-refs keeps true.
  • CI and branch protection treat every PR as if it targeted the stack’s base, so workflows on pull_request to main run 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.

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[].file is where each one is). Dragging an image into the PR on GitHub uploads it; they then replace the assets/<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.