From 3a0125606e3b3b1acf5626e2f73d9534184c273e Mon Sep 17 00:00:00 2001 From: Alain Schlesser Date: Thu, 15 Oct 2026 23:47:28 +0200 Subject: [PATCH] docs: integrate gh-stack with the pull request workflow --- .agents/skills/gh-stack/SKILL.md | 197 ++++++++++++++++ .../skills/gh-stack/references/commands.md | 214 ++++++++++++++++++ .../gh-stack/references/stack-design.md | 97 ++++++++ .../gh-stack/references/troubleshooting.md | 186 +++++++++++++++ .agents/skills/github/SKILL.md | 11 +- .agents/skills/work-issue/SKILL.md | 16 +- .../work-issue/references/ci-and-merge.md | 36 ++- .claude/skills/gh-stack | 1 + .github/PULL_REQUEST_TEMPLATE.md | 6 + AGENTS.md | 5 +- CLAUDE.md | 3 + CONTRIBUTING.md | 5 + docs/process/github.md | 49 +++- docs/process/testing.md | 3 + 14 files changed, 822 insertions(+), 7 deletions(-) create mode 100644 .agents/skills/gh-stack/SKILL.md create mode 100644 .agents/skills/gh-stack/references/commands.md create mode 100644 .agents/skills/gh-stack/references/stack-design.md create mode 100644 .agents/skills/gh-stack/references/troubleshooting.md create mode 120000 .claude/skills/gh-stack diff --git a/.agents/skills/gh-stack/SKILL.md b/.agents/skills/gh-stack/SKILL.md new file mode 100644 index 000000000..a35520a42 --- /dev/null +++ b/.agents/skills/gh-stack/SKILL.md @@ -0,0 +1,197 @@ +--- +description: | + Manages stacked PRs and splits multi-part work into reviewable branches with gh-stack. Use for stack creation, viewing, edits, push, submit, sync, rebase, merge, or checkout; when asked to split or isolate work for review; whenever a user mentions a stack, branch layers, dependent PRs, or gh stack; or when a stack is checked out. +metadata: + author: github + github-path: skills/gh-stack + github-ref: refs/tags/v0.2.1 + github-repo: https://github.com/github/gh-stack + github-tree-sha: bde7bcf5a96697d31aa55b33565fa0ff002167ca + version: 0.2.0 +name: gh-stack +--- +# gh-stack + +`gh stack` is a [GitHub CLI](https://cli.github.com/) extension for stacked branches and pull +requests. A stack is an ordered chain of branches rooted on a trunk, where each branch has one PR +based on the branch below it, so a reviewer sees only that layer's diff. + +`gh stack` prints a stack trunk-first, left to right: + +``` +(main) <- auth <- api <- frontend +``` + +Left is the **bottom**, right is the **top**. `auth` is based on `main` and merges first; +`frontend` merges last. `up` moves toward the top, away from trunk; `down` moves toward it. +Foundational work belongs at the bottom, code that depends on it above. For how to choose the +layers, read `references/stack-design.md`. + +## Setup + +Requires Git 2.36+ and an authenticated GitHub CLI. + +```bash +gh extension install github/gh-stack +git config rerere.enabled true # remember conflict resolutions +git config remote.pushDefault origin # required if the repo has more than one remote +``` + +## Non-interactive use + +`gh stack` branches on whether **stdout is a TTY**. Piped, most commands error cleanly or print +static text; under a PTY the same commands open a prompt or a full-screen TUI and block forever. +Agent harnesses differ, so always pass the flags below instead of relying on that detection. + +**Multiple remotes:** never run `push`, `submit`, `sync`, `rebase`, or `link` without +`--remote ` unless `remote.pushDefault` is configured. `checkout` and `trunk` have no +`--remote` flag and require the config. + +| Always run | Never run bare | Why | +|---|---|---| +| `gh stack view --json` | `gh stack view` | opens a TUI under a PTY | +| `gh stack submit --auto` | `gh stack submit` | prompts for a title per new PR | +| `gh stack merge --yes` | `gh pr merge` | `gh pr merge` cannot merge a stack | +| `gh stack init ...` | `gh stack init` | prompts for branch names | +| `gh stack add ` | `gh stack add` | prompts for a name, and fails even when piped | +| `gh stack checkout ` | `gh stack checkout` | opens a selection menu | +| `gh stack up` / `down` / `top` / `bottom` | `gh stack switch` | `switch` is menu-only | +| — | `gh stack modify` | TUI-only, no non-interactive path | + +- `view --short` is safe in both modes, but it is formatted for humans. Use `--json` to parse. +- **`checkout ` when a different local stack already covers those branches** cannot be forced. + Run `gh stack unstack --local` first (this keeps the stack on GitHub), then retry. +- **Worktrees:** local stacks share one common-directory catalog. Use `--print-path` with + navigation or explicit-target `checkout` to locate a foreign-owned branch without stealing its + checkout. Unoccupied targets are checked out here first. Check the exit status before changing + directories; parse only successful path-mode stdout, never status messages. + +## Branch placement + +- **Starting multi-part work:** create the stack before writing files. Do not implement every + concern on trunk and split it later. Put one dependent concern in each layer, bottom to top. +- **Editing an existing stack:** check out the layer that owns the change before editing. Never + commit a lower layer's concern on the current top branch. Run `gh stack view --json`; if + ownership is unclear, inspect `git log --all -- `. Then check out the owner, edit, commit, + rebase upstack, and return to top. + +```bash +gh stack down # or: gh stack checkout api +git add ... && git commit -m "Add get-user endpoint" +gh stack rebase --upstack # replay every branch above onto the change +gh stack top # return to where you were +gh stack push +``` + +## Core loop + +```bash +gh stack init auth # create the stack and check out its branch +git add ... && git commit -m "Add auth middleware" +gh stack add api # next layer, branched from the current one +git add ... && git commit -m "Add API routes" +gh stack submit --auto # push every branch and open draft PRs +gh stack view --json # confirm +``` + +Add `--open` to `submit` to create PRs ready for review instead of drafts. Branch names are +verbatim — `gh stack add refactor/foo` creates `refactor/foo`. + +## Staying in sync + +```bash +gh stack sync # fetch, reconcile with GitHub, rebase, push, refresh PR state +gh stack sync --prune # also delete local branches for merged PRs +``` + +Pruning never happens without `--prune` when non-interactive. If the local and remote stacks have +diverged, `sync` prints both chains, makes no changes, and exits 0 with `Sync aborted` — see +`references/troubleshooting.md`. + +## Merging + +Scope the merge with an argument: + +```bash +gh stack merge 42 --yes # stack #42 if it exists; otherwise through PR #42 +gh stack merge 7 --yes # every unmerged PR in stack #7 +gh stack merge 42 --yes --squash # or --merge, --rebase, --merge-method +``` + +Pass a PR number to merge that PR and every unmerged PR below it, or a stack number to merge every +unmerged PR in that stack. The operation is all-or-nothing: if any PR in that set cannot merge, +none do. + +A bare number resolves as a **stack number first**, then a PR number. Confirm the resolved +membership before merging; a PR number that also names a stack selects that stack instead. + +Without a method flag the last-used method is reused. If the base branch uses a merge queue, the +stack is queued instead and the queue picks the method, ignoring any flag you passed with a +warning; queued PRs may land in separate groups. + +## Reading state + +`gh stack view --json` writes JSON to **stdout**. Status messages go to **stderr** — do not parse +them, branch on exit codes instead. + +``` +trunk string +currentBranch string +branches[] name, head, base, isCurrent, isMerged, isQueued, needsRebase +branches[].pr number, url, state ("OPEN" | "MERGED" | "QUEUED"); absent when no PR exists +``` + +`base` is the saved SHA of the parent branch that this branch was last known to contain. It may be +older than the parent's current tip. `needsRebase` is true when the current parent tip is no longer +an ancestor of the branch. + +## Exit codes + +| Code | Meaning | Recovery | +|---|---|---| +| 0 | Success | — | +| 1 | Generic error | Read stderr | +| 2 | Not in a stack | `gh stack init`, or `gh stack checkout ` | +| 3 | Rebase conflict | Follow the Exit 3 recovery below | +| 4 | GitHub API failure | Check `gh auth status`, retry | +| 5 | Invalid arguments | Fix the invocation; see ` --help` | +| 6 | Disambiguation required | Branch is in several stacks; check out a non-shared branch | +| 7 | Rebase already in progress | `gh stack rebase --continue` or `--abort` | +| 8 | Stack file locked | Another `gh stack` process is writing; retry after ~5s | +| 9 | Stacked PRs unavailable | Not enabled on the repository; tell the user | +| 10 | Modify recovery required | `gh stack modify --abort` | + +**Exit 3 recovery:** + +- After `gh stack rebase`: resolve the files, run `git add`, then + `gh stack rebase --continue`; use `gh stack rebase --abort` to restore the stack. +- After `gh stack sync`: the stack has already been restored. Run `gh stack rebase` to recreate the + conflict, then resolve and continue as above. + +## Constraints + +- Stacks are strictly linear: one parent, at most one child. Use separate stacks for parallel work. +- `rebase` and `sync` automatically update affected clean worktrees; they never auto-stash or + create/remove worktrees. Mutations serialize across the clone, and paused operations require + recovery in their recorded owners. +- `modify` supports distributed stack branches, but its editor remains TUI-only. Actions run in + affected clean owners; unoccupied branches use the origin. Drop/fold source branches and + worktrees are preserved. Recovery flags may run from any worktree and use recorded native + operation owners; never resolve/stage in the caller's tree unless the diagnostic names it. +- There is no non-interactive reorder or removal. Errors may suggest `gh stack modify`, but it is + TUI-only — restructure with `unstack` then `init` instead. +- PR titles and bodies are auto-generated. Use `gh pr edit` afterwards to change them. + +## More detail + +`gh stack --help` is authoritative for flags and arguments. Note that +`gh stack help ` does **not** work — it prints the top-level help. + +Open the reference whose trigger matches the task; no need to preload all three. + +- `references/stack-design.md` — read before creating a stack, when deciding how many layers to + use, what belongs in each one, or whether work belongs in a new stack. +- `references/commands.md` — read when a command fails unexpectedly or you need its preconditions, + side effects, atomicity, or ordering guarantees. +- `references/troubleshooting.md` — read on a rebase conflict, after a squash-merge, on local and + remote divergence, when restructuring a stack, or when driving stacks from another tool. diff --git a/.agents/skills/gh-stack/references/commands.md b/.agents/skills/gh-stack/references/commands.md new file mode 100644 index 000000000..a2b698b7c --- /dev/null +++ b/.agents/skills/gh-stack/references/commands.md @@ -0,0 +1,214 @@ +# Command behavior + +`gh stack --help` is authoritative for flags and arguments. (`gh stack help ` only prints the top-level help.) This file only covers behavior `--help` does not +explain: preconditions, side effects, atomicity, and failure modes. + +## Contents + +- [init](#init) +- [add](#add) +- [push](#push) +- [submit](#submit) +- [link](#link) +- [sync](#sync) +- [rebase](#rebase) +- [view](#view) +- [checkout](#checkout) +- [unstack](#unstack) +- [merge](#merge) +- [Navigation](#navigation) + +## init + +Creates the stack and checks out the **last** branch in the list, so a single `init` can lay down +the whole chain: `gh stack init auth api frontend`. + +If the final existing branch is already checked out in another worktree, adoption still succeeds: +the command reports its owner and leaves the invoking checkout unchanged. + +`init` processes branch arguments from bottom to top. Existing branches are adopted. If the first +branch does not exist, it is created from the trunk; each later new branch is created from the +branch immediately before it. There is no separate adopt mode — existence decides. `--base` +selects a non-default trunk. + +`init` also enables `git rerere`. Under a TTY the first run in a repo asks for confirmation; set +`git config rerere.enabled true` beforehand to skip it. + +## add + +- **Must run from the top branch** of the stack (or the trunk when the stack is still empty). + Anywhere else it exits **5** with `can only add branches on top of the stack`. Run `gh stack top` + first. +- **Uncommitted changes carry over.** Without `-Am`, `add` does not touch the working tree, so + staged and unstaged changes follow you onto the new branch. Commit or stash first for a clean start. +- **`add -Am` commits in place when the current branch has no commits yet** — for example + immediately after `init` — instead of creating a branch. This is deliberate: the first layer + usually needs its content before a second layer exists. +- `-A` and `-u` are mutually exclusive, and both require `-m`. +- Existing branches owned by another worktree may be adopted without checkout. Commit/stage + shortcuts are rejected before staging or membership changes; they never commit another + worktree's files. + +## push + +Pushes every active (non-merged, non-queued) branch in one multi-ref push with per-branch +`--force-with-lease`. + +**Not atomic.** Some branches may update while another is rejected. A rejection means that branch +moved on the remote; fix that branch and rerun — rerunning is safe and skips what already landed. + +`push` never creates or updates pull requests. Use `submit` for that. + +## submit + +Pushes each active branch, then creates a PR for every branch that lacks one, basing it on the +first non-merged ancestor, then links them into a Stack on GitHub. + +- **Not atomic.** Branches are pushed sequentially with per-branch `--force-with-lease`. If a later + push is rejected, earlier pushes and PR updates stand. Fix the rejection and rerun the same command. +- **A fully merged stack cannot be extended.** When every PR in the current stack is already merged, + `submit` forks the remaining unmerged branches into a **new** stack rooted at the trunk and creates + it on GitHub, leaving the merged stack untouched. +- **Title generation with `--auto`:** a branch with a single commit uses that commit's subject as + the title and its body as the PR body. A branch with multiple commits humanizes the branch name + (hyphens and underscores become spaces). There is no flag for a custom title or body; use + `gh pr edit` afterwards. +- `--open` marks new *and existing* PRs ready for review; without it new PRs are drafts. +- Requires stacked PRs to be enabled on the repository. If not, `submit` exits **9** when + non-interactive (under a TTY it offers to create ordinary unstacked PRs instead). + +## link + +Creates or updates a stack on GitHub **without any local tracking state**. This is the path for +branches managed by another tool. Worktrees alone do not require `link`: local tracking is shared. + +- Arguments are given bottom to top. Each is a branch name or a PR number; a numeric argument is + tried as a PR number first and falls back to a branch name. +- **A numeric first argument is treated as a stack number only when a stack with that number + exists.** In that case the remaining arguments are appended to the top of that stack and you do + not re-list its current PRs: `gh stack link 7 feature-c`. Arguments already in the stack are + skipped; arguments belonging to a different stack are rejected. +- Branch arguments are pushed automatically (non-force, atomic). Missing PRs are created with + auto-generated titles and correctly chained bases; existing PRs with a wrong base are corrected. +- Stack membership is **additive only** — `link` never removes a PR from a stack. + +## sync + +The routine command. Steps, in order: + +1. **Fetch** from the remote. +2. **Reconcile with the GitHub stack.** PRs added to the stack on github.com are pulled down and + appended locally. On divergence, aborts when non-interactive (see `troubleshooting.md`). +3. **Fast-forward the trunk.** Skipped when already current; warns when diverged. +4. **Cascade rebase when needed.** This runs if the trunk moved, a stack branch was fast-forwarded + from its remote, or a branch no longer contains its expected parent. Merged PRs are handled + automatically. On conflict, **all branches are restored** to their pre-rebase state and the + command exits **3**. +5. **Push** all active branches, atomically. +6. **Refresh PR state** from GitHub. +7. **Sync the stack object** — link open PRs into a stack, additively. Only when two or more PRs + exist. `sync` never opens PRs; that is `submit`. +8. **Prune** local branches for merged PRs, only when `--prune` is passed in a non-interactive + environment. + +Affected clean worktrees are updated automatically. Dirty/busy/unavailable owners stop unsafe +updates, and pruning skips branches occupied elsewhere. Cascade rollback does not undo prior +fetches or completed fast-forwards; partial restoration failures retain recovery state. + +## rebase + +Pulls from the remote and cascade-rebases. Use it when `sync` reported a conflict or when you need +to rebase only part of the stack. + +- `--upstack` rebases from the current branch to the top. This is what you run after editing a + lower layer. +- `--downstack` rebases from the trunk to the current branch. +- `--no-trunk` skips fetching and the trunk rebase entirely, aligning stack branches with each + other only. +- `--continue` after staging resolutions; `--abort` restores every branch. +- A merged PR is detected automatically and replayed with `--onto` against the correct target, so a + squash-merged parent does not produce spurious conflicts. +- Starting a rebase while one is in progress exits **7**. +- Occupied branches are rebased in their clean owning worktrees; unoccupied branches use the + origin. Resolve/stage conflicts at the reported path. `--continue`/`--abort` may run from any + linked worktree and use the recorded owners. No auto-stash or worktree lifecycle management. + +## view + +- `--json` writes the machine-readable payload to stdout. Its schema is in `SKILL.md`. +- Bare `view` opens a full-screen TUI when stdout is a TTY, and prints static text when piped. +- `--short` prints a compact one-line-per-branch summary and never opens the TUI, but it is + formatted for humans; parse `--json` instead. +- `view` refreshes PR state from GitHub as a side effect, best-effort — it does not fail when the + API is unreachable. + +## checkout + +Accepts a stack number, PR number, PR URL, or branch name. + +- A bare number resolves as a **stack number first**, then a PR number, then a branch name. +- Stack numbers, PR numbers, and PR URLs fetch from GitHub, pull the branches down, and set the + stack up locally. +- If a local stack already exists over those branches with a different composition, `checkout` + cannot be forced past it. Run `gh stack unstack --local` first, then retry. +- `checkout` relies on `remote.pushDefault` when several remotes exist. +- `--print-path` requires an explicit target and never prompts. It prints a foreign owner's path + without switching, or checks out an unoccupied target here before printing the current root. + Without path mode, a foreign-owned target is a nonzero error, not a successful switch. + +## unstack + +Removes the stack **grouping** only. It never deletes pull requests or branches. + +- With no argument it targets the active stack — the one containing the current branch — removing + it on GitHub and locally. +- With a stack number it works from anywhere in the repository, tracked locally or not, via the API. + Local tracking is also removed when present. +- `--local` removes local tracking only and never contacts GitHub. Combining `--local` with a stack + number that is not tracked locally is an error. +- An unknown stack number exits **2**. + +## merge + +- Scope with an argument: pass a PR number to merge that PR and every unmerged PR below it in the + stack, or pass a stack number to merge every unmerged PR in that stack. +- A bare number resolves as a **stack number first**, then a PR number. Confirm the intended + merge membership when the identifiers overlap. +- **All-or-nothing.** If any PR in that exact merge set cannot be merged, none are, and the reason + is reported. +- The method comes from `--squash`, `--rebase`, `--merge`, or `--merge-method `. Without + one, the last-used method is reused. +- Only basic PR state is checked before merging: open and not a draft. Bypassing merge requirements + is not supported for stacks. +- **A merge queue on the base branch overrides everything.** The stack is added to the queue rather + than merged; the queue chooses the method and any method flag you passed is ignored with a + warning. Queued PRs are submitted together but land as the queue processes them, so they may merge + in separate groups rather than all at once. +- `gh pr merge` cannot merge a stack. Always use `gh stack merge`. + +## Navigation + +`up`, `down`, `top`, `bottom`, and `trunk` are always non-interactive. `up` and `down` accept a +count (`gh stack up 3`). Movement clamps at the stack bounds, and merged branches are skipped when +navigating from an active branch, so `bottom` lands on the lowest *unmerged* branch. + +`gh stack switch` is a selection menu with no non-interactive path. Use the commands above instead. + +All five navigation commands support `--print-path`, as does explicit-target `checkout`. Success +writes only an absolute raw path and newline to stdout; diagnostics go to stderr and errors leave +stdout empty. Check the exit status before using the path: + +```bash +gscd() { + local target + target=$(gh stack "$@" --print-path) || return $? + if [ -z "$target" ]; then + printf '%s\n' 'gh stack returned an empty path' >&2 + return 1 + fi + cd -- "$target" +} +gscd checkout auth +``` + +This is a Bash/Zsh function, not something gh-stack installs. Never use `eval` on path output. diff --git a/.agents/skills/gh-stack/references/stack-design.md b/.agents/skills/gh-stack/references/stack-design.md new file mode 100644 index 000000000..f64543ace --- /dev/null +++ b/.agents/skills/gh-stack/references/stack-design.md @@ -0,0 +1,97 @@ +# Designing a stack + +How to decide what goes in each layer. Read this before running `gh stack init`. + +## Contents + +- [Plan the layers before writing code](#plan-the-layers-before-writing-code) +- [Branch naming](#branch-naming) +- [Staging changes deliberately](#staging-changes-deliberately) +- [When to add a layer](#when-to-add-a-layer) +- [One stack, one story](#one-stack-one-story) + +## Plan the layers before writing code + +A stack is a dependency chain. If code in one layer depends on code in another, the dependency must +live in the same branch or a lower one. That constraint is much cheaper to satisfy by planning than +by restructuring later, because there is no non-interactive in-place reorder — fixing the order +means`unstack` and `init` again. + +Decide the layers first, then write code into them: + +``` +(main) <- todo-app/models <- todo-app/api <- todo-app/frontend <- todo-app/integration +``` + +- `todo-app/models` — shared types and schema +- `todo-app/api` — routes that use the models +- `todo-app/frontend` — components that call the routes +- `todo-app/integration` — tests exercising the whole feature + +This is illustrative. Infer the stack topic and layer names from the actual task; do not reuse +`todo-app` or these layer names literally. + +The failure mode to avoid is writing everything on one branch and trying to split it afterwards. +If a task is large enough to warrant a stack, create the stack at the start. + +## Branch naming + +Prefer a shared topic prefix plus the layer's concern: +`/` — for example, `billing/schema`, `billing/api`, `billing/ui`. +This keeps related branches recognizable without using generic names that could belong to any +stack. **User and repository branch naming conventions take precedence; follow them instead.** + +Names are used exactly as given — nothing is prepended or transformed, and slashes are kept, so +`gh stack add refactor/foo` creates a branch literally named `refactor/foo`. + +If you pass `-m` without a branch name, the name is generated from the commit message in +date-and-slug form (for example `03-24-add_api_routes`). Prefer naming the branch yourself. + +## Staging changes deliberately + +Use `git add` and `git commit` directly rather than the `add -Am` shortcut. The point is control +over which changes land in which branch. With several modified files in the working tree, stage the +subset that belongs to the current layer, commit it, then create the next branch and stage the rest +there: + +```bash +git add internal/models/user.go internal/models/session.go +git commit -m "Add user and session models" + +gh stack add api-routes +git add internal/api/routes.go internal/api/handlers.go +git commit -m "Add user API routes" +``` + +Multiple commits per branch are fine. What matters is that every commit in a branch serves the same +concern, and that a change belonging to a different concern goes in a different branch. + +Note that `gh stack add ` without `-Am` does not touch the working tree, so uncommitted +changes carry over to the new branch. Commit or stash first if you want the new layer to start clean. + +## When to add a layer + +Add a branch when you start a **different concern that depends on what you have built so far**. +Signals: + +- Moving from backend to frontend, or from core logic to tests or documentation +- The next changes have a different reviewer audience +- The current branch's diff is already large enough to review on its own + +A layer that cannot be described in one sentence is usually two layers. + +## One stack, one story + +A stack should read as a coherent progression: a reviewer walks the PRs bottom to top and sees the +feature being built. + +**Use a single stack** when every branch serves the same feature or project, even if the layers span +different concerns. + +**Start a separate stack** for unrelated work — a different feature, an unrelated bug fix, an +independent refactor. Do not mix efforts into one stack just because you happened to work on both. +Use `gh stack init` for the new effort, or `gh stack checkout ` to move between existing +stacks. + +A trivial incidental fix can ride along in the current stack. Once it grows into its own project, it +deserves its own stack. diff --git a/.agents/skills/gh-stack/references/troubleshooting.md b/.agents/skills/gh-stack/references/troubleshooting.md new file mode 100644 index 000000000..3bd45fb7f --- /dev/null +++ b/.agents/skills/gh-stack/references/troubleshooting.md @@ -0,0 +1,186 @@ +# Troubleshooting and recovery + +## Contents + +- [Rebase conflicts (exit 3)](#rebase-conflicts-exit-3) +- [After a squash merge](#after-a-squash-merge) +- [Local and remote stacks have diverged](#local-and-remote-stacks-have-diverged) +- [Restructuring a stack](#restructuring-a-stack) +- [Branch belongs to several stacks (exit 6)](#branch-belongs-to-several-stacks-exit-6) +- [Driving stacks from another tool or worktree](#driving-stacks-from-another-tool-or-worktree) +- [Stack file is locked (exit 8)](#stack-file-is-locked-exit-8) +- [An interrupted modify session (exit 10)](#an-interrupted-modify-session-exit-10) + +## Rebase conflicts (exit 3) + +`rebase` and `sync` both exit 3 on conflict. `sync` restores every branch to its pre-rebase state +first, so a failed `sync` leaves nothing half-applied; a failed `rebase` stops mid-flight and waits. + +```bash +gh stack rebase +# exit 3 — conflicted paths are listed on stderr +git add +gh stack rebase --continue # repeat if the next branch also conflicts +``` + +`gh stack rebase --abort` restores every branch in the stack, not just the current one. + +Because `init` enables `git rerere`, a conflict you resolve once is replayed automatically the next +time the same conflict appears — which is common, since a change low in the stack is rebased through +every branch above it. Without `rerere`, repeated conflicts may need manual resolution on each +affected layer. + +## After a squash merge + +A squash merge replaces the branch's commits with one new commit, so the originals no longer exist +in the trunk's history and an ordinary rebase would try to replay them again. + +`gh stack sync` detects this and rebases with `--onto` against the correct target, skipping the +merged branch: + +```bash +gh stack sync +gh stack view --json # merged branch reports "isMerged": true, "state": "MERGED" +``` + +No manual action is needed. If the replay conflicts, `sync` restores all branches and exits 3. +Run `gh stack rebase` to rerun the rebase, which will stop at the conflict and allow you to resolve +and then `--continue` until complete. Use `gh stack sync --prune` to also delete local branches for +merged PRs. + +## Local and remote stacks have diverged + +Divergence means the local stack and the stack on GitHub changed in different ways — for example +branches were added locally while a PR was added to the stack on github.com. + +When non-interactive, `sync` prints both chains, changes nothing, and exits **0** with +`Sync aborted`. Success here does not mean the sync happened; check for that message, or re-run +`gh stack view --json` and compare. + +Two resolution paths: + +- **Keep the remote version.** Drop local tracking and pull the stack back down. + + ```bash + gh stack unstack --local # keeps the stack on GitHub + gh stack checkout # or a PR number + ``` + +- **Keep the local version.** Remove the grouping on GitHub, then recreate it from local state. + + ```bash + gh stack unstack # removes the grouping; PRs and branches survive + gh stack submit --auto + ``` + +Neither path deletes pull requests or branches. +Remote unstacking leaves PRs that are merging (auto-merge enabled) or are queued (in a merge queue) +stacked. If needed, clear that state before retrying. + +## Restructuring a stack + +There is no non-interactive reorder, rename, or removal. `add` run from the wrong branch suggests +`gh stack modify`, but that is TUI-only. Tear the stack down and rebuild it instead: + +```bash +gh stack unstack # removes local tracking and the GitHub grouping +# Rename or drop branches, and rewrite ancestry as needed. +gh stack init --base main branch-1 branch-2 branch-3 +gh stack submit --auto # re-link on GitHub +``` + +`init` adopts branches that already exist, so the rebuild reuses them rather than creating new ones. +Existing PRs survive. Once Git ancestry is correct, `submit` updates their base branches and +re-links the stack on GitHub. + +Changing metadata does **not** change Git ancestry. Reorder commits first, then rebuild the stack. +For example, to change `main <- models <- migration <- ui` into +`main <- migration <- models <- ui`: + +```bash +old_models=$(git rev-parse models) +old_migration=$(git rev-parse migration) +git rebase --onto main "$old_models" migration +git rebase --onto migration main models +git rebase --onto models "$old_migration" ui +gh stack unstack +gh stack init --base main migration models ui +``` + +The first rebase moves migration-only commits onto trunk, the second replays model commits above +them, and the third replays UI-only commits above models. Preserve the old boundary SHAs before +moving any branch. For a different reorder, identify each layer's range with +`git log ..`, then replay the ranges bottom to top. + +## Branch belongs to several stacks (exit 6) + +Commands exit 6 when the current branch cannot identify a single stack — typically because it is the +trunk of more than one stack. There is no flag to disambiguate. + +```bash +gh stack checkout +``` + +Then rerun. Commands that take an explicit stack number (`merge 7`, `unstack 7`) sidestep the +problem entirely, since they do not infer the stack from the current branch. + +## Driving stacks from another tool or worktree + +`gh stack link` creates and updates stacks purely through the API, with no local tracking state. +Use it when branches are managed by jj, Sapling, git-town, or another external workflow that does +not use gh-stack's local catalog. Linked worktrees themselves are supported: they share +`/gh-stack` and do not require `link`. + +```bash +gh stack link branch-a branch-b branch-c # bottom to top +gh stack link --base develop --open a b c # non-default trunk, ready for review +gh stack link 10 20 30 # by PR number +gh stack link 7 feature-d # append to existing stack #7 +``` + +Because `link` writes no local state, the local navigation commands (`up`, `down`, `top`, `bottom`) +will not work on the result. Use `gh stack checkout ` if you later want local tracking. + +Git 2.36+ is required. Legacy worktree catalogs are consolidated automatically only when their +definitions agree or are disjoint; originals are preserved. On migration conflicts, reconcile the +reported source definitions rather than choosing the newest file. Finish legacy operations in +their original worktree first, and do not mix old and new gh-stack writers in one clone. + +Navigation does not take over another worktree's checkout. Use `--print-path` with an explicit +target, check the exit status, and change directory to the quoted output. Only affected clean +owners are updated by rebase/sync; commit or stash manually when those owners are dirty. gh-stack +does not automatically stash or create/remove worktrees. + +For `git init --separate-git-dir` repositories, main invocation and existing absolute/relative +`core.worktree` backlinks are supported, including settings in the main `config.worktree`. The +discovery caveat is only linked invocation without a main-worktree backlink. A required unresolved +main owner produces actionable guidance to run from the main worktree or supply the backlink; +unaffected worktrees continue. Never navigate to an administration directory or guess its checkout. +Existing backlinks are read without adding a private registry or changing Git configuration. + +## Stack file is locked (exit 8) + +Another `gh stack` process holds either the short catalog lock (`/gh-stack.lock`) or +the clone-wide mutation lock (`/gh-stack-operation.lock`). Wait and retry; read-only +views remain available. Do not delete lock files to bypass coordination. Paused operations are +also guarded by shared recovery journals after their process lock has been released. + +## An interrupted modify session (exit 10) + +`gh stack modify` is TUI-only and should never be invoked by an agent. If a repository is left in +this state by someone else, restore it: + +```bash +gh stack modify --abort +``` + +Related: `submit` also detects a pending modify state, and under a TTY asks before overwriting the +matching stack on GitHub with local state. An unrelated stack cannot consume or clear that journal. + +Modify supports stacks distributed across worktrees, but agents must still not launch its TUI. +Renames, fold-down cherry-picks, and rebases execute in the appropriate clean owners; only the +origin switches for unoccupied branches. Drop/fold sources and their worktrees remain intact. +Resolve and stage in the worktree reported by the conflict, then invoke `modify --continue` from +any linked worktree. A later conflict can be in a different owner. Abort reverses owner-local +renames, restores only operation-touched refs, and deletes only proven operation-created refs, +never worktrees. Missing owners, externally changed refs, or save failures retain the journal. diff --git a/.agents/skills/github/SKILL.md b/.agents/skills/github/SKILL.md index 13e56e1e9..392937564 100644 --- a/.agents/skills/github/SKILL.md +++ b/.agents/skills/github/SKILL.md @@ -382,8 +382,17 @@ below exists because the tracker drifted without it. ## Closing it +For stacked work, load `gh-stack` and follow +[the project stack rules](../../../docs/process/github.md#stacked-pull-requests). +Upper layers use their immediate parent ref for local `--base`. Submit with +`gh stack submit --auto`, then edit every PR to the repository template and +assign its labels/milestone. Partial slices use `Refs`; the completing layer +uses `Closes`. Inspect proof and release assignments for every PR included in +`gh stack merge`, and verify each result after landing. + Before opening or marking ready, run `bun run check:pr --base origin/main` -for focused local preflight and record actual behavioral/mutation receipts. +(`--base ` for an upper stack layer) for focused local preflight +and record actual behavioral/mutation receipts. Complete repeatable proof is authoritative in affected hosted jobs under #1326; `--full`/`--all` retain local fallback/release commands. Missing tools, failed or cancelled checks and unexpected skips cannot pass. Keep drafts cheap and batch diff --git a/.agents/skills/work-issue/SKILL.md b/.agents/skills/work-issue/SKILL.md index 2b71aa10e..3dacd1d5f 100644 --- a/.agents/skills/work-issue/SKILL.md +++ b/.agents/skills/work-issue/SKILL.md @@ -122,6 +122,12 @@ gaps; workflow status lives on the board. Then **Finish or hand off** and stop. ## Prepare the issue checkout +For a slice in an existing stack, load `gh-stack`, identify the owning layer, +and use its clean dedicated checkout. A new upper layer starts from its +immediate parent under the [stack rules](../../../docs/process/github.md#stacked-pull-requests), +instead of starting again from `origin/main`. Keep the isolation and ownership +requirements below when locating or creating that checkout. + Reuse an environment-provided worktree only when it is dedicated to this invocation and its branch/base are appropriate for the issue. Otherwise, when isolation is required, create a worktree from fetched `origin/main` @@ -183,7 +189,8 @@ never identifies a private-instance follow-up; sanitize the public handoff. ## PR, CI and merge Before creating a PR or marking a draft ready, run `bun run check:pr --base -origin/main` in the issue checkout. This supplies focused local preflight; +origin/main` in the issue checkout (the verified immediate parent ref for an +upper stack layer). This supplies focused local preflight; `--plan` is inspection only. Record behavioral failing-first/restored mutation receipts and visual review where relevant. Complete repeatable proof runs in hosted CI on ready PRs under #1326: types, packaging, full affected discovery, @@ -204,7 +211,9 @@ and the relevance assessment, satisfying existing branch rules. Never relabel older green evidence as a new combined-tree receipt. Before every push, confirm the branch's PR is not already merged/closed. -Open one PR against `main` using the repository template and appropriate +Open one PR against `main`, or one PR per dependent slice targeting its parent +under [the stack rules](../../../docs/process/github.md#stacked-pull-requests). +Load `gh-stack` before working in a stack. Use the repository template and appropriate type/area/`contract` labels; do not copy readiness labels onto the PR. - `Closes #N` means merging meets all acceptance criteria and required @@ -225,7 +234,8 @@ type/area/`contract` labels; do not copy readiness labels onto the PR. under the GitHub skill. This applies to every merge, including docs/test/CI work and `Refs` slices; choose the confirmed upcoming release for unreleased work and preserve the initial release of unchanged shipped behavior. Then - merge using `--match-head-commit` and squash. Respect a merge queue and + squash with `--match-head-commit` for an ordinary PR, or follow the stack + merge/evidence procedure for native stacked PRs. Respect a merge queue and monitor actual completion. Do not ask again for permission included in the end-to-end request. diff --git a/.agents/skills/work-issue/references/ci-and-merge.md b/.agents/skills/work-issue/references/ci-and-merge.md index 2e88b2943..60060525e 100644 --- a/.agents/skills/work-issue/references/ci-and-merge.md +++ b/.agents/skills/work-issue/references/ci-and-merge.md @@ -16,7 +16,8 @@ means CI has not started on that commit, which is not the same as green. ## Required evidence Before opening/marking ready, execute `bun run check:pr --base origin/main` -for focused preflight. Record actual behavioral failing-first/restored mutation +(`--base ` for an upper stack layer) for focused preflight. +Record actual behavioral failing-first/restored mutation receipts and visual judgment. Complete repeatable proof runs in selected hosted jobs under #1326, including full affected discovery, pinned browser/layout/ endurance/editorial and native runtime categories. Optional `--full` and `--all` @@ -33,6 +34,9 @@ unknown domains, main pushes and scheduled/manual coverage remain fresh. ## Read the actual head +For stacked PRs, apply [Stacked PR evidence](#stacked-pr-evidence) below; +the `main` commands in this reference describe an ordinary PR or bottom layer. + Record the PR head and query live main independently. Run the comparison blocks with shell error checking enabled; a failed comparison stops the merge decision. PR `baseRefOid` is not a substitute for the live Git reference. @@ -174,6 +178,36 @@ verify affected behavior. A required unavailable review or new ruling is a blocker; an optional review is not a reason to invent an approval step. Never bypass required reviewers or use `--admin`. +## Stacked PR evidence + +Load `gh-stack` and follow the +[project stack rules](../../../../docs/process/github.md#stacked-pull-requests). +Run local preflight against the immediate parent ref for each upper layer. +For every PR in the intended merge prefix, record its head and declared base +branch, independently query that base's live Git ref, and inspect its own +selected jobs/checkouts. A two-parent synthetic checkout must have that recorded +parent tip and PR head as parents. Keep contract head-only receipts separate. +Do not substitute live `main` for an upper layer's base or claim its CI tested +the eventual integrated trunk tree. + +Read back every included PR's release milestone and all issues it advances, +along with current heads, bases, checks and review state. Confirm the resolved +merge target: numeric arguments prefer stack numbers over PR numbers. +Use `gh stack merge --yes --squash` for a native stack. The installed +CLI has no matching-head option; a final readback reduces the race window but +does not lock heads. Retain the inspected heads and reconcile the actual result +against them. Never substitute `gh pr merge` or `--admin` to bypass a refused +stack merge. + +After landing, read each included PR's state and reported merge commit. Fetch +main, prove those commits are ancestors, and inspect their actual parents, +trees and delivered diffs against the inspected layers. Squashing can replace +parent commit identities; do not require every upper squash to have its old +parent branch SHA or apply the ordinary single-PR tree equality blindly. +Reconcile any different integrated tree and verify automatic main-push proof, +then read back issue closure and milestone assignments for every delivered +slice. Queued PRs require actual merge-group and eventual landing evidence. + ## Merge and verify Before every merge, assign the PR and each completed/advanced issue a release diff --git a/.claude/skills/gh-stack b/.claude/skills/gh-stack new file mode 120000 index 000000000..5a8efb5f2 --- /dev/null +++ b/.claude/skills/gh-stack @@ -0,0 +1 @@ +../../.agents/skills/gh-stack \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5504e67f5..f03707759 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,8 @@ A change to the machine surface is prefixed CONTRACT: instead. required verification. If a human action remains, use Refs # instead, name the action below, and hand the issue over with needs: human after independent agent work is complete. --> + Closes # ## What changed @@ -32,6 +34,10 @@ Closes # bun run check:pr --base origin/main ``` + + ## Checklist - [ ] Focused local preflight/behavioral receipts and complete selected hosted proof passed; actual head/base/job checkout evidence is in Proof. diff --git a/AGENTS.md b/AGENTS.md index 9518f2726..aebd48687 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,7 +141,9 @@ on, links to or describes them. - A review finding is one instance of a bug class: grep the diff and its file for every other instance before calling it closed. - Before opening or marking a PR ready, run `bun run check:pr --base - origin/main` and keep focused failing-first/mutation receipts. Ready-PR CI + origin/main` (use the immediate parent ref for an upper stack layer; + [stack rules](docs/process/github.md#stacked-pull-requests)) and keep focused + failing-first/mutation receipts. Ready-PR CI supplies the complete affected proof; every selected job must pass, and missing, cancelled or unexpectedly skipped jobs are not green. Policy: [the CI decision](docs/decisions/ci-utility.md) and @@ -192,6 +194,7 @@ In `.agents/skills/`, symlinked into `.claude/skills/`. | --- | --- | | `release` | Versioning, publishing, or changing what a release ships. | | `github` | Filing, triaging, picking up or closing work in either tracker. | +| `gh-stack` | Creating or working in stacked branches/PRs, including rebasing, submitting and merging a stack. Apply the [project stack rules](docs/process/github.md#stacked-pull-requests) alongside it. | | `work-issue` | Taking one unassigned, highest-priority ready brain-kit issue through implementation and merge, or a blocked handoff. | | `triage-issues` | Preparing open brain-kit issues without `agent-ready`: resolve factual gaps, maintain prompts, and reconcile labels and epic membership. | diff --git a/CLAUDE.md b/CLAUDE.md index 39bb6f180..ba7dba181 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,3 +12,6 @@ Repo-local skills live in `.agents/skills/` and are symlinked into release ships. - **`github`** — load it before filing, triaging, picking up or closing work in either issue tracker. +- **`gh-stack`** — load it before creating or working in stacked branches or + pull requests. Follow the [project stack rules](docs/process/github.md#stacked-pull-requests) + alongside the skill. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc8beff7e..af04f57f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,6 +131,11 @@ host PATH to a doctor fixture. The hostile-sentinel tests exercise both ## Local feedback and hosted proof +For stacked PRs, use the immediate parent branch as `--base` on upper layers; +the bottom layer uses `origin/main`. Each layer must meet the same proof and +metadata requirements. See [Stacked pull requests](docs/process/github.md#stacked-pull-requests) +for branch ownership, issue closure and merge verification. + Run from the contribution checkout using the relevant base: ```sh diff --git a/docs/process/github.md b/docs/process/github.md index 7cb806910..6b36094e7 100644 --- a/docs/process/github.md +++ b/docs/process/github.md @@ -269,7 +269,8 @@ sit in the Ready view as traps. stranger could implement it from the body alone. Task templates do not add it automatically; triage establishes readiness. 4. **In progress.** Assign yourself, move the card. One issue at a time per - branch: `/-`. + branch: `/-`. A dependent slice may use a separate + branch under the [stack rules](#stacked-pull-requests). 5. **In review.** Open the PR with `Closes #` in the body when merging meets every acceptance criterion and verification step. If a required human step will remain, use `Refs #` instead and name the step under "Anything left @@ -297,6 +298,52 @@ its last blocker closes (the `project-sync` workflow does it for a readable `Blocked by` line), and a duplicate is closed into one survivor. The `github` skill's "Keeping an issue true" has the procedure. +## Stacked pull requests + +Use a stack when independently reviewable slices depend on each other. A single +concern stays in one PR; unrelated work uses separate branches or stacks. Load +the [gh-stack skill](../../.agents/skills/gh-stack/SKILL.md) for commands and +non-interactive operation. These project rules apply alongside it: + +- Plan layers before implementation. Each branch owns one reviewable slice, + including its tests and required docs/changeset; every merged prefix must be + usable and satisfy the existing merge bar. Keep the issue branch naming + convention, adding a slice suffix when several layers advance one issue. +- The bottom PR targets `main`; each upper PR targets its immediate parent. + Run `bun run check:pr --base origin/main` at the bottom and + `bun run check:pr --base ` above it, after fetching or verifying + that parent. CI and contract gates already use the PR's declared base. + Recheck the diff and affected proof after rebasing or retargeting. +- Each PR uses the repository template and carries its own title, labels, + release milestone and proof. Replace generated titles/bodies after + `gh stack submit --auto`; inspect each layer's contract diff and changeset + requirement against its parent. Do not copy a lower layer's proof as an + upper layer's receipt. +- A partial slice uses `Refs #N` and states what remains. Only the layer that + completes all criteria and required verification uses `Closes #N`. + [GitHub interprets closing keywords only for PRs targeting the default branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue). + Verify actual issue state after the stack lands and reconcile closure with + completion evidence when automatic closure did not happen. Every merge + includes milestone readback for all included PRs and the issues each + completes or advances. +- Edit the owning layer, commit, then rebase upstack and push with the skill's + commands. Inspect the resulting heads and current checks for every affected + PR. Respect clean worktree ownership and never push to a merged branch. +- Merge stacked PRs with `gh stack merge --yes --squash`. A target + includes its unmerged ancestors; a bare number resolves as a stack number + before a PR number. Confirm the resolved membership and inspect all included + PRs' current heads, bases, reviews, milestones and selected checks immediately + before merging. The installed command has no `--match-head-commit` flag; + do not describe that readback as an atomic head lock or bypass branch rules. + If native stacks are unavailable, report the CLI error and use ordinary PRs + only after deliberately reconciling their ancestry and bases. +- Upper-layer CI proves a merge with its parent branch, not with live `main`. + Record that actual parent/head/checkout tree. After merging, verify every + included PR's merge result on `main`, the integrated commits/trees, issue + states and milestones, and automatic main-push proof. A queued stack is not + yet merged. Use the stack-specific guidance in + [CI and merge](../../.agents/skills/work-issue/references/ci-and-merge.md#stacked-pr-evidence). + ## What an agent does before writing code This repo is operated by coding agents, so this is a contract and not advice. diff --git a/docs/process/testing.md b/docs/process/testing.md index a78cd5360..717acfcfb 100644 --- a/docs/process/testing.md +++ b/docs/process/testing.md @@ -243,6 +243,9 @@ in the [CI and merge reference](../../.agents/skills/work-issue/references/ci-an keyless funnel, pinned browser proof, strict types and packaging on ready PRs. Agents keep focused local behavioral proof. - Run `bun run check:pr --base origin/main` before opening or marking ready. + For an upper stack layer, substitute its verified immediate parent ref; + see [stack rules](github.md#stacked-pull-requests). Each included PR needs + its own selected proof and actual base/head/checkout evidence. This is local preflight and affected fast feedback, including working-tree inputs. Agents retain targeted debugging, failing-first/restored mutation receipts and visual judgment; ordinary PRs do not duplicate complete types,