gh-stack 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -278,6 +278,19 @@ doctor
278
278
  Migrate old (v2) metadata to the v3 layout, reconcile git branch
279
279
  config against the topology files, and flag stacks whose base
280
280
  stack appears already-merged into main. Safe to run repeatedly.
281
+
282
+ learn [--skill] [--harness <name>] [--global] [--force]
283
+ Teach a coding agent how to drive gh-stack. By default prints the
284
+ canonical skill (Markdown, stamped with the running version) to
285
+ stdout — the text is compiled into the binary, so it never drifts
286
+ out of sync with the installed version. --skill installs it as a
287
+ skill file instead, choosing the path from the harness:
288
+ Claude Code <root>/.claude/skills/using-gh-stack/SKILL.md
289
+ Codex <root>/.codex/skills/using-gh-stack/SKILL.md
290
+ Cursor <root>/.cursor/skills/using-gh-stack/SKILL.md
291
+ --harness claude | codex | cursor (skips the interactive prompt)
292
+ --global Install under ~/.<harness>/ instead of the project root
293
+ --force Overwrite an existing skill file without confirming
281
294
  ```
282
295
 
283
296
  ## Global Options
@@ -460,7 +473,16 @@ gh-stack merge # finishes base PR → main + archives the stack
460
473
 
461
474
  ## Agent/CI Usage
462
475
 
463
- gh-stack is designed to be used by AI agents and CI pipelines:
476
+ gh-stack is designed to be used by AI agents and CI pipelines. The fastest way
477
+ to onboard an agent is `gh-stack learn` — it prints a version-matched skill (or
478
+ installs one with `--skill`) so the agent learns the current commands, flags,
479
+ and gotchas straight from the binary:
480
+
481
+ ```bash
482
+ # Teach an agent the current API (always in sync with the installed version)
483
+ gh-stack learn # print the skill to stdout
484
+ gh-stack learn --skill --harness claude # install it for Claude Code
485
+ ```
464
486
 
465
487
  ```bash
466
488
  # Non-interactive mode — all prompts auto-resolved
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-stack",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Stacked PR manager for squash-merge workflows",
5
5
  "author": "Kiliman <kiliman@gmail.com>",
6
6
  "license": "MIT",
@@ -0,0 +1,152 @@
1
+ // gh-stack learn — teach agents how to drive gh-stack.
2
+ //
3
+ // gh-stack learn Print the canonical, version-stamped skill to stdout.
4
+ // gh-stack learn --skill Install it as a skill file for a coding harness.
5
+ //
6
+ // The skill text is compiled into the binary (src/lib/skill.ts), so it's always
7
+ // in lockstep with the installed version — no hand-maintained skill to drift.
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import pc from "picocolors";
11
+ import * as p from "../lib/output.ts";
12
+ import * as git from "../lib/git.ts";
13
+ import { isAutoYes } from "../lib/ui.ts";
14
+ import { SKILL_NAME, skillContent } from "../lib/skill.ts";
15
+
16
+ const { version: VERSION } = await import("../../package.json");
17
+
18
+ interface Harness {
19
+ key: string;
20
+ label: string;
21
+ dir: string; // the per-harness config dir, e.g. ".claude"
22
+ }
23
+
24
+ // Project-root install locations per harness. Global installs swap the repo
25
+ // root for the home directory (same `<dir>/skills/<name>/SKILL.md` shape).
26
+ const HARNESSES: Harness[] = [
27
+ { key: "claude", label: "Claude Code", dir: ".claude" },
28
+ { key: "codex", label: "Codex", dir: ".codex" },
29
+ { key: "cursor", label: "Cursor", dir: ".cursor" },
30
+ ];
31
+
32
+ function flagValue(args: string[], name: string): string | undefined {
33
+ const eq = args.find((a) => a.startsWith(`${name}=`));
34
+ if (eq) return eq.slice(name.length + 1);
35
+ const i = args.indexOf(name);
36
+ if (i !== -1 && args[i + 1] && !args[i + 1]!.startsWith("-")) return args[i + 1];
37
+ return undefined;
38
+ }
39
+
40
+ export default async function learn(args: string[]): Promise<void> {
41
+ if (args.includes("--help")) {
42
+ console.log(`
43
+ gh-stack learn — teach an agent how to use gh-stack
44
+
45
+ USAGE
46
+ gh-stack learn [--skill] [options]
47
+
48
+ By default, prints the canonical gh-stack skill (Markdown, stamped with the
49
+ current version) to stdout. The text is compiled into the binary, so it never
50
+ drifts out of sync with the installed version.
51
+
52
+ OPTIONS
53
+ --skill Install the skill into a project (or --global) instead
54
+ of printing it
55
+ --harness <name> Target harness: claude | codex | cursor
56
+ (skips the interactive prompt)
57
+ --global, -g Install under your home dir (~/.<harness>/skills/...)
58
+ instead of the project root
59
+ --force Overwrite an existing skill file without confirming
60
+ --help Show this help
61
+
62
+ INSTALL PATHS
63
+ Claude Code <root>/.claude/skills/${SKILL_NAME}/SKILL.md
64
+ Codex <root>/.codex/skills/${SKILL_NAME}/SKILL.md
65
+ Cursor <root>/.cursor/skills/${SKILL_NAME}/SKILL.md
66
+
67
+ EXAMPLES
68
+ gh-stack learn # print the skill to stdout
69
+ gh-stack learn > AGENTS.md # capture it however you like
70
+ gh-stack learn --skill # interactive install (pick harness)
71
+ gh-stack learn --skill --harness claude # install for Claude Code, no prompt
72
+ gh-stack learn --skill --global -g # install into your home dir
73
+ `);
74
+ return;
75
+ }
76
+
77
+ const content = skillContent(VERSION);
78
+
79
+ // Default: just print the skill. No chrome — pipe-friendly for agents.
80
+ if (!args.includes("--skill")) {
81
+ console.log(content);
82
+ return;
83
+ }
84
+
85
+ // ── Install mode ──
86
+ const global = args.includes("--global") || args.includes("-g");
87
+ const force = args.includes("--force");
88
+ const autoYes = isAutoYes();
89
+
90
+ // Resolve harness: explicit flag, else interactive prompt.
91
+ let harness: Harness | undefined;
92
+ const harnessFlag = flagValue(args, "--harness");
93
+ if (harnessFlag) {
94
+ harness = HARNESSES.find((h) => h.key === harnessFlag.toLowerCase());
95
+ if (!harness) {
96
+ p.log.error(
97
+ `Unknown harness "${harnessFlag}". Choose one of: ${HARNESSES.map((h) => h.key).join(", ")}.`,
98
+ );
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+ } else if (autoYes) {
103
+ p.log.error("Non-interactive mode needs an explicit harness: --harness <claude|codex|cursor>.");
104
+ process.exitCode = 1;
105
+ return;
106
+ } else {
107
+ const selected = await p.select({
108
+ message: "Which harness is this skill for?",
109
+ options: HARNESSES.map((h) => ({
110
+ value: h.key,
111
+ label: h.label,
112
+ hint: `${global ? "~" : "."}/${h.dir}/skills/${SKILL_NAME}/`,
113
+ })),
114
+ });
115
+ if (p.isCancel(selected)) {
116
+ p.cancel("Cancelled.");
117
+ return;
118
+ }
119
+ harness = HARNESSES.find((h) => h.key === selected)!;
120
+ }
121
+
122
+ // Resolve install root: home dir (--global) or repo working-tree root.
123
+ let root: string;
124
+ if (global) {
125
+ root = homedir();
126
+ } else if (await git.isGitRepo()) {
127
+ root = await git.repoRoot();
128
+ } else {
129
+ p.log.error("Not inside a git repository. Run from a project, or use --global.");
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ const target = join(root, harness.dir, "skills", SKILL_NAME, "SKILL.md");
134
+
135
+ // Confirm overwrite unless --force / auto-yes.
136
+ if ((await Bun.file(target).exists()) && !force && !autoYes) {
137
+ const ok = await p.confirm({
138
+ message: `${pc.yellow(target)} already exists. Overwrite?`,
139
+ });
140
+ if (p.isCancel(ok) || !ok) {
141
+ p.cancel("Left existing skill in place.");
142
+ return;
143
+ }
144
+ }
145
+
146
+ await Bun.write(target, content); // creates parent dirs as needed
147
+
148
+ p.log.success(`Installed ${harness.label} skill → ${pc.cyan(target)}`);
149
+ p.log.info(
150
+ `Stamped from gh-stack v${VERSION}. Refresh after upgrading with ${pc.dim("gh-stack learn --skill")}.`,
151
+ );
152
+ }
@@ -132,24 +132,32 @@ ALIASES
132
132
  * For each branch, compare its local tip against origin. Returns the ones that
133
133
  * differ (or whose remote ref is missing), so restack can report exactly which
134
134
  * refs didn't land instead of falsely claiming success. (#12)
135
+ *
136
+ * The remote sha comes from a single authoritative `git ls-remote` (see
137
+ * git.remoteHeads), NOT from `rev-parse origin/<branch>`. A force-push can leave
138
+ * the local remote-tracking ref stale even though the push landed, which made
139
+ * this pass report a false "un-pushed ref" and exit non-zero after a fully
140
+ * successful run (#23). Reading the real remote state makes the signal
141
+ * trustworthy in both directions: a genuinely un-pushed ref is still caught.
135
142
  */
136
- async function findStaleRefs(
143
+ export async function findStaleRefs(
137
144
  branches: string[],
138
145
  ): Promise<{ branch: string; local: string; remote: string }[]> {
139
146
  const stale: { branch: string; local: string; remote: string }[] = [];
140
147
  const seen = new Set<string>();
148
+ const remoteHeads = await git.remoteHeads();
141
149
  for (const branch of branches) {
142
150
  if (seen.has(branch) || branch === "main" || branch === "master") continue;
143
151
  seen.add(branch);
144
152
  if (!(await git.localBranchExists(branch))) continue;
145
153
  const local = await git.revParse(branch);
146
- if (!(await git.remoteBranchExists(branch))) {
154
+ const remote = remoteHeads.get(branch);
155
+ if (!remote) {
147
156
  stale.push({ branch, local, remote: "(missing)" });
148
157
  continue;
149
158
  }
150
- const remote = await git.revParse(`origin/${branch}`).catch(() => "");
151
159
  if (remote !== local) {
152
- stale.push({ branch, local, remote: remote ? remote.slice(0, 8) : "(unknown)" });
160
+ stale.push({ branch, local, remote: remote.slice(0, 8) });
153
161
  }
154
162
  }
155
163
  return stale;
package/src/index.ts CHANGED
@@ -46,8 +46,12 @@ if (parsed.plain) {
46
46
  setPlain(true);
47
47
  }
48
48
 
49
- // Ensure we're in a git repo for all commands
50
- await ensureGitRepo();
49
+ // Ensure we're in a git repo for all commands — except `learn`, which just
50
+ // emits/installs the agent skill and is useful outside any repo (e.g. an agent
51
+ // onboarding before cloning, or `learn --skill --global`).
52
+ if (command !== "learn") {
53
+ await ensureGitRepo();
54
+ }
51
55
 
52
56
  // Route to subcommand — strip global flags from command args
53
57
  const commandArgs = parsed.commandArgs;
@@ -143,6 +147,10 @@ switch (command) {
143
147
  await (await import("./commands/bottom.ts")).default(commandArgs);
144
148
  break;
145
149
 
150
+ case "learn":
151
+ await (await import("./commands/learn.ts")).default(commandArgs);
152
+ break;
153
+
146
154
  case "--help":
147
155
  case "help":
148
156
  printHelp();
@@ -196,6 +204,7 @@ ${bold("INFO & MAINTENANCE")}
196
204
  ${green("archive")} Manage archived stacks
197
205
  ${green("update-prs")} Refresh stack visualization in PR descriptions
198
206
  ${green("doctor")} Migrate & repair stack metadata
207
+ ${green("learn")} Print the agent skill (--skill installs it)
199
208
 
200
209
  ${bold("GLOBAL OPTIONS")}
201
210
  --yes, -y Skip confirmations ${dim("(for agents/CI)")}
package/src/lib/git.ts CHANGED
@@ -38,6 +38,14 @@ export async function gitDir(): Promise<string> {
38
38
  return result.trim();
39
39
  }
40
40
 
41
+ /**
42
+ * Get the absolute path to the repository's working-tree root.
43
+ */
44
+ export async function repoRoot(): Promise<string> {
45
+ const result = await $`git rev-parse --show-toplevel`.text();
46
+ return result.trim();
47
+ }
48
+
41
49
  /**
42
50
  * Check if working tree is clean (no uncommitted changes).
43
51
  * Refreshes the index first to avoid false positives from stale stat info.
@@ -149,6 +157,29 @@ export async function pushStackedBranch(branch: string): Promise<boolean> {
149
157
  return exitCode === 0;
150
158
  }
151
159
 
160
+ /**
161
+ * Read the actual tip sha of every branch on origin in a single `ls-remote`
162
+ * call. This queries the remote directly, so it is **authoritative** — unlike
163
+ * `rev-parse origin/<branch>`, which reads the local remote-tracking ref that a
164
+ * force-push can leave stale (a fresh fetch would correct it). Use this when a
165
+ * correct answer about the remote's real state matters (e.g. the post-restack
166
+ * sync verification — #23). Returns a map of branch name → sha.
167
+ */
168
+ export async function remoteHeads(): Promise<Map<string, string>> {
169
+ const heads = new Map<string, string>();
170
+ const { exitCode, stdout } = await $`git ls-remote --heads origin`.nothrow().quiet();
171
+ if (exitCode !== 0) return heads;
172
+ const prefix = "refs/heads/";
173
+ for (const line of stdout.toString().split("\n")) {
174
+ const tab = line.indexOf("\t");
175
+ if (tab === -1) continue;
176
+ const sha = line.slice(0, tab).trim();
177
+ const ref = line.slice(tab + 1).trim();
178
+ if (sha && ref.startsWith(prefix)) heads.set(ref.slice(prefix.length), sha);
179
+ }
180
+ return heads;
181
+ }
182
+
152
183
  /**
153
184
  * Check if a branch exists on the remote.
154
185
  */
@@ -0,0 +1,7 @@
1
+ // Bun bundles `*.md` files imported with `{ type: "text" }` into the binary as
2
+ // their raw string contents (survives `bun build --compile`). This declaration
3
+ // lets TypeScript type those imports.
4
+ declare module "*.md" {
5
+ const content: string;
6
+ export default content;
7
+ }
@@ -0,0 +1,22 @@
1
+ // Canonical, version-stamped agent skill for gh-stack.
2
+ //
3
+ // The skill text is a plain Markdown file — `using-gh-stack.md` — bundled into
4
+ // the binary at build time via Bun's `type: "text"` import (it survives
5
+ // `bun build --compile`). Editing the skill means editing that .md file; this
6
+ // module only stamps the running version into the `{{VERSION}}` placeholder.
7
+ //
8
+ // It is the SINGLE SOURCE OF TRUTH for "how an agent should drive gh-stack":
9
+ // `gh-stack learn` prints it, `gh-stack learn --skill` installs it. Update the
10
+ // .md whenever a command or flag changes and the next release ships it.
11
+ import skillTemplate from "./using-gh-stack.md" with { type: "text" };
12
+
13
+ export const SKILL_NAME = "using-gh-stack";
14
+
15
+ /**
16
+ * Build the full skill document, stamped with the running gh-stack version so
17
+ * an installed copy always records which release generated it (and can be
18
+ * refreshed with `gh-stack learn --skill`).
19
+ */
20
+ export function skillContent(version: string): string {
21
+ return skillTemplate.replaceAll("{{VERSION}}", version);
22
+ }
@@ -0,0 +1,234 @@
1
+ ---
2
+ name: using-gh-stack
3
+ description: "MUST be used when working with stacked PRs. gh-stack is a CLI tool for managing stacked PR workflows in squash-merge repos (a Graphite alternative). Covers creating stacks, pushing/creating PRs with submit, navigating (up/down/top/bottom), syncing with main, restacking, merging, and agent/CI usage. Triggers on: stacked PR, gh-stack, create stack, submit stack, push stack, restack, sync stack, stack navigation, up down top bottom, PR dependencies, stack visualization, manage PRs, stacked branches."
4
+ ---
5
+
6
+ # Using gh-stack
7
+
8
+ > Generated by `gh-stack learn` from **gh-stack v{{VERSION}}**.
9
+ > This file is produced by the binary — don't hand-edit it. Refresh after
10
+ > upgrading with `gh-stack learn --skill`.
11
+
12
+ `gh-stack` is a CLI for managing **stacked PRs** in squash-merge repositories
13
+ (a Graphite/`gt` alternative). A _stack_ is a chain of dependent branches, each
14
+ its own PR, sitting on top of the trunk (usually `main`).
15
+
16
+ **Install:** `npm install -g gh-stack` (or `bun install -g gh-stack`)
17
+
18
+ ## Terms
19
+
20
+ | Term | Meaning |
21
+ | ------------- | ------------------------------------ |
22
+ | **trunk** | Base branch (usually `main`) |
23
+ | **stack** | A chain of dependent branches |
24
+ | **upstack** | Child branches (depend on current) |
25
+ | **downstack** | Parent branches (current depends on) |
26
+
27
+ ## The current stack follows your current branch
28
+
29
+ There is no "switch stack" state to manage — **gh-stack always operates on the
30
+ stack that contains the branch you're on.** It resolves this from your current
31
+ git branch: the branch's own stack, or (for a new branch built on top of one)
32
+ the nearest tracked ancestor's stack. Every command (`log`, `submit`, `status`,
33
+ `restack`, …) acts on that stack automatically.
34
+
35
+ So to switch stacks, just switch branches — there's nothing gh-stack-specific to
36
+ run:
37
+
38
+ ```bash
39
+ git switch other-feature-branch # any branch on the other stack
40
+ gh-stack log # now shows that stack
41
+ ```
42
+
43
+ Or use the interactive picker when you don't remember the branch names:
44
+
45
+ ```bash
46
+ gh-stack checkout --stack # pick a stack, jump to it (alias: co --stack)
47
+ ```
48
+
49
+ (Plain `gh-stack checkout` picks a _branch within the current stack_; add
50
+ `--stack` to pick a _different stack_.)
51
+
52
+ ## Agent/CI mode — read this first
53
+
54
+ Run non-interactively with `--yes`/`-y` or `GH_STACK_YES=1` (also enables
55
+ `--plain`: no spinners/colors/box-drawing — clean for log capture).
56
+
57
+ ```bash
58
+ export GH_STACK_YES=1 # skip every confirmation, plain output
59
+ ```
60
+
61
+ - **Discover topology without parsing internals:** `gh-stack stacks --json`
62
+ (network-free) or `gh-stack status --json` (adds PR/CI/review state).
63
+ - **Always pass `-t`/`-b` to `submit`** so PRs get real titles/bodies instead
64
+ of generic auto-generated ones (see below).
65
+ - Commands that rebase (`restack`, `sync`, `merge`) **refuse to run on a dirty
66
+ tree** — commit or stash first.
67
+
68
+ ## Core workflow
69
+
70
+ ### 1. Start a stack
71
+
72
+ **From existing branches** (e.g. `main → feat-1 → feat-2`) — go to the TOP;
73
+ `init` walks down and reconstructs the chain automatically:
74
+
75
+ ```bash
76
+ git checkout feat-2
77
+ gh-stack init # detects main → feat-1 → feat-2, adopts existing PRs
78
+ ```
79
+
80
+ **From scratch:**
81
+
82
+ ```bash
83
+ git checkout -b kiliman/first-WEB-1234
84
+ gh-stack init # create stack, add current branch
85
+ gh-stack create kiliman/second-WEB-5678 # new branch stacked on top
86
+ # ...work, commit...
87
+ gh-stack create kiliman/third-WEB-9012 # stack another
88
+ ```
89
+
90
+ `init` options: `--name <stack>`, `--parent <trunk>`, `--description <text>`.
91
+
92
+ ### 2. Push & create PRs — `submit`
93
+
94
+ `submit` is the workhorse: **idempotent, scope-aware, self-healing**. It pushes
95
+ changed branches with `--force-with-lease` (skips up-to-date ones), creates
96
+ missing PRs with the correct base, fixes PR bases, and refreshes the stack
97
+ visualization in every PR body.
98
+
99
+ ```bash
100
+ gh-stack submit # push downstack + create/update PRs
101
+ gh-stack submit -d # create new PRs as drafts
102
+ gh-stack submit --dry-run # preview, no side effects
103
+ gh-stack submit --restack # then propagate up to children (see below)
104
+ gh-stack submit -t "<title>" -b "<body>" # explicit title/body for new PRs
105
+ gh-stack submit -t "<title>" --body-file /tmp/body.md
106
+ gh-stack submit -n # auto-generate titles (generic — avoid)
107
+ ```
108
+
109
+ **Agents: provide title + body explicitly.** `--title` implies `--no-edit`
110
+ (no prompts); title/body apply to all new PRs created in scope:
111
+
112
+ ```bash
113
+ gh-stack submit \
114
+ -t "Store raw transcription JSON and WebVTT [WEB-7409]" \
115
+ -b "$(cat <<'EOF'
116
+ ## Summary
117
+ - Store raw transcription JSON alongside pre-rendered WebVTT
118
+
119
+ ## Test plan
120
+ - [ ] Verify both formats persist
121
+ EOF
122
+ )"
123
+ ```
124
+
125
+ **`submit -t`/`-b` also updates an existing PR in place** — pass them while on a
126
+ branch that already has a PR to change just that one (visualization-safe).
127
+
128
+ **`--restack`** chains a restack after submit. `submit` owns DOWNSTACK (trunk →
129
+ current); `restack` owns UPSTACK (the children). Together they're the everyday
130
+ downstack-fix loop in one command: _drop downstack → fix → commit →
131
+ `gh-stack submit --restack`_ pushes the fix AND rebases the children onto it.
132
+
133
+ ### 3. Navigate
134
+
135
+ ```bash
136
+ gh-stack log # stack tree (default command)
137
+ gh-stack ls # numbered branch list
138
+ gh-stack up [n] # toward children (upstack)
139
+ gh-stack down [n] # toward parents (downstack)
140
+ gh-stack top # tip (leaf)
141
+ gh-stack bottom # base (first branch above trunk)
142
+ gh-stack checkout # interactive picker (alias: co)
143
+ ```
144
+
145
+ ### 4. Sync & restack
146
+
147
+ ```bash
148
+ gh-stack sync # fetch trunk, rebase base onto it, restack ALL children
149
+ gh-stack restack # rebase current branch + descendants onto their parents
150
+ gh-stack restack --dry-run
151
+ ```
152
+
153
+ **⚠️ Restack propagates UPWARD from the current branch.** Run it from the
154
+ **lowest branch you changed**, never from the top:
155
+
156
+ ```bash
157
+ gh-stack bottom # go to the changed branch
158
+ # ...edit, commit...
159
+ gh-stack submit # push it
160
+ gh-stack restack # propagates the change UP through children
161
+ ```
162
+
163
+ **Changing multiple branches?** Finish each one fully before moving up —
164
+ `edit → test → commit → push → restack`, then `gh-stack up` and repeat. Don't
165
+ edit several branches then restack once at the end (only the last branch ends
166
+ up pushed).
167
+
168
+ **On rebase conflict:** resolve, `git add <files>`, `git rebase --continue`,
169
+ then `gh-stack restack --resume` to finish the remaining branches.
170
+
171
+ **`sync` is self-correcting:** if you merged the bottom PR in the GitHub UI,
172
+ `sync` detects it and advances the stack past the merged branch (re-rooting onto
173
+ trunk) instead of replaying squashed commits and conflicting.
174
+
175
+ ### 5. Status
176
+
177
+ ```bash
178
+ gh-stack status # PR dashboard: CI, reviews, merge readiness
179
+ gh-stack status --current # current stack only
180
+ gh-stack status --json # structured, for agents
181
+ ```
182
+
183
+ ### 6. Merge & ship
184
+
185
+ ```bash
186
+ gh-stack merge # top-down: collapse stack into one squash commit → trunk
187
+ gh-stack merge --approved # bottom-up: merge approved PRs (each its own commit),
188
+ # re-root the unreviewed tail, leave it as a clean stack
189
+ gh-stack merge --dry-run
190
+ ```
191
+
192
+ - **`merge`** (top-down): the whole stack is approved → lands as a unit.
193
+ - **`merge --approved`** (bottom-up): merge PRs as they're reviewed, keep
194
+ stacking the rest. Stops at the first un-approved PR; auto-advances past any
195
+ bottom PR already merged outside gh-stack.
196
+
197
+ ### 7. Maintenance
198
+
199
+ ```bash
200
+ gh-stack delete # remove a branch from the stack (re-parents children)
201
+ gh-stack split # cut a stack into two at a branch
202
+ gh-stack rename # rename a tracked branch (keeps metadata in sync)
203
+ gh-stack undo # restore from the last snapshot
204
+ gh-stack archive # manage archived stacks
205
+ gh-stack doctor # migrate & repair stack metadata
206
+ gh-stack update-prs # refresh stack visualization in PR descriptions
207
+ ```
208
+
209
+ ## Stack visualization
210
+
211
+ `submit` auto-maintains a `### 📚 Stacked on` footer in every PR body. **Do not
212
+ hand-write your own stack list** — it's regenerated each run, so a manual copy
213
+ just goes stale and contradicts the auto footer. Describe _this_ PR in its body;
214
+ let `submit` describe the stack. Link a specific sibling inline in prose if
215
+ needed.
216
+
217
+ ## Safety model
218
+
219
+ - **Snapshots** before every destructive op (restack/sync/merge/remove); roll
220
+ back with `gh-stack undo`. Think of them as a reflog for stack operations.
221
+ - **Squash-merge safe** — uses temporary tags for stable rebasing (handles the
222
+ merge-base problem squash-merge creates).
223
+ - **Never force-pushes trunk.**
224
+ - **Metadata is local and never committed:** per-stack files under
225
+ `.git/.gh-stack/` plus git branch config. Inspect it via `gh-stack stacks
226
+ --json`, not by reading the store. (`gh-stack doctor` migrates the old v2
227
+ `.git/gh-stack-metadata.json` monolith.)
228
+
229
+ ## Don'ts
230
+
231
+ - **Don't use `gt`/Graphite commands** — this is `gh-stack`.
232
+ - **Don't restack from the top** of the stack — restack flows upward.
233
+ - **Don't edit multiple branches then restack once** — finish each branch first.
234
+ - **Don't hand-write a stack section** in PR bodies — `submit` owns it.