From b3dca45ad1f2bc061fa9a05bb4be582b5cbd1610 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 3 Aug 2026 04:33:20 -0400 Subject: [PATCH 1/7] break up skill into multiple files --- skills/gh-stack/SKILL.md | 942 ++---------------- skills/gh-stack/references/commands.md | 177 ++++ skills/gh-stack/references/stack-design.md | 93 ++ skills/gh-stack/references/troubleshooting.md | 137 +++ 4 files changed, 514 insertions(+), 835 deletions(-) create mode 100644 skills/gh-stack/references/commands.md create mode 100644 skills/gh-stack/references/stack-design.md create mode 100644 skills/gh-stack/references/troubleshooting.md diff --git a/skills/gh-stack/SKILL.md b/skills/gh-stack/SKILL.md index 3f8a60d..bd89e8d 100644 --- a/skills/gh-stack/SKILL.md +++ b/skills/gh-stack/SKILL.md @@ -1,891 +1,163 @@ --- name: gh-stack description: > - Manage stacked branches and pull requests with the gh-stack GitHub CLI extension. - Use when the user wants to create, push, rebase, sync, navigate, or view stacks of - dependent PRs. Triggers on tasks involving stacked diffs, dependent pull requests, - branch chains, or incremental code review workflows. + Creates and manages stacked branches and pull requests with the gh-stack GitHub CLI + extension. Use when working with stacked PRs, stacked diffs, dependent pull requests, + or branch chains, when running any gh stack command, or before splitting a large + change into a chain of dependent PRs. metadata: author: github - version: "0.0.9" + version: "0.1.0" --- # gh-stack -`gh stack` is a [GitHub CLI](https://cli.github.com/) extension for managing **stacked branches and pull requests**. A stack is an ordered list of branches where each branch builds on the one below it, rooted on a trunk branch (typically the repo's default branch). Each branch maps to one PR whose base is the branch below it, so reviewers see only the diff for that layer. +`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. -``` -main (trunk) - └── auth-layer → PR #1 (base: main) - bottom (closest to trunk) - └── api-endpoints → PR #2 (base: auth-layer) - └── frontend → PR #3 (base: api-endpoints) - top (furthest from trunk) -``` - -The **bottom** of the stack is the branch closest to the trunk, and the **top** is the branch furthest from the trunk. Each branch inherits from the one below it. Navigation commands (`up`, `down`, `top`, `bottom`) follow this model: `up` moves away from trunk, `down` moves toward it. - -## When to use this skill - -Use this skill when the user wants to: - -- Break a large change into a chain of small, reviewable PRs -- Create, rebase, push, or sync a stack of dependent branches -- Navigate between layers of a branch stack -- View the status of stacked PRs -- Tear down and rebuild a stack to remove, reorder, or rename branches - -## Prerequisites - -The GitHub CLI (`gh`) v2.0+ must be installed and authenticated. Install the extension with: - -```bash -gh extension install github/gh-stack -``` - -Before using `gh stack`, configure git to prevent interactive prompts: - -```bash -git config rerere.enabled true # remember conflict resolutions (skips prompt on init) -git config remote.pushDefault origin # if multiple remotes exist (skips remote picker) -``` - -## Agent rules - -**All `gh stack` commands must be run non-interactively.** Every command invocation must include the flags and positional arguments needed to avoid prompts, TUIs, and interactive menus. If a command would prompt for input, it will hang indefinitely. - -1. **Always supply branch names as positional arguments** to `init`, `add`, and `checkout`. Running these commands without arguments triggers interactive prompts. Branch names are used exactly as given — a name is never prefixed or transformed, so `gh stack add refactor/foo` creates a branch named `refactor/foo`. -2. **Always use `--auto` with `gh stack submit`** to auto-generate PR titles. Without `--auto`, `submit` prompts for a title for each new PR. -3. **Always use `--json` with `gh stack view`.** Without `--json`, the command launches an interactive TUI that cannot be operated by agents. There is no other appropriate flag — always pass `--json`. -4. **Handle multiple remotes.** If more than one remote is configured, pre-configure `git config remote.pushDefault origin`, or pass `--remote ` to the commands that accept it: `push`, `submit`, `sync`, `rebase`, and `link`. `checkout`, `modify`, and `trunk` resolve a remote but have **no `--remote` flag** — they rely on `remote.pushDefault`. With multiple remotes and no configured default, these commands exit with an error in non-interactive mode. -5. **Avoid branches shared across multiple stacks.** If a branch belongs to multiple stacks, commands exit with code 6. Check out a non-shared branch first. -6. **Plan your stack layers by dependency order before writing code.** Foundational changes (models, APIs, shared utilities) go in lower branches; dependent changes (UI, consumers) go in higher branches. Think through the dependency chain before running `gh stack init`. -7. **Use standard `git add` and `git commit` for staging and committing.** This gives you full control over which changes go into each branch. The `-Am` shortcut is available but should not be the default approach—stacked PRs are most effective when each branch contains a deliberate, logical set of changes. -8. **Navigate down the stack when you need to change a lower layer.** If you're working on a frontend branch and realize you need API changes, don't hack around it at the current layer. Navigate to the appropriate branch (`gh stack down`, `gh stack checkout`, or `gh stack bottom`), make and commit the changes there, run `gh stack rebase --upstack`, then navigate back up to continue. -9. **Use `gh stack link` for external tool workflows.** When branches are managed by an external tool (jj, Sapling, etc.), use `gh stack link branch-a branch-b`. `link` does not rely on local tracking state and is intended for API-driven PR and stack management. Provide at least two branches/PRs to create or update a stack, or a stack number followed by the new branches/PRs to append them to the top of an existing stack (e.g. `gh stack link 7 branch-c`). -10. **Use `gh stack merge --yes` to merge stacked PRs.** `gh pr merge` does not work with stacked PRs. In a non-interactive terminal `gh stack merge` runs without prompting and merges the entire stack (bottom to top) atomically; pass `--yes` to be explicit. Scope the merge by passing a pull request number (`gh stack merge 42 --yes` merges everything up to and including PR #42) or a stack number (`gh stack merge 7 --yes`, which needs no local checkout). Choose the method with `--squash`, `--rebase`, `--merge`, or `--merge-method `; without one, the last-used method is used. The merge is all-or-nothing — if any PR can't be merged, none are, and the failure reason is reported. Only basic pull request state is checked before merging (open and not a draft); bypassing merge requirements is not supported for stacks. If the base branch uses a merge queue, the stack is added to the queue instead of merging directly: the queue chooses the merge method (any method you pass is ignored with a warning), and the pull requests are added to the queue together but merge as the queue processes them, so they may land in separate groups rather than all at once. - -**Never do any of the following — each triggers an interactive prompt or TUI that will hang:** -- ❌ `gh stack view` or `gh stack view --short` — always use `gh stack view --json` -- ❌ `gh stack submit` without `--auto` — always use `gh stack submit --auto` -- ❌ `gh stack init` without branch arguments — always provide branch names -- ❌ `gh stack add` without a branch name — always provide a branch name -- ❌ `gh stack checkout` without an argument — always provide a PR number or branch name -- ❌ `gh stack checkout ` when a different local stack already exists on those branches — this triggers an unbypassable conflict resolution prompt; use `gh stack unstack --local` first to remove the local tracking state (this keeps the stack on GitHub intact), then retry the checkout - -## Thinking about stack structure - -Each branch in a stack should represent a **discrete, logical unit of work** that can be reviewed independently. The changes within a branch should be cohesive—they belong together and make sense as a single PR. - -### Dependency chain - -Stacked branches form a dependency chain: each branch builds on the one below it. This means **foundational changes must go in lower (earlier) branches**, and code that depends on them goes in higher (later) branches. - -**Plan your layers before writing code.** For example, a full-stack feature might be structured like this (use branch names relevant to your actual task, not these generic ones): - -``` -main (trunk) - └── data-models ← shared types, database schema - └── api-endpoints ← API routes that use the models - └── frontend-ui ← UI components that call the APIs - └── integration ← tests that exercise the full stack -``` - -This is illustrative — choose branch names and layer boundaries that reflect the specific work you're doing. The key principle is: if code in one layer depends on code in another, the dependency must be in the same branch or a lower one. - -### Branch naming - -Choose a clear, descriptive branch name for each layer that reflects the concern it contains (e.g., `auth`, `api-routes`, `frontend`). Branch names are used exactly as you provide them to `init` and `add` — nothing is prepended or transformed. Slashes are allowed and are treated as part of the name (e.g., `gh stack add refactor/foo` creates a branch named `refactor/foo`). - -### Staging changes deliberately - -The main reason to use `git add` and `git commit` directly is to control **which changes go into which branch**. When you have multiple files in your working tree, you can stage a subset for the current branch, commit them, then create a new branch and stage the rest there: - -```bash -# You're on data-models with several new files in your working tree. -# Stage only the model files for this branch: -git add internal/models/user.go internal/models/session.go -git commit -m "Add user and session models" - -git add db/migrations/001_create_users.sql -git commit -m "Add user table migration" - -# Now create a new branch for the API layer and stage the API files there: -gh stack add api-routes # created & switched to the api-routes branch -git add internal/api/routes.go internal/api/handlers.go -git commit -m "Add user API routes" -``` - -This keeps each branch focused on one concern. Multiple commits per branch are fine — the key is that all commits in a branch relate to the same logical concern, and changes that belong to a different concern go in a different branch. - -### When to create a new branch - -Create a new branch (`gh stack add`) when you're starting a **different concern** that depends on what you've built so far. Signs it's time for a new branch: - -- You're switching from backend to frontend work -- You're moving from core logic to tests or documentation -- The next set of changes has a different reviewer audience -- The current branch's PR is already large enough to review - -### One stack, one story - -Think of a stack from the reviewer's perspective: the stack of PRs should **tell a cohesive story** about a feature or project. A reviewer should be able to read the PRs in sequence and understand the progression of changes, with each PR being a small, logical piece of the whole. - -**When to use a single stack:** All the branches are part of the same feature, project, or closely related effort. Even if the work spans multiple concerns (models, API, frontend), they're all building toward the same goal. - -**When to create a separate stack:** The work is unrelated to your current stack — a different feature, a bug fix in an unrelated area, or an independent refactor. Don't mix unrelated work into a single stack just because you happen to be working on both. Start a new stack with `gh stack init` or switch to an existing stack with `gh stack checkout` for each distinct effort. - -Small, incidental fixes (e.g., fixing a typo you noticed) can go in the current stack if they're trivial. But if a change grows into its own project, it deserves its own stack. - -## Quick reference - -| Task | Command | -|------|---------| -| Create a stack | `gh stack init auth` | -| Create a stack of multiple branches | `gh stack init auth api frontend` | -| Adopt existing branches | `gh stack init existing-branch-a existing-branch-b` | -| Set custom trunk | `gh stack init --base develop branch-a` | -| Add a branch to stack | `gh stack add api-routes` | -| Add branch + stage all + commit | `gh stack add -Am "message" api-routes` | -| Push branches to remote | `gh stack push` | -| Push to specific remote | `gh stack push --remote origin` | -| Push branches + create draft PRs | `gh stack submit --auto` | -| Create PRs as ready for review | `gh stack submit --auto --open` | -| Sync (fetch, rebase, push) | `gh stack sync` | -| Sync with specific remote | `gh stack sync --remote origin` | -| Sync and prune merged branches | `gh stack sync --prune` | -| Rebase entire stack | `gh stack rebase` | -| Rebase upstack only | `gh stack rebase --upstack` | -| Rebase without trunk | `gh stack rebase --no-trunk` | -| Continue after conflict | `gh stack rebase --continue` | -| Abort rebase | `gh stack rebase --abort` | -| View stack details (JSON) | `gh stack view --json` | -| Switch branches up/down in stack | `gh stack up [n]` / `gh stack down [n]` | -| Switch to top/bottom branch | `gh stack top` / `gh stack bottom` | -| Check out by stack number | `gh stack checkout 7` | -| Check out by PR | `gh stack checkout 42` | -| Check out by branch (local only) | `gh stack checkout feature-auth` | -| Tear down the current stack to restructure it | `gh stack unstack` | -| Tear down a specific stack by number | `gh stack unstack 7` | -| Merge the whole current stack | `gh stack merge --yes` | -| Merge a stack by number | `gh stack merge 7 --yes` | -| Merge up to a specific PR | `gh stack merge 42 --yes` | -| Merge with a specific method | `gh stack merge --yes --squash` | - ---- - -## Workflows - -### End-to-end: create a stack from scratch - -```bash -# 1. Initialize a stack with the first branch -gh stack init auth -# → creates auth and checks it out - -# 2. Write code for the first layer (auth) -cat > auth.go << 'EOF' -package auth - -func Middleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // verify token - next.ServeHTTP(w, r) - }) -} -EOF - -# 3. Stage and commit using standard git commands -git add auth.go -git commit -m "Add auth middleware" - -# You can make multiple commits on the same branch -cat > auth_test.go << 'EOF' -package auth - -func TestMiddleware(t *testing.T) { - // test auth middleware -} -EOF -git add auth_test.go -git commit -m "Add auth middleware tests" - -# 4. When you're ready for a new concern, add the next branch -gh stack add api-routes -# → creates api-routes - -# 5. Write code for the API layer -cat > api.go << 'EOF' -package api - -func RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("/users", handleUsers) -} -EOF -git add api.go -git commit -m "Add API routes" - -# 6. Add a third layer for frontend -gh stack add frontend -# → creates frontend - -cat > frontend.go << 'EOF' -package frontend - -func RenderDashboard(w http.ResponseWriter) { - // calls the API endpoints from the layer below -} -EOF -git add frontend.go -git commit -m "Add frontend dashboard" - -# ── Stack complete: auth → api-routes → frontend ── - -# 7. Push everything and create PRs (drafts by default) -gh stack submit --auto - -# 8. Verify the stack -gh stack view --json -``` - -> **Shortcut:** If you prefer a faster flow, `gh stack add -Am "message" branch-name` combines staging, committing, and branch creation into one command. This is useful for single-commit layers but bypasses deliberate staging. - -### Making mid-stack changes - -This is a critical workflow for agents. When you're working on a higher layer and realize you need to change something in a lower layer (e.g., you're building frontend components but need to add an API endpoint), **navigate down to the correct branch, make the change there, and rebase**. - -```bash -# You're on frontend but need to add an API endpoint - -# 1. Navigate to the API branch -gh stack down -# or: gh stack checkout api-routes - -# 2. Make the change where it belongs -cat > users_api.go << 'EOF' -package api - -func handleGetUser(w http.ResponseWriter, r *http.Request) { - // new endpoint the frontend needs -} -EOF -git add users_api.go -git commit -m "Add get-user endpoint" - -# 3. Rebase everything above to pick up the change -gh stack rebase --upstack - -# 4. Navigate back to where you were working -gh stack top -# or: gh stack checkout frontend - -# 5. Continue working — the API changes are now available -``` - -**Why this matters:** If you make API changes on the frontend branch, those changes will end up in the wrong PR. The API PR won't include them, and the frontend PR will have unrelated API diffs mixed in. Always put changes in the branch where they logically belong. +`gh stack` prints a stack trunk-first, left to right: -### Modify a mid-stack branch and sync - -When you need to revisit a branch after the initial creation (e.g., responding to review feedback): - -```bash -# 1. Navigate to the branch that needs changes -gh stack bottom -# or: gh stack checkout auth -# or: gh stack checkout 42 (by PR number) - -# 2. Make changes and commit -cat > auth.go << 'EOF' -package auth -// updated implementation -EOF -git add auth.go -git commit -m "Fix auth token validation" - -# 3. Rebase everything above this branch -gh stack rebase --upstack - -# 4. Push the updated stack -gh stack push ``` - -### Routine sync after merges - -```bash -# Single command: fetch, rebase, push, sync PR and stack state -gh stack sync - -# Sync and automatically clean up local branches for merged PRs -gh stack sync --prune -``` - -> **Note for agents:** In non-interactive environments, the prune prompt is not shown. Use `--prune` explicitly to delete local branches for merged PRs. - -> **Note for agents:** `sync` also mirrors the stack on GitHub locally. If PRs were added to the stack on github.com, their branches are pulled down and appended to the local stack automatically. If the local and remote stacks have **diverged** (you changed the local stack while the remote stack changed differently), sync can only prompt to resolve it in an interactive terminal — in non-interactive environments it aborts the sync (nothing is pushed or updated) and exits successfully with `ℹ Sync aborted`. Resolve a divergence by unstacking and recreating the stack. - -### Squash-merge recovery - -When a PR is squash-merged on GitHub, the original branch's commits no longer exist in the trunk history. `gh stack` detects this automatically and uses `git rebase --onto` to correctly replay remaining commits. - -```bash -# After PR #1 (auth) is squash-merged on GitHub: -gh stack sync -# → fetches latest, detects the merge, fast-forwards trunk -# → rebases api-routes onto updated trunk (skips merged branch) -# → rebases frontend onto api-routes -# → pushes updated branches -# → reports: "Merged: #1" - -# Verify the result -gh stack view --json -# → auth shows "isMerged": true, "state": "MERGED" -# → api-routes and frontend show updated heads +(main) <- auth <- api <- frontend ``` -If `sync` hits a conflict during this process, it restores all branches to their pre-rebase state and exits with code 3. See [Handle rebase conflicts](#handle-rebase-conflicts-agent-workflow) for the resolution workflow. +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`. -### Handle rebase conflicts (agent workflow) +## Setup ```bash -# 1. Start the rebase -gh stack rebase - -# 2. If exit code 3 (conflict): -# - Parse stderr for conflicted file paths -# - Read those files to find <<<<<<< / ======= / >>>>>>> markers -# - Edit files to resolve conflicts -# - Stage resolved files: -git add path/to/resolved-file.go - -# 3. Continue the rebase -gh stack rebase --continue - -# 4. If another conflict occurs, repeat steps 2-3 - -# 5. If unable to resolve, abort to restore everything -gh stack rebase --abort -``` - -### Parsing `--json` output - -```bash -# Get stack state as JSON -output=$(gh stack view --json) - -# Check if any branch needs a rebase, and rebase if so -needs_rebase=$(echo "$output" | jq '[.branches[] | select(.needsRebase == true)] | length') -if [ "$needs_rebase" -gt 0 ]; then - echo "Branches need rebase, rebasing stack..." - gh stack rebase -fi - -# Get all open PR URLs -echo "$output" | jq -r '.branches[] | select(.pr.state == "OPEN") | .pr.url' - -# Find merged branches -echo "$output" | jq -r '.branches[] | select(.isMerged == true) | .name' - -# Get the current branch -echo "$output" | jq -r '.currentBranch' - -# Check if the stack is fully merged (all branches merged) -echo "$output" | jq '[.branches[] | .isMerged] | all' -``` - -### Restructure a stack (remove a branch, reorder, or rename) - -Use `unstack` to tear down the stack, make structural changes, then re-init: - -```bash -# 1. Remove the local tracking and the GitHub stack grouping (PRs are NOT deleted) -gh stack unstack - -# 2. Make structural changes — e.g. delete a branch, reorder, rename -git branch -m old-branch-1 new-branch-1 - -# 3. Re-create the stack with the new structure -gh stack init --base main new-branch-1 new-branch-2 new-branch-3 -``` - ---- - -## Commands - -### Initialize a stack — `gh stack init` - -Creates a new stack. **Always provide at least one branch name as a positional argument** — running without branch arguments triggers interactive prompts that agents cannot use. - -``` -gh stack init [flags] -``` - -```bash -# Create a stack with a new branch -gh stack init auth -# → creates auth and checks it out - -# Create a stack with new branches -gh stack init branch-a branch-b branch-c - -# Use a different trunk branch -gh stack init --base develop branch-a branch-b - -# Adopt existing branches into a stack (handled automatically if the branches exist) -gh stack init branch-a branch-b branch-c -``` - -| Flag | Description | -|------|-------------| -| `-b, --base ` | Trunk branch (defaults to the repo's default branch) | - -**Behavior:** - -- Branch names are created exactly as given (slashes are allowed and kept as-is) -- Creates any branches that don't already exist (branching from the trunk branch) -- Existing branches are adopted automatically; missing branches are created from the trunk -- Checks out the last branch in the list -- Enables `git rerere` so conflict resolutions are remembered across rebases. On first run in a repo, this may trigger a confirmation prompt — pre-configure with `git config rerere.enabled true` to avoid it - ---- - -### Add a branch — `gh stack add` - -Add a new branch on top of the current stack. Must be run while on the topmost branch (or the trunk if the stack has no branches yet). **Always provide a branch name** — running without one triggers an interactive prompt. - -``` -gh stack add [flags] +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 ``` -**Recommended workflow — create the branch, then use standard git:** +## Non-interactive use -```bash -# Create a new branch and switch to it -gh stack add api-routes +`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. -# Write code, stage deliberately, and commit -git add internal/api/routes.go internal/api/handlers.go -git commit -m "Add user API routes" +| 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 | -# Make more commits on the same branch as needed -git add internal/api/middleware.go -git commit -m "Add rate limiting middleware" -``` +- `view --short` is safe in both modes, but it is formatted for humans. Use `--json` to parse. +- **Multiple remotes:** set `remote.pushDefault`, or pass `--remote ` to `push`, `submit`, + `sync`, `rebase`, and `link`. `checkout` and `trunk` have no `--remote` flag and rely on the + config. +- **`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. -**Shortcut — stage, commit, and branch in one command:** +## Core loop ```bash -# Create a new branch, stage all changes, and commit -gh stack add -Am "Add API routes" api-routes - -# Create a new branch, stage tracked files only, and commit -gh stack add -um "Fix auth bug" auth-fix +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 ``` -| Flag | Description | -|------|-------------| -| `-m, --message ` | Create a commit with this message | -| `-A, --all` | Stage all changes including untracked files (requires `-m`) | -| `-u, --update` | Stage tracked files only (requires `-m`) | +Add `--open` to `submit` to create PRs ready for review instead of drafts. Branch names are used +verbatim — `gh stack add refactor/foo` creates `refactor/foo`. -**Behavior notes:** +## Changing a lower layer -- `-A` and `-u` are mutually exclusive. -- When the current branch has no commits (e.g., right after `init`), `add -Am` commits directly on the current branch instead of creating a new one. -- **Branch names are used verbatim.** `gh stack add refactor/foo` creates a branch named `refactor/foo` — names are never prefixed or transformed. When `-m` is given without a branch name, the name is auto-generated from the commit message in date+slug format (e.g., `03-24-add_api_routes`). -- If called from a branch that is not the topmost in the stack, exits with code 5: `"can only add branches on top of the stack"`. Use `gh stack top` to switch first. -- **Uncommitted changes:** When using `gh stack add branch-name` without `-Am`, any uncommitted changes (staged or unstaged) in your working tree carry over to the new branch. This is standard git behavior — the working tree is not touched. Commit or stash changes on the current branch before running `add` if you want a clean starting point on the new branch. - ---- - -### Push branches to remote — `gh stack push` - -Push active stack branches to the remote. - -``` -gh stack push [flags] -``` +Agents get this wrong most often. If work belongs in a lower layer, do not patch around it at the +current layer — the change lands in the wrong PR. ```bash -# Push all branches +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 - -# Push to specific remote -gh stack push --remote upstream -``` - -| Flag | Description | -|------|-------------| -| `--remote ` | Remote to push to (use if multiple remotes exist) | - -**Behavior:** - -- Pushes all active (non-merged, non-queued) branches in one non-atomic multi-ref push with explicit per-branch `--force-with-lease` checks -- Some branches may update if another is rejected; fix the rejected branch and rerun the command -- Does **not** create or update pull requests — use `gh stack submit` for that - -**Output (stderr):** - -- `Pushed N branches` summary - ---- - -### Submit branches and create PRs — `gh stack submit` - -Push all stack branches and create PRs on GitHub. **Always pass `--auto`** — without it, `submit` prompts for a PR title for each new branch. - -```bash -# Submit and auto-title new PRs (required for non-interactive use) -gh stack submit --auto - -# Submit and create PRs as ready for review (not drafts) -gh stack submit --auto --open -``` - -| Flag | Description | -|------|-------------| -| `--auto` | Auto-generate PR titles without prompting (**required** for non-interactive use) | -| `--open` | Mark new and existing PRs as ready for review | -| `--remote ` | Remote to push to (use if multiple remotes exist) | - -**Behavior:** - -- Pushes each active (non-merged, non-queued) branch sequentially with explicit per-branch `--force-with-lease` checks; the overall submit is not atomic -- If a later branch push is rejected, earlier branch pushes and PR updates remain; fix the rejection and rerun the same command -- Creates a new PR for each branch that doesn't have one (base set to the first non-merged ancestor branch) -- After creating PRs, links them together as a **Stack** on GitHub (requires the repository to have stacks enabled) -- If every PR in the stack has already been merged, the stack is complete and can't be extended. `submit` automatically forks your unmerged branches into a **new** stack rooted at the trunk and creates it on GitHub, leaving the merged stack untouched. -- If stacks are not available (exit code 9), the repository does not have stacked PRs enabled. In interactive mode, `submit` offers to create regular (unstacked) PRs instead. In non-interactive mode, it exits with code 9. -- Syncs PR metadata for branches that already have PRs - -**PR title auto-generation (`--auto`):** - -- Single commit on branch → uses the commit subject as the PR title, commit body as PR body -- Multiple commits on branch → humanizes the branch name (hyphens/underscores → spaces) as the title - -**Output (stderr):** - -- `Created PR #N for ` for each newly created PR -- `PR #N for is up to date` for existing PRs -- `Pushed and synced N branches` summary - ---- - -### Link branches as a stack (no local tracking) — `gh stack link` - -Link PRs into a stack on GitHub without creating any local tracking state. This is the recommended approach if you are managing stacked branches with other tools (jj, Sapling, git-town) and want to simply create GitHub Stacked PRs via an API. - -``` -gh stack link [flags] [...] -``` - -```bash -# Link branches into a stack (pushes, creates PRs, creates stack) -gh stack link branch-a branch-b branch-c - -# Use a different base branch and mark PRs as ready for review -gh stack link --base develop --open branch-a branch-b branch-c - -# Link existing PRs by number -gh stack link 10 20 30 - -# Add branches to an existing stack of PRs -gh stack link 42 43 feature-auth feature-ui - -# Append to the top of an existing stack by its stack number -# (7 is a stack number; only the new PRs/branches are listed) -gh stack link 7 48 feature-auth -``` - -When the first argument is a stack number, the remaining arguments are appended to the top of that stack, so you don't have to re-list its current PRs. Arguments already in the stack are skipped; arguments in a different stack are rejected. A numeric first argument is treated as a stack only when it matches an existing stack — otherwise it is a PR or branch. - -| Flag | Description | -|------|---------| -| `--base ` | Base branch for the bottom of the stack (defaults to the repository's default branch) | -| `--open` | Mark new and existing PRs as ready for review | -| `--remote ` | Remote to push to (use if multiple remotes exist) | - -**Behavior:** - -- Arguments are provided in stack order (bottom to top) -- Each argument can be a branch name or a PR number. Numeric arguments are tried as PR numbers first; if no PR with that number exists, the argument is treated as a branch name -- Branch arguments are pushed to the remote automatically (non-force, atomic) -- For branches without open PRs, new PRs are created with auto-generated titles and the correct base branch chaining (first branch uses `--base`, subsequent branches use the previous branch) -- Existing PRs whose base branch doesn't match the expected chain are corrected automatically -- If the PRs are not yet in a stack, a new stack is created. If some PRs are already in a stack, the stack is updated (additive only — existing PRs are never removed) -- Does **not** create or modify any local state - -**Output (stderr):** - -- `Pushing N branches to ...` -- `Found PR #N for branch ` for branches with existing PRs -- `Created PR #N for (base: )` for newly created PRs -- `Updated base branch for PR #N to ` when base branches are corrected -- `Created stack with N PRs` or `Updated stack to N PRs` - ---- - -### Sync the stack — `gh stack sync` - -Fetch, rebase, push, and sync PR state in a single command. This is the recommended command for routine synchronization. - -``` -gh stack sync [flags] ``` -| Flag | Description | -|------|-------------| -| `--remote ` | Remote to fetch from and push to (use if multiple remotes exist) | -| `--prune` | Delete local branches for merged PRs | - -**What it does (in order):** - -1. **Fetch** latest changes from the remote -2. **Reconcile the remote stack** — mirror the GitHub stack locally. If PRs were added to the stack on GitHub, pull their branches down and append them to the local stack. If the local and remote stacks have diverged, aborts the sync in a non-interactive terminal. In an interactive terminal, offers prompts to resolve any divergence (replace local stack with remote version, delete stack on GitHub so it can be recreated, or cancel). -3. **Fast-forward trunk** to match remote (skips if already up to date, warns if diverged) -4. **Cascade rebase** all stack branches onto their updated parents (only if trunk moved). Handles merged PRs automatically. If a conflict is detected, **all branches are restored** to their pre-rebase state and the command exits with code 3 — see [Handle rebase conflicts](#handle-rebase-conflicts-agent-workflow) for the resolution workflow -5. **Push** all active branches atomically -6. **Sync PR state** from GitHub and report the status of each PR -7. **Sync the stack object** — link the open PRs into a stack on GitHub. If the PRs are not yet in a stack, a new stack is created; if some PRs are already in a stack, it is updated (additive only). This only happens when two or more PRs exist. Sync **never opens PRs** — use `gh stack submit` for that -8. **Prune** — in interactive terminals, prompts to delete local branches for merged PRs. Use `--prune` to skip the prompt. In non-interactive environments, pruning only happens when `--prune` is passed explicitly - -**Output (stderr):** - -- `✓ Fetched latest changes from origin` -- `Pulling N new branches from the remote stack ...` then `✓ Pulled N new branches into the stack from the remote` (when the remote stack is ahead) -- `⚠ Your local stack has diverged from the stack on GitHub` (with `Local:` / `Remote:` chains) when the stacks have diverged -- `ℹ Sync aborted — no changes were made` when a sync is cancelled -- `✓ Trunk main fast-forwarded to ` or `✓ Trunk main is already up to date` -- `✓ Rebased onto ` per branch (if base moved) -- `✓ Pushed N branches` -- `✓ PR #N () — Open` per branch -- `Merged: #N, #M` for merged branches -- `✓ Stack created on GitHub with N PRs` / `✓ Stack updated on GitHub with N PRs` / `✓ Linked to the existing stack on GitHub` (when two or more PRs exist) -- `✓ Pruned (merged)` per pruned branch (when pruning) -- `✓ Stack synced` when the stack object on GitHub was created/updated to match local, or `✓ Branches synced` when only the branches were synced (fewer than two PRs or stacked PRs unavailable) - ---- - -### Rebase the stack — `gh stack rebase` - -Pull from remote and cascade-rebase stack branches. Use this when `sync` reports a conflict or when you need finer control (e.g., rebase only part of the stack). - -``` -gh stack rebase [flags] [branch] -``` +## Staying in sync ```bash -# Rebase the entire stack -gh stack rebase - -# Rebase only branches from trunk to current branch -gh stack rebase --downstack - -# Rebase only branches from current branch to top -gh stack rebase --upstack - -# Rebase stack branches without pulling from or rebasing with trunk -gh stack rebase --no-trunk - -# After resolving a conflict: stage files with `git add`, then: -gh stack rebase --continue - -# Abort and restore all branches to pre-rebase state -gh stack rebase --abort +gh stack sync # fetch, reconcile with GitHub, rebase, push, refresh PR state +gh stack sync --prune # also delete local branches for merged PRs ``` -| Flag | Description | -|------|-------------| -| `--downstack` | Only rebase branches from trunk to the current branch | -| `--upstack` | Only rebase branches from the current branch to the top | -| `--no-trunk` | Skip trunk — only rebase stack branches onto each other (no fetch, no trunk rebase) | -| `--continue` | Continue after resolving conflicts | -| `--abort` | Abort and restore all branches | -| `--remote ` | Remote to fetch from (use if multiple remotes exist) | - -| Argument | Description | -|----------|-------------| -| `[branch]` | Target branch (defaults to the current branch) | - -**Conflict handling:** See [Handle rebase conflicts](#handle-rebase-conflicts-agent-workflow) in the Workflows section for the full resolution workflow. - -**Merged PR detection:** If a branch's PR was merged on GitHub, the rebase automatically handles this using `--onto` mode and correctly replays commits on top of the merge target. - -**Rerere (conflict memory):** `git rerere` is enabled by `init` so previously resolved conflicts are auto-resolved in future rebases. - -**No-trunk mode:** Use `--no-trunk` to skip fetching from the remote and rebasing with the trunk branch. Only inter-branch rebases are performed (branch 2 onto branch 1, branch 3 onto branch 2, etc.). Useful when you only need to align stack branches with each other without pulling upstream changes. - ---- - -### View the stack — `gh stack view` +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`. -Display the current stack's branches, PR status, and recent commits. **Always pass `--json`** — without it, this command launches an interactive TUI that agents cannot operate. +## Merging ```bash -# Always use --json -gh stack view --json +gh stack merge --yes # merge the whole current stack, bottom to top +gh stack merge 42 --yes # merge up to and including PR #42 +gh stack merge 7 --yes # merge stack #7, no local checkout needed +gh stack merge --yes --squash # or --merge, --rebase, --merge-method ``` -| Flag | Description | -|------|-------------| -| `--json` | Output stack data as JSON to stdout (**required** for non-interactive use) | - -**`--json` output format:** - -```json -{ - "trunk": "main", - "currentBranch": "api-routes", - "branches": [ - { - "name": "auth", - "head": "abc1234...", - "base": "def5678...", - "isCurrent": false, - "isMerged": true, - "isQueued": false, - "needsRebase": false, - "pr": { - "number": 42, - "url": "https://github.com/owner/repo/pull/42", - "state": "MERGED" - } - }, - { - "name": "api-routes", - "head": "789abcd...", - "base": "abc1234...", - "isCurrent": true, - "isMerged": false, - "isQueued": false, - "needsRebase": false, - "pr": { - "number": 43, - "url": "https://github.com/owner/repo/pull/43", - "state": "OPEN" - } - } - ] -} -``` +All-or-nothing: if any PR 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. -Fields per branch: -- `name` — branch name -- `head` — current HEAD SHA -- `base` — parent branch's HEAD SHA at last sync -- `isCurrent` — whether this is the checked-out branch -- `isMerged` — whether the PR has been merged -- `isQueued` — whether the PR is queued for merge (in a merge queue) -- `needsRebase` — whether the base branch is not an ancestor (non-linear history) -- `pr` — PR metadata (omitted if no PR exists). `state` is `"OPEN"`, `"MERGED"`, or `"QUEUED"`. +## Reading state ---- +`gh stack view --json` writes JSON to **stdout**. Status messages go to **stderr** — do not parse +them, branch on exit codes instead. -### Navigate the stack - -Move between branches without remembering branch names. These commands are fully non-interactive. - -```bash -gh stack up # Move up one branch (further from trunk) -gh stack up 3 # Move up three branches -gh stack down # Move down one branch (closer to trunk) -gh stack down 2 # Move down two branches -gh stack top # Jump to the top of the stack (furthest from trunk) -gh stack bottom # Jump to the bottom (first non-merged branch above trunk) -gh stack trunk # Jump to the trunk branch (e.g. main) ``` - -Navigation clamps to stack bounds. Merged branches are skipped when navigating from active branches. - ---- - -### Check out a stack — `gh stack checkout` - -Check out a stack by stack number, pull request number, PR URL, or branch name. **Always provide an argument** — running `gh stack checkout` without arguments triggers an interactive selection menu. - -``` -gh stack checkout +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 ``` -```bash -# By stack number (the identifier shown in the GitHub stack UI) -gh stack checkout 7 - -# By PR number (pulls from GitHub) -gh stack checkout 42 - -# By PR URL -gh stack checkout https://github.com/owner/repo/pull/42 - -# By branch name (local only) -gh stack checkout feature-auth -``` - -A bare number is resolved as a **stack number first** (the identifier shown in the GitHub stack UI); if no stack has that number it is tried as a PR number, then a branch name. When a stack or PR number (or PR URL) is provided, the command fetches the stack on GitHub, pulls the branches, and sets up the stack locally. If the stack already exists locally and matches, it switches to the branch. - -> **⚠️ Agent warning:** If the local and remote stacks have different branch compositions, this command triggers an interactive conflict-resolution prompt that cannot be bypassed with a flag. To avoid this: run `gh stack unstack --local` first to remove the conflicting local tracking state (this keeps the stack on GitHub intact), then retry `gh stack checkout `. +`base` is the parent's HEAD SHA at the last sync. `needsRebase` is true when the base is no longer +an ancestor of the branch. -When a branch name is provided, the command resolves it against locally tracked stacks only. This is always safe for non-interactive use. +## Exit codes ---- - -### Remove a stack — `gh stack unstack` - -Tear down a stack so you can restructure it — remove a branch, reorder branches, rename branches, or make other large changes. After unstacking, use `gh stack init` to re-create the stack with the desired structure. - -Unstacking only removes the stack grouping (on GitHub and/or locally); it never deletes the underlying pull requests or branches. - -With no argument, the command targets the active stack — the one containing the currently checked out branch — unstacking it on GitHub and removing local tracking. - -Provide a stack number to unstack a specific stack on GitHub. This works from anywhere in the repository, whether or not the stack is checked out locally — the number is unstacked directly through the GitHub API (like `gh stack link`, no local tracking required). If the stack is also tracked locally, its local tracking is removed as well. - -``` -gh stack unstack [] [flags] -``` +| 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 | Resolve, `git add`, `gh stack rebase --continue`, or `--abort` | +| 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` | -```bash -# Tear down the current stack — removes local tracking and the GitHub grouping (PRs are NOT deleted), then rebuild -gh stack unstack -gh stack init --base main branch-2 branch-1 branch-3 # reordered +## Constraints -# Unstack a specific stack by its number, from anywhere in the repo -gh stack unstack 7 +- 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. -# Only remove local tracking (keep the stack on GitHub) -gh stack unstack --local -``` +## More detail -| Flag | Description | -|------|-------------| -| `--local` | Only remove the stack locally (keep it on GitHub); never contacts GitHub | - -> **Note for agents:** `gh stack unstack ` is a remote-first API wrapper — it unstacks on GitHub by number from anywhere in the repo, tracked locally or not, and is safe for non-interactive use. `--local` never contacts GitHub; combining `--local` with a number that isn't tracked locally is an error. An unknown stack number returns a "not found on GitHub" error (exit code 2). - ---- +`gh stack --help` is authoritative for flags and arguments. Note that +`gh stack help ` does **not** work — it prints the top-level help. -## Output conventions - -- **Status messages** go to **stderr** with emoji prefixes: `✓` (success), `✗` (error), `⚠` (warning), `ℹ` (info). -- **Data output** (e.g., `view --json`) goes to **stdout**. -- When piping output, use `2>/dev/null` to suppress status messages if only data output is needed. - -## Exit codes and error recovery - -| Code | Meaning | Agent action | -|------|---------|-------------| -| 0 | Success | Proceed normally | -| 1 | Generic error | Read stderr for details; may indicate commit/push failure | -| 2 | Not in a stack | Run `gh stack init` to create a stack first | -| 3 | Rebase conflict | Parse stderr for conflicted file paths, resolve conflicts, run `gh stack rebase --continue` | -| 4 | GitHub API failure | Check `gh auth status`, retry the command | -| 5 | Invalid arguments | Fix the command invocation (check flags and arguments) | -| 6 | Disambiguation required | A branch belongs to multiple stacks. Run `gh stack checkout ` to switch to a non-shared branch first | -| 7 | Rebase already in progress | Run `gh stack rebase --continue` (after resolving conflicts) or `gh stack rebase --abort` to start over | -| 8 | Stack is locked | Another `gh stack` process is writing the stack file. Wait and retry — the lock times out after 5 seconds | -| 9 | Stacked PRs unavailable | The repository does not have stacked PRs enabled. Tell the user that stacks must be enabled on the repository first | -| 10 | Modify recovery required | A `gh stack modify` session was interrupted. This skill does not use `modify`, so agents should not produce this; if the repo is left in this state, run `gh stack modify --abort` to restore the pre-modify state | - -## Known limitations - -1. **Stacks are strictly linear.** Branching stacks (multiple children on a single parent) are not supported. Each branch has exactly one parent and at most one child. If you need parallel workstreams, use separate stacks. -2. **Stack disambiguation cannot be bypassed.** If the current branch is the trunk of multiple stacks, commands error with code 6. Check out a non-shared branch first. -3. **Multiple remotes require `--remote` or config.** If more than one remote is configured, set `remote.pushDefault` in git config, or pass `--remote ` to the commands that accept it (`push`, `submit`, `sync`, `rebase`, `link`). `checkout`, `modify`, and `trunk` have no `--remote` flag and rely on `remote.pushDefault`. -4. **Remote stack checkout requires a stack or PR number.** `checkout` with a branch name only works with locally tracked stacks. Use a stack number or PR number (e.g. `gh stack checkout 7` or `gh stack checkout 123`) to pull a stack from GitHub. -5. **PR title and body are auto-generated.** There is no flag to set a custom PR title or body during `submit`. The title and body are generated from commit messages plus a footer. Use `gh pr edit` to modify PR title and body after creation. +- `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/skills/gh-stack/references/commands.md b/skills/gh-stack/references/commands.md new file mode 100644 index 0000000..c2c7f22 --- /dev/null +++ b/skills/gh-stack/references/commands.md @@ -0,0 +1,177 @@ +# Command behavior + +`gh stack --help` is authoritative for flags and arguments. (`gh stack help ` +does not work — it prints the top-level help.) This file covers only 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`. + +Branches that already exist are adopted; branches that do not are created from the trunk. There is +no separate adopt command and no flag to choose — existence decides. `--base` sets the trunk when +it should not be the repository default. + +`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** every branch onto its updated parent — *only if the trunk moved*. 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 + +- **All-or-nothing.** If any PR cannot be merged, none are, and the reason is reported. +- Scope with an argument: a PR number merges everything up to and including that PR; a stack number + merges that stack and needs no local checkout. +- 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/skills/gh-stack/references/stack-design.md b/skills/gh-stack/references/stack-design.md new file mode 100644 index 0000000..ad1634a --- /dev/null +++ b/skills/gh-stack/references/stack-design.md @@ -0,0 +1,93 @@ +# 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 in-place reorder — fixing the order means `unstack` and +`init` again. + +Decide the layers first, then write code into them: + +``` +(main) <- data-models <- api-endpoints <- frontend-ui <- integration +``` + +- `data-models` — shared types and schema +- `api-endpoints` — routes that use the models +- `frontend-ui` — components that call the routes +- `integration` — tests exercising the whole feature + +Those names are illustrative. Pick boundaries that reflect the actual work. + +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 + +Each layer's name should describe the concern it contains: `auth`, `api-routes`, `frontend`. + +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/skills/gh-stack/references/troubleshooting.md b/skills/gh-stack/references/troubleshooting.md new file mode 100644 index 0000000..e5ca87a --- /dev/null +++ b/skills/gh-stack/references/troubleshooting.md @@ -0,0 +1,137 @@ +# 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. If `rerere` was declined, the same conflict must be resolved once per 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 — +resolve as above. 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. + +## 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 +git branch -m old-name new-name # reorder, rename, or drop branches 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 and are re-based onto their new parents by `submit`. + +Only the branch *order* and *membership* change this way. To move commits between layers, rebase or +cherry-pick them onto the right branch first, then run `gh stack rebase --upstack`. + +## 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 a `gh stack` process is +hung — most often one blocked on a prompt or TUI under a PTY. Terminate it and retry. + +## 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. From c28b2d04e7e15a4cd5d236dd0193be66830f4bb6 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 3 Aug 2026 08:10:13 -0400 Subject: [PATCH 2/7] better reordering instructions --- skills/gh-stack/references/troubleshooting.md | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/skills/gh-stack/references/troubleshooting.md b/skills/gh-stack/references/troubleshooting.md index e5ca87a..fdcd963 100644 --- a/skills/gh-stack/references/troubleshooting.md +++ b/skills/gh-stack/references/troubleshooting.md @@ -87,8 +87,24 @@ 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 and are re-based onto their new parents by `submit`. -Only the branch *order* and *membership* change this way. To move commits between layers, rebase or -cherry-pick them onto the right branch first, then run `gh stack rebase --upstack`. +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) From a76a986ded977d51906de9913f33e9ad0ae9630e Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 3 Aug 2026 08:12:49 -0400 Subject: [PATCH 3/7] updated branch placement instructions --- skills/gh-stack/SKILL.md | 49 +++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/skills/gh-stack/SKILL.md b/skills/gh-stack/SKILL.md index bd89e8d..5b7a087 100644 --- a/skills/gh-stack/SKILL.md +++ b/skills/gh-stack/SKILL.md @@ -1,10 +1,10 @@ --- name: gh-stack description: > - Creates and manages stacked branches and pull requests with the gh-stack GitHub CLI - extension. Use when working with stacked PRs, stacked diffs, dependent pull requests, - or branch chains, when running any gh stack command, or before splitting a large - change into a chain of dependent PRs. + 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" @@ -41,6 +41,10 @@ git config remote.pushDefault origin # required if the repo has more than one 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 | @@ -53,12 +57,26 @@ Agent harnesses differ, so always pass the flags below instead of relying on tha | — | `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. -- **Multiple remotes:** set `remote.pushDefault`, or pass `--remote ` to `push`, `submit`, - `sync`, `rebase`, and `link`. `checkout` and `trunk` have no `--remote` flag and rely on the - config. - **`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 @@ -70,22 +88,9 @@ 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 used +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`. -## Changing a lower layer - -Agents get this wrong most often. If work belongs in a lower layer, do not patch around it at the -current layer — the change lands in the wrong PR. - -```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 -``` - ## Staying in sync ```bash @@ -155,6 +160,8 @@ an ancestor of the branch. `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, From 0a3095a6224e743208245824214bf0666825589e Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 3 Aug 2026 08:54:17 -0400 Subject: [PATCH 4/7] clarify branch naming --- skills/gh-stack/references/stack-design.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/skills/gh-stack/references/stack-design.md b/skills/gh-stack/references/stack-design.md index ad1634a..7ee6176 100644 --- a/skills/gh-stack/references/stack-design.md +++ b/skills/gh-stack/references/stack-design.md @@ -20,22 +20,26 @@ by restructuring later, because there is no in-place reorder — fixing the orde Decide the layers first, then write code into them: ``` -(main) <- data-models <- api-endpoints <- frontend-ui <- integration +(main) <- todo-app/models <- todo-app/api <- todo-app/frontend <- todo-app/integration ``` -- `data-models` — shared types and schema -- `api-endpoints` — routes that use the models -- `frontend-ui` — components that call the routes -- `integration` — tests exercising the whole feature +- `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 -Those names are illustrative. Pick boundaries that reflect the actual work. +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 -Each layer's name should describe the concern it contains: `auth`, `api-routes`, `frontend`. +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`. From 42a40d48964ffbae730fdb1122ebf63f5bb9d3e6 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 3 Aug 2026 09:17:54 -0400 Subject: [PATCH 5/7] recommend explicit arg for merge --- skills/gh-stack/SKILL.md | 21 +++++++++++++-------- skills/gh-stack/references/commands.md | 12 ++++++------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/skills/gh-stack/SKILL.md b/skills/gh-stack/SKILL.md index 5b7a087..684ad16 100644 --- a/skills/gh-stack/SKILL.md +++ b/skills/gh-stack/SKILL.md @@ -49,7 +49,7 @@ Agent harnesses differ, so always pass the flags below instead of relying on tha |---|---|---| | `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 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 | @@ -104,16 +104,21 @@ diverged, `sync` prints both chains, makes no changes, and exits 0 with `Sync ab ## Merging +Scope the merge with an argument: + ```bash -gh stack merge --yes # merge the whole current stack, bottom to top -gh stack merge 42 --yes # merge up to and including PR #42 -gh stack merge 7 --yes # merge stack #7, no local checkout needed -gh stack merge --yes --squash # or --merge, --rebase, --merge-method +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 ``` -All-or-nothing: if any PR 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. +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 diff --git a/skills/gh-stack/references/commands.md b/skills/gh-stack/references/commands.md index c2c7f22..624822a 100644 --- a/skills/gh-stack/references/commands.md +++ b/skills/gh-stack/references/commands.md @@ -1,7 +1,6 @@ # Command behavior -`gh stack --help` is authoritative for flags and arguments. (`gh stack help ` -does not work — it prints the top-level help.) This file covers only behavior `--help` does not +`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 @@ -26,7 +25,7 @@ the whole chain: `gh stack init auth api frontend`. Branches that already exist are adopted; branches that do not are created from the trunk. There is no separate adopt command and no flag to choose — existence decides. `--base` sets the trunk when -it should not be the repository default. +it should be something other than the repository default. `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. @@ -155,9 +154,10 @@ Removes the stack **grouping** only. It never deletes pull requests or branches. ## merge -- **All-or-nothing.** If any PR cannot be merged, none are, and the reason is reported. -- Scope with an argument: a PR number merges everything up to and including that PR; a stack number - merges that stack and needs no local checkout. +- 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 From 14686d916ef20726cab0ad29c25080366c7da6fa Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 3 Aug 2026 09:24:23 -0400 Subject: [PATCH 6/7] refine troubleshooting text --- skills/gh-stack/references/troubleshooting.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/skills/gh-stack/references/troubleshooting.md b/skills/gh-stack/references/troubleshooting.md index fdcd963..23a4040 100644 --- a/skills/gh-stack/references/troubleshooting.md +++ b/skills/gh-stack/references/troubleshooting.md @@ -27,7 +27,8 @@ gh stack rebase --continue # repeat if the next branch also conflicts 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. If `rerere` was declined, the same conflict must be resolved once per layer. +every branch above it. Without `rerere`, repeated conflicts may need manual resolution on each +affected layer. ## After a squash merge @@ -71,6 +72,8 @@ Two resolution paths: ``` 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 those state before retrying. ## Restructuring a stack @@ -79,13 +82,14 @@ There is no non-interactive reorder, rename, or removal. `add` run from the wron ```bash gh stack unstack # removes local tracking and the GitHub grouping -git branch -m old-name new-name # reorder, rename, or drop branches as needed +# 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 and are re-based onto their new parents by `submit`. +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 @@ -137,8 +141,8 @@ will not work on the result. Use `gh stack checkout ` if you later ## 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 a `gh stack` process is -hung — most often one blocked on a prompt or TUI under a PTY. Terminate it and retry. +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) From ce64e1446b852e1e1238ddc9e7970c70941af9eb Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 3 Aug 2026 11:19:58 -0400 Subject: [PATCH 7/7] address review comments --- skills/gh-stack/SKILL.md | 12 ++++++++++-- skills/gh-stack/references/commands.md | 14 ++++++++------ skills/gh-stack/references/stack-design.md | 4 ++-- skills/gh-stack/references/troubleshooting.md | 8 +++++--- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/skills/gh-stack/SKILL.md b/skills/gh-stack/SKILL.md index 684ad16..96aab64 100644 --- a/skills/gh-stack/SKILL.md +++ b/skills/gh-stack/SKILL.md @@ -132,7 +132,8 @@ branches[] name, head, base, isCurrent, isMerged, isQueued, needsRebase branches[].pr number, url, state ("OPEN" | "MERGED" | "QUEUED"); absent when no PR exists ``` -`base` is the parent's HEAD SHA at the last sync. `needsRebase` is true when the base is no longer +`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 @@ -142,7 +143,7 @@ an ancestor of the branch. | 0 | Success | — | | 1 | Generic error | Read stderr | | 2 | Not in a stack | `gh stack init`, or `gh stack checkout ` | -| 3 | Rebase conflict | Resolve, `git add`, `gh stack rebase --continue`, or `--abort` | +| 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 | @@ -151,6 +152,13 @@ an ancestor of the branch. | 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. diff --git a/skills/gh-stack/references/commands.md b/skills/gh-stack/references/commands.md index 624822a..46e5833 100644 --- a/skills/gh-stack/references/commands.md +++ b/skills/gh-stack/references/commands.md @@ -23,9 +23,10 @@ explain: preconditions, side effects, atomicity, and failure modes. 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`. -Branches that already exist are adopted; branches that do not are created from the trunk. There is -no separate adopt command and no flag to choose — existence decides. `--base` sets the trunk when -it should be something other than the repository default. +`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. @@ -93,9 +94,10 @@ The routine command. Steps, in order: 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** every branch onto its updated parent — *only if the trunk moved*. Merged PRs - are handled automatically. On conflict, **all branches are restored** to their pre-rebase state - and the command exits **3**. +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 diff --git a/skills/gh-stack/references/stack-design.md b/skills/gh-stack/references/stack-design.md index 7ee6176..f64543a 100644 --- a/skills/gh-stack/references/stack-design.md +++ b/skills/gh-stack/references/stack-design.md @@ -14,8 +14,8 @@ How to decide what goes in each layer. Read this before running `gh stack init`. 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 in-place reorder — fixing the order means `unstack` and -`init` again. +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: diff --git a/skills/gh-stack/references/troubleshooting.md b/skills/gh-stack/references/troubleshooting.md index 23a4040..fc97b41 100644 --- a/skills/gh-stack/references/troubleshooting.md +++ b/skills/gh-stack/references/troubleshooting.md @@ -43,8 +43,10 @@ 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 — -resolve as above. Use `gh stack sync --prune` to also delete local branches for merged PRs. +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 @@ -73,7 +75,7 @@ Two resolution paths: 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 those state before retrying. +stacked. If needed, clear that state before retrying. ## Restructuring a stack