pi-graphite 0.2.2 → 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.
- package/README.md +69 -34
- package/package.json +6 -2
- package/skills/graphite/SKILL.md +218 -0
- package/src/index.ts +40 -53
- package/src/lib/argv.ts +59 -0
- package/src/lib/exec.ts +98 -17
- package/src/lib/result.ts +29 -13
- package/src/lib/schema.ts +5 -4
- package/src/tools/change.ts +140 -0
- package/src/tools/navigate.ts +99 -0
- package/src/tools/recover.ts +147 -0
- package/src/tools/setup.ts +121 -0
- package/src/tools/status.ts +55 -0
- package/src/tools/submit.ts +127 -0
- package/src/tools/sync.ts +79 -0
- package/src/tools/branch.ts +0 -428
- package/src/tools/pr.ts +0 -225
- package/src/tools/recovery.ts +0 -53
- package/src/tools/remote.ts +0 -108
- package/src/tools/repo.ts +0 -122
- package/src/tools/stack.ts +0 -185
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawn, type ChildProcessByStdio } from "node:child_process";
|
|
2
|
+
import type { Readable } from "node:stream";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
6
|
+
killProcessGroup,
|
|
7
|
+
runGt,
|
|
8
|
+
safeNoninteractiveEnv,
|
|
9
|
+
} from "../lib/exec";
|
|
10
|
+
import { ensureSuccess, renderText } from "../lib/result";
|
|
11
|
+
import { CwdParam, StringEnum, Type, type ToolReturn } from "../lib/schema";
|
|
12
|
+
|
|
13
|
+
function runGit(
|
|
14
|
+
args: string[],
|
|
15
|
+
cwd: string,
|
|
16
|
+
signal?: AbortSignal,
|
|
17
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
let out = "";
|
|
20
|
+
let err = "";
|
|
21
|
+
let child: ChildProcessByStdio<null, Readable, Readable>;
|
|
22
|
+
try {
|
|
23
|
+
child = spawn("git", args, {
|
|
24
|
+
cwd,
|
|
25
|
+
env: safeNoninteractiveEnv(),
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
detached: process.platform !== "win32",
|
|
28
|
+
});
|
|
29
|
+
} catch (e) {
|
|
30
|
+
resolve({ exitCode: -1, stdout: "", stderr: (e as Error).message });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
child.stdout.on("data", (d) => (out += d.toString()));
|
|
34
|
+
child.stderr.on("data", (d) => (err += d.toString()));
|
|
35
|
+
let killed = false;
|
|
36
|
+
const killChild = () => {
|
|
37
|
+
killed = true;
|
|
38
|
+
killProcessGroup(child, "SIGTERM");
|
|
39
|
+
setTimeout(() => killProcessGroup(child, "SIGKILL"), 1500).unref?.();
|
|
40
|
+
};
|
|
41
|
+
const timeout = setTimeout(killChild, DEFAULT_COMMAND_TIMEOUT_MS);
|
|
42
|
+
timeout.unref?.();
|
|
43
|
+
const onAbort = killChild;
|
|
44
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
45
|
+
child.on("error", (e) => {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
signal?.removeEventListener("abort", onAbort);
|
|
48
|
+
resolve({ exitCode: -1, stdout: out, stderr: err + e.message });
|
|
49
|
+
});
|
|
50
|
+
child.on("close", (code) => {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
signal?.removeEventListener("abort", onAbort);
|
|
53
|
+
resolve({
|
|
54
|
+
exitCode: killed ? -1 : (code ?? -1),
|
|
55
|
+
stdout: out,
|
|
56
|
+
stderr: err,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function findUnresolvedConflictMarkers(
|
|
63
|
+
cwd: string,
|
|
64
|
+
signal?: AbortSignal,
|
|
65
|
+
): Promise<string[]> {
|
|
66
|
+
const r = await runGit(["grep", "-l", "-E", "^<{7} "], cwd, signal);
|
|
67
|
+
if (r.exitCode > 1) return [];
|
|
68
|
+
return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
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) {
|
|
80
|
+
pi.registerTool({
|
|
81
|
+
name: "graphite_recover",
|
|
82
|
+
label: "Graphite: recover",
|
|
83
|
+
description:
|
|
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`.",
|
|
85
|
+
promptSnippet:
|
|
86
|
+
"graphite_recover: continue / abort / undo — never use `git rebase --continue`",
|
|
87
|
+
promptGuidelines: [
|
|
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.",
|
|
90
|
+
],
|
|
91
|
+
parameters: Type.Object({
|
|
92
|
+
cwd: CwdParam,
|
|
93
|
+
action: StringEnum(["continue", "abort", "undo"] as const),
|
|
94
|
+
stageAll: Type.Optional(
|
|
95
|
+
Type.Boolean({
|
|
96
|
+
description: "action=continue: stage all changes first (--all).",
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
allowConflictMarkers: Type.Optional(
|
|
100
|
+
Type.Boolean({
|
|
101
|
+
description:
|
|
102
|
+
"action=continue: bypass the pre-flight check that refuses to continue when tracked files still contain `<<<<<<<` markers. Default false.",
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
force: Type.Optional(
|
|
106
|
+
Type.Boolean({
|
|
107
|
+
description: "action=abort|undo: skip confirmation (--force).",
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
}),
|
|
111
|
+
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
112
|
+
let args: string[];
|
|
113
|
+
switch (p.action) {
|
|
114
|
+
case "continue": {
|
|
115
|
+
if (!p.allowConflictMarkers) {
|
|
116
|
+
const dirty = await findUnresolvedConflictMarkers(p.cwd, signal);
|
|
117
|
+
if (dirty.length) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`graphite_recover: refusing to continue — ${dirty.length} tracked file(s) still contain conflict markers (\`<<<<<<<\`):\n` +
|
|
120
|
+
dirty.map((f) => ` - ${f}`).join("\n") +
|
|
121
|
+
`\n\nResolve each file, then re-run. If markers are intentional, pass allowConflictMarkers:true.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
args = ["continue"];
|
|
126
|
+
if (p.stageAll) args.push("--all");
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "abort":
|
|
130
|
+
args = ["abort"];
|
|
131
|
+
if (p.force) args.push("--force");
|
|
132
|
+
break;
|
|
133
|
+
case "undo":
|
|
134
|
+
args = ["undo"];
|
|
135
|
+
if (p.force) args.push("--force");
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const label = `gt ${args.join(" ")}`;
|
|
139
|
+
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
140
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
143
|
+
details: { action: p.action, result: f },
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { runGt } from "../lib/exec";
|
|
3
|
+
import { ensureSuccess, renderText } from "../lib/result";
|
|
4
|
+
import {
|
|
5
|
+
CwdParam,
|
|
6
|
+
Type,
|
|
7
|
+
requireConfirm,
|
|
8
|
+
type ToolReturn,
|
|
9
|
+
} from "../lib/schema";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* graphite_sync — the "start of day / after merge" workflow.
|
|
13
|
+
*
|
|
14
|
+
* Wraps `gt sync` only. Pulls trunk, deletes merged branches, restacks
|
|
15
|
+
* remaining branches. This is the canonical way to recover from merged PRs.
|
|
16
|
+
*
|
|
17
|
+
* Prefer this over manual restack when trunk may have moved or when PRs in
|
|
18
|
+
* the stack may have merged.
|
|
19
|
+
*/
|
|
20
|
+
export function registerSync(pi: ExtensionAPI) {
|
|
21
|
+
pi.registerTool({
|
|
22
|
+
name: "graphite_sync",
|
|
23
|
+
label: "Graphite: sync",
|
|
24
|
+
description:
|
|
25
|
+
"Pull trunk, delete merged branches, and restack remaining branches via `gt sync`. Run at session start and after any PR in the stack merges. Destructive flags (`force`, `deleteAll`) require `confirmDestructive`.",
|
|
26
|
+
promptSnippet:
|
|
27
|
+
"graphite_sync: `gt sync` — start-of-day + after-merge cleanup and restack",
|
|
28
|
+
promptGuidelines: [
|
|
29
|
+
"Run graphite_sync at the start of a session and any time PRs in the stack may have merged.",
|
|
30
|
+
"graphite_sync with force=true or deleteAll=true is destructive; pass confirmDestructive:true.",
|
|
31
|
+
],
|
|
32
|
+
parameters: Type.Object({
|
|
33
|
+
cwd: CwdParam,
|
|
34
|
+
restack: Type.Optional(
|
|
35
|
+
Type.Boolean({
|
|
36
|
+
description:
|
|
37
|
+
"Restack after fetching (default true; pass false for --no-restack).",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
allTrunks: Type.Optional(
|
|
41
|
+
Type.Boolean({ description: "Sync all configured trunks (--all)." }),
|
|
42
|
+
),
|
|
43
|
+
deleteAll: Type.Optional(
|
|
44
|
+
Type.Boolean({
|
|
45
|
+
description:
|
|
46
|
+
"Delete all merged/closed branches without prompting (--delete-all). Requires confirmDestructive.",
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
force: Type.Optional(
|
|
50
|
+
Type.Boolean({
|
|
51
|
+
description:
|
|
52
|
+
"Overwrite local branches with remote (--force). Requires confirmDestructive.",
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
confirmDestructive: Type.Optional(Type.Boolean()),
|
|
56
|
+
}),
|
|
57
|
+
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
58
|
+
if (p.force || p.deleteAll) {
|
|
59
|
+
requireConfirm(
|
|
60
|
+
p.confirmDestructive,
|
|
61
|
+
"gt sync with --force/--delete-all (may overwrite branches)",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const args = ["sync"];
|
|
65
|
+
if (p.allTrunks) args.push("--all");
|
|
66
|
+
if (p.deleteAll) args.push("--delete-all");
|
|
67
|
+
if (p.force) args.push("--force");
|
|
68
|
+
if (p.restack === false) args.push("--no-restack");
|
|
69
|
+
|
|
70
|
+
const label = `gt ${args.join(" ")}`;
|
|
71
|
+
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
72
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
75
|
+
details: { result: f },
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|