pi-graphite 0.2.3 → 0.3.1

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/src/lib/result.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { DEFAULT_COMMAND_TIMEOUT_MS, killProcessGroup, safeNoninteractiveEnv, type GtRunResult } from "./exec";
2
+ import { shellJoin } from "./argv";
3
+ import { DEFAULT_COMMAND_TIMEOUT_MS, killProcessGroup, runGt, safeNoninteractiveEnv, type GtRunResult } from "./exec";
3
4
 
4
5
  /**
5
6
  * Structured failure hints. Only populated when the underlying `gt` command
@@ -108,7 +109,7 @@ export function formatResult(r: GtRunResult): FormattedResult {
108
109
  export function renderText(label: string, f: FormattedResult): string {
109
110
  const r = f.result;
110
111
  const lines: string[] = [];
111
- lines.push(`$ gt ${r.args.join(" ")}`);
112
+ lines.push(`$ gt ${shellJoin(r.args)}`);
112
113
  lines.push(
113
114
  `# cwd=${r.cwd} exit=${r.exitCode}${r.timedOut ? " (aborted)" : ""}${
114
115
  r.spawnError ? ` (spawn-error: ${r.spawnError})` : ""
@@ -176,7 +177,11 @@ async function detectCurrentBranch(cwd: string): Promise<string | undefined> {
176
177
  }
177
178
 
178
179
  async function detectTrunk(cwd: string): Promise<string | undefined> {
179
- const out = await execText("gt", ["--cwd", cwd, "--no-interactive", "trunk"], cwd);
180
+ // Route through the hardened runner so cwd resolve, forbidden-token scan,
181
+ // trailing --no-interactive injection, and env scrubbing all apply.
182
+ const r = await runGt(["trunk"], { cwd }).catch(() => undefined);
183
+ if (!r || r.exitCode !== 0) return undefined;
184
+ const out = r.stdout;
180
185
  // gt trunk prints the trunk name on its own line. Take last non-empty line.
181
186
  const cleaned = out
182
187
  .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
@@ -204,28 +209,28 @@ export async function enrichFailure(cwd: string, f: FormattedResult): Promise<vo
204
209
  const b = branch ?? "<current-branch>";
205
210
  const p = trunk ?? "<trunk>";
206
211
  parts.push(
207
- `Current branch (${b}) is not tracked by Graphite. To track it, call: ` +
208
- `graphite_branch_tracking({ action: "track", branch: "${b}", parent: "${p}" }). ` +
209
- `Verify parent is correct before applying.`,
212
+ `Current branch (${b}) is not tracked by Graphite. To track it after verifying the intended parent, call: ` +
213
+ `graphite_setup({ action: "track_branch", branch: "${b}", parent: "${p}", confirmParent: true }). ` +
214
+ `Do not guess the parent if unclear; ask the user first.`,
210
215
  );
211
216
  }
212
217
  if (f.hints.notInitialized) {
213
218
  parts.push(
214
- `Graphite not initialized in this repo. Call: graphite_repo({ action: "init", trunk: "<trunk-branch>" }).`,
219
+ `Graphite not initialized in this repo. Call: graphite_setup({ action: "init_repo", trunk: "<trunk-branch>" }).`,
215
220
  );
216
221
  }
217
222
  if (f.hints.conflictHalted) {
218
223
  parts.push(
219
224
  `A Graphite command is halted by a conflict. After resolving in git, call: ` +
220
- `graphite_recovery({ action: "continue" }) (or "abort").`,
225
+ `graphite_recover({ action: "continue" }) (or "abort").`,
221
226
  );
222
227
  }
223
228
  if (f.hints.restackNeeded) {
224
- parts.push(`Stack is out of date. Call: graphite_stack_restack().`);
229
+ parts.push(`Stack is out of date. Call: graphite_sync() to sync and restack.`);
225
230
  }
226
231
  if (f.hints.trunkOutOfSync) {
227
232
  parts.push(
228
- `Trunk is out of sync with remote. Call: graphite_remote_sync({ action: "sync" }) first.`,
233
+ `Trunk is out of sync with remote. Call: graphite_sync() first.`,
229
234
  );
230
235
  }
231
236
  if (f.hints.notAuthenticated) {
@@ -237,7 +242,7 @@ export async function enrichFailure(cwd: string, f: FormattedResult): Promise<vo
237
242
  const b = f.hints.checkedOutElsewhere.branch ?? "<branch>";
238
243
  parts.push(
239
244
  `Branch ${b} is checked out in another worktree. Switch to that worktree, or use ` +
240
- `\`graphite_branch_create({ onto: "${b}", ... })\` to stack a new branch on top.`,
245
+ `switch to a different branch with graphite_navigate before mutating the stack.`,
241
246
  );
242
247
  }
243
248
  if (f.hints.invalidArgument) {
@@ -247,7 +252,7 @@ export async function enrichFailure(cwd: string, f: FormattedResult): Promise<vo
247
252
  }
248
253
  if (f.hints.operatingOnTrunk) {
249
254
  parts.push(
250
- `Operation refused on the trunk branch. Check out a non-trunk branch first (graphite_branch_navigate).`,
255
+ `Operation refused on the trunk branch. Check out a non-trunk branch first with graphite_navigate.`,
251
256
  );
252
257
  }
253
258
 
@@ -0,0 +1,140 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runGt } from "../lib/exec";
3
+ import { assertSafeRef, flagEq, shellJoin } from "../lib/argv";
4
+ import { ensureSuccess, renderText } from "../lib/result";
5
+ import {
6
+ CwdParam,
7
+ StringEnum,
8
+ Type,
9
+ type ToolReturn,
10
+ } from "../lib/schema";
11
+
12
+ /**
13
+ * graphite_change — the only blessed branch-mutation path.
14
+ *
15
+ * action=create gt create -am "<message>" (new branch on top of current)
16
+ * action=amend gt modify -am "<message>" (amend current branch's commit)
17
+ * action=amend_into gt modify --into <branch> -am "<message>"
18
+ * action=absorb gt absorb (dry-run by default)
19
+ *
20
+ * Always stages all changes (matches the golden-path `-am`). No editor, no
21
+ * patch/hunk picker, no AI metadata.
22
+ */
23
+ export function registerChange(pi: ExtensionAPI) {
24
+ pi.registerTool({
25
+ name: "graphite_change",
26
+ label: "Graphite: change",
27
+ description:
28
+ "Create or amend a branch commit in the Graphite stack. action=create stacks a new branch on top of the current one. action=amend updates the current branch's commit. action=amend_into pushes staged hunks into a downstack branch. action=absorb auto-routes staged hunks to the correct commits (dry-run by default).",
29
+ promptSnippet:
30
+ "graphite_change: create | amend | amend_into | absorb — the only branch mutation tool",
31
+ promptGuidelines: [
32
+ "Use graphite_change action=create to start a new PR branch on top of the current branch. Always provide `message`.",
33
+ "Use graphite_change action=amend to update the current PR's commit. Always provide `message`.",
34
+ "Run graphite_status first to confirm you are on the intended branch.",
35
+ "graphite_change always stages all changes (matches `gt create -am` / `gt modify -am`). Stage selectively with `git add -p` outside this tool if you need a partial commit, then call graphite_change.",
36
+ ],
37
+ parameters: Type.Object({
38
+ cwd: CwdParam,
39
+ action: StringEnum([
40
+ "create",
41
+ "amend",
42
+ "amend_into",
43
+ "absorb",
44
+ ] as const),
45
+ message: Type.Optional(
46
+ Type.String({
47
+ description:
48
+ "Commit message. Required for create/amend/amend_into.",
49
+ }),
50
+ ),
51
+ name: Type.Optional(
52
+ Type.String({
53
+ description: "action=create: branch name (default generated from message).",
54
+ }),
55
+ ),
56
+ insert: Type.Optional(
57
+ Type.Boolean({
58
+ description:
59
+ "action=create: insert between current branch and its child, rebasing children (--insert).",
60
+ }),
61
+ ),
62
+ includeUntracked: Type.Optional(
63
+ Type.Boolean({
64
+ description:
65
+ "action=create|amend|amend_into: include untracked files (--update). Default false; staged + tracked-modified are always included via --all.",
66
+ }),
67
+ ),
68
+ into: Type.Optional(
69
+ Type.String({
70
+ description: "action=amend_into: target downstack branch to amend into.",
71
+ }),
72
+ ),
73
+ apply: Type.Optional(
74
+ Type.Boolean({
75
+ description:
76
+ "action=absorb: false (default) => --dry-run; true => --force (apply).",
77
+ }),
78
+ ),
79
+ }),
80
+ async execute(_id, p, signal): Promise<ToolReturn> {
81
+ let args: string[];
82
+ switch (p.action) {
83
+ case "create": {
84
+ if (!p.message) {
85
+ throw new Error("graphite_change action=create requires `message`.");
86
+ }
87
+ args = ["create"];
88
+ if (p.name) args.push(assertSafeRef(p.name, "name"));
89
+ args.push(flagEq("--message", p.message));
90
+ // `-am` semantics: always stage tracked modifications.
91
+ args.push("--all");
92
+ if (p.includeUntracked) args.push("--update");
93
+ if (p.insert) args.push("--insert");
94
+ args.push("--no-ai");
95
+ break;
96
+ }
97
+ case "amend": {
98
+ if (!p.message) {
99
+ throw new Error("graphite_change action=amend requires `message`.");
100
+ }
101
+ args = ["modify", "--all"];
102
+ if (p.includeUntracked) args.push("--update");
103
+ args.push(flagEq("--message", p.message));
104
+ break;
105
+ }
106
+ case "amend_into": {
107
+ if (!p.into) {
108
+ throw new Error("graphite_change action=amend_into requires `into`.");
109
+ }
110
+ if (!p.message) {
111
+ throw new Error(
112
+ "graphite_change action=amend_into requires `message`.",
113
+ );
114
+ }
115
+ args = ["modify", "--all"];
116
+ if (p.includeUntracked) args.push("--update");
117
+ args.push(flagEq("--into", assertSafeRef(p.into, "into")));
118
+ args.push(flagEq("--message", p.message));
119
+ break;
120
+ }
121
+ case "absorb": {
122
+ const apply = p.apply === true;
123
+ args = ["absorb"];
124
+ if (!apply) args.push("--dry-run");
125
+ else args.push("--force");
126
+ // Match `-am` style: include tracked modifications for absorb too.
127
+ args.push("--all");
128
+ break;
129
+ }
130
+ }
131
+ const label = `gt ${shellJoin(args)}`;
132
+ const r = await runGt(args, { cwd: p.cwd, signal });
133
+ const f = await ensureSuccess(label, r, p.cwd);
134
+ return {
135
+ content: [{ type: "text", text: renderText(label, f) }],
136
+ details: { action: p.action, result: f },
137
+ };
138
+ },
139
+ });
140
+ }
@@ -0,0 +1,99 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runGt } from "../lib/exec";
3
+ import { assertSafeRef, flagEq, shellJoin } from "../lib/argv";
4
+ import { ensureSuccess, renderText } from "../lib/result";
5
+ import {
6
+ CwdParam,
7
+ StringEnum,
8
+ Type,
9
+ type ToolReturn,
10
+ } from "../lib/schema";
11
+
12
+ /**
13
+ * graphite_navigate — move around the current stack.
14
+ *
15
+ * Stacked PRs encode "which branch you are on = which PR you will modify",
16
+ * so navigation is a core part of the workflow. Use this before
17
+ * graphite_change to make sure you are on the right branch:
18
+ * - to update an existing PR, checkout that PR's branch
19
+ * - to add a child PR, navigate to its intended parent first
20
+ * - to add a base PR, navigate to trunk
21
+ */
22
+ export function registerNavigate(pi: ExtensionAPI) {
23
+ pi.registerTool({
24
+ name: "graphite_navigate",
25
+ label: "Graphite: navigate",
26
+ description:
27
+ "Move around the current Graphite stack: checkout a specific branch, jump to trunk, or step up/down/top/bottom. Use this before graphite_change so you are mutating the right PR.",
28
+ promptSnippet:
29
+ "graphite_navigate: checkout / trunk / up / down / top / bottom in the current stack",
30
+ promptGuidelines: [
31
+ "Before any graphite_change, confirm you are on the intended branch via graphite_status or graphite_navigate.",
32
+ "To create a child PR, navigate to its intended parent first. To create a base PR, navigate to trunk.",
33
+ ],
34
+ parameters: Type.Object({
35
+ cwd: CwdParam,
36
+ action: StringEnum([
37
+ "checkout",
38
+ "trunk",
39
+ "up",
40
+ "down",
41
+ "top",
42
+ "bottom",
43
+ ] as const),
44
+ branch: Type.Optional(
45
+ Type.String({ description: "action=checkout: branch to checkout." }),
46
+ ),
47
+ steps: Type.Optional(
48
+ Type.Integer({
49
+ minimum: 1,
50
+ description: "action=up|down: step count.",
51
+ }),
52
+ ),
53
+ to: Type.Optional(
54
+ Type.String({
55
+ description:
56
+ "action=up: target descendant when the current branch has multiple children (--to).",
57
+ }),
58
+ ),
59
+ }),
60
+ async execute(_id, p, signal): Promise<ToolReturn> {
61
+ let args: string[];
62
+ switch (p.action) {
63
+ case "checkout":
64
+ if (!p.branch) {
65
+ throw new Error(
66
+ "action=checkout requires `branch` (interactive selector disabled). Use action=trunk to jump to trunk.",
67
+ );
68
+ }
69
+ args = ["checkout", assertSafeRef(p.branch, "branch")];
70
+ break;
71
+ case "trunk":
72
+ args = ["checkout", "--trunk"];
73
+ break;
74
+ case "up":
75
+ args = ["up"];
76
+ if (p.steps != null) args.push(flagEq("--steps", p.steps));
77
+ if (p.to) args.push(flagEq("--to", assertSafeRef(p.to, "to")));
78
+ break;
79
+ case "down":
80
+ args = ["down"];
81
+ if (p.steps != null) args.push(flagEq("--steps", p.steps));
82
+ break;
83
+ case "top":
84
+ args = ["top"];
85
+ break;
86
+ case "bottom":
87
+ args = ["bottom"];
88
+ break;
89
+ }
90
+ const label = `gt ${shellJoin(args)}`;
91
+ const r = await runGt(args, { cwd: p.cwd, signal });
92
+ const f = await ensureSuccess(label, r, p.cwd);
93
+ return {
94
+ content: [{ type: "text", text: renderText(label, f) }],
95
+ details: { action: p.action, result: f },
96
+ };
97
+ },
98
+ });
99
+ }
@@ -1,7 +1,13 @@
1
1
  import { spawn, type ChildProcessByStdio } from "node:child_process";
2
2
  import type { Readable } from "node:stream";
3
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import { DEFAULT_COMMAND_TIMEOUT_MS, killProcessGroup, runGt, safeNoninteractiveEnv } from "../lib/exec";
4
+ import {
5
+ DEFAULT_COMMAND_TIMEOUT_MS,
6
+ killProcessGroup,
7
+ runGt,
8
+ safeNoninteractiveEnv,
9
+ } from "../lib/exec";
10
+ import { shellJoin } from "../lib/argv";
5
11
  import { ensureSuccess, renderText } from "../lib/result";
6
12
  import { CwdParam, StringEnum, Type, type ToolReturn } from "../lib/schema";
7
13
 
@@ -45,53 +51,62 @@ function runGit(
45
51
  child.on("close", (code) => {
46
52
  clearTimeout(timeout);
47
53
  signal?.removeEventListener("abort", onAbort);
48
- resolve({ exitCode: killed ? -1 : (code ?? -1), stdout: out, stderr: err });
54
+ resolve({
55
+ exitCode: killed ? -1 : (code ?? -1),
56
+ stdout: out,
57
+ stderr: err,
58
+ });
49
59
  });
50
60
  });
51
61
  }
52
62
 
53
- /** Return list of tracked files that still contain conflict markers. */
54
63
  async function findUnresolvedConflictMarkers(
55
64
  cwd: string,
56
65
  signal?: AbortSignal,
57
66
  ): Promise<string[]> {
58
- // grep for the canonical 7-char start marker at line start across tracked files.
59
- // `git grep` is cheap and respects gitignore / tracked-only by default.
60
- const r = await runGit(
61
- ["grep", "-l", "-E", "^<{7} "],
62
- cwd,
63
- signal,
64
- );
65
- if (r.exitCode > 1) return []; // git grep returns 1 for no matches, >1 is real error
67
+ const r = await runGit(["grep", "-l", "-E", "^<{7} "], cwd, signal);
68
+ if (r.exitCode > 1) return [];
66
69
  return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
67
70
  }
68
71
 
69
- export function registerRecovery(pi: ExtensionAPI) {
72
+ /**
73
+ * graphite_recover — `gt continue` / `gt abort` / `gt undo`.
74
+ *
75
+ * After a conflict during sync/restack/create/modify, resolve the files
76
+ * (and `git add` them), then call action=continue. Never use
77
+ * `git rebase --continue` — Graphite needs to propagate the resolution to
78
+ * dependent branches.
79
+ */
80
+ export function registerRecover(pi: ExtensionAPI) {
70
81
  pi.registerTool({
71
- name: "graphite_recovery",
72
- label: "Graphite: recovery",
82
+ name: "graphite_recover",
83
+ label: "Graphite: recover",
73
84
  description:
74
- "Recover from conflicts or mistakes: continue a halted command, abort it, or undo the most recent Graphite mutation in this worktree.",
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`.",
75
86
  promptSnippet:
76
- "graphite_recovery: continue / abort / undo Graphite commands",
87
+ "graphite_recover: continue / abort / undo never use `git rebase --continue`",
77
88
  promptGuidelines: [
78
- "After resolving a rebase conflict, run graphite_recovery action=continue to resume the original gt command.",
79
- "graphite_recovery action=undo only undoes commands run from the current worktree.",
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
+ "graphite_recover action=undo only undoes commands run from the current worktree.",
80
91
  ],
81
92
  parameters: Type.Object({
82
93
  cwd: CwdParam,
83
94
  action: StringEnum(["continue", "abort", "undo"] as const),
84
95
  stageAll: Type.Optional(
85
- Type.Boolean({ description: "action=continue: stage all changes first (--all)." }),
96
+ Type.Boolean({
97
+ description: "action=continue: stage all changes first (--all).",
98
+ }),
86
99
  ),
87
100
  allowConflictMarkers: Type.Optional(
88
101
  Type.Boolean({
89
102
  description:
90
- "action=continue: bypass the pre-flight check that refuses to continue when tracked files still contain `<<<<<<<` conflict markers. Default false. Only set true if you know the markers are intentional (e.g. tests).",
103
+ "action=continue: bypass the pre-flight check that refuses to continue when tracked files still contain `<<<<<<<` markers. Default false.",
91
104
  }),
92
105
  ),
93
106
  force: Type.Optional(
94
- Type.Boolean({ description: "action=abort|undo: skip confirmation prompt (--force)." }),
107
+ Type.Boolean({
108
+ description: "action=abort|undo: skip confirmation (--force).",
109
+ }),
95
110
  ),
96
111
  }),
97
112
  async execute(_id, p, signal): Promise<ToolReturn> {
@@ -102,10 +117,9 @@ export function registerRecovery(pi: ExtensionAPI) {
102
117
  const dirty = await findUnresolvedConflictMarkers(p.cwd, signal);
103
118
  if (dirty.length) {
104
119
  throw new Error(
105
- `graphite_recovery: refusing to continue — ${dirty.length} tracked file(s) still contain conflict markers (\`<<<<<<<\`):\n` +
120
+ `graphite_recover: refusing to continue — ${dirty.length} tracked file(s) still contain conflict markers (\`<<<<<<<\`):\n` +
106
121
  dirty.map((f) => ` - ${f}`).join("\n") +
107
- `\n\nResolve each file (remove <<<<<<< / ======= / >>>>>>> blocks), then re-run. ` +
108
- `If markers are intentional, pass allowConflictMarkers:true.`,
122
+ `\n\nResolve each file, then re-run. If markers are intentional, pass allowConflictMarkers:true.`,
109
123
  );
110
124
  }
111
125
  }
@@ -122,7 +136,7 @@ export function registerRecovery(pi: ExtensionAPI) {
122
136
  if (p.force) args.push("--force");
123
137
  break;
124
138
  }
125
- const label = `gt ${args.join(" ")}`;
139
+ const label = `gt ${shellJoin(args)}`;
126
140
  const r = await runGt(args, { cwd: p.cwd, signal });
127
141
  const f = await ensureSuccess(label, r, p.cwd);
128
142
  return {
@@ -0,0 +1,121 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runGt } from "../lib/exec";
3
+ import { assertSafeRef, flagEq, shellJoin } from "../lib/argv";
4
+ import { ensureSuccess, renderText } from "../lib/result";
5
+ import {
6
+ CwdParam,
7
+ StringEnum,
8
+ Type,
9
+ requireConfirm,
10
+ type ToolReturn,
11
+ } from "../lib/schema";
12
+
13
+ /**
14
+ * graphite_setup — initialize a repo or adopt an existing git branch into
15
+ * Graphite tracking.
16
+ *
17
+ * This is a precondition tool, not a daily workflow tool. Use it only when
18
+ * graphite_status / graphite_change reports that Graphite is not initialized
19
+ * or the current branch is not tracked.
20
+ */
21
+ export function registerSetup(pi: ExtensionAPI) {
22
+ pi.registerTool({
23
+ name: "graphite_setup",
24
+ label: "Graphite: setup",
25
+ description:
26
+ "Initialize Graphite in a repo or track an existing Git branch with an explicit Graphite parent. Use only when a repo/branch is not Graphite-ready. Tracking requires an explicit branch, explicit parent, and confirmParent:true.",
27
+ promptSnippet:
28
+ "graphite_setup: init_repo | track_branch for Graphite preconditions",
29
+ promptGuidelines: [
30
+ "Use graphite_setup only when graphite_status or another tool reports notInitialized or branchNotTracked.",
31
+ "For track_branch, never infer the parent silently. Ask the user if the intended parent is unclear, then pass confirmParent:true.",
32
+ "Do not use graphite_setup for untrack/freeze/unfreeze; those are outside the core workflow.",
33
+ ],
34
+ parameters: Type.Object({
35
+ cwd: CwdParam,
36
+ action: StringEnum(["init_repo", "track_branch"] as const),
37
+ trunk: Type.Optional(
38
+ Type.String({
39
+ description: "action=init_repo: trunk branch name (for example main or master).",
40
+ }),
41
+ ),
42
+ reset: Type.Optional(
43
+ Type.Boolean({
44
+ description:
45
+ "action=init_repo: pass --reset and untrack existing Graphite branches. Requires confirmDestructive:true.",
46
+ }),
47
+ ),
48
+ branch: Type.Optional(
49
+ Type.String({ description: "action=track_branch: existing Git branch to track." }),
50
+ ),
51
+ parent: Type.Optional(
52
+ Type.String({
53
+ description:
54
+ "action=track_branch: explicit Graphite parent branch. Required; do not guess if unclear.",
55
+ }),
56
+ ),
57
+ confirmParent: Type.Optional(
58
+ Type.Boolean({
59
+ description:
60
+ "action=track_branch: required true to confirm the supplied parent is intentional.",
61
+ }),
62
+ ),
63
+ force: Type.Optional(
64
+ Type.Boolean({
65
+ description:
66
+ "action=track_branch: pass --force to overwrite existing tracking metadata. Requires confirmDestructive:true.",
67
+ }),
68
+ ),
69
+ confirmDestructive: Type.Optional(Type.Boolean()),
70
+ }),
71
+ async execute(_id, p, signal): Promise<ToolReturn> {
72
+ let args: string[];
73
+
74
+ if (p.action === "init_repo") {
75
+ if (!p.trunk) {
76
+ throw new Error("graphite_setup action=init_repo requires `trunk`.");
77
+ }
78
+ if (p.reset) {
79
+ requireConfirm(
80
+ p.confirmDestructive,
81
+ "gt init --reset (untracks existing Graphite branches)",
82
+ );
83
+ }
84
+ args = ["init", flagEq("--trunk", assertSafeRef(p.trunk, "trunk"))];
85
+ if (p.reset) args.push("--reset");
86
+ } else {
87
+ if (!p.branch) {
88
+ throw new Error("graphite_setup action=track_branch requires `branch`.");
89
+ }
90
+ if (!p.parent) {
91
+ throw new Error("graphite_setup action=track_branch requires `parent`.");
92
+ }
93
+ if (p.confirmParent !== true) {
94
+ throw new Error(
95
+ "graphite_setup action=track_branch requires confirmParent:true. Confirm the intended Graphite parent with the user if unclear.",
96
+ );
97
+ }
98
+ if (p.force) {
99
+ requireConfirm(
100
+ p.confirmDestructive,
101
+ "gt track --force (overwrites existing tracking metadata)",
102
+ );
103
+ }
104
+ args = [
105
+ "track",
106
+ assertSafeRef(p.branch, "branch"),
107
+ flagEq("--parent", assertSafeRef(p.parent, "parent")),
108
+ ];
109
+ if (p.force) args.push("--force");
110
+ }
111
+
112
+ const label = `gt ${shellJoin(args)}`;
113
+ const r = await runGt(args, { cwd: p.cwd, signal });
114
+ const f = await ensureSuccess(label, r, p.cwd);
115
+ return {
116
+ content: [{ type: "text", text: renderText(label, f) }],
117
+ details: { action: p.action, result: f },
118
+ };
119
+ },
120
+ });
121
+ }
@@ -0,0 +1,55 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runGt } from "../lib/exec";
3
+ import { ensureAllSuccess, renderText } from "../lib/result";
4
+ import { CwdParam, Type, type ToolReturn } from "../lib/schema";
5
+
6
+ /**
7
+ * graphite_status — single read-only entry point.
8
+ *
9
+ * Always returns:
10
+ * gt log --stack -> current stack tree
11
+ * gt info -> current branch summary (parent, PR url, restack hint)
12
+ *
13
+ * Run this before touching the stack.
14
+ */
15
+ export function registerStatus(pi: ExtensionAPI) {
16
+ pi.registerTool({
17
+ name: "graphite_status",
18
+ label: "Graphite: status",
19
+ description:
20
+ "Read-only Graphite snapshot. Runs `gt log --stack` and `gt info` so you can see the current stack, the current branch's parent + PR, and any restack/conflict hints. Use this before any other graphite_* tool.",
21
+ promptSnippet:
22
+ "graphite_status: inspect current stack + current branch before mutating",
23
+ promptGuidelines: [
24
+ "Run graphite_status at the start of any Graphite workflow, and again whenever you are unsure where you are in the stack.",
25
+ ],
26
+ parameters: Type.Object({
27
+ cwd: CwdParam,
28
+ }),
29
+ async execute(_id, p, signal): Promise<ToolReturn> {
30
+ const [log, info] = await Promise.all([
31
+ runGt(["log", "--stack"], { cwd: p.cwd, signal }),
32
+ runGt(["info"], { cwd: p.cwd, signal }),
33
+ ]);
34
+ const [fl, fi] = await ensureAllSuccess(
35
+ [
36
+ { label: "gt log --stack", result: log },
37
+ { label: "gt info", result: info },
38
+ ],
39
+ p.cwd,
40
+ );
41
+ return {
42
+ content: [
43
+ {
44
+ type: "text",
45
+ text: [
46
+ renderText("gt log --stack", fl),
47
+ renderText("gt info", fi),
48
+ ].join("\n\n"),
49
+ },
50
+ ],
51
+ details: { log: fl, info: fi },
52
+ };
53
+ },
54
+ });
55
+ }