From 0b49249ff76dcb644f94eaefbcb03efc05115418 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Fri, 21 Aug 2026 11:27:53 -0400 Subject: [PATCH 1/5] Document when to stack a PR and how to drive gh stack --- AGENTS.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 046588f..25b066e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -622,6 +622,83 @@ after every merge. - New dev-only files belong in `.gitattributes` as `export-ignore` so they stay out of consumer installs. +### Stacking with `gh stack` + +**Stack whenever the work divides into portions a human could review separately.** The size cap +above is the floor, not the test: a change that fits in ten files still gets split if a reviewer +would have to hold two unrelated arguments in their head to sign it off. The unit is a decision a +reviewer can accept or reject on its own — one hook wired, one collaborator extracted, one +invariant enforced — with its own tests and its own `Why this way` block. A reviewer who can finish +a PR in one sitting reads it; a reviewer facing everything at once skims it, and skimming is how a +wrong decision reaches `main` with an approval on it. + +What that rules out is splitting for its own sake. Each branch has to leave `main` releasable, so a +portion that cannot compile, pass the suite or be described in one `What` line is not a portion — +it belongs with the branch that completes it. When the work genuinely is one decision, one PR is +the right answer and a stack of one is nothing but ceremony. + +**A PR based on another branch is not automatically a stack.** The commonest reason to cut from an +open branch instead of `main` is that both touch the same file and cutting from `main` would +guarantee a conflict — that is a base branch and nothing more. Point the PR's base at the branch +below it and stop; do not run `gh stack init`. A Stack on GitHub is a claim to the reviewer that the +PRs are one piece of work meant to be read in order, and making that claim about two changes that +merely share a file sends them looking for a through-line that was never there. Reach for the +tooling below only when the ordering is the argument. + +The stack is managed with the **`github/gh-stack`** extension, invoked `gh stack`. Do not hand-roll +the stack with merges: `rebase` and `submit` force-update the branches the stack owns, which is what +keeps each PR's diff to its own commits. Four operations cover the whole workflow, and none of them +are guessable from `--help`, so use these verbatim. + +**Adopt existing branches into a stack**, bottom to top, naming the trunk with `--base` so the trunk +gets no PR of its own: + +```bash +gh stack init --base main 34-first 35-second 36-third +gh stack view # confirm the order before submitting +``` + +`--base` is whatever the bottom branch cut from. That is `main` for an ordinary series — and it is +the **tip branch of the lower stack** when a second stack is built on top of one that is still open, +which is the only way to keep the lower stack's PRs out of the new one. + +**Append one already-existing branch** to a stack that exists. `gh stack init` refuses this with +"already exists in a stack". Check out a member of the stack — the tip is the natural one — and: + +```bash +git checkout 36-third +gh stack add 37-fourth +``` + +`gh stack modify` also adds branches, but it is an interactive TUI and is unusable headlessly. + +**Propagate an edit made low in the stack** by cascading rebase, never by merging the lower branch +upward: commit the edit on the lower branch, check out the **lowest changed** branch, and rebase +that branch and everything above it: + +```bash +git checkout 35-second +gh stack rebase --upstack --preserve-dates +``` + +`--preserve-dates` — the alias for `--committer-date-is-author-date` — goes on *every* rebase, not +just this one. Without it each rebase restamps the committer date of every commit it rewrites, and +a stack is rebased repeatedly, so the whole history walks forward to whenever the last submit +happened. + +`--upstack` is the other half: a rebase with no scope flag re-creates *every* commit in the stack, so +the submit after it force-pushes lower branches nothing changed on. Take the full-stack form only +when the trunk moved or the bottom branch itself did — and with the same flag: + +```bash +gh stack rebase --preserve-dates +``` + +**Publish** with `gh stack submit --auto`: it pushes every branch, force where the rebase made that +necessary, and links them as a Stack on GitHub. It preserves existing PR titles, bodies and draft +state, so it is safe to re-run; `--open` is the flag that marks them ready for review. A +non-interactive run implies `--auto`. + ## Known, and deliberately not fixed in 1.0.0 **`Activator::maybe_run()` can double-run under concurrency.** It reads the option, runs the From 3822f63ef513cc4958b4fda1d12ec083561186a3 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Fri, 21 Aug 2026 11:31:15 -0400 Subject: [PATCH 2/5] Ignore upstack in AGENTS.md for cspell and rephrase headlessly --- AGENTS.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 25b066e..686597e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,5 @@ + + # AGENTS.md This file provides guidance to coding agents when working with code in this repository. `CLAUDE.md` @@ -670,7 +672,8 @@ git checkout 36-third gh stack add 37-fourth ``` -`gh stack modify` also adds branches, but it is an interactive TUI and is unusable headlessly. +`gh stack modify` also adds branches, but it is an interactive TUI, so it cannot be driven from a +script or by an agent. **Propagate an edit made low in the stack** by cascading rebase, never by merging the lower branch upward: commit the edit on the lower branch, check out the **lowest changed** branch, and rebase From 3471af3ce63cc9f89878ef65658c017d9aa0508a Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Fri, 21 Aug 2026 11:40:18 -0400 Subject: [PATCH 3/5] Move the upstack ignore into cspell.json and correct the gh stack view and add guidance --- AGENTS.md | 19 ++++++++++++------- cspell.json | 1 + 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 686597e..874f360 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,3 @@ - - # AGENTS.md This file provides guidance to coding agents when working with code in this repository. `CLAUDE.md` @@ -657,23 +655,30 @@ gets no PR of its own: ```bash gh stack init --base main 34-first 35-second 36-third -gh stack view # confirm the order before submitting +gh stack view --json # confirm the order before submitting ``` +`view` takes `--json` because a bare `gh stack view` is the interactive TUI; `--short` is the +one-line-per-branch form for a human reading along. + `--base` is whatever the bottom branch cut from. That is `main` for an ordinary series — and it is the **tip branch of the lower stack** when a second stack is built on top of one that is still open, which is the only way to keep the lower stack's PRs out of the new one. **Append one already-existing branch** to a stack that exists. `gh stack init` refuses this with -"already exists in a stack". Check out a member of the stack — the tip is the natural one — and: +"already exists in a stack", and `gh stack add` is what takes it: a name that is already a branch in +git is adopted, and only a name that is not gets created, which is the same rule `init` follows. +`add` has to run from the **top** of the stack, so get there with `gh stack top` rather than by +naming a branch — anywhere else it exits `5` with "can only add branches on top of the stack": ```bash -git checkout 36-third +gh stack top gh stack add 37-fourth ``` -`gh stack modify` also adds branches, but it is an interactive TUI, so it cannot be driven from a -script or by an agent. +`add` leaves the working tree alone, so uncommitted changes follow you onto the branch it checks +out; commit or stash first. `gh stack modify` also adds branches, but it is an interactive TUI, so +it cannot be driven from a script or by an agent. **Propagate an edit made low in the stack** by cascading rebase, never by merging the lower branch upward: commit the edit on the lower branch, check out the **lowest changed** branch, and rebase diff --git a/cspell.json b/cspell.json index 1777d7e..b4c52d8 100644 --- a/cspell.json +++ b/cspell.json @@ -72,6 +72,7 @@ "unwiring", "uopz", "upgrader", + "upstack", "vlucas", "worktree", "wpautop", From 35207176157bae20fc6c7587d99c6988d069ae38 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Fri, 21 Aug 2026 11:49:58 -0400 Subject: [PATCH 4/5] Require a shared prefix and stack position in stacked PR titles --- AGENTS.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 874f360..b7d60d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -637,6 +637,16 @@ portion that cannot compile, pass the suite or be described in one `What` line i it belongs with the branch that completes it. When the work genuinely is one decision, one PR is the right answer and a stack of one is nothing but ceremony. +**Every PR in a stack is titled ` [X/Y]: `** — so +`Conflict handling [2/4]: resolve the standalone conflict` sits between the `[1/4]` and the `[3/4]` +of the same prefix. The prefix names the work all of them belong to and `X/Y` names the position and +the size, which is the whole of what a reviewer scanning the PR list needs: that these belong +together, which one to open first, and how much more is coming. `Y` is a count, so appending a +branch renumbers every PR already open — retitle them in the same pass as the `gh stack add`, or the +numbering says the series is shorter than it is. `gh stack submit --auto` generates a title from the +branch's commits and carries no prefix, so titles are written or corrected with `gh pr edit --title` +afterwards; a later submit preserves them. + **A PR based on another branch is not automatically a stack.** The commonest reason to cut from an open branch instead of `main` is that both touch the same file and cutting from `main` would guarantee a conflict — that is a base branch and nothing more. Point the PR's base at the branch From 7abe3150e509b99befb063451a3c3b0be6a59322 Mon Sep 17 00:00:00 2001 From: Eric Defore Date: Fri, 21 Aug 2026 11:55:56 -0400 Subject: [PATCH 5/5] State both of the --auto title rules --- AGENTS.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b7d60d9..1045b8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -643,9 +643,10 @@ of the same prefix. The prefix names the work all of them belong to and `X/Y` na the size, which is the whole of what a reviewer scanning the PR list needs: that these belong together, which one to open first, and how much more is coming. `Y` is a count, so appending a branch renumbers every PR already open — retitle them in the same pass as the `gh stack add`, or the -numbering says the series is shorter than it is. `gh stack submit --auto` generates a title from the -branch's commits and carries no prefix, so titles are written or corrected with `gh pr edit --title` -afterwards; a later submit preserves them. +numbering says the series is shorter than it is. Nothing about the prefix survives `gh stack submit +--auto`, which titles a single-commit branch with that commit's subject and a multi-commit branch +with its own name, hyphens and underscores turned into spaces. There is no flag for a title, so set +it with `gh pr edit --title` after submitting; a later submit leaves what is already there alone. **A PR based on another branch is not automatically a stack.** The commonest reason to cut from an open branch instead of `main` is that both touch the same file and cutting from `main` would