pi-graphite 0.4.1 → 0.5.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
@@ -1,7 +1,7 @@
1
1
  # pi-graphite
2
2
 
3
3
  Opinionated pi tools + skill that wrap the [Graphite](https://graphite.com)
4
- `gt` CLI for stacked PR workflows. Seven tools, one correct path.
4
+ `gt` CLI for stacked PR workflows. A small set of tools, one correct path.
5
5
 
6
6
  ```
7
7
  graphite_status → (graphite_setup if needed) → graphite_sync → graphite_navigate
@@ -46,10 +46,11 @@ agent loads it on demand.
46
46
  | `graphite_status` | Read-only snapshot: current stack + current branch + PR + restack hints | `gt log --stack`, `gt info` |
47
47
  | `graphite_setup` | Initialize Graphite or track an existing Git branch with explicit parent | `gt init --trunk`, `gt track --parent` |
48
48
  | `graphite_sync` | Start-of-day / after-merge cleanup + restack | `gt sync` |
49
+ | `graphite_get` | Pull a branch / stack from the remote | `gt get <branch>` |
49
50
  | `graphite_navigate` | Move around the stack | `gt checkout`, `gt up`/`down`/`top`/`bottom` |
50
51
  | `graphite_change` | Create / amend a stacked branch | `gt create -am`, `gt modify -am`, `gt modify --into`, `gt absorb` |
51
52
  | `graphite_submit` | Push the entire stack and open/update PRs (dry-run by default) | `gt submit --stack --no-edit --no-ai` |
52
- | `graphite_recover` | Continue / abort / undo | `gt continue`, `gt abort`, `gt undo` |
53
+ | `graphite_recover` | Continue / abort / undo / restack | `gt continue`, `gt abort`, `gt undo`, `gt restack` |
53
54
 
54
55
  ## Golden path
55
56
 
@@ -80,7 +81,9 @@ dependent branches.
80
81
  - Every tool requires absolute `cwd`.
81
82
  - `gt` is invoked with `--cwd <cwd> --no-interactive`, no shell strings. Tools that support AI metadata pass `--no-ai`.
82
83
  - Editor / pager / browser env is forced safe (`GT_EDITOR=true`, `GT_PAGER=`,
83
- `BROWSER=true`, …). Commands have a hard timeout.
84
+ `BROWSER=true`, …). Commands have a hard timeout. `GRAPHITE_INTERACTIVE` is
85
+ deliberately **not** set — some `gt` builds treat it as "running inside
86
+ Graphite Interactive" and silently return empty output for read commands.
84
87
  - Interactive editor / hunk / browser / reorder paths are not exposed.
85
88
  - Commands echoed in tool output are safe to copy-paste back into a shell.
86
89
  - `graphite_setup action=track_branch` requires explicit `branch`, explicit
@@ -93,10 +96,20 @@ dependent branches.
93
96
  still contain `<<<<<<<` markers, unless `allowConflictMarkers:true`.
94
97
  - Output is ANSI-stripped, branded ("Graphite" not "Charcoal"), and truncated
95
98
  to ~50 KB / 2000 lines.
96
- - Stderr is parsed into structured `hints`
99
+ - Failure output is parsed into structured `hints`
97
100
  (`notInitialized`, `conflictHalted`, `restackNeeded`, `trunkOutOfSync`,
98
101
  `branchNotTracked`, `noChangesStaged`, `checkedOutElsewhere`,
99
- `operatingOnTrunk`, …).
102
+ `operatingOnTrunk`, `emptyOutput`, …).
103
+ - Read commands that must produce output (`graphite_status`) treat an
104
+ exit-0 **empty stdout** as a failure (`emptyOutput` hint) instead of a
105
+ misleading "ok".
106
+ - Output is also scanned (on success **and** failure) for non-fatal
107
+ `warnings` (`skippedBranches`, `remoteChanged`, `alreadyMerged`,
108
+ `needsRestack`). A result with warnings is reported as `ok (with warnings)`
109
+ so a `gt sync` / `gt submit` that silently skipped work is not mistaken for
110
+ a clean success.
111
+ - A **mutating** command that fails appends a "partial side effects possible"
112
+ note, prompting a follow-up `graphite_status`.
100
113
 
101
114
  ### Git hooks
102
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-graphite",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Opinionated pi tools + skill for stacked PR workflows with the Graphite (gt) CLI.",
5
5
  "keywords": [
6
6
  "pi",
@@ -26,27 +26,47 @@ Do not use it for:
26
26
  dedicated `gh` tool/extension; see the `gh` rule below
27
27
  - reading PR review comments or CI status — same
28
28
  - rewriting history beyond create/amend (split / fold / move / squash /
29
- reorder). The extension does not expose stack surgery. Do not invoke
30
- `gt` directly from bash for these those subcommands prompt
31
- interactively (base selectors, hunk pickers, editors) and will hang.
32
- Ask the user to run them manually in their own terminal.
29
+ reorder). The extension does not expose stack surgery, and those
30
+ subcommands prompt interactively (base selectors, hunk pickers, editors)
31
+ and will hang. Ask the user to run them in their own terminal.
33
32
 
34
33
  ## Tools
35
34
 
36
- The extension registers seven tools. Prefer them over `gt`/`git`/`gh` in bash.
35
+ The extension registers these tools. Prefer them over `gt`/`git`/`gh` in bash.
37
36
 
38
37
  | Tool | Purpose |
39
38
  |---|---|
40
39
  | `graphite_status` | Read-only snapshot: current stack + current branch + PR + restack hint |
41
40
  | `graphite_setup` | Initialize Graphite or track an existing Git branch with explicit parent |
42
41
  | `graphite_sync` | `gt sync` — pull trunk, drop merged branches, restack |
42
+ | `graphite_get` | `gt get <branch>` — pull a branch / stack from the remote |
43
43
  | `graphite_navigate` | `gt checkout` / `up` / `down` / `top` / `bottom` / trunk |
44
44
  | `graphite_change` | `gt create` / `gt modify` / `gt modify --into` / `gt absorb` |
45
45
  | `graphite_submit` | `gt submit --stack --no-edit` (dry-run by default) |
46
- | `graphite_recover` | `gt continue` / `gt abort` / `gt undo` |
46
+ | `graphite_recover` | `gt continue` / `gt abort` / `gt undo` / `gt restack` |
47
47
 
48
48
  All tools require an absolute `cwd`.
49
49
 
50
+ ## Reading tool output (don't trust a bare "ok")
51
+
52
+ Each result starts with `[<label>] ok | ok (with warnings) | fail`. Read past
53
+ the status line:
54
+
55
+ - **`fail`** — read the `--- hints ---` and `--- suggestion ---` blocks; they
56
+ tell you exactly which recovery tool to call.
57
+ - **`ok (with warnings)`** — gt exited 0 but its output mentioned skipped /
58
+ remotely-changed / already-merged branches or a needed restack. Treat this
59
+ as "succeeded but verify": run `graphite_status` before assuming the stack
60
+ is in the expected shape.
61
+ - **`emptyOutput` hint on a status call** — gt returned nothing where output
62
+ was expected. Usually means the branch is untracked, the repo is not
63
+ Graphite-initialized, or the gt build short-circuited. Follow the
64
+ suggestion (often `graphite_setup`), and you may run a **read-only** gt
65
+ command directly to confirm (see the direct-`gt` rule).
66
+ - After a **failed mutating** command (change / submit apply / sync / get /
67
+ recover / setup) the suggestion warns "partial side effects possible".
68
+ Always run `graphite_status` to see the real state before retrying.
69
+
50
70
  ## Golden path
51
71
 
52
72
  ```
@@ -179,6 +199,34 @@ graphite_status({ cwd })
179
199
 
180
200
  If `gt sync` halts on conflict, use the conflict recipe below.
181
201
 
202
+ ### Restack without pulling from remote
203
+
204
+ When `graphite_status` shows branches out of date with their parent but trunk
205
+ has not moved (no remote pull needed), restack directly:
206
+
207
+ ```
208
+ graphite_recover({ cwd, action: "restack" })
209
+ graphite_status({ cwd })
210
+ ```
211
+
212
+ Use `graphite_sync` instead when trunk itself may have advanced on the remote.
213
+ If restack halts on a conflict, follow the conflict recipe.
214
+
215
+ ### Pull a branch / stack from the remote
216
+
217
+ To check out a teammate's branch, or re-pull a branch that changed remotely:
218
+
219
+ ```
220
+ graphite_get({ cwd, branch: "<branch>" })
221
+ graphite_status({ cwd })
222
+ ```
223
+
224
+ If local commits should be overwritten by the remote version:
225
+
226
+ ```
227
+ graphite_get({ cwd, branch: "<branch>", force: true, confirmDestructive: true })
228
+ ```
229
+
182
230
  ### Resolve a conflict
183
231
 
184
232
  1. Read the failing tool's `--- stderr ---` and `hints` block.
@@ -210,8 +258,12 @@ graphite_recover({ cwd, action: "undo" })
210
258
  use `graphite_change action="create"` instead.
211
259
  - **Never guess tracking parent.** `track_branch` requires explicit branch,
212
260
  explicit parent, and `confirmParent:true`.
213
- - **Prefer `graphite_sync` over manual restack.** The extension does not
214
- expose a standalone restack tool. Sync covers both pull + restack.
261
+ - **Restack vs sync.** Use `graphite_recover action="restack"` to rebase the
262
+ stack onto each parent's latest commit when no remote pull is needed. Use
263
+ `graphite_sync` when trunk may have advanced remotely (it pulls + restacks).
264
+ - **Pull remote branches with `graphite_get`.** `graphite_sync` only touches
265
+ trunk + already-tracked local branches; use `graphite_get` to download a
266
+ branch/stack from the remote.
215
267
  - **Always dry-run first.** Show the user the dry-run plan from
216
268
  `graphite_submit apply=false` before pushing.
217
269
  - **`apply:true` requires `confirmRemote:true`.** The tool will refuse
@@ -225,5 +277,18 @@ graphite_recover({ cwd, action: "undo" })
225
277
  available. If you must shell out to `gh` from bash, pass fully explicit
226
278
  non-interactive arguments only — never `gh auth login`, `--web`, or any
227
279
  command that opens a browser, editor, or prompt.
280
+ - **Direct `gt` is allowed only when no tool covers the command.** The tools
281
+ above are the default path. If you genuinely need a `gt` subcommand this
282
+ extension does not expose (and the user has not asked to run it
283
+ themselves), you may call `gt` from bash, but ONLY with explicit
284
+ non-interactive flags and never an interactive subcommand:
285
+ - Always pass `--no-interactive` (and `--cwd <abs>`).
286
+ - Safe read-only fallbacks: `gt log`, `gt log --stack`, `gt info`,
287
+ `gt children`, `gt parent`, `gt trunk`, `gt state`.
288
+ - Never run interactive surgery (`gt split` / `fold` / `move` / `squash` /
289
+ `reorder`) or anything that opens an editor, pager, hunk picker, or
290
+ browser — those hang. Ask the user to run those in their own terminal.
291
+ - Prefer the dedicated tool whenever one exists; direct `gt` skips the
292
+ safety confirmations, hint parsing, and warning detection the tools add.
228
293
  - **No interactive editor / browser / hunk picker.** All paths are
229
294
  non-interactive; pass explicit messages.
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { registerStatus } from "./tools/status";
4
4
  import { registerSetup } from "./tools/setup";
5
5
  import { registerSync } from "./tools/sync";
6
+ import { registerGet } from "./tools/get";
6
7
  import { registerNavigate } from "./tools/navigate";
7
8
  import { registerChange } from "./tools/change";
8
9
  import { registerSubmit } from "./tools/submit";
@@ -11,15 +12,16 @@ import { registerRecover } from "./tools/recover";
11
12
  /**
12
13
  * pi-graphite — opinionated `gt` wrapper for stacked PR workflows.
13
14
  *
14
- * Seven workflow tools, one correct path:
15
+ * Workflow tools, one correct path:
15
16
  *
16
17
  * graphite_status — see where you are in the stack
17
18
  * graphite_setup — init repo / track existing branch when needed
18
19
  * graphite_sync — start-of-day / after-merge cleanup + restack
20
+ * graphite_get — pull a branch / stack from the remote
19
21
  * graphite_navigate — move to the branch / PR you want to mutate
20
22
  * graphite_change — create or amend a stacked branch
21
23
  * graphite_submit — push the whole stack and open/update PRs
22
- * graphite_recover — continue / abort / undo after conflicts or mistakes
24
+ * graphite_recover — continue / abort / undo / restack
23
25
  *
24
26
  * Golden path:
25
27
  *
@@ -45,6 +47,7 @@ export default function (pi: ExtensionAPI) {
45
47
  registerStatus(pi);
46
48
  registerSetup(pi);
47
49
  registerSync(pi);
50
+ registerGet(pi);
48
51
  registerNavigate(pi);
49
52
  registerChange(pi);
50
53
  registerSubmit(pi);
package/src/lib/exec.ts CHANGED
@@ -41,10 +41,13 @@ export const SAFE_NONINTERACTIVE_ENV: Record<string, string> = {
41
41
  LESS: "FRX",
42
42
  BROWSER: "true",
43
43
  GH_BROWSER: "true",
44
- // gt-gh treats this as "invoked from Graphite Interactive" and forces
45
- // non-interactive behavior regardless of argv. Other gt builds should
46
- // ignore it if unsupported; keep --no-interactive argv guards too.
47
- GRAPHITE_INTERACTIVE: "1",
44
+ // NOTE: we intentionally do NOT set GRAPHITE_INTERACTIVE here. Some gt
45
+ // builds treat GRAPHITE_INTERACTIVE=1 as "invoked from Graphite Interactive"
46
+ // and short-circuit read commands (`gt log --stack`, `gt info`) to exit 0
47
+ // with EMPTY stdout — which made this extension blind while reporting
48
+ // success. Non-interactive behavior is already enforced via the
49
+ // --no-interactive argv guards in runGt() plus the editor/pager/browser
50
+ // overrides above.
48
51
  };
49
52
 
50
53
  export function safeNoninteractiveEnv(extra?: Record<string, string>): NodeJS.ProcessEnv {
package/src/lib/result.ts CHANGED
@@ -19,6 +19,40 @@ export interface GtHints {
19
19
  prMissing?: boolean;
20
20
  operatingOnTrunk?: boolean;
21
21
  invalidArgument?: string;
22
+ /** exit 0 but stdout empty when output was expected (e.g. status). */
23
+ emptyOutput?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Non-fatal warnings parsed from BOTH stdout and stderr regardless of exit
28
+ * code. gt frequently exits 0 while skipping branches or noting that a
29
+ * branch changed remotely / already merged; surfacing these keeps the agent
30
+ * from trusting a misleadingly "ok" result.
31
+ */
32
+ export interface GtWarnings {
33
+ skippedBranches?: boolean;
34
+ remoteChanged?: boolean;
35
+ alreadyMerged?: boolean;
36
+ needsRestack?: boolean;
37
+ }
38
+
39
+ const WARNING_PATTERNS: Array<[keyof GtWarnings, RegExp]> = [
40
+ ["skippedBranches", /\bskipp(?:ing|ed)\b/i],
41
+ ["remoteChanged", /updated\s+remotely|changed\s+on\s+remote|newer\s+(?:commit|version)\s+(?:on|exists)|diverged\s+from\s+remote/i],
42
+ ["alreadyMerged", /already\s+(?:been\s+)?merged|has\s+been\s+merged|closed\s+remotely/i],
43
+ // Negative lookbehind avoids matching "does not need to be restacked".
44
+ ["needsRestack", /(?<!not\s)needs?\s+(?:to\s+be\s+)?restack|run\s+`?gt\s+restack`?|out\s+of\s+date\s+with\s+(?:its\s+)?parent/i],
45
+ ];
46
+
47
+ function parseWarnings(r: GtRunResult): GtWarnings {
48
+ const text = `${r.stdout}\n${r.stderr}`;
49
+ const w: GtWarnings = {};
50
+ // A help/usage dump is full of command descriptions, not real warnings.
51
+ if (looksLikeUsageDump(text)) return w;
52
+ for (const [key, re] of WARNING_PATTERNS) {
53
+ if (re.test(text)) (w as Record<string, unknown>)[key] = true;
54
+ }
55
+ return w;
22
56
  }
23
57
 
24
58
  // Patterns run only on failure text.
@@ -68,9 +102,29 @@ const PATTERNS: Array<[Exclude<keyof GtHints, "checkedOutElsewhere" | "invalidAr
68
102
  ],
69
103
  ];
70
104
 
105
+ /**
106
+ * gt dumps its full usage/help (Commands:/Options: blocks) on an unknown
107
+ * argument. Those command descriptions contain phrases ("halted by a rebase
108
+ * conflict", "restack", …) that would otherwise trigger false-positive
109
+ * hints. Detect the dump so we can suppress content-derived hints and keep
110
+ * only the invalidArgument signal.
111
+ */
112
+ function looksLikeUsageDump(text: string): boolean {
113
+ return /\n\s*Commands:\s*\n/i.test(text) && /\n\s*Options:\s*\n/i.test(text);
114
+ }
115
+
71
116
  function parseHints(r: GtRunResult): GtHints {
72
117
  const text = `${r.stdout}\n${r.stderr}`;
73
118
  const hints: GtHints = {};
119
+
120
+ // Match both singular and plural ("Unknown argument(s):").
121
+ const invalid = text.match(/Unknown arguments?:\s*([^\n]+)/i);
122
+ if (invalid) hints.invalidArgument = invalid[1].trim();
123
+
124
+ // When gt printed its help dump, the only trustworthy signal is the
125
+ // unknown-argument line; everything else is description text.
126
+ if (looksLikeUsageDump(text)) return hints;
127
+
74
128
  for (const [key, re] of PATTERNS) {
75
129
  if (re.test(text)) (hints as Record<string, unknown>)[key] = true;
76
130
  }
@@ -80,28 +134,56 @@ function parseHints(r: GtRunResult): GtHints {
80
134
  );
81
135
  hints.checkedOutElsewhere = { branch: m?.[1], worktree: m?.[2] };
82
136
  }
83
- const invalid = text.match(/Unknown argument:\s*([^\n]+)/i);
84
- if (invalid) hints.invalidArgument = invalid[1].trim();
85
137
  return hints;
86
138
  }
87
139
 
140
+ export interface FormatOptions {
141
+ /**
142
+ * When true, an exit-0 result with empty stdout is treated as a FAILURE
143
+ * (with an emptyOutput hint). Use for read commands that must produce
144
+ * output, e.g. `gt log --stack` / `gt info`. Without this, gt silently
145
+ * returning nothing would be reported as "ok" and blind the agent.
146
+ */
147
+ requireStdout?: boolean;
148
+ /**
149
+ * When true, a FAILURE appends a "partial side effects possible" note so
150
+ * the agent knows the command may have mutated state before erroring
151
+ * (e.g. `gt create` made a branch then hit a metadata lock). Set for any
152
+ * command that can change local/remote state.
153
+ */
154
+ mutating?: boolean;
155
+ }
156
+
157
+ const PARTIAL_SIDE_EFFECTS_NOTE =
158
+ "This command can mutate state, and it failed mid-flight: partial side effects are possible " +
159
+ "(e.g. a created branch, partial restack, a metadata lock, or some PRs already pushed). " +
160
+ "Run graphite_status to confirm the actual stack state before retrying.";
161
+
88
162
  export interface FormattedResult {
89
163
  ok: boolean;
90
164
  isFailure: boolean;
91
165
  result: GtRunResult;
92
166
  /** Empty object on success. Populated only on failure. */
93
167
  hints: GtHints;
168
+ /** Non-fatal warnings parsed on success AND failure. */
169
+ warnings: GtWarnings;
94
170
  /** Recovery suggestion derived from hints + auxiliary probes. */
95
171
  suggestion?: string;
96
172
  }
97
173
 
98
- export function formatResult(r: GtRunResult): FormattedResult {
99
- const isFailure = r.exitCode !== 0 || r.timedOut || !!r.spawnError;
174
+ export function formatResult(r: GtRunResult, opts?: FormatOptions): FormattedResult {
175
+ const hardFailure = r.exitCode !== 0 || r.timedOut || !!r.spawnError;
176
+ const emptyOutput =
177
+ !hardFailure && !!opts?.requireStdout && r.stdout.trim() === "";
178
+ const isFailure = hardFailure || emptyOutput;
179
+ const hints = isFailure ? parseHints(r) : {};
180
+ if (emptyOutput) hints.emptyOutput = true;
100
181
  return {
101
182
  ok: !isFailure,
102
183
  isFailure,
103
184
  result: r,
104
- hints: isFailure ? parseHints(r) : {},
185
+ hints,
186
+ warnings: parseWarnings(r),
105
187
  };
106
188
  }
107
189
 
@@ -127,11 +209,24 @@ export function renderText(label: string, f: FormattedResult): string {
127
209
  lines.push("--- hints ---");
128
210
  lines.push(JSON.stringify(f.hints));
129
211
  }
212
+ if (Object.keys(f.warnings).length) {
213
+ lines.push("--- warnings ---");
214
+ lines.push(JSON.stringify(f.warnings));
215
+ lines.push(
216
+ "gt reported success but the above conditions were detected in its output. " +
217
+ "Verify the result (e.g. run graphite_status) before assuming the stack is in the expected state.",
218
+ );
219
+ }
130
220
  if (f.isFailure && f.suggestion) {
131
221
  lines.push("--- suggestion ---");
132
222
  lines.push(f.suggestion);
133
223
  }
134
- return `[${label}] ${f.ok ? "ok" : "fail"}\n${lines.join("\n")}`;
224
+ const status = f.ok
225
+ ? Object.keys(f.warnings).length
226
+ ? "ok (with warnings)"
227
+ : "ok"
228
+ : "fail";
229
+ return `[${label}] ${status}\n${lines.join("\n")}`;
135
230
  }
136
231
 
137
232
  /* ----------------------- auxiliary probes (best-effort) ----------------------- */
@@ -255,6 +350,13 @@ export async function enrichFailure(cwd: string, f: FormattedResult): Promise<vo
255
350
  `Operation refused on the trunk branch. Check out a non-trunk branch first with graphite_navigate.`,
256
351
  );
257
352
  }
353
+ if (f.hints.emptyOutput) {
354
+ parts.push(
355
+ `gt exited 0 but produced no output where output was expected. This usually means the current branch is not tracked by Graphite, you are not in a Graphite-initialized repo, or the gt build short-circuited (do NOT set GRAPHITE_INTERACTIVE). ` +
356
+ `Confirm with \`git -C <cwd> rev-parse --abbrev-ref HEAD\` and \`gt --version\`, and consider running graphite_setup. ` +
357
+ `As a fallback you may run a read-only gt command directly (e.g. \`gt log --stack\`) with non-interactive flags.`,
358
+ );
359
+ }
258
360
 
259
361
  if (parts.length) f.suggestion = parts.join(" ");
260
362
  }
@@ -268,10 +370,16 @@ export async function ensureSuccess(
268
370
  label: string,
269
371
  r: GtRunResult,
270
372
  cwd: string,
373
+ opts?: FormatOptions,
271
374
  ): Promise<FormattedResult> {
272
- const f = formatResult(r);
375
+ const f = formatResult(r, opts);
273
376
  if (f.isFailure) {
274
377
  await enrichFailure(cwd, f);
378
+ if (opts?.mutating) {
379
+ f.suggestion = f.suggestion
380
+ ? `${f.suggestion} ${PARTIAL_SIDE_EFFECTS_NOTE}`
381
+ : PARTIAL_SIDE_EFFECTS_NOTE;
382
+ }
275
383
  throw new Error(renderText(label, f));
276
384
  }
277
385
  return f;
@@ -283,10 +391,13 @@ export async function ensureSuccess(
283
391
  * agent sees the full picture, not just the first error.
284
392
  */
285
393
  export async function ensureAllSuccess(
286
- items: Array<{ label: string; result: GtRunResult }>,
394
+ items: Array<{ label: string; result: GtRunResult; requireStdout?: boolean }>,
287
395
  cwd: string,
288
396
  ): Promise<FormattedResult[]> {
289
- const formatted = items.map((i) => ({ label: i.label, f: formatResult(i.result) }));
397
+ const formatted = items.map((i) => ({
398
+ label: i.label,
399
+ f: formatResult(i.result, { requireStdout: i.requireStdout }),
400
+ }));
290
401
  const failed = formatted.filter((x) => x.f.isFailure);
291
402
  if (failed.length) {
292
403
  await Promise.all(failed.map((x) => enrichFailure(cwd, x.f)));
@@ -130,7 +130,7 @@ export function registerChange(pi: ExtensionAPI) {
130
130
  }
131
131
  const label = `gt ${shellJoin(args)}`;
132
132
  const r = await runGt(args, { cwd: p.cwd, signal });
133
- const f = await ensureSuccess(label, r, p.cwd);
133
+ const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
134
134
  return {
135
135
  content: [{ type: "text", text: renderText(label, f) }],
136
136
  details: { action: p.action, result: f },
@@ -0,0 +1,67 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runGt } from "../lib/exec";
3
+ import { assertSafeRef, shellJoin } from "../lib/argv";
4
+ import { ensureSuccess, renderText } from "../lib/result";
5
+ import {
6
+ CwdParam,
7
+ Type,
8
+ requireConfirm,
9
+ type ToolReturn,
10
+ } from "../lib/schema";
11
+
12
+ /**
13
+ * graphite_get — `gt get <branch>`.
14
+ *
15
+ * Download a branch (and its descendants) from the Graphite remote and check
16
+ * it out. Used to pull a teammate's stack, or to re-pull a branch after it
17
+ * changed remotely. Distinct from graphite_sync, which operates on trunk +
18
+ * already-tracked local branches.
19
+ *
20
+ * Because `gt get` can overwrite local commits with the remote version,
21
+ * `force` requires `confirmDestructive:true`.
22
+ */
23
+ export function registerGet(pi: ExtensionAPI) {
24
+ pi.registerTool({
25
+ name: "graphite_get",
26
+ label: "Graphite: get",
27
+ description:
28
+ "Download a branch and its descendants from the Graphite remote and check it out via `gt get <branch>`. Use to pull a teammate's stack or re-pull a branch that changed remotely. `force` (overwrite local) requires confirmDestructive.",
29
+ promptSnippet:
30
+ "graphite_get: `gt get <branch>` — pull a branch/stack from remote",
31
+ promptGuidelines: [
32
+ "Use graphite_get to pull a branch (and its descendants) from the remote. graphite_sync only handles trunk + already-tracked local branches.",
33
+ "graphite_get with force=true may overwrite local commits; pass confirmDestructive:true.",
34
+ ],
35
+ parameters: Type.Object({
36
+ cwd: CwdParam,
37
+ branch: Type.String({
38
+ description: "Branch to download from the Graphite remote and check out.",
39
+ }),
40
+ force: Type.Optional(
41
+ Type.Boolean({
42
+ description:
43
+ "Overwrite local branch state with remote (--force). Requires confirmDestructive.",
44
+ }),
45
+ ),
46
+ confirmDestructive: Type.Optional(Type.Boolean()),
47
+ }),
48
+ async execute(_id, p, signal): Promise<ToolReturn> {
49
+ if (p.force) {
50
+ requireConfirm(
51
+ p.confirmDestructive,
52
+ "gt get --force (may overwrite local commits with the remote version)",
53
+ );
54
+ }
55
+ const args = ["get", assertSafeRef(p.branch, "branch")];
56
+ if (p.force) args.push("--force");
57
+
58
+ const label = `gt ${shellJoin(args)}`;
59
+ const r = await runGt(args, { cwd: p.cwd, signal });
60
+ const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
61
+ return {
62
+ content: [{ type: "text", text: renderText(label, f) }],
63
+ details: { result: f },
64
+ };
65
+ },
66
+ });
67
+ }
@@ -82,16 +82,17 @@ export function registerRecover(pi: ExtensionAPI) {
82
82
  name: "graphite_recover",
83
83
  label: "Graphite: recover",
84
84
  description:
85
- "Recover from a halted Graphite operation or a recent mistake: continue (resume a paused gt command after resolving conflicts), abort (cancel the in-flight operation), or undo (revert the most recent gt mutation in this worktree). Always prefer this over `git rebase --continue`.",
85
+ "Recover or repair Graphite stack state: continue (resume a paused gt command after resolving conflicts), abort (cancel the in-flight operation), undo (revert the most recent gt mutation in this worktree), or restack (rebase the current stack so each branch sits on its parent's latest commit). Always prefer this over `git rebase --continue`.",
86
86
  promptSnippet:
87
- "graphite_recover: continue / abort / undo — never use `git rebase --continue`",
87
+ "graphite_recover: continue / abort / undo / restack — never use `git rebase --continue`",
88
88
  promptGuidelines: [
89
89
  "After resolving a rebase or cherry-pick conflict from a gt command, call graphite_recover action=continue (not `git rebase --continue`) so Graphite propagates the fix to dependent branches.",
90
90
  "graphite_recover action=undo only undoes commands run from the current worktree.",
91
+ "Use graphite_recover action=restack when graphite_status reports branches out of date with their parent but no remote pull is needed; use graphite_sync when trunk itself may have moved.",
91
92
  ],
92
93
  parameters: Type.Object({
93
94
  cwd: CwdParam,
94
- action: StringEnum(["continue", "abort", "undo"] as const),
95
+ action: StringEnum(["continue", "abort", "undo", "restack"] as const),
95
96
  stageAll: Type.Optional(
96
97
  Type.Boolean({
97
98
  description: "action=continue: stage all changes first (--all).",
@@ -135,10 +136,13 @@ export function registerRecover(pi: ExtensionAPI) {
135
136
  args = ["undo"];
136
137
  if (p.force) args.push("--force");
137
138
  break;
139
+ case "restack":
140
+ args = ["restack"];
141
+ break;
138
142
  }
139
143
  const label = `gt ${shellJoin(args)}`;
140
144
  const r = await runGt(args, { cwd: p.cwd, signal });
141
- const f = await ensureSuccess(label, r, p.cwd);
145
+ const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
142
146
  return {
143
147
  content: [{ type: "text", text: renderText(label, f) }],
144
148
  details: { action: p.action, result: f },
@@ -111,7 +111,7 @@ export function registerSetup(pi: ExtensionAPI) {
111
111
 
112
112
  const label = `gt ${shellJoin(args)}`;
113
113
  const r = await runGt(args, { cwd: p.cwd, signal });
114
- const f = await ensureSuccess(label, r, p.cwd);
114
+ const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
115
115
  return {
116
116
  content: [{ type: "text", text: renderText(label, f) }],
117
117
  details: { action: p.action, result: f },
@@ -33,8 +33,8 @@ export function registerStatus(pi: ExtensionAPI) {
33
33
  ]);
34
34
  const [fl, fi] = await ensureAllSuccess(
35
35
  [
36
- { label: "gt log --stack", result: log },
37
- { label: "gt info", result: info },
36
+ { label: "gt log --stack", result: log, requireStdout: true },
37
+ { label: "gt info", result: info, requireStdout: true },
38
38
  ],
39
39
  p.cwd,
40
40
  );
@@ -129,7 +129,8 @@ export function registerSubmit(pi: ExtensionAPI) {
129
129
 
130
130
  const label = `gt ${shellJoin(args)}`;
131
131
  const r = await runGt(args, { cwd: p.cwd, signal });
132
- const f = await ensureSuccess(label, r, p.cwd);
132
+ // A dry-run does not mutate; an applied submit does.
133
+ const f = await ensureSuccess(label, r, p.cwd, { mutating: apply });
133
134
  return {
134
135
  content: [{ type: "text", text: renderText(label, f) }],
135
136
  details: { apply, result: f },
package/src/tools/sync.ts CHANGED
@@ -70,7 +70,7 @@ export function registerSync(pi: ExtensionAPI) {
70
70
 
71
71
  const label = `gt ${shellJoin(args)}`;
72
72
  const r = await runGt(args, { cwd: p.cwd, signal });
73
- const f = await ensureSuccess(label, r, p.cwd);
73
+ const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
74
74
  return {
75
75
  content: [{ type: "text", text: renderText(label, f) }],
76
76
  details: { result: f },