pi-graphite 0.4.2 → 0.5.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 +18 -5
- package/package.json +1 -1
- package/skills/graphite/SKILL.md +73 -8
- package/src/index.ts +5 -2
- package/src/lib/exec.ts +7 -4
- package/src/lib/result.ts +120 -9
- package/src/tools/change.ts +1 -1
- package/src/tools/get.ts +67 -0
- package/src/tools/recover.ts +8 -4
- package/src/tools/setup.ts +1 -1
- package/src/tools/status.ts +2 -2
- package/src/tools/submit.ts +2 -1
- package/src/tools/sync.ts +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pi-graphite
|
|
2
2
|
|
|
3
3
|
Opinionated pi tools + skill that wrap the [Graphite](https://graphite.com)
|
|
4
|
-
`gt` CLI for stacked PR workflows.
|
|
4
|
+
`gt` CLI for stacked PR workflows. A small set of tools, one correct path.
|
|
5
5
|
|
|
6
6
|
```
|
|
7
7
|
graphite_status → (graphite_setup if needed) → graphite_sync → graphite_navigate
|
|
@@ -46,10 +46,11 @@ agent loads it on demand.
|
|
|
46
46
|
| `graphite_status` | Read-only snapshot: current stack + current branch + PR + restack hints | `gt log --stack`, `gt info` |
|
|
47
47
|
| `graphite_setup` | Initialize Graphite or track an existing Git branch with explicit parent | `gt init --trunk`, `gt track --parent` |
|
|
48
48
|
| `graphite_sync` | Start-of-day / after-merge cleanup + restack | `gt sync` |
|
|
49
|
+
| `graphite_get` | Pull a branch / stack from the remote | `gt get <branch>` |
|
|
49
50
|
| `graphite_navigate` | Move around the stack | `gt checkout`, `gt up`/`down`/`top`/`bottom` |
|
|
50
51
|
| `graphite_change` | Create / amend a stacked branch | `gt create -am`, `gt modify -am`, `gt modify --into`, `gt absorb` |
|
|
51
52
|
| `graphite_submit` | Push the entire stack and open/update PRs (dry-run by default) | `gt submit --stack --no-edit --no-ai` |
|
|
52
|
-
| `graphite_recover` | Continue / abort / undo
|
|
53
|
+
| `graphite_recover` | Continue / abort / undo / restack | `gt continue`, `gt abort`, `gt undo`, `gt restack` |
|
|
53
54
|
|
|
54
55
|
## Golden path
|
|
55
56
|
|
|
@@ -80,7 +81,9 @@ dependent branches.
|
|
|
80
81
|
- Every tool requires absolute `cwd`.
|
|
81
82
|
- `gt` is invoked with `--cwd <cwd> --no-interactive`, no shell strings. Tools that support AI metadata pass `--no-ai`.
|
|
82
83
|
- Editor / pager / browser env is forced safe (`GT_EDITOR=true`, `GT_PAGER=`,
|
|
83
|
-
`BROWSER=true`, …). Commands have a hard timeout.
|
|
84
|
+
`BROWSER=true`, …). Commands have a hard timeout. `GRAPHITE_INTERACTIVE` is
|
|
85
|
+
deliberately **not** set — some `gt` builds treat it as "running inside
|
|
86
|
+
Graphite Interactive" and silently return empty output for read commands.
|
|
84
87
|
- Interactive editor / hunk / browser / reorder paths are not exposed.
|
|
85
88
|
- Commands echoed in tool output are safe to copy-paste back into a shell.
|
|
86
89
|
- `graphite_setup action=track_branch` requires explicit `branch`, explicit
|
|
@@ -93,10 +96,20 @@ dependent branches.
|
|
|
93
96
|
still contain `<<<<<<<` markers, unless `allowConflictMarkers:true`.
|
|
94
97
|
- Output is ANSI-stripped, branded ("Graphite" not "Charcoal"), and truncated
|
|
95
98
|
to ~50 KB / 2000 lines.
|
|
96
|
-
-
|
|
99
|
+
- Failure output is parsed into structured `hints`
|
|
97
100
|
(`notInitialized`, `conflictHalted`, `restackNeeded`, `trunkOutOfSync`,
|
|
98
101
|
`branchNotTracked`, `noChangesStaged`, `checkedOutElsewhere`,
|
|
99
|
-
`operatingOnTrunk`, …).
|
|
102
|
+
`operatingOnTrunk`, `emptyOutput`, …).
|
|
103
|
+
- Read commands that must produce output (`graphite_status`) treat an
|
|
104
|
+
exit-0 **empty stdout** as a failure (`emptyOutput` hint) instead of a
|
|
105
|
+
misleading "ok".
|
|
106
|
+
- Output is also scanned (on success **and** failure) for non-fatal
|
|
107
|
+
`warnings` (`skippedBranches`, `remoteChanged`, `alreadyMerged`,
|
|
108
|
+
`needsRestack`). A result with warnings is reported as `ok (with warnings)`
|
|
109
|
+
so a `gt sync` / `gt submit` that silently skipped work is not mistaken for
|
|
110
|
+
a clean success.
|
|
111
|
+
- A **mutating** command that fails appends a "partial side effects possible"
|
|
112
|
+
note, prompting a follow-up `graphite_status`.
|
|
100
113
|
|
|
101
114
|
### Git hooks
|
|
102
115
|
|
package/package.json
CHANGED
package/skills/graphite/SKILL.md
CHANGED
|
@@ -26,27 +26,47 @@ Do not use it for:
|
|
|
26
26
|
dedicated `gh` tool/extension; see the `gh` rule below
|
|
27
27
|
- reading PR review comments or CI status — same
|
|
28
28
|
- rewriting history beyond create/amend (split / fold / move / squash /
|
|
29
|
-
reorder). The extension does not expose stack surgery
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Ask the user to run them manually in their own terminal.
|
|
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.
|
|
33
32
|
|
|
34
33
|
## Tools
|
|
35
34
|
|
|
36
|
-
The extension registers
|
|
35
|
+
The extension registers these tools. Prefer them over `gt`/`git`/`gh` in bash.
|
|
37
36
|
|
|
38
37
|
| Tool | Purpose |
|
|
39
38
|
|---|---|
|
|
40
39
|
| `graphite_status` | Read-only snapshot: current stack + current branch + PR + restack hint |
|
|
41
40
|
| `graphite_setup` | Initialize Graphite or track an existing Git branch with explicit parent |
|
|
42
41
|
| `graphite_sync` | `gt sync` — pull trunk, drop merged branches, restack |
|
|
42
|
+
| `graphite_get` | `gt get <branch>` — pull a branch / stack from the remote |
|
|
43
43
|
| `graphite_navigate` | `gt checkout` / `up` / `down` / `top` / `bottom` / trunk |
|
|
44
44
|
| `graphite_change` | `gt create` / `gt modify` / `gt modify --into` / `gt absorb` |
|
|
45
45
|
| `graphite_submit` | `gt submit --stack --no-edit` (dry-run by default) |
|
|
46
|
-
| `graphite_recover` | `gt continue` / `gt abort` / `gt undo` |
|
|
46
|
+
| `graphite_recover` | `gt continue` / `gt abort` / `gt undo` / `gt restack` |
|
|
47
47
|
|
|
48
48
|
All tools require an absolute `cwd`.
|
|
49
49
|
|
|
50
|
+
## Reading tool output (don't trust a bare "ok")
|
|
51
|
+
|
|
52
|
+
Each result starts with `[<label>] ok | ok (with warnings) | fail`. Read past
|
|
53
|
+
the status line:
|
|
54
|
+
|
|
55
|
+
- **`fail`** — read the `--- hints ---` and `--- suggestion ---` blocks; they
|
|
56
|
+
tell you exactly which recovery tool to call.
|
|
57
|
+
- **`ok (with warnings)`** — gt exited 0 but its output mentioned skipped /
|
|
58
|
+
remotely-changed / already-merged branches or a needed restack. Treat this
|
|
59
|
+
as "succeeded but verify": run `graphite_status` before assuming the stack
|
|
60
|
+
is in the expected shape.
|
|
61
|
+
- **`emptyOutput` hint on a status call** — gt returned nothing where output
|
|
62
|
+
was expected. Usually means the branch is untracked, the repo is not
|
|
63
|
+
Graphite-initialized, or the gt build short-circuited. Follow the
|
|
64
|
+
suggestion (often `graphite_setup`), and you may run a **read-only** gt
|
|
65
|
+
command directly to confirm (see the direct-`gt` rule).
|
|
66
|
+
- After a **failed mutating** command (change / submit apply / sync / get /
|
|
67
|
+
recover / setup) the suggestion warns "partial side effects possible".
|
|
68
|
+
Always run `graphite_status` to see the real state before retrying.
|
|
69
|
+
|
|
50
70
|
## Golden path
|
|
51
71
|
|
|
52
72
|
```
|
|
@@ -179,6 +199,34 @@ graphite_status({ cwd })
|
|
|
179
199
|
|
|
180
200
|
If `gt sync` halts on conflict, use the conflict recipe below.
|
|
181
201
|
|
|
202
|
+
### Restack without pulling from remote
|
|
203
|
+
|
|
204
|
+
When `graphite_status` shows branches out of date with their parent but trunk
|
|
205
|
+
has not moved (no remote pull needed), restack directly:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
graphite_recover({ cwd, action: "restack" })
|
|
209
|
+
graphite_status({ cwd })
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Use `graphite_sync` instead when trunk itself may have advanced on the remote.
|
|
213
|
+
If restack halts on a conflict, follow the conflict recipe.
|
|
214
|
+
|
|
215
|
+
### Pull a branch / stack from the remote
|
|
216
|
+
|
|
217
|
+
To check out a teammate's branch, or re-pull a branch that changed remotely:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
graphite_get({ cwd, branch: "<branch>" })
|
|
221
|
+
graphite_status({ cwd })
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
If local commits should be overwritten by the remote version:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
graphite_get({ cwd, branch: "<branch>", force: true, confirmDestructive: true })
|
|
228
|
+
```
|
|
229
|
+
|
|
182
230
|
### Resolve a conflict
|
|
183
231
|
|
|
184
232
|
1. Read the failing tool's `--- stderr ---` and `hints` block.
|
|
@@ -210,8 +258,12 @@ graphite_recover({ cwd, action: "undo" })
|
|
|
210
258
|
use `graphite_change action="create"` instead.
|
|
211
259
|
- **Never guess tracking parent.** `track_branch` requires explicit branch,
|
|
212
260
|
explicit parent, and `confirmParent:true`.
|
|
213
|
-
- **
|
|
214
|
-
|
|
261
|
+
- **Restack vs sync.** Use `graphite_recover action="restack"` to rebase the
|
|
262
|
+
stack onto each parent's latest commit when no remote pull is needed. Use
|
|
263
|
+
`graphite_sync` when trunk may have advanced remotely (it pulls + restacks).
|
|
264
|
+
- **Pull remote branches with `graphite_get`.** `graphite_sync` only touches
|
|
265
|
+
trunk + already-tracked local branches; use `graphite_get` to download a
|
|
266
|
+
branch/stack from the remote.
|
|
215
267
|
- **Always dry-run first.** Show the user the dry-run plan from
|
|
216
268
|
`graphite_submit apply=false` before pushing.
|
|
217
269
|
- **`apply:true` requires `confirmRemote:true`.** The tool will refuse
|
|
@@ -225,5 +277,18 @@ graphite_recover({ cwd, action: "undo" })
|
|
|
225
277
|
available. If you must shell out to `gh` from bash, pass fully explicit
|
|
226
278
|
non-interactive arguments only — never `gh auth login`, `--web`, or any
|
|
227
279
|
command that opens a browser, editor, or prompt.
|
|
280
|
+
- **Direct `gt` is allowed only when no tool covers the command.** The tools
|
|
281
|
+
above are the default path. If you genuinely need a `gt` subcommand this
|
|
282
|
+
extension does not expose (and the user has not asked to run it
|
|
283
|
+
themselves), you may call `gt` from bash, but ONLY with explicit
|
|
284
|
+
non-interactive flags and never an interactive subcommand:
|
|
285
|
+
- Always pass `--no-interactive` (and `--cwd <abs>`).
|
|
286
|
+
- Safe read-only fallbacks: `gt log`, `gt log --stack`, `gt info`,
|
|
287
|
+
`gt children`, `gt parent`, `gt trunk`, `gt state`.
|
|
288
|
+
- Never run interactive surgery (`gt split` / `fold` / `move` / `squash` /
|
|
289
|
+
`reorder`) or anything that opens an editor, pager, hunk picker, or
|
|
290
|
+
browser — those hang. Ask the user to run those in their own terminal.
|
|
291
|
+
- Prefer the dedicated tool whenever one exists; direct `gt` skips the
|
|
292
|
+
safety confirmations, hint parsing, and warning detection the tools add.
|
|
228
293
|
- **No interactive editor / browser / hunk picker.** All paths are
|
|
229
294
|
non-interactive; pass explicit messages.
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { registerStatus } from "./tools/status";
|
|
4
4
|
import { registerSetup } from "./tools/setup";
|
|
5
5
|
import { registerSync } from "./tools/sync";
|
|
6
|
+
import { registerGet } from "./tools/get";
|
|
6
7
|
import { registerNavigate } from "./tools/navigate";
|
|
7
8
|
import { registerChange } from "./tools/change";
|
|
8
9
|
import { registerSubmit } from "./tools/submit";
|
|
@@ -11,15 +12,16 @@ import { registerRecover } from "./tools/recover";
|
|
|
11
12
|
/**
|
|
12
13
|
* pi-graphite — opinionated `gt` wrapper for stacked PR workflows.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
+
* Workflow tools, one correct path:
|
|
15
16
|
*
|
|
16
17
|
* graphite_status — see where you are in the stack
|
|
17
18
|
* graphite_setup — init repo / track existing branch when needed
|
|
18
19
|
* graphite_sync — start-of-day / after-merge cleanup + restack
|
|
20
|
+
* graphite_get — pull a branch / stack from the remote
|
|
19
21
|
* graphite_navigate — move to the branch / PR you want to mutate
|
|
20
22
|
* graphite_change — create or amend a stacked branch
|
|
21
23
|
* graphite_submit — push the whole stack and open/update PRs
|
|
22
|
-
* graphite_recover — continue / abort / undo
|
|
24
|
+
* graphite_recover — continue / abort / undo / restack
|
|
23
25
|
*
|
|
24
26
|
* Golden path:
|
|
25
27
|
*
|
|
@@ -45,6 +47,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
45
47
|
registerStatus(pi);
|
|
46
48
|
registerSetup(pi);
|
|
47
49
|
registerSync(pi);
|
|
50
|
+
registerGet(pi);
|
|
48
51
|
registerNavigate(pi);
|
|
49
52
|
registerChange(pi);
|
|
50
53
|
registerSubmit(pi);
|
package/src/lib/exec.ts
CHANGED
|
@@ -41,10 +41,13 @@ export const SAFE_NONINTERACTIVE_ENV: Record<string, string> = {
|
|
|
41
41
|
LESS: "FRX",
|
|
42
42
|
BROWSER: "true",
|
|
43
43
|
GH_BROWSER: "true",
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
44
|
+
// NOTE: we intentionally do NOT set GRAPHITE_INTERACTIVE here. Some gt
|
|
45
|
+
// builds treat GRAPHITE_INTERACTIVE=1 as "invoked from Graphite Interactive"
|
|
46
|
+
// and short-circuit read commands (`gt log --stack`, `gt info`) to exit 0
|
|
47
|
+
// with EMPTY stdout — which made this extension blind while reporting
|
|
48
|
+
// success. Non-interactive behavior is already enforced via the
|
|
49
|
+
// --no-interactive argv guards in runGt() plus the editor/pager/browser
|
|
50
|
+
// overrides above.
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
export function safeNoninteractiveEnv(extra?: Record<string, string>): NodeJS.ProcessEnv {
|
package/src/lib/result.ts
CHANGED
|
@@ -19,6 +19,40 @@ export interface GtHints {
|
|
|
19
19
|
prMissing?: boolean;
|
|
20
20
|
operatingOnTrunk?: boolean;
|
|
21
21
|
invalidArgument?: string;
|
|
22
|
+
/** exit 0 but stdout empty when output was expected (e.g. status). */
|
|
23
|
+
emptyOutput?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Non-fatal warnings parsed from BOTH stdout and stderr regardless of exit
|
|
28
|
+
* code. gt frequently exits 0 while skipping branches or noting that a
|
|
29
|
+
* branch changed remotely / already merged; surfacing these keeps the agent
|
|
30
|
+
* from trusting a misleadingly "ok" result.
|
|
31
|
+
*/
|
|
32
|
+
export interface GtWarnings {
|
|
33
|
+
skippedBranches?: boolean;
|
|
34
|
+
remoteChanged?: boolean;
|
|
35
|
+
alreadyMerged?: boolean;
|
|
36
|
+
needsRestack?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const WARNING_PATTERNS: Array<[keyof GtWarnings, RegExp]> = [
|
|
40
|
+
["skippedBranches", /\bskipp(?:ing|ed)\b/i],
|
|
41
|
+
["remoteChanged", /updated\s+remotely|changed\s+on\s+remote|newer\s+(?:commit|version)\s+(?:on|exists)|diverged\s+from\s+remote/i],
|
|
42
|
+
["alreadyMerged", /already\s+(?:been\s+)?merged|has\s+been\s+merged|closed\s+remotely/i],
|
|
43
|
+
// Negative lookbehind avoids matching "does not need to be restacked".
|
|
44
|
+
["needsRestack", /(?<!not\s)needs?\s+(?:to\s+be\s+)?restack|run\s+`?gt\s+restack`?|out\s+of\s+date\s+with\s+(?:its\s+)?parent/i],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function parseWarnings(r: GtRunResult): GtWarnings {
|
|
48
|
+
const text = `${r.stdout}\n${r.stderr}`;
|
|
49
|
+
const w: GtWarnings = {};
|
|
50
|
+
// A help/usage dump is full of command descriptions, not real warnings.
|
|
51
|
+
if (looksLikeUsageDump(text)) return w;
|
|
52
|
+
for (const [key, re] of WARNING_PATTERNS) {
|
|
53
|
+
if (re.test(text)) (w as Record<string, unknown>)[key] = true;
|
|
54
|
+
}
|
|
55
|
+
return w;
|
|
22
56
|
}
|
|
23
57
|
|
|
24
58
|
// Patterns run only on failure text.
|
|
@@ -68,9 +102,29 @@ const PATTERNS: Array<[Exclude<keyof GtHints, "checkedOutElsewhere" | "invalidAr
|
|
|
68
102
|
],
|
|
69
103
|
];
|
|
70
104
|
|
|
105
|
+
/**
|
|
106
|
+
* gt dumps its full usage/help (Commands:/Options: blocks) on an unknown
|
|
107
|
+
* argument. Those command descriptions contain phrases ("halted by a rebase
|
|
108
|
+
* conflict", "restack", …) that would otherwise trigger false-positive
|
|
109
|
+
* hints. Detect the dump so we can suppress content-derived hints and keep
|
|
110
|
+
* only the invalidArgument signal.
|
|
111
|
+
*/
|
|
112
|
+
function looksLikeUsageDump(text: string): boolean {
|
|
113
|
+
return /\n\s*Commands:\s*\n/i.test(text) && /\n\s*Options:\s*\n/i.test(text);
|
|
114
|
+
}
|
|
115
|
+
|
|
71
116
|
function parseHints(r: GtRunResult): GtHints {
|
|
72
117
|
const text = `${r.stdout}\n${r.stderr}`;
|
|
73
118
|
const hints: GtHints = {};
|
|
119
|
+
|
|
120
|
+
// Match both singular and plural ("Unknown argument(s):").
|
|
121
|
+
const invalid = text.match(/Unknown arguments?:\s*([^\n]+)/i);
|
|
122
|
+
if (invalid) hints.invalidArgument = invalid[1].trim();
|
|
123
|
+
|
|
124
|
+
// When gt printed its help dump, the only trustworthy signal is the
|
|
125
|
+
// unknown-argument line; everything else is description text.
|
|
126
|
+
if (looksLikeUsageDump(text)) return hints;
|
|
127
|
+
|
|
74
128
|
for (const [key, re] of PATTERNS) {
|
|
75
129
|
if (re.test(text)) (hints as Record<string, unknown>)[key] = true;
|
|
76
130
|
}
|
|
@@ -80,28 +134,56 @@ function parseHints(r: GtRunResult): GtHints {
|
|
|
80
134
|
);
|
|
81
135
|
hints.checkedOutElsewhere = { branch: m?.[1], worktree: m?.[2] };
|
|
82
136
|
}
|
|
83
|
-
const invalid = text.match(/Unknown argument:\s*([^\n]+)/i);
|
|
84
|
-
if (invalid) hints.invalidArgument = invalid[1].trim();
|
|
85
137
|
return hints;
|
|
86
138
|
}
|
|
87
139
|
|
|
140
|
+
export interface FormatOptions {
|
|
141
|
+
/**
|
|
142
|
+
* When true, an exit-0 result with empty stdout is treated as a FAILURE
|
|
143
|
+
* (with an emptyOutput hint). Use for read commands that must produce
|
|
144
|
+
* output, e.g. `gt log --stack` / `gt info`. Without this, gt silently
|
|
145
|
+
* returning nothing would be reported as "ok" and blind the agent.
|
|
146
|
+
*/
|
|
147
|
+
requireStdout?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* When true, a FAILURE appends a "partial side effects possible" note so
|
|
150
|
+
* the agent knows the command may have mutated state before erroring
|
|
151
|
+
* (e.g. `gt create` made a branch then hit a metadata lock). Set for any
|
|
152
|
+
* command that can change local/remote state.
|
|
153
|
+
*/
|
|
154
|
+
mutating?: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const PARTIAL_SIDE_EFFECTS_NOTE =
|
|
158
|
+
"This command can mutate state, and it failed mid-flight: partial side effects are possible " +
|
|
159
|
+
"(e.g. a created branch, partial restack, a metadata lock, or some PRs already pushed). " +
|
|
160
|
+
"Run graphite_status to confirm the actual stack state before retrying.";
|
|
161
|
+
|
|
88
162
|
export interface FormattedResult {
|
|
89
163
|
ok: boolean;
|
|
90
164
|
isFailure: boolean;
|
|
91
165
|
result: GtRunResult;
|
|
92
166
|
/** Empty object on success. Populated only on failure. */
|
|
93
167
|
hints: GtHints;
|
|
168
|
+
/** Non-fatal warnings parsed on success AND failure. */
|
|
169
|
+
warnings: GtWarnings;
|
|
94
170
|
/** Recovery suggestion derived from hints + auxiliary probes. */
|
|
95
171
|
suggestion?: string;
|
|
96
172
|
}
|
|
97
173
|
|
|
98
|
-
export function formatResult(r: GtRunResult): FormattedResult {
|
|
99
|
-
const
|
|
174
|
+
export function formatResult(r: GtRunResult, opts?: FormatOptions): FormattedResult {
|
|
175
|
+
const hardFailure = r.exitCode !== 0 || r.timedOut || !!r.spawnError;
|
|
176
|
+
const emptyOutput =
|
|
177
|
+
!hardFailure && !!opts?.requireStdout && r.stdout.trim() === "";
|
|
178
|
+
const isFailure = hardFailure || emptyOutput;
|
|
179
|
+
const hints = isFailure ? parseHints(r) : {};
|
|
180
|
+
if (emptyOutput) hints.emptyOutput = true;
|
|
100
181
|
return {
|
|
101
182
|
ok: !isFailure,
|
|
102
183
|
isFailure,
|
|
103
184
|
result: r,
|
|
104
|
-
hints
|
|
185
|
+
hints,
|
|
186
|
+
warnings: parseWarnings(r),
|
|
105
187
|
};
|
|
106
188
|
}
|
|
107
189
|
|
|
@@ -127,11 +209,24 @@ export function renderText(label: string, f: FormattedResult): string {
|
|
|
127
209
|
lines.push("--- hints ---");
|
|
128
210
|
lines.push(JSON.stringify(f.hints));
|
|
129
211
|
}
|
|
212
|
+
if (Object.keys(f.warnings).length) {
|
|
213
|
+
lines.push("--- warnings ---");
|
|
214
|
+
lines.push(JSON.stringify(f.warnings));
|
|
215
|
+
lines.push(
|
|
216
|
+
"gt reported success but the above conditions were detected in its output. " +
|
|
217
|
+
"Verify the result (e.g. run graphite_status) before assuming the stack is in the expected state.",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
130
220
|
if (f.isFailure && f.suggestion) {
|
|
131
221
|
lines.push("--- suggestion ---");
|
|
132
222
|
lines.push(f.suggestion);
|
|
133
223
|
}
|
|
134
|
-
|
|
224
|
+
const status = f.ok
|
|
225
|
+
? Object.keys(f.warnings).length
|
|
226
|
+
? "ok (with warnings)"
|
|
227
|
+
: "ok"
|
|
228
|
+
: "fail";
|
|
229
|
+
return `[${label}] ${status}\n${lines.join("\n")}`;
|
|
135
230
|
}
|
|
136
231
|
|
|
137
232
|
/* ----------------------- auxiliary probes (best-effort) ----------------------- */
|
|
@@ -255,6 +350,13 @@ export async function enrichFailure(cwd: string, f: FormattedResult): Promise<vo
|
|
|
255
350
|
`Operation refused on the trunk branch. Check out a non-trunk branch first with graphite_navigate.`,
|
|
256
351
|
);
|
|
257
352
|
}
|
|
353
|
+
if (f.hints.emptyOutput) {
|
|
354
|
+
parts.push(
|
|
355
|
+
`gt exited 0 but produced no output where output was expected. This usually means the current branch is not tracked by Graphite, you are not in a Graphite-initialized repo, or the gt build short-circuited (do NOT set GRAPHITE_INTERACTIVE). ` +
|
|
356
|
+
`Confirm with \`git -C <cwd> rev-parse --abbrev-ref HEAD\` and \`gt --version\`, and consider running graphite_setup. ` +
|
|
357
|
+
`As a fallback you may run a read-only gt command directly (e.g. \`gt log --stack\`) with non-interactive flags.`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
258
360
|
|
|
259
361
|
if (parts.length) f.suggestion = parts.join(" ");
|
|
260
362
|
}
|
|
@@ -268,10 +370,16 @@ export async function ensureSuccess(
|
|
|
268
370
|
label: string,
|
|
269
371
|
r: GtRunResult,
|
|
270
372
|
cwd: string,
|
|
373
|
+
opts?: FormatOptions,
|
|
271
374
|
): Promise<FormattedResult> {
|
|
272
|
-
const f = formatResult(r);
|
|
375
|
+
const f = formatResult(r, opts);
|
|
273
376
|
if (f.isFailure) {
|
|
274
377
|
await enrichFailure(cwd, f);
|
|
378
|
+
if (opts?.mutating) {
|
|
379
|
+
f.suggestion = f.suggestion
|
|
380
|
+
? `${f.suggestion} ${PARTIAL_SIDE_EFFECTS_NOTE}`
|
|
381
|
+
: PARTIAL_SIDE_EFFECTS_NOTE;
|
|
382
|
+
}
|
|
275
383
|
throw new Error(renderText(label, f));
|
|
276
384
|
}
|
|
277
385
|
return f;
|
|
@@ -283,10 +391,13 @@ export async function ensureSuccess(
|
|
|
283
391
|
* agent sees the full picture, not just the first error.
|
|
284
392
|
*/
|
|
285
393
|
export async function ensureAllSuccess(
|
|
286
|
-
items: Array<{ label: string; result: GtRunResult }>,
|
|
394
|
+
items: Array<{ label: string; result: GtRunResult; requireStdout?: boolean }>,
|
|
287
395
|
cwd: string,
|
|
288
396
|
): Promise<FormattedResult[]> {
|
|
289
|
-
const formatted = items.map((i) => ({
|
|
397
|
+
const formatted = items.map((i) => ({
|
|
398
|
+
label: i.label,
|
|
399
|
+
f: formatResult(i.result, { requireStdout: i.requireStdout }),
|
|
400
|
+
}));
|
|
290
401
|
const failed = formatted.filter((x) => x.f.isFailure);
|
|
291
402
|
if (failed.length) {
|
|
292
403
|
await Promise.all(failed.map((x) => enrichFailure(cwd, x.f)));
|
package/src/tools/change.ts
CHANGED
|
@@ -130,7 +130,7 @@ export function registerChange(pi: ExtensionAPI) {
|
|
|
130
130
|
}
|
|
131
131
|
const label = `gt ${shellJoin(args)}`;
|
|
132
132
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
133
|
-
const f = await ensureSuccess(label, r, p.cwd);
|
|
133
|
+
const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
|
|
134
134
|
return {
|
|
135
135
|
content: [{ type: "text", text: renderText(label, f) }],
|
|
136
136
|
details: { action: p.action, result: f },
|
package/src/tools/get.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { runGt } from "../lib/exec";
|
|
3
|
+
import { assertSafeRef, 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_get — `gt get <branch>`.
|
|
14
|
+
*
|
|
15
|
+
* Download a branch (and its descendants) from the Graphite remote and check
|
|
16
|
+
* it out. Used to pull a teammate's stack, or to re-pull a branch after it
|
|
17
|
+
* changed remotely. Distinct from graphite_sync, which operates on trunk +
|
|
18
|
+
* already-tracked local branches.
|
|
19
|
+
*
|
|
20
|
+
* Because `gt get` can overwrite local commits with the remote version,
|
|
21
|
+
* `force` requires `confirmDestructive:true`.
|
|
22
|
+
*/
|
|
23
|
+
export function registerGet(pi: ExtensionAPI) {
|
|
24
|
+
pi.registerTool({
|
|
25
|
+
name: "graphite_get",
|
|
26
|
+
label: "Graphite: get",
|
|
27
|
+
description:
|
|
28
|
+
"Download a branch and its descendants from the Graphite remote and check it out via `gt get <branch>`. Use to pull a teammate's stack or re-pull a branch that changed remotely. `force` (overwrite local) requires confirmDestructive.",
|
|
29
|
+
promptSnippet:
|
|
30
|
+
"graphite_get: `gt get <branch>` — pull a branch/stack from remote",
|
|
31
|
+
promptGuidelines: [
|
|
32
|
+
"Use graphite_get to pull a branch (and its descendants) from the remote. graphite_sync only handles trunk + already-tracked local branches.",
|
|
33
|
+
"graphite_get with force=true may overwrite local commits; pass confirmDestructive:true.",
|
|
34
|
+
],
|
|
35
|
+
parameters: Type.Object({
|
|
36
|
+
cwd: CwdParam,
|
|
37
|
+
branch: Type.String({
|
|
38
|
+
description: "Branch to download from the Graphite remote and check out.",
|
|
39
|
+
}),
|
|
40
|
+
force: Type.Optional(
|
|
41
|
+
Type.Boolean({
|
|
42
|
+
description:
|
|
43
|
+
"Overwrite local branch state with remote (--force). Requires confirmDestructive.",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
confirmDestructive: Type.Optional(Type.Boolean()),
|
|
47
|
+
}),
|
|
48
|
+
async execute(_id, p, signal): Promise<ToolReturn> {
|
|
49
|
+
if (p.force) {
|
|
50
|
+
requireConfirm(
|
|
51
|
+
p.confirmDestructive,
|
|
52
|
+
"gt get --force (may overwrite local commits with the remote version)",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const args = ["get", assertSafeRef(p.branch, "branch")];
|
|
56
|
+
if (p.force) args.push("--force");
|
|
57
|
+
|
|
58
|
+
const label = `gt ${shellJoin(args)}`;
|
|
59
|
+
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
60
|
+
const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: renderText(label, f) }],
|
|
63
|
+
details: { result: f },
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
package/src/tools/recover.ts
CHANGED
|
@@ -82,16 +82,17 @@ export function registerRecover(pi: ExtensionAPI) {
|
|
|
82
82
|
name: "graphite_recover",
|
|
83
83
|
label: "Graphite: recover",
|
|
84
84
|
description:
|
|
85
|
-
"Recover
|
|
85
|
+
"Recover or repair Graphite stack state: continue (resume a paused gt command after resolving conflicts), abort (cancel the in-flight operation), undo (revert the most recent gt mutation in this worktree), or restack (rebase the current stack so each branch sits on its parent's latest commit). Always prefer this over `git rebase --continue`.",
|
|
86
86
|
promptSnippet:
|
|
87
|
-
"graphite_recover: continue / abort / undo — never use `git rebase --continue`",
|
|
87
|
+
"graphite_recover: continue / abort / undo / restack — never use `git rebase --continue`",
|
|
88
88
|
promptGuidelines: [
|
|
89
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
90
|
"graphite_recover action=undo only undoes commands run from the current worktree.",
|
|
91
|
+
"Use graphite_recover action=restack when graphite_status reports branches out of date with their parent but no remote pull is needed; use graphite_sync when trunk itself may have moved.",
|
|
91
92
|
],
|
|
92
93
|
parameters: Type.Object({
|
|
93
94
|
cwd: CwdParam,
|
|
94
|
-
action: StringEnum(["continue", "abort", "undo"] as const),
|
|
95
|
+
action: StringEnum(["continue", "abort", "undo", "restack"] as const),
|
|
95
96
|
stageAll: Type.Optional(
|
|
96
97
|
Type.Boolean({
|
|
97
98
|
description: "action=continue: stage all changes first (--all).",
|
|
@@ -135,10 +136,13 @@ export function registerRecover(pi: ExtensionAPI) {
|
|
|
135
136
|
args = ["undo"];
|
|
136
137
|
if (p.force) args.push("--force");
|
|
137
138
|
break;
|
|
139
|
+
case "restack":
|
|
140
|
+
args = ["restack"];
|
|
141
|
+
break;
|
|
138
142
|
}
|
|
139
143
|
const label = `gt ${shellJoin(args)}`;
|
|
140
144
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
141
|
-
const f = await ensureSuccess(label, r, p.cwd);
|
|
145
|
+
const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
|
|
142
146
|
return {
|
|
143
147
|
content: [{ type: "text", text: renderText(label, f) }],
|
|
144
148
|
details: { action: p.action, result: f },
|
package/src/tools/setup.ts
CHANGED
|
@@ -111,7 +111,7 @@ export function registerSetup(pi: ExtensionAPI) {
|
|
|
111
111
|
|
|
112
112
|
const label = `gt ${shellJoin(args)}`;
|
|
113
113
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
114
|
-
const f = await ensureSuccess(label, r, p.cwd);
|
|
114
|
+
const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
|
|
115
115
|
return {
|
|
116
116
|
content: [{ type: "text", text: renderText(label, f) }],
|
|
117
117
|
details: { action: p.action, result: f },
|
package/src/tools/status.ts
CHANGED
|
@@ -33,8 +33,8 @@ export function registerStatus(pi: ExtensionAPI) {
|
|
|
33
33
|
]);
|
|
34
34
|
const [fl, fi] = await ensureAllSuccess(
|
|
35
35
|
[
|
|
36
|
-
{ label: "gt log --stack", result: log },
|
|
37
|
-
{ label: "gt info", result: info },
|
|
36
|
+
{ label: "gt log --stack", result: log, requireStdout: true },
|
|
37
|
+
{ label: "gt info", result: info, requireStdout: true },
|
|
38
38
|
],
|
|
39
39
|
p.cwd,
|
|
40
40
|
);
|
package/src/tools/submit.ts
CHANGED
|
@@ -129,7 +129,8 @@ export function registerSubmit(pi: ExtensionAPI) {
|
|
|
129
129
|
|
|
130
130
|
const label = `gt ${shellJoin(args)}`;
|
|
131
131
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
132
|
-
|
|
132
|
+
// A dry-run does not mutate; an applied submit does.
|
|
133
|
+
const f = await ensureSuccess(label, r, p.cwd, { mutating: apply });
|
|
133
134
|
return {
|
|
134
135
|
content: [{ type: "text", text: renderText(label, f) }],
|
|
135
136
|
details: { apply, result: f },
|
package/src/tools/sync.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function registerSync(pi: ExtensionAPI) {
|
|
|
70
70
|
|
|
71
71
|
const label = `gt ${shellJoin(args)}`;
|
|
72
72
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
73
|
-
const f = await ensureSuccess(label, r, p.cwd);
|
|
73
|
+
const f = await ensureSuccess(label, r, p.cwd, { mutating: true });
|
|
74
74
|
return {
|
|
75
75
|
content: [{ type: "text", text: renderText(label, f) }],
|
|
76
76
|
details: { result: f },
|