From 9a9a1dea06e6b1f4ec57bc1e374190096b16ceb3 Mon Sep 17 00:00:00 2001 From: grim <75869731+ripgrim@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:35:53 -0700 Subject: [PATCH] CMP-62 chore: add gh-stack skill --- .agents/skills/gh-stack/SKILL.md | 183 ++++++++++++++++++ .../skills/gh-stack/references/commands.md | 179 +++++++++++++++++ .../gh-stack/references/stack-design.md | 97 ++++++++++ .../gh-stack/references/troubleshooting.md | 159 +++++++++++++++ 4 files changed, 618 insertions(+) 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 diff --git a/.agents/skills/gh-stack/SKILL.md b/.agents/skills/gh-stack/SKILL.md new file mode 100644 index 00000000..96aab643 --- /dev/null +++ b/.agents/skills/gh-stack/SKILL.md @@ -0,0 +1,183 @@ +--- +name: gh-stack +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 + version: "0.1.0" +--- + +# 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 + +```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. + +## 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 # PR #42 plus every unmerged PR below it +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. + +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. +- 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. +- `checkout ` resolves against local stacks only. Use a stack or PR number to pull a + stack down from GitHub. + +## 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 00000000..46e58336 --- /dev/null +++ b/.agents/skills/gh-stack/references/commands.md @@ -0,0 +1,179 @@ +# 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`. + +`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`. + +## 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 or living in another worktree — see `troubleshooting.md`. + +- 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. + +## 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**. + +## 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. +- A **branch name resolves against locally tracked stacks only** and never contacts GitHub. Use a + stack or PR number to pull a stack that is not tracked 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` has no flags. It relies on `remote.pushDefault` when several remotes exist. + +## 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. +- **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. 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 00000000..f64543ac --- /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 00000000..fc97b41a --- /dev/null +++ b/.agents/skills/gh-stack/references/troubleshooting.md @@ -0,0 +1,159 @@ +# 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, a separate worktree, or any workflow +where the local `.git/gh-stack` file would be wrong or absent. + +```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. + +## Stack file is locked (exit 8) + +Another `gh stack` process holds the exclusive lock on `.git/gh-stack.lock`. The lock times out +after about five seconds, so wait and retry. A persistent exit 8 means another process still holds +the lock; identify and stop that process before retrying. + +## 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 +stack on GitHub with local state.