diff --git a/.agents/skills/gh-stack/SKILL.md b/.agents/skills/gh-stack/SKILL.md index 9f271dfbf..814c458d2 100644 --- a/.agents/skills/gh-stack/SKILL.md +++ b/.agents/skills/gh-stack/SKILL.md @@ -4,10 +4,10 @@ description: | metadata: author: github github-path: skills/gh-stack - github-ref: refs/tags/v0.0.7 + github-ref: refs/tags/v0.0.8 github-repo: https://github.com/github/gh-stack - github-tree-sha: c68c56b3414753690759b148af1ce5a5c717d774 - version: 0.0.7 + github-tree-sha: 26320e8be554b0cc1d122c353e8897f0c69739cd + version: 0.0.8 name: gh-stack --- # gh-stack @@ -16,9 +16,9 @@ name: gh-stack ``` main (trunk) - └── feat/auth-layer → PR #1 (base: main) - bottom (closest to trunk) - └── feat/api-endpoints → PR #2 (base: feat/auth-layer) - └── feat/frontend → PR #3 (base: feat/api-endpoints) - top (furthest from 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. @@ -52,16 +52,15 @@ git config remote.pushDefault origin # if multiple remotes exist (skips remo **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. -2. **When a prefix is set, pass only the suffix to `add`.** `gh stack add auth` with prefix `feat` → `feat/auth`. Passing `feat/auth` creates `feat/feat/auth`. -3. **Always use `--auto` with `gh stack submit`** to auto-generate PR titles. Without `--auto`, `submit` prompts for a title for each new PR. -4. **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`. -5. **Use `--remote ` when multiple remotes are configured**, or pre-configure `git config remote.pushDefault origin`. Without this, `push`, `submit`, `sync`, `link`, and `checkout` trigger an interactive remote picker. -6. **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. -7. **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`. -8. **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. -9. **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. -10. **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. Always provide at least 2 branch names or PR numbers. +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. **Use `--remote ` when multiple remotes are configured**, or pre-configure `git config remote.pushDefault origin`. Without this, `push`, `submit`, `sync`, `link`, and `checkout` trigger an interactive remote picker. +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`). **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` @@ -83,24 +82,24 @@ Stacked branches form a dependency chain: each branch builds on the one below it ``` main (trunk) - └── feat/data-models ← shared types, database schema - └── feat/api-endpoints ← API routes that use the models - └── feat/frontend-ui ← UI components that call the APIs - └── feat/integration ← tests that exercise the full stack + └── 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 -Prefer initializing stacks with a prefix (`-p`). Prefixes group branches under a namespace (e.g., `feat/auth`, `feat/api`) and keep branch names clean and consistent. When a prefix is set, pass only the suffix to subsequent `add` calls — the prefix is applied automatically. Without a prefix, you'll need to pass the full branch name each time. +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 feat/data-models with several new files in your working tree. +# 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" @@ -109,7 +108,7 @@ 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 feat/api-routes branch +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" ``` @@ -139,12 +138,11 @@ Small, incidental fixes (e.g., fixing a typo you noticed) can go in the current | Task | Command | |------|---------| -| Create a stack (recommended) | `gh stack init -p feat auth` | -| Create a stack without prefix | `gh stack init auth` | +| 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 (suffix only if prefix set) | `gh stack add api-routes` | +| 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` | @@ -161,9 +159,11 @@ Small, incidental fixes (e.g., fixing a typo you noticed) can go in the current | 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 a stack to restructure it | `gh stack unstack` | +| Tear down the current stack to restructure it | `gh stack unstack` | +| Tear down a specific stack by number | `gh stack unstack 7` | --- @@ -173,8 +173,8 @@ Small, incidental fixes (e.g., fixing a typo you noticed) can go in the current ```bash # 1. Initialize a stack with the first branch -gh stack init -p feat auth -# → creates feat/auth and checks it out +gh stack init auth +# → creates auth and checks it out # 2. Write code for the first layer (auth) cat > auth.go << 'EOF' @@ -205,7 +205,7 @@ 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 feat/api-routes (prefix applied automatically — just pass the suffix) +# → creates api-routes # 5. Write code for the API layer cat > api.go << 'EOF' @@ -220,7 +220,7 @@ git commit -m "Add API routes" # 6. Add a third layer for frontend gh stack add frontend -# → creates feat/frontend (just the suffix — prefix is automatic) +# → creates frontend cat > frontend.go << 'EOF' package frontend @@ -232,7 +232,7 @@ EOF git add frontend.go git commit -m "Add frontend dashboard" -# ── Stack complete: feat/auth → feat/api-routes → feat/frontend ── +# ── Stack complete: auth → api-routes → frontend ── # 7. Push everything and create PRs (drafts by default) gh stack submit --auto @@ -248,11 +248,11 @@ gh stack view --json 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 feat/frontend but need to add an API endpoint +# You're on frontend but need to add an API endpoint # 1. Navigate to the API branch gh stack down -# or: gh stack checkout feat/api-routes +# or: gh stack checkout api-routes # 2. Make the change where it belongs cat > users_api.go << 'EOF' @@ -270,7 +270,7 @@ gh stack rebase --upstack # 4. Navigate back to where you were working gh stack top -# or: gh stack checkout feat/frontend +# or: gh stack checkout frontend # 5. Continue working — the API changes are now available ``` @@ -284,7 +284,7 @@ When you need to revisit a branch after the initial creation (e.g., responding t ```bash # 1. Navigate to the branch that needs changes gh stack bottom -# or: gh stack checkout feat/auth +# or: gh stack checkout auth # or: gh stack checkout 42 (by PR number) # 2. Make changes and commit @@ -305,7 +305,7 @@ gh stack push ### Routine sync after merges ```bash -# Single command: fetch, rebase, push, sync PR state +# Single command: fetch, rebase, push, sync PR and stack state gh stack sync # Sync and automatically clean up local branches for merged PRs @@ -314,23 +314,25 @@ 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 (feat/auth) is squash-merged on GitHub: +# After PR #1 (auth) is squash-merged on GitHub: gh stack sync # → fetches latest, detects the merge, fast-forwards trunk -# → rebases feat/api-routes onto updated trunk (skips merged branch) -# → rebases feat/frontend onto feat/api-routes +# → 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 -# → feat/auth shows "isMerged": true, "state": "MERGED" -# → feat/api-routes and feat/frontend show updated heads +# → auth shows "isMerged": true, "state": "MERGED" +# → api-routes and frontend show updated heads ``` 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. @@ -411,15 +413,11 @@ gh stack init [flags] ``` ```bash -# Set a branch prefix (recommended — subsequent `add` calls only need the suffix) -gh stack init -p feat auth -# → creates feat/auth - -# Multi-part prefix (slashes are fine — suffix-only rule still applies) -gh stack init -p monalisa/billing auth -# → creates monalisa/billing/auth +# Create a stack with a new branch +gh stack init auth +# → creates auth and checks it out -# Create a stack with new branches (no prefix — use full branch names) +# Create a stack with new branches gh stack init branch-a branch-b branch-c # Use a different trunk branch @@ -432,11 +430,10 @@ gh stack init branch-a branch-b branch-c | Flag | Description | |------|-------------| | `-b, --base ` | Trunk branch (defaults to the repo's default branch) | -| `-p, --prefix ` | Branch name prefix. Subsequent `add` calls only need the suffix (e.g., with `-p feat`, `gh stack add auth` creates `feat/auth`) | **Behavior:** -- Using `-p` is recommended — it simplifies branch naming for subsequent `add` calls +- 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 @@ -455,7 +452,7 @@ gh stack add [flags] **Recommended workflow — create the branch, then use standard git:** ```bash -# Create a new branch and switch to it (just the suffix — prefix is applied automatically) +# Create a new branch and switch to it gh stack add api-routes # Write code, stage deliberately, and commit @@ -487,7 +484,7 @@ gh stack add -um "Fix auth bug" auth-fix - `-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. -- **Prefix handling:** Only pass the suffix when a prefix is set. `gh stack add api` with prefix `todo` → `todo/api`. Passing `todo/api` creates `todo/todo/api`. Without a prefix, pass the full branch name. +- **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. @@ -569,7 +566,7 @@ gh stack submit --auto --open 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] [...] +gh stack link [flags] [...] ``` ```bash @@ -584,8 +581,14 @@ 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 (default: `main`) | @@ -628,16 +631,20 @@ gh stack sync [flags] **What it does (in order):** 1. **Fetch** latest changes from the remote -2. **Fast-forward trunk** to match remote (skips if already up to date, warns if diverged) -3. **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 -4. **Push** all active branches atomically -5. **Sync PR state** from GitHub and report the status of each PR -6. **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 -7. **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 +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` @@ -645,7 +652,7 @@ gh stack sync [flags] - `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, stacked PRs unavailable, or a divergence) +- `✓ 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) --- @@ -718,11 +725,10 @@ gh stack view --json ```json { "trunk": "main", - "prefix": "feat", - "currentBranch": "feat/api-routes", + "currentBranch": "api-routes", "branches": [ { - "name": "feat/auth", + "name": "auth", "head": "abc1234...", "base": "def5678...", "isCurrent": false, @@ -735,7 +741,7 @@ gh stack view --json } }, { - "name": "feat/api-routes", + "name": "api-routes", "head": "789abcd...", "base": "abc1234...", "isCurrent": true, @@ -807,24 +813,31 @@ When a branch name is provided, the command resolves it against locally tracked 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. -You must have a branch from the stack checked out locally. The command targets the active stack — the one that contains the currently checked out branch. +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] +gh stack unstack [] [flags] ``` ```bash -# Tear down the stack (locally and on GitHub), then rebuild +# Tear down the current stack (locally and on GitHub), then rebuild gh stack unstack gh stack init --base main branch-2 branch-1 branch-3 # reordered +# Unstack a specific stack by its number, from anywhere in the repo +gh stack unstack 7 + # Only remove local tracking (keep the stack on GitHub) gh stack unstack --local ``` | Flag | Description | |------|-------------| -| `--local` | Only delete the stack locally (keep it on GitHub) | +| `--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). ---