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/README.md +80 -37
- package/package.json +6 -2
- package/skills/graphite/SKILL.md +218 -0
- package/src/index.ts +40 -57
- package/src/lib/argv.ts +81 -0
- package/src/lib/exec.ts +35 -3
- package/src/lib/result.ts +17 -12
- package/src/tools/change.ts +140 -0
- package/src/tools/navigate.ts +99 -0
- package/src/tools/{recovery.ts → recover.ts} +39 -25
- package/src/tools/setup.ts +121 -0
- package/src/tools/status.ts +55 -0
- package/src/tools/submit.ts +139 -0
- package/src/tools/sync.ts +80 -0
- package/src/tools/branch.ts +0 -432
- package/src/tools/pr.ts +0 -298
- package/src/tools/remote.ts +0 -108
- package/src/tools/repo.ts +0 -114
- package/src/tools/stack.ts +0 -572
package/src/lib/result.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
`
|
|
209
|
-
`
|
|
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:
|
|
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
|
-
`
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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({
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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: "
|
|
72
|
-
label: "Graphite:
|
|
82
|
+
name: "graphite_recover",
|
|
83
|
+
label: "Graphite: recover",
|
|
73
84
|
description:
|
|
74
|
-
"Recover from
|
|
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
|
-
"
|
|
87
|
+
"graphite_recover: continue / abort / undo — never use `git rebase --continue`",
|
|
77
88
|
promptGuidelines: [
|
|
78
|
-
"After resolving a rebase conflict,
|
|
79
|
-
"
|
|
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({
|
|
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 `<<<<<<<`
|
|
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({
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
+
}
|