pi-graphite 0.2.3 → 0.3.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.
@@ -0,0 +1,140 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runGt } from "../lib/exec";
3
+ import { assertSafeRef, flagEq } 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 ${args.join(" ")}`;
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 } 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 ${args.join(" ")}`;
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,12 @@
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";
5
10
  import { ensureSuccess, renderText } from "../lib/result";
6
11
  import { CwdParam, StringEnum, Type, type ToolReturn } from "../lib/schema";
7
12
 
@@ -45,53 +50,62 @@ function runGit(
45
50
  child.on("close", (code) => {
46
51
  clearTimeout(timeout);
47
52
  signal?.removeEventListener("abort", onAbort);
48
- resolve({ exitCode: killed ? -1 : (code ?? -1), stdout: out, stderr: err });
53
+ resolve({
54
+ exitCode: killed ? -1 : (code ?? -1),
55
+ stdout: out,
56
+ stderr: err,
57
+ });
49
58
  });
50
59
  });
51
60
  }
52
61
 
53
- /** Return list of tracked files that still contain conflict markers. */
54
62
  async function findUnresolvedConflictMarkers(
55
63
  cwd: string,
56
64
  signal?: AbortSignal,
57
65
  ): 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
66
+ const r = await runGit(["grep", "-l", "-E", "^<{7} "], cwd, signal);
67
+ if (r.exitCode > 1) return [];
66
68
  return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
67
69
  }
68
70
 
69
- export function registerRecovery(pi: ExtensionAPI) {
71
+ /**
72
+ * graphite_recover — `gt continue` / `gt abort` / `gt undo`.
73
+ *
74
+ * After a conflict during sync/restack/create/modify, resolve the files
75
+ * (and `git add` them), then call action=continue. Never use
76
+ * `git rebase --continue` — Graphite needs to propagate the resolution to
77
+ * dependent branches.
78
+ */
79
+ export function registerRecover(pi: ExtensionAPI) {
70
80
  pi.registerTool({
71
- name: "graphite_recovery",
72
- label: "Graphite: recovery",
81
+ name: "graphite_recover",
82
+ label: "Graphite: recover",
73
83
  description:
74
- "Recover from conflicts or mistakes: continue a halted command, abort it, or undo the most recent Graphite mutation in this worktree.",
84
+ "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
85
  promptSnippet:
76
- "graphite_recovery: continue / abort / undo Graphite commands",
86
+ "graphite_recover: continue / abort / undo never use `git rebase --continue`",
77
87
  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.",
88
+ "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.",
89
+ "graphite_recover action=undo only undoes commands run from the current worktree.",
80
90
  ],
81
91
  parameters: Type.Object({
82
92
  cwd: CwdParam,
83
93
  action: StringEnum(["continue", "abort", "undo"] as const),
84
94
  stageAll: Type.Optional(
85
- Type.Boolean({ description: "action=continue: stage all changes first (--all)." }),
95
+ Type.Boolean({
96
+ description: "action=continue: stage all changes first (--all).",
97
+ }),
86
98
  ),
87
99
  allowConflictMarkers: Type.Optional(
88
100
  Type.Boolean({
89
101
  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).",
102
+ "action=continue: bypass the pre-flight check that refuses to continue when tracked files still contain `<<<<<<<` markers. Default false.",
91
103
  }),
92
104
  ),
93
105
  force: Type.Optional(
94
- Type.Boolean({ description: "action=abort|undo: skip confirmation prompt (--force)." }),
106
+ Type.Boolean({
107
+ description: "action=abort|undo: skip confirmation (--force).",
108
+ }),
95
109
  ),
96
110
  }),
97
111
  async execute(_id, p, signal): Promise<ToolReturn> {
@@ -102,10 +116,9 @@ export function registerRecovery(pi: ExtensionAPI) {
102
116
  const dirty = await findUnresolvedConflictMarkers(p.cwd, signal);
103
117
  if (dirty.length) {
104
118
  throw new Error(
105
- `graphite_recovery: refusing to continue — ${dirty.length} tracked file(s) still contain conflict markers (\`<<<<<<<\`):\n` +
119
+ `graphite_recover: refusing to continue — ${dirty.length} tracked file(s) still contain conflict markers (\`<<<<<<<\`):\n` +
106
120
  dirty.map((f) => ` - ${f}`).join("\n") +
107
- `\n\nResolve each file (remove <<<<<<< / ======= / >>>>>>> blocks), then re-run. ` +
108
- `If markers are intentional, pass allowConflictMarkers:true.`,
121
+ `\n\nResolve each file, then re-run. If markers are intentional, pass allowConflictMarkers:true.`,
109
122
  );
110
123
  }
111
124
  }
@@ -0,0 +1,121 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runGt } from "../lib/exec";
3
+ import { assertSafeRef, flagEq } 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 ${args.join(" ")}`;
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
+ }
@@ -0,0 +1,127 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { runGt } from "../lib/exec";
3
+ import { assertSafeRef, flagEq } 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_submit_stack — the only blessed submit path.
14
+ *
15
+ * Wraps `gt submit --stack --no-edit --no-ai`. Defaults to --dry-run so the
16
+ * caller can review the plan. Actually pushing requires `apply:true` AND
17
+ * `confirmRemote:true`.
18
+ *
19
+ * No PR title/body fields, no editor, no browser, no gh, no
20
+ * current-branch-only submit. The skill calls out that the correct workflow
21
+ * is always to submit the entire stack.
22
+ */
23
+ export function registerSubmitStack(pi: ExtensionAPI) {
24
+ pi.registerTool({
25
+ name: "graphite_submit_stack",
26
+ label: "Graphite: submit stack",
27
+ description:
28
+ "Push the entire current stack and create/update PRs via `gt submit --stack --no-edit`. Defaults to a dry-run plan; pass apply:true with confirmRemote:true to actually push. PR title/body editing is intentionally not exposed.",
29
+ promptSnippet:
30
+ "graphite_submit_stack: plan or apply `gt submit --stack` for the full stack",
31
+ promptGuidelines: [
32
+ "Always call graphite_submit_stack with apply:false (default) first to review the dry-run plan, then call again with apply:true and confirmRemote:true to actually push.",
33
+ "This extension does not edit PR titles/bodies. If you need to set them, do it outside this extension after the push.",
34
+ ],
35
+ parameters: Type.Object({
36
+ cwd: CwdParam,
37
+ apply: Type.Optional(
38
+ Type.Boolean({
39
+ description:
40
+ "false => --dry-run (default). true => actually push (requires confirmRemote).",
41
+ }),
42
+ ),
43
+ confirmRemote: Type.Optional(Type.Boolean()),
44
+ draft: Type.Optional(
45
+ Type.Boolean({ description: "Create new PRs as drafts (--draft)." }),
46
+ ),
47
+ publish: Type.Optional(
48
+ Type.Boolean({ description: "Take PRs out of draft (--publish)." }),
49
+ ),
50
+ updateOnly: Type.Optional(
51
+ Type.Boolean({
52
+ description: "Only update existing PRs, do not create new ones (--update-only).",
53
+ }),
54
+ ),
55
+ mergeWhenReady: Type.Optional(
56
+ Type.Boolean({ description: "Enable auto-merge (--merge-when-ready)." }),
57
+ ),
58
+ rerequestReview: Type.Optional(
59
+ Type.Boolean({
60
+ description: "Re-request review from existing reviewers (--rerequest-review).",
61
+ }),
62
+ ),
63
+ reviewers: Type.Optional(
64
+ Type.Array(Type.String(), {
65
+ description: "User reviewers (--reviewers).",
66
+ }),
67
+ ),
68
+ teamReviewers: Type.Optional(
69
+ Type.Array(Type.String(), {
70
+ description: "Team reviewers (--team-reviewers).",
71
+ }),
72
+ ),
73
+ forcePush: Type.Optional(
74
+ Type.Boolean({
75
+ description:
76
+ "--force (instead of default --force-with-lease). Requires confirmRemote.",
77
+ }),
78
+ ),
79
+ ignoreOutOfSyncTrunk: Type.Optional(
80
+ Type.Boolean({
81
+ description: "Submit even if trunk is out of sync (--ignore-out-of-sync-trunk).",
82
+ }),
83
+ ),
84
+ }),
85
+ async execute(_id, p, signal): Promise<ToolReturn> {
86
+ const apply = p.apply === true;
87
+ if (apply) {
88
+ requireConfirm(
89
+ p.confirmRemote,
90
+ "gt submit --stack (push branches + create/update PRs)",
91
+ );
92
+ }
93
+ if (p.forcePush) {
94
+ requireConfirm(p.confirmRemote, "gt submit --force");
95
+ }
96
+
97
+ const args = ["submit", "--stack"];
98
+ if (!apply) args.push("--dry-run");
99
+ args.push("--no-edit", "--no-ai");
100
+
101
+ if (p.updateOnly) args.push("--update-only");
102
+ if (p.draft) args.push("--draft");
103
+ if (p.publish) args.push("--publish");
104
+ if (p.mergeWhenReady) args.push("--merge-when-ready");
105
+ if (p.rerequestReview) args.push("--rerequest-review");
106
+
107
+ if (p.reviewers && p.reviewers.length) {
108
+ for (const rv of p.reviewers) assertSafeRef(rv, "reviewers[]");
109
+ args.push(flagEq("--reviewers", p.reviewers.join(",")));
110
+ }
111
+ if (p.teamReviewers && p.teamReviewers.length) {
112
+ for (const rv of p.teamReviewers) assertSafeRef(rv, "teamReviewers[]");
113
+ args.push(flagEq("--team-reviewers", p.teamReviewers.join(",")));
114
+ }
115
+ if (p.forcePush) args.push("--force");
116
+ if (p.ignoreOutOfSyncTrunk) args.push("--ignore-out-of-sync-trunk");
117
+
118
+ const label = `gt ${args.join(" ")}`;
119
+ const r = await runGt(args, { cwd: p.cwd, signal });
120
+ const f = await ensureSuccess(label, r, p.cwd);
121
+ return {
122
+ content: [{ type: "text", text: renderText(label, f) }],
123
+ details: { apply, result: f },
124
+ };
125
+ },
126
+ });
127
+ }