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
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
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
|
+
const assertReviewer = (rv: string, label: string) => {
|
|
108
|
+
assertSafeRef(rv, label);
|
|
109
|
+
// gt joins reviewers on ',' before calling gh. Reject any element
|
|
110
|
+
// that itself contains a comma or whitespace so a single array
|
|
111
|
+
// entry cannot expand into multiple reviewers.
|
|
112
|
+
if (/[,\s]/.test(rv)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`${label} must not contain commas or whitespace (got ${JSON.stringify(rv)}). ` +
|
|
115
|
+
`Pass each reviewer as a separate array element.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
if (p.reviewers && p.reviewers.length) {
|
|
120
|
+
for (const rv of p.reviewers) assertReviewer(rv, "reviewers[]");
|
|
121
|
+
args.push(flagEq("--reviewers", p.reviewers.join(",")));
|
|
122
|
+
}
|
|
123
|
+
if (p.teamReviewers && p.teamReviewers.length) {
|
|
124
|
+
for (const rv of p.teamReviewers) assertReviewer(rv, "teamReviewers[]");
|
|
125
|
+
args.push(flagEq("--team-reviewers", p.teamReviewers.join(",")));
|
|
126
|
+
}
|
|
127
|
+
if (p.forcePush) args.push("--force");
|
|
128
|
+
if (p.ignoreOutOfSyncTrunk) args.push("--ignore-out-of-sync-trunk");
|
|
129
|
+
|
|
130
|
+
const label = `gt ${shellJoin(args)}`;
|
|
131
|
+
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
132
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
135
|
+
details: { apply, result: f },
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { shellJoin } from "../lib/argv";
|
|
3
|
+
import { runGt } from "../lib/exec";
|
|
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_sync — the "start of day / after merge" workflow.
|
|
14
|
+
*
|
|
15
|
+
* Wraps `gt sync` only. Pulls trunk, deletes merged branches, restacks
|
|
16
|
+
* remaining branches. This is the canonical way to recover from merged PRs.
|
|
17
|
+
*
|
|
18
|
+
* Prefer this over manual restack when trunk may have moved or when PRs in
|
|
19
|
+
* the stack may have merged.
|
|
20
|
+
*/
|
|
21
|
+
export function registerSync(pi: ExtensionAPI) {
|
|
22
|
+
pi.registerTool({
|
|
23
|
+
name: "graphite_sync",
|
|
24
|
+
label: "Graphite: sync",
|
|
25
|
+
description:
|
|
26
|
+
"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`.",
|
|
27
|
+
promptSnippet:
|
|
28
|
+
"graphite_sync: `gt sync` — start-of-day + after-merge cleanup and restack",
|
|
29
|
+
promptGuidelines: [
|
|
30
|
+
"Run graphite_sync at the start of a session and any time PRs in the stack may have merged.",
|
|
31
|
+
"graphite_sync with force=true or deleteAll=true is destructive; pass confirmDestructive:true.",
|
|
32
|
+
],
|
|
33
|
+
parameters: Type.Object({
|
|
34
|
+
cwd: CwdParam,
|
|
35
|
+
restack: Type.Optional(
|
|
36
|
+
Type.Boolean({
|
|
37
|
+
description:
|
|
38
|
+
"Restack after fetching (default true; pass false for --no-restack).",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
allTrunks: Type.Optional(
|
|
42
|
+
Type.Boolean({ description: "Sync all configured trunks (--all)." }),
|
|
43
|
+
),
|
|
44
|
+
deleteAll: Type.Optional(
|
|
45
|
+
Type.Boolean({
|
|
46
|
+
description:
|
|
47
|
+
"Delete all merged/closed branches without prompting (--delete-all). Requires confirmDestructive.",
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
force: Type.Optional(
|
|
51
|
+
Type.Boolean({
|
|
52
|
+
description:
|
|
53
|
+
"Overwrite local branches with remote (--force). Requires confirmDestructive.",
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
confirmDestructive: Type.Optional(Type.Boolean()),
|
|
57
|
+
}),
|
|
58
|
+
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
59
|
+
if (p.force || p.deleteAll) {
|
|
60
|
+
requireConfirm(
|
|
61
|
+
p.confirmDestructive,
|
|
62
|
+
"gt sync with --force/--delete-all (may overwrite branches)",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const args = ["sync"];
|
|
66
|
+
if (p.allTrunks) args.push("--all");
|
|
67
|
+
if (p.deleteAll) args.push("--delete-all");
|
|
68
|
+
if (p.force) args.push("--force");
|
|
69
|
+
if (p.restack === false) args.push("--no-restack");
|
|
70
|
+
|
|
71
|
+
const label = `gt ${shellJoin(args)}`;
|
|
72
|
+
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
73
|
+
const f = await ensureSuccess(label, r, p.cwd);
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
76
|
+
details: { result: f },
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
package/src/tools/branch.ts
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { runGt, type GtRunResult } from "../lib/exec";
|
|
3
|
-
import {
|
|
4
|
-
ensureAllSuccess,
|
|
5
|
-
ensureSuccess,
|
|
6
|
-
renderText,
|
|
7
|
-
type FormattedResult,
|
|
8
|
-
} from "../lib/result";
|
|
9
|
-
import {
|
|
10
|
-
CwdParam,
|
|
11
|
-
StageMode,
|
|
12
|
-
StringEnum,
|
|
13
|
-
Type,
|
|
14
|
-
requireConfirm,
|
|
15
|
-
stageArgs,
|
|
16
|
-
type StageModeValue,
|
|
17
|
-
type ToolReturn,
|
|
18
|
-
} from "../lib/schema";
|
|
19
|
-
|
|
20
|
-
/* --------------------------- branch_inspect --------------------------- */
|
|
21
|
-
|
|
22
|
-
interface InspectSection {
|
|
23
|
-
label: string;
|
|
24
|
-
args: string[];
|
|
25
|
-
/** Display heading; null hides the section in the rendered output. */
|
|
26
|
-
heading: string | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function registerBranchInspect(pi: ExtensionAPI) {
|
|
30
|
-
pi.registerTool({
|
|
31
|
-
name: "graphite_branch_inspect",
|
|
32
|
-
label: "Graphite: branch inspect",
|
|
33
|
-
description:
|
|
34
|
-
"Inspect a branch with separately-labeled sections: summary (parent/PR), parent, children, diffstat, body, and (optionally) diff or patch. Read-only.",
|
|
35
|
-
promptSnippet:
|
|
36
|
-
"graphite_branch_inspect: structured `gt info` + parent/children sections, optional diffstat/body/diff",
|
|
37
|
-
parameters: Type.Object({
|
|
38
|
-
cwd: CwdParam,
|
|
39
|
-
branch: Type.Optional(
|
|
40
|
-
Type.String({ description: "Branch to inspect (default current)." }),
|
|
41
|
-
),
|
|
42
|
-
body: Type.Optional(
|
|
43
|
-
Type.Boolean({ description: "Include the PR body section." }),
|
|
44
|
-
),
|
|
45
|
-
stat: Type.Optional(
|
|
46
|
-
Type.Boolean({ description: "Include the diffstat section." }),
|
|
47
|
-
),
|
|
48
|
-
diff: Type.Optional(
|
|
49
|
-
Type.Boolean({ description: "Include the full diff. Takes precedence over `patch`." }),
|
|
50
|
-
),
|
|
51
|
-
patch: Type.Optional(
|
|
52
|
-
Type.Boolean({ description: "Include per-commit patch. Ignored if `diff` is set." }),
|
|
53
|
-
),
|
|
54
|
-
withParentChildren: Type.Optional(
|
|
55
|
-
Type.Boolean({
|
|
56
|
-
description:
|
|
57
|
-
"Also run `gt parent` and `gt children` (only meaningful when inspecting the currently checked-out branch).",
|
|
58
|
-
}),
|
|
59
|
-
),
|
|
60
|
-
}),
|
|
61
|
-
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
62
|
-
const branchArg = p.branch ? [p.branch] : [];
|
|
63
|
-
|
|
64
|
-
const sections: InspectSection[] = [
|
|
65
|
-
{
|
|
66
|
-
label: "gt info",
|
|
67
|
-
heading: "## summary",
|
|
68
|
-
args: ["info", ...branchArg],
|
|
69
|
-
},
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
if (p.withParentChildren) {
|
|
73
|
-
sections.push({ label: "gt parent", heading: "## parent", args: ["parent"] });
|
|
74
|
-
sections.push({ label: "gt children", heading: "## children", args: ["children"] });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (p.stat && !p.diff && !p.patch) {
|
|
78
|
-
sections.push({
|
|
79
|
-
label: "gt info --stat",
|
|
80
|
-
heading: "## diffstat",
|
|
81
|
-
args: ["info", ...branchArg, "--stat"],
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (p.body) {
|
|
86
|
-
sections.push({
|
|
87
|
-
label: "gt info --body",
|
|
88
|
-
heading: "## body",
|
|
89
|
-
args: ["info", ...branchArg, "--body"],
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (p.diff) {
|
|
94
|
-
const args = ["info", ...branchArg, "--diff"];
|
|
95
|
-
if (p.stat) args.push("--stat");
|
|
96
|
-
sections.push({ label: "gt info --diff", heading: "## diff", args });
|
|
97
|
-
} else if (p.patch) {
|
|
98
|
-
const args = ["info", ...branchArg, "--patch"];
|
|
99
|
-
if (p.stat) args.push("--stat");
|
|
100
|
-
sections.push({ label: "gt info --patch", heading: "## patch", args });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const results = await Promise.all(
|
|
104
|
-
sections.map((s) => runGt(s.args, { cwd: p.cwd, signal })),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const formatted = await ensureAllSuccess(
|
|
108
|
-
sections.map((s, i) => ({ label: s.label, result: results[i] })),
|
|
109
|
-
p.cwd,
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
// Compose section-by-section structured output.
|
|
113
|
-
const blocks: string[] = [];
|
|
114
|
-
formatted.forEach((f, i) => {
|
|
115
|
-
const s = sections[i];
|
|
116
|
-
if (s.heading) blocks.push(s.heading);
|
|
117
|
-
blocks.push(stripChrome(f.result.stdout));
|
|
118
|
-
blocks.push("");
|
|
119
|
-
});
|
|
120
|
-
blocks.push("--- raw ---");
|
|
121
|
-
formatted.forEach((f, i) => {
|
|
122
|
-
blocks.push(renderText(sections[i].label, f));
|
|
123
|
-
blocks.push("");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const details: Record<string, FormattedResult> = {};
|
|
127
|
-
sections.forEach((s, i) => {
|
|
128
|
-
const key = s.label.replace(/^gt\s+/, "").replace(/[^a-z0-9]/gi, "_");
|
|
129
|
-
details[key] = formatted[i];
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
content: [{ type: "text", text: blocks.join("\n").trim() }],
|
|
134
|
-
details,
|
|
135
|
-
};
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function stripChrome(s: string): string {
|
|
141
|
-
return s.replace(/\s+$/, "");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/* --------------------------- branch_create --------------------------- */
|
|
145
|
-
|
|
146
|
-
export function registerBranchCreate(pi: ExtensionAPI) {
|
|
147
|
-
pi.registerTool({
|
|
148
|
-
name: "graphite_branch_create",
|
|
149
|
-
label: "Graphite: branch create",
|
|
150
|
-
description:
|
|
151
|
-
"Create a new branch stacked on top of the current (or `onto`) branch and commit staged changes. Local mutation.",
|
|
152
|
-
promptSnippet:
|
|
153
|
-
"graphite_branch_create: `gt create` with explicit message + stage mode",
|
|
154
|
-
promptGuidelines: [
|
|
155
|
-
"When using graphite_branch_create, always provide `message` unless the user explicitly asked for AI-generated metadata.",
|
|
156
|
-
],
|
|
157
|
-
parameters: Type.Object({
|
|
158
|
-
cwd: CwdParam,
|
|
159
|
-
message: Type.Optional(
|
|
160
|
-
Type.String({ description: "Commit message. Required unless ai=true." }),
|
|
161
|
-
),
|
|
162
|
-
name: Type.Optional(Type.String({ description: "Branch name (default generated)." })),
|
|
163
|
-
stage: Type.Optional(StageMode),
|
|
164
|
-
onto: Type.Optional(Type.String({ description: "Create on top of this branch instead of HEAD (--onto)." })),
|
|
165
|
-
insert: Type.Optional(
|
|
166
|
-
Type.Boolean({ description: "Insert between current branch and its child (--insert)." }),
|
|
167
|
-
),
|
|
168
|
-
ai: Type.Optional(
|
|
169
|
-
Type.Boolean({ description: "Use AI to generate branch name + message (--ai). Default false (--no-ai)." }),
|
|
170
|
-
),
|
|
171
|
-
}),
|
|
172
|
-
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
173
|
-
if (!p.ai && !p.message) {
|
|
174
|
-
throw new Error("graphite_branch_create requires `message` unless ai=true.");
|
|
175
|
-
}
|
|
176
|
-
const args = ["create"];
|
|
177
|
-
if (p.name) args.push(p.name);
|
|
178
|
-
if (p.message) args.push("--message", p.message);
|
|
179
|
-
if ((p.stage as string | undefined) === "patch") throw new Error("stage:'patch' is disabled; use stage:'all' or stage:'update'.");
|
|
180
|
-
args.push(...stageArgs((p.stage ?? "none") as StageModeValue));
|
|
181
|
-
if (p.onto) args.push("--onto", p.onto);
|
|
182
|
-
if (p.insert) args.push("--insert");
|
|
183
|
-
args.push(p.ai ? "--ai" : "--no-ai");
|
|
184
|
-
const label = `gt ${args.join(" ")}`;
|
|
185
|
-
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
186
|
-
const f = await ensureSuccess(label, r, p.cwd);
|
|
187
|
-
return {
|
|
188
|
-
content: [{ type: "text", text: renderText(label, f) }],
|
|
189
|
-
details: { result: f },
|
|
190
|
-
};
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/* --------------------------- branch_update --------------------------- */
|
|
196
|
-
|
|
197
|
-
export function registerBranchUpdate(pi: ExtensionAPI) {
|
|
198
|
-
pi.registerTool({
|
|
199
|
-
name: "graphite_branch_update",
|
|
200
|
-
label: "Graphite: branch update",
|
|
201
|
-
description:
|
|
202
|
-
"Mutate the current (or named) branch: amend, add a new commit, absorb staged hunks, squash, pop, rename, or delete. Auto-restacks descendants where applicable.",
|
|
203
|
-
promptSnippet:
|
|
204
|
-
"graphite_branch_update: amend/new_commit/absorb/squash/pop/rename/delete on a branch",
|
|
205
|
-
promptGuidelines: [
|
|
206
|
-
"Use graphite_branch_update with action=absorb (dryRun:true first) to distribute staged hunks across downstack commits.",
|
|
207
|
-
"graphite_branch_update with action=delete and close:true requires confirmRemote.",
|
|
208
|
-
"Editor and patch/hunk modes are disabled. Pass explicit `message` where needed.",
|
|
209
|
-
],
|
|
210
|
-
parameters: Type.Object({
|
|
211
|
-
cwd: CwdParam,
|
|
212
|
-
action: StringEnum([
|
|
213
|
-
"amend",
|
|
214
|
-
"new_commit",
|
|
215
|
-
"absorb",
|
|
216
|
-
"squash",
|
|
217
|
-
"pop",
|
|
218
|
-
"rename",
|
|
219
|
-
"delete",
|
|
220
|
-
] as const),
|
|
221
|
-
message: Type.Optional(Type.String()),
|
|
222
|
-
stage: Type.Optional(StageMode),
|
|
223
|
-
into: Type.Optional(
|
|
224
|
-
Type.String({ description: "action=amend: amend into a downstack branch (`gt modify --into`)." }),
|
|
225
|
-
),
|
|
226
|
-
edit: Type.Optional(Type.Boolean({ description: "Rejected: editor mode is disabled for agent safety." })),
|
|
227
|
-
resetAuthor: Type.Optional(Type.Boolean({ description: "action=amend: reset commit author." })),
|
|
228
|
-
newName: Type.Optional(Type.String({ description: "action=rename: new branch name." })),
|
|
229
|
-
branch: Type.Optional(Type.String({ description: "action=delete: branch name to delete." })),
|
|
230
|
-
force: Type.Optional(Type.Boolean({ description: "action=delete or rename: force." })),
|
|
231
|
-
close: Type.Optional(
|
|
232
|
-
Type.Boolean({ description: "action=delete: also close associated PR (requires confirmRemote)." }),
|
|
233
|
-
),
|
|
234
|
-
downstack: Type.Optional(Type.Boolean({ description: "action=delete: also delete ancestors." })),
|
|
235
|
-
upstack: Type.Optional(Type.Boolean({ description: "action=delete: also delete descendants." })),
|
|
236
|
-
dryRun: Type.Optional(
|
|
237
|
-
Type.Boolean({ description: "action=absorb: dry-run only (default true)." }),
|
|
238
|
-
),
|
|
239
|
-
patch: Type.Optional(Type.Boolean({ description: "Rejected: interactive hunk mode is disabled." })),
|
|
240
|
-
confirmRemote: Type.Optional(Type.Boolean()),
|
|
241
|
-
}),
|
|
242
|
-
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
243
|
-
let args: string[];
|
|
244
|
-
if (p.edit) throw new Error("edit:true is disabled; pass an explicit message instead.");
|
|
245
|
-
if (p.patch) throw new Error("patch:true is disabled; interactive hunk selection is not exposed.");
|
|
246
|
-
if ((p.stage as string | undefined) === "patch") throw new Error("stage:'patch' is disabled; use stage:'all' or stage:'update'.");
|
|
247
|
-
|
|
248
|
-
switch (p.action) {
|
|
249
|
-
case "amend": {
|
|
250
|
-
args = ["modify"];
|
|
251
|
-
args.push(...stageArgs((p.stage ?? "none") as StageModeValue));
|
|
252
|
-
if (p.message) args.push("--message", p.message);
|
|
253
|
-
else args.push("--no-edit");
|
|
254
|
-
if (p.resetAuthor) args.push("--reset-author");
|
|
255
|
-
if (p.into) args.push("--into", p.into);
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
case "new_commit": {
|
|
259
|
-
if (!p.message) {
|
|
260
|
-
throw new Error("action=new_commit requires `message` (editor mode disabled).");
|
|
261
|
-
}
|
|
262
|
-
args = ["modify", "--commit"];
|
|
263
|
-
args.push(...stageArgs((p.stage ?? "none") as StageModeValue));
|
|
264
|
-
args.push("--message", p.message);
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
case "absorb": {
|
|
268
|
-
const dryRun = p.dryRun !== false; // default true
|
|
269
|
-
args = ["absorb"];
|
|
270
|
-
if (dryRun) args.push("--dry-run");
|
|
271
|
-
else args.push("--force");
|
|
272
|
-
const stage = (p.stage ?? "none") as StageModeValue;
|
|
273
|
-
if (stage === "all") args.push("--all");
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
case "squash": {
|
|
277
|
-
args = ["squash"];
|
|
278
|
-
if (p.message) args.push("--message", p.message);
|
|
279
|
-
else args.push("--no-edit");
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case "pop": {
|
|
283
|
-
args = ["pop"];
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
case "rename": {
|
|
287
|
-
if (!p.newName) throw new Error("action=rename requires `newName`.");
|
|
288
|
-
args = ["rename", p.newName];
|
|
289
|
-
if (p.force) args.push("--force");
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
case "delete": {
|
|
293
|
-
if (p.close) requireConfirm(p.confirmRemote, "delete --close (closes PR on GitHub)");
|
|
294
|
-
args = ["delete"];
|
|
295
|
-
if (p.branch) args.push(p.branch);
|
|
296
|
-
if (p.force) args.push("--force");
|
|
297
|
-
if (p.close) args.push("--close");
|
|
298
|
-
if (p.downstack) args.push("--downstack");
|
|
299
|
-
if (p.upstack) args.push("--upstack");
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const label = `gt ${args.join(" ")}`;
|
|
305
|
-
const r: GtRunResult = await runGt(args, { cwd: p.cwd, signal });
|
|
306
|
-
const f = await ensureSuccess(label, r, p.cwd);
|
|
307
|
-
return {
|
|
308
|
-
content: [{ type: "text", text: renderText(label, f) }],
|
|
309
|
-
details: { action: p.action, result: f },
|
|
310
|
-
};
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/* --------------------------- branch_tracking --------------------------- */
|
|
316
|
-
|
|
317
|
-
export function registerBranchTracking(pi: ExtensionAPI) {
|
|
318
|
-
pi.registerTool({
|
|
319
|
-
name: "graphite_branch_tracking",
|
|
320
|
-
label: "Graphite: branch tracking",
|
|
321
|
-
description:
|
|
322
|
-
"Track / untrack a branch with Graphite, or freeze / unfreeze to prevent local modifications.",
|
|
323
|
-
promptSnippet:
|
|
324
|
-
"graphite_branch_tracking: track/untrack/freeze/unfreeze a branch",
|
|
325
|
-
parameters: Type.Object({
|
|
326
|
-
cwd: CwdParam,
|
|
327
|
-
action: StringEnum(["track", "untrack", "freeze", "unfreeze"] as const),
|
|
328
|
-
branch: Type.Optional(Type.String()),
|
|
329
|
-
parent: Type.Optional(
|
|
330
|
-
Type.String({ description: "action=track: explicit parent branch." }),
|
|
331
|
-
),
|
|
332
|
-
force: Type.Optional(Type.Boolean()),
|
|
333
|
-
}),
|
|
334
|
-
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
335
|
-
let args: string[];
|
|
336
|
-
switch (p.action) {
|
|
337
|
-
case "track":
|
|
338
|
-
args = ["track"];
|
|
339
|
-
if (p.branch) args.push(p.branch);
|
|
340
|
-
if (p.parent) args.push("--parent", p.parent);
|
|
341
|
-
if (p.force) args.push("--force");
|
|
342
|
-
break;
|
|
343
|
-
case "untrack":
|
|
344
|
-
args = ["untrack"];
|
|
345
|
-
if (p.branch) args.push(p.branch);
|
|
346
|
-
if (p.force) args.push("--force");
|
|
347
|
-
break;
|
|
348
|
-
case "freeze":
|
|
349
|
-
args = ["freeze"];
|
|
350
|
-
if (p.branch) args.push(p.branch);
|
|
351
|
-
break;
|
|
352
|
-
case "unfreeze":
|
|
353
|
-
args = ["unfreeze"];
|
|
354
|
-
if (p.branch) args.push(p.branch);
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
const label = `gt ${args.join(" ")}`;
|
|
358
|
-
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
359
|
-
const f = await ensureSuccess(label, r, p.cwd);
|
|
360
|
-
return {
|
|
361
|
-
content: [{ type: "text", text: renderText(label, f) }],
|
|
362
|
-
details: { action: p.action, result: f },
|
|
363
|
-
};
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/* --------------------------- branch_navigate --------------------------- */
|
|
369
|
-
|
|
370
|
-
export function registerBranchNavigate(pi: ExtensionAPI) {
|
|
371
|
-
pi.registerTool({
|
|
372
|
-
name: "graphite_branch_navigate",
|
|
373
|
-
label: "Graphite: navigate",
|
|
374
|
-
description:
|
|
375
|
-
"Navigate the current stack: checkout a branch, move up/down N levels, or jump to top/bottom.",
|
|
376
|
-
promptSnippet:
|
|
377
|
-
"graphite_branch_navigate: checkout/up/down/top/bottom across the stack",
|
|
378
|
-
parameters: Type.Object({
|
|
379
|
-
cwd: CwdParam,
|
|
380
|
-
action: StringEnum(["checkout", "up", "down", "top", "bottom"] as const),
|
|
381
|
-
branch: Type.Optional(Type.String({ description: "action=checkout: branch to checkout." })),
|
|
382
|
-
steps: Type.Optional(Type.Integer({ minimum: 1, description: "action=up|down step count." })),
|
|
383
|
-
to: Type.Optional(
|
|
384
|
-
Type.String({ description: "action=up: target descendant when multiple children exist (--to)." }),
|
|
385
|
-
),
|
|
386
|
-
showAllTrunks: Type.Optional(Type.Boolean({ description: "action=checkout: --all." })),
|
|
387
|
-
showUntracked: Type.Optional(Type.Boolean({ description: "action=checkout: --show-untracked." })),
|
|
388
|
-
stackOnly: Type.Optional(Type.Boolean({ description: "action=checkout: only stack branches (--stack)." })),
|
|
389
|
-
checkoutTrunk: Type.Optional(Type.Boolean({ description: "action=checkout: jump to trunk (--trunk)." })),
|
|
390
|
-
}),
|
|
391
|
-
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
392
|
-
let args: string[];
|
|
393
|
-
switch (p.action) {
|
|
394
|
-
case "checkout":
|
|
395
|
-
args = ["checkout"];
|
|
396
|
-
if (p.branch) args.push(p.branch);
|
|
397
|
-
if (p.showAllTrunks) args.push("--all");
|
|
398
|
-
if (p.showUntracked) args.push("--show-untracked");
|
|
399
|
-
if (p.stackOnly) args.push("--stack");
|
|
400
|
-
if (p.checkoutTrunk) args.push("--trunk");
|
|
401
|
-
if (!p.branch && !p.checkoutTrunk) {
|
|
402
|
-
throw new Error(
|
|
403
|
-
"action=checkout requires `branch` or `checkoutTrunk:true` (interactive selector disabled).",
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
break;
|
|
407
|
-
case "up":
|
|
408
|
-
args = ["up"];
|
|
409
|
-
if (p.steps != null) args.push("--steps", String(p.steps));
|
|
410
|
-
if (p.to) args.push("--to", p.to);
|
|
411
|
-
break;
|
|
412
|
-
case "down":
|
|
413
|
-
args = ["down"];
|
|
414
|
-
if (p.steps != null) args.push("--steps", String(p.steps));
|
|
415
|
-
break;
|
|
416
|
-
case "top":
|
|
417
|
-
args = ["top"];
|
|
418
|
-
break;
|
|
419
|
-
case "bottom":
|
|
420
|
-
args = ["bottom"];
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
423
|
-
const label = `gt ${args.join(" ")}`;
|
|
424
|
-
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
425
|
-
const f = await ensureSuccess(label, r, p.cwd);
|
|
426
|
-
return {
|
|
427
|
-
content: [{ type: "text", text: renderText(label, f) }],
|
|
428
|
-
details: { action: p.action, result: f },
|
|
429
|
-
};
|
|
430
|
-
},
|
|
431
|
-
});
|
|
432
|
-
}
|