Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,102 @@ after every merge.
- New dev-only files belong in `.gitattributes` as `export-ignore` so they stay out of consumer
installs.

### Stacking with `gh stack`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have this somewhere global and standardized, but since we had the other PR creation guidelines here I figured it was appropriate to include.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of did this here: https://github.com/stellarwp/nexcess-maintenance-ai-guidelines/pull/30

That PR and its draft followup (https://github.com/stellarwp/nexcess-maintenance-ai-guidelines/pull/31) are a little more detailed than this in some ways.


**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.

**Every PR in a stack is titled `<shared prefix> [X/Y]: <rest of the title>`** — 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. 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
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 --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", 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
gh stack top
gh stack add 37-fourth
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

`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
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
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"unwiring",
"uopz",
"upgrader",
"upstack",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to a global ignore because adding a local ignore to AGENTS.md would eat up more of the context window to only benefit CI.

"vlucas",
"worktree",
"wpautop",
Expand Down
Loading