pi-graphite 0.5.0 → 0.6.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 CHANGED
@@ -9,8 +9,10 @@ graphite_status → (graphite_setup if needed) → graphite_sync → graphite_na
9
9
  ```
10
10
 
11
11
  The extension wraps `gt` only. It deliberately does **not** call `gh`, edit
12
- PR titles/bodies, fetch review comments, or perform stack surgery (split /
13
- fold / move / squash / reorder). For those flows, run the underlying `gt`
12
+ PR titles/bodies, fetch review comments, or perform interactive stack surgery
13
+ (split / fold / squash / reorder). Reparenting a tracked branch IS supported
14
+ via `graphite_move` (`gt move --source --onto`, non-interactive). For the
15
+ remaining surgery flows, run the underlying `gt`
14
16
  or `gh` command yourself in your own terminal; the agent should not invoke
15
17
  them from bash, as their defaults open interactive prompts, hunk pickers,
16
18
  or editors that will hang non-interactive sessions.
@@ -48,6 +50,7 @@ agent loads it on demand.
48
50
  | `graphite_sync` | Start-of-day / after-merge cleanup + restack | `gt sync` |
49
51
  | `graphite_get` | Pull a branch / stack from the remote | `gt get <branch>` |
50
52
  | `graphite_navigate` | Move around the stack | `gt checkout`, `gt up`/`down`/`top`/`bottom` |
53
+ | `graphite_move` | Reparent a tracked branch + restack descendants (dry-run by default) | `gt move --source --onto` |
51
54
  | `graphite_change` | Create / amend a stacked branch | `gt create -am`, `gt modify -am`, `gt modify --into`, `gt absorb` |
52
55
  | `graphite_submit` | Push the entire stack and open/update PRs (dry-run by default) | `gt submit --stack --no-edit --no-ai` |
53
56
  | `graphite_recover` | Continue / abort / undo / restack | `gt continue`, `gt abort`, `gt undo`, `gt restack` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-graphite",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Opinionated pi tools + skill for stacked PR workflows with the Graphite (gt) CLI.",
5
5
  "keywords": [
6
6
  "pi",
@@ -12,7 +12,7 @@
12
12
  "license": "MIT",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+ssh://git@github.com/tianrendong/pi-graphite.git"
15
+ "url": "git+https://github.com/tianrendong/pi-graphite.git"
16
16
  },
17
17
  "bugs": {
18
18
  "url": "https://github.com/tianrendong/pi-graphite/issues"
@@ -25,10 +25,14 @@ Do not use it for:
25
25
  - editing PR titles / bodies / labels / reviewers metadata — prefer a
26
26
  dedicated `gh` tool/extension; see the `gh` rule below
27
27
  - reading PR review comments or CI status — same
28
- - rewriting history beyond create/amend (split / fold / move / squash /
29
- reorder). The extension does not expose stack surgery, and those
30
- subcommands prompt interactively (base selectors, hunk pickers, editors)
31
- and will hang. Ask the user to run them in their own terminal.
28
+ - reparenting a tracked branch onto a different parent use
29
+ `graphite_move` (it runs `gt move --source --onto` non-interactively and
30
+ rebases descendants). Do NOT use `track_branch --force` for this; that only
31
+ rewrites tracking metadata and leaves commits unrebased.
32
+ - rewriting history beyond create/amend/move (split / fold / squash /
33
+ reorder). The extension does not expose those, and they prompt
34
+ interactively (base selectors, hunk pickers, editors) and will hang. Ask
35
+ the user to run them in their own terminal.
32
36
 
33
37
  ## Tools
34
38
 
@@ -41,6 +45,7 @@ The extension registers these tools. Prefer them over `gt`/`git`/`gh` in bash.
41
45
  | `graphite_sync` | `gt sync` — pull trunk, drop merged branches, restack |
42
46
  | `graphite_get` | `gt get <branch>` — pull a branch / stack from the remote |
43
47
  | `graphite_navigate` | `gt checkout` / `up` / `down` / `top` / `bottom` / trunk |
48
+ | `graphite_move` | `gt move --source --onto` — reparent a tracked branch + restack descendants (dry-run by default) |
44
49
  | `graphite_change` | `gt create` / `gt modify` / `gt modify --into` / `gt absorb` |
45
50
  | `graphite_submit` | `gt submit --stack --no-edit` (dry-run by default) |
46
51
  | `graphite_recover` | `gt continue` / `gt abort` / `gt undo` / `gt restack` |
@@ -212,6 +217,24 @@ graphite_status({ cwd })
212
217
  Use `graphite_sync` instead when trunk itself may have advanced on the remote.
213
218
  If restack halts on a conflict, follow the conflict recipe.
214
219
 
220
+ ### Reparent a tracked branch onto a new parent
221
+
222
+ When an existing tracked branch is stacked on the wrong parent and you need a
223
+ real rebase (not just a metadata fix):
224
+
225
+ ```
226
+ graphite_status({ cwd }) # confirm shapes
227
+ graphite_move({ cwd, branch: "<branch>", parent: "<new-parent>" }) # dry-run plan
228
+ # review the plan, then apply:
229
+ graphite_move({ cwd, branch: "<branch>", parent: "<new-parent>", apply: true, confirmDestructive: true })
230
+ ```
231
+
232
+ The move rebases `<branch>` and all of its descendants onto `<new-parent>`.
233
+ If it halts on a conflict, follow the conflict recipe (resolve →
234
+ `graphite_recover action="continue"`). To chain several reparents (e.g.
235
+ rebuild a stack A→B→C), move the lowest branch first, then each next branch
236
+ onto its new parent, running `graphite_status` between steps.
237
+
215
238
  ### Pull a branch / stack from the remote
216
239
 
217
240
  To check out a teammate's branch, or re-pull a branch that changed remotely:
@@ -261,6 +284,11 @@ graphite_recover({ cwd, action: "undo" })
261
284
  - **Restack vs sync.** Use `graphite_recover action="restack"` to rebase the
262
285
  stack onto each parent's latest commit when no remote pull is needed. Use
263
286
  `graphite_sync` when trunk may have advanced remotely (it pulls + restacks).
287
+ - **Reparent with `graphite_move`, not `track_branch --force`.**
288
+ `graphite_move` runs `gt move --source --onto`, which rebases commits and
289
+ restacks descendants. `track_branch --force` only rewrites tracking
290
+ metadata and leaves the stack in an inconsistent shape. Always dry-run
291
+ first; `apply:true` requires `confirmDestructive:true`.
264
292
  - **Pull remote branches with `graphite_get`.** `graphite_sync` only touches
265
293
  trunk + already-tracked local branches; use `graphite_get` to download a
266
294
  branch/stack from the remote.
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { registerSetup } from "./tools/setup";
5
5
  import { registerSync } from "./tools/sync";
6
6
  import { registerGet } from "./tools/get";
7
7
  import { registerNavigate } from "./tools/navigate";
8
+ import { registerMove } from "./tools/move";
8
9
  import { registerChange } from "./tools/change";
9
10
  import { registerSubmit } from "./tools/submit";
10
11
  import { registerRecover } from "./tools/recover";
@@ -19,6 +20,7 @@ import { registerRecover } from "./tools/recover";
19
20
  * graphite_sync — start-of-day / after-merge cleanup + restack
20
21
  * graphite_get — pull a branch / stack from the remote
21
22
  * graphite_navigate — move to the branch / PR you want to mutate
23
+ * graphite_move — reparent an existing tracked branch (stack surgery)
22
24
  * graphite_change — create or amend a stacked branch
23
25
  * graphite_submit — push the whole stack and open/update PRs
24
26
  * graphite_recover — continue / abort / undo / restack
@@ -40,8 +42,10 @@ import { registerRecover } from "./tools/recover";
40
42
  * `apply:true` AND `confirmRemote:true`.
41
43
  * - graphite_sync with force / deleteAll requires `confirmDestructive:true`.
42
44
  * - This extension wraps `gt` only. It deliberately does not call `gh`,
43
- * touch PR titles/bodies, run reviews, or do stack surgery
44
- * (split/fold/move/squash). Use the gt CLI or another tool for those.
45
+ * touch PR titles/bodies, run reviews, or do interactive stack surgery
46
+ * (split/fold/squash/reorder). Reparenting via `gt move` IS exposed
47
+ * (graphite_move) because it is non-interactive with explicit
48
+ * --source/--onto. Use the gt CLI or another tool for the rest.
45
49
  */
46
50
  export default function (pi: ExtensionAPI) {
47
51
  registerStatus(pi);
@@ -49,6 +53,7 @@ export default function (pi: ExtensionAPI) {
49
53
  registerSync(pi);
50
54
  registerGet(pi);
51
55
  registerNavigate(pi);
56
+ registerMove(pi);
52
57
  registerChange(pi);
53
58
  registerSubmit(pi);
54
59
  registerRecover(pi);
@@ -0,0 +1,155 @@
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 {
5
+ ensureAllSuccess,
6
+ ensureSuccess,
7
+ renderText,
8
+ } from "../lib/result";
9
+ import {
10
+ CwdParam,
11
+ Type,
12
+ requireConfirm,
13
+ type ToolReturn,
14
+ } from "../lib/schema";
15
+
16
+ /**
17
+ * graphite_move — `gt move --source <branch> --onto <parent>`.
18
+ *
19
+ * Reparent an existing, already-tracked branch onto a new parent and restack
20
+ * the moved branch plus all of its descendants. This is the safe stack-surgery
21
+ * primitive that was previously missing: track_branch --force only rewrites
22
+ * tracking metadata, it does NOT rebase commits, so it leaves the stack in an
23
+ * inconsistent shape. graphite_move performs a real rebase.
24
+ *
25
+ * Wrapper guarantees:
26
+ * 1. Dry-run plan first (apply defaults to false; no mutation).
27
+ * 2. Both `branch` and `parent` are explicit and required.
28
+ * 3. Ambiguous / nonsensical moves are rejected up front (empty refs,
29
+ * flag-injection, branch == parent). gt rejects cycles itself.
30
+ * 4. On apply, gt rebases the moved branch and all descendants.
31
+ * 5. Conflicts surface as a failure with a conflictHalted hint, routing the
32
+ * agent to graphite_recover continue/abort.
33
+ * 6. apply:true requires confirmDestructive:true.
34
+ * 7. On a successful apply, the resulting stack is shown (gt log --stack +
35
+ * gt info) so the new shape is visible without a second call.
36
+ */
37
+ export function registerMove(pi: ExtensionAPI) {
38
+ pi.registerTool({
39
+ name: "graphite_move",
40
+ label: "Graphite: move (reparent)",
41
+ description:
42
+ "Reparent an already-tracked branch onto a new parent and rebase it plus all descendants via `gt move --source <branch> --onto <parent>`. Defaults to a dry-run plan; pass apply:true with confirmDestructive:true to actually rebase. Unlike track_branch --force, this rewrites commits, not just tracking metadata.",
43
+ promptSnippet:
44
+ "graphite_move: reparent a tracked branch onto a new parent (`gt move --source --onto`)",
45
+ promptGuidelines: [
46
+ "Use graphite_move to reparent an existing tracked branch onto a different parent. It rebases the moved branch and all its descendants. Do NOT use graphite_setup track_branch --force for this — that only rewrites tracking metadata and leaves commits unrebased.",
47
+ "Always call graphite_move with apply:false (default) first to review the dry-run plan, then call again with apply:true and confirmDestructive:true to actually rebase.",
48
+ "Pass explicit branch and explicit parent. The wrapper never picks them interactively.",
49
+ "If the move halts on a conflict, follow the hint and use graphite_recover action=continue (or abort).",
50
+ ],
51
+ parameters: Type.Object({
52
+ cwd: CwdParam,
53
+ branch: Type.String({
54
+ description: "The already-tracked branch to reparent (gt move --source).",
55
+ }),
56
+ parent: Type.String({
57
+ description: "The new parent branch to move `branch` onto (gt move --onto).",
58
+ }),
59
+ apply: Type.Optional(
60
+ Type.Boolean({
61
+ description:
62
+ "false => dry-run plan only, no mutation (default). true => actually rebase (requires confirmDestructive).",
63
+ }),
64
+ ),
65
+ confirmDestructive: Type.Optional(Type.Boolean()),
66
+ }),
67
+ async execute(_id, p, signal): Promise<ToolReturn> {
68
+ const branch = assertSafeRef(p.branch, "branch");
69
+ const parent = assertSafeRef(p.parent, "parent");
70
+ if (branch === parent) {
71
+ throw new Error(
72
+ `graphite_move: branch and parent are the same (${JSON.stringify(branch)}). ` +
73
+ `A branch cannot be its own parent.`,
74
+ );
75
+ }
76
+
77
+ const apply = p.apply === true;
78
+
79
+ // --- dry-run: show current stack + the planned operation, no mutation.
80
+ if (!apply) {
81
+ const log = await runGt(["log", "--stack"], { cwd: p.cwd, signal });
82
+ const [fl] = await ensureAllSuccess(
83
+ [{ label: "gt log --stack", result: log, requireStdout: true }],
84
+ p.cwd,
85
+ );
86
+ const planned = `gt ${shellJoin([
87
+ "move",
88
+ flagEq("--source", branch),
89
+ flagEq("--onto", parent),
90
+ ])}`;
91
+ const plan = [
92
+ `[graphite_move] dry-run (no changes made)`,
93
+ ``,
94
+ `Planned: reparent ${branch} onto ${parent}.`,
95
+ `This will rebase ${branch} and ALL of its descendants onto ${parent}.`,
96
+ `Command that would run: ${planned}`,
97
+ ``,
98
+ `To apply: call graphite_move again with apply:true and confirmDestructive:true.`,
99
+ `If it halts on a conflict, resolve files then graphite_recover action="continue".`,
100
+ ``,
101
+ `--- current stack ---`,
102
+ renderText("gt log --stack", fl),
103
+ ].join("\n");
104
+ return {
105
+ content: [{ type: "text", text: plan }],
106
+ details: { apply: false, branch, parent, log: fl },
107
+ };
108
+ }
109
+
110
+ // --- apply: real rebase. Destructive, so require confirmation.
111
+ requireConfirm(
112
+ p.confirmDestructive,
113
+ `gt move --source ${branch} --onto ${parent} (rebases the branch and its descendants)`,
114
+ );
115
+
116
+ const args = [
117
+ "move",
118
+ flagEq("--source", branch),
119
+ flagEq("--onto", parent),
120
+ ];
121
+ const label = `gt ${shellJoin(args)}`;
122
+ const r = await runGt(args, { cwd: p.cwd, signal });
123
+ const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
124
+
125
+ // Requirement 7: show the resulting stack after a successful move.
126
+ const [log, info] = await Promise.all([
127
+ runGt(["log", "--stack"], { cwd: p.cwd, signal }),
128
+ runGt(["info"], { cwd: p.cwd, signal }),
129
+ ]);
130
+ const [fl, fi] = await ensureAllSuccess(
131
+ [
132
+ { label: "gt log --stack", result: log, requireStdout: true },
133
+ { label: "gt info", result: info, requireStdout: true },
134
+ ],
135
+ p.cwd,
136
+ );
137
+
138
+ return {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text: [
143
+ renderText(label, f),
144
+ ``,
145
+ `--- stack after move ---`,
146
+ renderText("gt log --stack", fl),
147
+ renderText("gt info", fi),
148
+ ].join("\n"),
149
+ },
150
+ ],
151
+ details: { apply: true, branch, parent, result: f, log: fl, info: fi },
152
+ };
153
+ },
154
+ });
155
+ }