@henryqw/pi-herdr-tools 1.3.3 → 1.4.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 +39 -1
- package/docs/workflow-overview.html +33 -26
- package/docs/workflow-overview.svg +32 -25
- package/extensions/index.ts +2 -0
- package/internal/clone-tab.ts +42 -219
- package/internal/handoff.ts +108 -0
- package/internal/worktree.ts +244 -0
- package/package.json +6 -5
- package/prompts/handoff-worktree.md +13 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@henryqw/pi-herdr-tools`
|
|
2
2
|
|
|
3
|
-
Clone a conversation into a Herdr tab or worktree. Ask a side question and merge its answer back into Main. Give the conversation a name, and finish the worktree safely. One package and one extension entry point supply `/clone-tab`, `/clone-worktree`, `/btw`, `/rename`, and `/done`. Herdr also shows the pane as blocked while a Pi dialog waits for your answer.
|
|
3
|
+
Clone a conversation into a Herdr tab or worktree. Hand off a separate issue to a new agent in a new worktree. Ask a side question and merge its answer back into Main. Give the conversation a name, and finish the worktree safely. One package and one extension entry point supply `/clone-tab`, `/clone-worktree`, `/handoff-worktree`, `herdr_handoff`, `/btw`, `/rename`, and `/done`. Herdr also shows the pane as blocked while a Pi dialog waits for your answer.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -43,10 +43,14 @@ The shared [`@henryqw/pi-herdr`](https://pi.henry.wang/packages/pi-herdr) CLI li
|
|
|
43
43
|
|
|
44
44
|
Run `/clone-tab` to open a clone tab with an active-path clone. The original session stays open.
|
|
45
45
|
|
|
46
|
+
Commands and prompts are for humans. Tools are for agents.
|
|
47
|
+
|
|
46
48
|
| Surface | Type | Purpose |
|
|
47
49
|
| --- | --- | --- |
|
|
48
50
|
| `/clone-tab` | command | Clone the current conversation into a new tab of the current Herdr workspace. |
|
|
49
51
|
| `/clone-worktree` | command | Clone the current conversation into a new Herdr Git worktree workspace. |
|
|
52
|
+
| `/handoff-worktree <issue>` | prompt | For humans: tell the agent to write a self-contained task prompt for the issue and send it with `herdr_handoff`. |
|
|
53
|
+
| `herdr_handoff` | tool | For agents inside Herdr: create a worktree from local `main`, start Pi in it, and submit a task prompt. Focus does not change. |
|
|
50
54
|
| `/done [-f]` | command | Remove the current worktree and close its Herdr workspace tabs. `-f` (alias `--force`) skips confirmation. It permits forced removal and does not look for tabs in other workspaces. |
|
|
51
55
|
| `/btw [<question...>]` | command | For humans in Main: open an empty side pane, or start with an editable question draft. |
|
|
52
56
|
| `/btw ask <question...>` | command | For humans in Main: ask a question whose first word is `ask`, `config`, `merge`, or `help`. |
|
|
@@ -66,6 +70,19 @@ The active path goes from the session root to its current leaf at the time that
|
|
|
66
70
|
|
|
67
71
|
Commit or discard changes in a Herdr-managed linked worktree. Then run `/done` and confirm. Normal removal does not remove dirty worktrees or worktrees that another Herdr workspace uses. **`/done -f` can irreversibly delete uncommitted work and leave other workspaces' tabs pointing at the removed checkout.**
|
|
68
72
|
|
|
73
|
+
### Hand off an issue
|
|
74
|
+
|
|
75
|
+
In a Herdr-managed Pi pane, run `/handoff-worktree Fix the flaky login test`. The agent collects the missing facts and writes one self-contained task prompt. It asks one question only when the repository, expected outcome, or a critical constraint is not clear. Then it calls `herdr_handoff` and reports the workspace, branch, checkout path, and agent name. It does not wait for the new agent to finish. A worktree handoff does not copy the current conversation.
|
|
76
|
+
|
|
77
|
+
`herdr_handoff` requires `HERDR_ENV=1`, `HERDR_PANE_ID`, and a local `main` branch. The current directory and the Herdr workspace of the pane must be in the same Git repository. Pi registers the tool only when `HERDR_ENV=1`. It takes these parameters, and each must be a non-empty string:
|
|
78
|
+
|
|
79
|
+
| Parameter | Purpose |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `branch` | New Git branch. Herdr creates it from local `main`. The branch must not exist. |
|
|
82
|
+
| `label` | Herdr workspace label. |
|
|
83
|
+
| `agentName` | Unique Herdr agent name for the new Pi agent. It must match `^[a-z][a-z0-9_-]{0,31}$`. |
|
|
84
|
+
| `prompt` | Self-contained task prompt. |
|
|
85
|
+
|
|
69
86
|
### Ask and merge side questions
|
|
70
87
|
|
|
71
88
|
In a Herdr-managed Pi pane with a conversation, run `/btw Why is this test failing?`. A side pane opens with the question as an editable draft. Submit the question there. After the answer, run `/btw merge Apply the smallest safe fix`. Main receives the side-thread transcript, gets the focus again, and continues with the merge prompt.
|
|
@@ -108,6 +125,15 @@ Nested dialogs count as one block. `ask_question` from `@henryqw/pi-ask-question
|
|
|
108
125
|
5. Start Pi in the chosen pane with `--session <absolute-clone-file>`.
|
|
109
126
|
6. Focus the tab of the clone after Pi starts successfully.
|
|
110
127
|
|
|
128
|
+
#### Hand off to a worktree
|
|
129
|
+
|
|
130
|
+
1. Make sure that the current directory and the workspace of the pane are in the same Git repository, that the repository has a local `main` branch, and that `branch` does not exist. If not, stop before Herdr changes.
|
|
131
|
+
2. Find the parent workspace of the repository, as `/clone-worktree` does.
|
|
132
|
+
3. Run `herdr worktree create --workspace <parent> --branch <branch> --base main --label <label> --no-focus`.
|
|
133
|
+
4. Select the root pane, or an additional tab when a worktree-layout plugin uses the root pane, as `/clone-worktree` does. Start Pi with the agent name and no session arguments.
|
|
134
|
+
5. Submit the prompt with `herdr agent prompt <agentName> <prompt> --wait --until working --timeout 30000`. The prompt goes to Herdr as one argument, not through a shell or a file.
|
|
135
|
+
6. Return the workspace ID, tab, pane, branch, checkout path, and agent name. Focus does not change.
|
|
136
|
+
|
|
111
137
|
#### Finish a worktree
|
|
112
138
|
|
|
113
139
|

|
|
@@ -198,6 +224,18 @@ The Pi session stores each generated display title and semantic branch in a `pi-
|
|
|
198
224
|
| Agent start attempted | The launch outcome can be unknown. Keep the target tab, panes, and session file. The error reports known IDs for recovery. |
|
|
199
225
|
| Focus failed after Pi started | Show a warning, but do not report the clone that already started as failed. If necessary, focus the clone tab in Herdr. |
|
|
200
226
|
|
|
227
|
+
### Worktree handoff
|
|
228
|
+
|
|
229
|
+
| Outcome | What remains and how to recover |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| No local `main`, an existing branch, a different repository, missing Herdr environment, or failed worktree creation | Nothing. The tool stops before it creates a worktree. |
|
|
232
|
+
| Tool call cancelled | Before worktree creation, nothing remains. After it, the tool does not start the agent or submit the prompt, and the error names the resources that remain. |
|
|
233
|
+
| Killed or incomplete worktree creation | Herdr can keep partial workspace state. The error reports all returned IDs and tells you to examine `herdr workspace list`. |
|
|
234
|
+
| Agent start or prompt delivery failed | The worktree, branch, workspace, tab, and pane remain. The error names each of them and the agent. The tool does no cleanup. A stalled or timed-out prompt can still be delivered. Before you send it again, examine the agent with `herdr agent get` and `herdr agent read`. Send the prompt again with `herdr agent prompt` only when the agent did not get it. Or run `/done` in the new worktree. |
|
|
235
|
+
| Agent was already blocked, or did not start to work | Herdr reports `agent_blocked` or `agent_prompt_stalled`. The tool reports this as a prompt delivery failure. |
|
|
236
|
+
|
|
237
|
+
If you also keep a personal prompt template with the name `handoff-worktree.md`, Pi loads only one of the two and reports a name collision. Remove the personal copy to use the package prompt.
|
|
238
|
+
|
|
201
239
|
### Worktree completion
|
|
202
240
|
|
|
203
241
|
- `/done` does not remove a dirty worktree. Commit or discard changes first. `/done -f` passes `--force` to Git and can irreversibly delete uncommitted work.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Herdr tools: one extension,
|
|
6
|
+
<title>Herdr tools: one extension, five workflows</title>
|
|
7
7
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
8
8
|
<style>
|
|
9
9
|
* { box-sizing: border-box; }
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<main>
|
|
19
19
|
<div class="diagram-container">
|
|
20
20
|
<svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="workflow-overview-title workflow-overview-desc">
|
|
21
|
-
<title id="workflow-overview-title">Herdr tools: one extension,
|
|
22
|
-
<desc id="workflow-overview-desc">One Pi extension registers clone, side-question, title, and completion handlers that share the Herdr CLI library, with task-model routing used only for side questions and titles.</desc>
|
|
21
|
+
<title id="workflow-overview-title">Herdr tools: one extension, five workflows</title>
|
|
22
|
+
<desc id="workflow-overview-desc">One Pi extension registers clone, handoff, side-question, title, and completion handlers that share the Herdr CLI library, with task-model routing used only for side questions and titles.</desc>
|
|
23
23
|
<defs>
|
|
24
24
|
<style>
|
|
25
25
|
.title { fill: #101828; font: 400 28px 'Instrument Serif', Georgia, serif; }
|
|
@@ -36,15 +36,16 @@
|
|
|
36
36
|
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0 0 L8 3 L0 6 Z" fill="#1d4ed8"/></marker>
|
|
37
37
|
</defs>
|
|
38
38
|
<rect width="1280" height="720" fill="#f0eee9"/>
|
|
39
|
-
<text class="title" x="40" y="56">One extension,
|
|
39
|
+
<text class="title" x="40" y="56">One extension, five workflows</text>
|
|
40
40
|
<text class="technical" x="1240" y="56" text-anchor="end">@henryqw/pi-herdr-tools</text>
|
|
41
41
|
<rect x="552" y="76" width="288" height="540" rx="8" fill="#e6e4de" stroke="#101828" stroke-opacity=".12"/>
|
|
42
42
|
<text class="tag" x="568" y="92">INTERNAL HANDLERS</text>
|
|
43
43
|
<path class="primary" d="M208 336 H248"/>
|
|
44
|
-
<path class="edge" d="M464
|
|
45
|
-
<path class="edge" d="M464
|
|
46
|
-
<path class="edge" d="M464
|
|
47
|
-
<path class="edge" d="M464
|
|
44
|
+
<path class="edge" d="M464 272 H488 Q496 272 496 264 V156 Q496 148 504 148 H568"/>
|
|
45
|
+
<path class="edge" d="M464 304 H500 Q508 304 508 296 V260 Q508 252 516 252 H568"/>
|
|
46
|
+
<path class="edge" d="M464 336 H512 Q520 336 520 344 V348 Q520 356 528 356 H568"/>
|
|
47
|
+
<path class="edge" d="M464 368 H536 Q544 368 544 376 V452 Q544 460 552 460 H568"/>
|
|
48
|
+
<path class="edge" d="M464 400 H524 Q532 400 532 408 V556 Q532 564 540 564 H568"/>
|
|
48
49
|
<path class="edge" d="M840 256 H1040"/>
|
|
49
50
|
<path class="edge" d="M840 436 H1040"/>
|
|
50
51
|
<rect x="896" y="232" width="88" height="14" rx="2" fill="#f0eee9"/>
|
|
@@ -62,38 +63,44 @@
|
|
|
62
63
|
<text class="tag" x="264" y="280">ONE LOADED EXTENSION</text>
|
|
63
64
|
<text class="name" x="264" y="316">Herdr tools</text>
|
|
64
65
|
<text class="technical" x="264" y="340">extensions/index.ts</text>
|
|
65
|
-
<text class="body" x="264" y="372">Registers all
|
|
66
|
+
<text class="body" x="264" y="372">Registers all five handlers.</text>
|
|
66
67
|
<text class="body" x="264" y="392">Not separate resources.</text>
|
|
67
68
|
</g>
|
|
68
69
|
<g>
|
|
69
|
-
<rect class="node" x="568" y="
|
|
70
|
-
<text class="technical" x="584" y="
|
|
71
|
-
<text class="name" x="584" y="
|
|
72
|
-
<text class="technical" x="584" y="
|
|
70
|
+
<rect class="node" x="568" y="104" width="240" height="88" rx="6"/>
|
|
71
|
+
<text class="technical" x="584" y="128">internal/clone-tab.ts</text>
|
|
72
|
+
<text class="name" x="584" y="152">Clone conversations</text>
|
|
73
|
+
<text class="technical" x="584" y="176">/clone-tab · /clone-worktree</text>
|
|
73
74
|
</g>
|
|
74
75
|
<g>
|
|
75
|
-
<rect class="node" x="568" y="
|
|
76
|
-
<text class="technical" x="584" y="
|
|
77
|
-
<text class="name" x="584" y="
|
|
78
|
-
<text class="technical" x="584" y="
|
|
76
|
+
<rect class="node" x="568" y="208" width="240" height="88" rx="6"/>
|
|
77
|
+
<text class="technical" x="584" y="232">internal/handoff.ts</text>
|
|
78
|
+
<text class="name" x="584" y="256">Hand off issues</text>
|
|
79
|
+
<text class="technical" x="584" y="280">/handoff-worktree · herdr_handoff</text>
|
|
79
80
|
</g>
|
|
80
81
|
<g>
|
|
81
|
-
<rect class="node" x="568" y="
|
|
82
|
-
<text class="technical" x="584" y="
|
|
83
|
-
<text class="name" x="584" y="
|
|
84
|
-
<text class="technical" x="584" y="
|
|
82
|
+
<rect class="node" x="568" y="312" width="240" height="88" rx="6"/>
|
|
83
|
+
<text class="technical" x="584" y="336">internal/btw.ts</text>
|
|
84
|
+
<text class="name" x="584" y="360">Ask and merge side questions</text>
|
|
85
|
+
<text class="technical" x="584" y="384">/btw · /btw merge</text>
|
|
85
86
|
</g>
|
|
86
87
|
<g>
|
|
87
|
-
<rect class="node" x="568" y="
|
|
88
|
-
<text class="technical" x="584" y="
|
|
89
|
-
<text class="name" x="584" y="
|
|
90
|
-
<text class="technical" x="584" y="
|
|
88
|
+
<rect class="node" x="568" y="416" width="240" height="88" rx="6"/>
|
|
89
|
+
<text class="technical" x="584" y="440">internal/rename.ts</text>
|
|
90
|
+
<text class="name" x="584" y="464">Name conversations</text>
|
|
91
|
+
<text class="technical" x="584" y="488">/rename · automatic titles</text>
|
|
92
|
+
</g>
|
|
93
|
+
<g>
|
|
94
|
+
<rect class="node" x="568" y="520" width="240" height="88" rx="6"/>
|
|
95
|
+
<text class="technical" x="584" y="544">internal/done.ts</text>
|
|
96
|
+
<text class="name" x="584" y="568">Finish worktrees</text>
|
|
97
|
+
<text class="technical" x="584" y="592">/done [--force]</text>
|
|
91
98
|
</g>
|
|
92
99
|
<g>
|
|
93
100
|
<rect class="node" x="1040" y="208" width="200" height="96" rx="6"/>
|
|
94
101
|
<text class="technical" x="1056" y="232">@henryqw/pi-herdr</text>
|
|
95
102
|
<text class="name" x="1056" y="256">Shared Herdr CLI library</text>
|
|
96
|
-
<text class="body" x="1056" y="280">Used by all
|
|
103
|
+
<text class="body" x="1056" y="280">Used by all five handlers.</text>
|
|
97
104
|
</g>
|
|
98
105
|
<g>
|
|
99
106
|
<rect class="node" x="1040" y="388" width="200" height="96" rx="6"/>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="workflow-overview-title workflow-overview-desc" id="workflow-overview-root">
|
|
3
|
-
<title id="workflow-overview-title">Herdr tools: one extension,
|
|
4
|
-
<desc id="workflow-overview-desc">One Pi extension registers clone, side-question, title, and completion handlers that share the Herdr CLI library, with task-model routing used only for side questions and titles.</desc>
|
|
3
|
+
<title id="workflow-overview-title">Herdr tools: one extension, five workflows</title>
|
|
4
|
+
<desc id="workflow-overview-desc">One Pi extension registers clone, handoff, side-question, title, and completion handlers that share the Herdr CLI library, with task-model routing used only for side questions and titles.</desc>
|
|
5
5
|
<defs>
|
|
6
6
|
<style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&family=Noto+Serif:ital@0;1&family=Noto+Sans+KR:wght@400;500;600&family=Noto+Serif+KR:wght@400&family=Noto+Sans+TC:wght@400;500;600&family=Noto+Serif+TC:wght@400&display=swap');
|
|
7
7
|
#workflow-overview-root { color: #101828; }
|
|
@@ -31,15 +31,16 @@
|
|
|
31
31
|
<marker id="workflow-overview-arrow-accent" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0 0 L8 3 L0 6 Z" fill="#1d4ed8"/></marker>
|
|
32
32
|
</defs>
|
|
33
33
|
<rect width="1280" height="720" fill="#f0eee9"/>
|
|
34
|
-
<text class="title" x="40" y="56">One extension,
|
|
34
|
+
<text class="title" x="40" y="56">One extension, five workflows</text>
|
|
35
35
|
<text class="technical" x="1240" y="56" text-anchor="end">@henryqw/pi-herdr-tools</text>
|
|
36
36
|
<rect x="552" y="76" width="288" height="540" rx="8" fill="#e6e4de" stroke="#101828" stroke-opacity=".12"/>
|
|
37
37
|
<text class="tag" x="568" y="92">INTERNAL HANDLERS</text>
|
|
38
38
|
<path class="primary" d="M208 336 H248"/>
|
|
39
|
-
<path class="edge" d="M464
|
|
40
|
-
<path class="edge" d="M464
|
|
41
|
-
<path class="edge" d="M464
|
|
42
|
-
<path class="edge" d="M464
|
|
39
|
+
<path class="edge" d="M464 272 H488 Q496 272 496 264 V156 Q496 148 504 148 H568"/>
|
|
40
|
+
<path class="edge" d="M464 304 H500 Q508 304 508 296 V260 Q508 252 516 252 H568"/>
|
|
41
|
+
<path class="edge" d="M464 336 H512 Q520 336 520 344 V348 Q520 356 528 356 H568"/>
|
|
42
|
+
<path class="edge" d="M464 368 H536 Q544 368 544 376 V452 Q544 460 552 460 H568"/>
|
|
43
|
+
<path class="edge" d="M464 400 H524 Q532 400 532 408 V556 Q532 564 540 564 H568"/>
|
|
43
44
|
<path class="edge" d="M840 256 H1040"/>
|
|
44
45
|
<path class="edge" d="M840 436 H1040"/>
|
|
45
46
|
<rect x="896" y="232" width="88" height="14" rx="2" fill="#f0eee9"/>
|
|
@@ -57,38 +58,44 @@
|
|
|
57
58
|
<text class="tag" x="264" y="280">ONE LOADED EXTENSION</text>
|
|
58
59
|
<text class="name" x="264" y="316">Herdr tools</text>
|
|
59
60
|
<text class="technical" x="264" y="340">extensions/index.ts</text>
|
|
60
|
-
<text class="body" x="264" y="372">Registers all
|
|
61
|
+
<text class="body" x="264" y="372">Registers all five handlers.</text>
|
|
61
62
|
<text class="body" x="264" y="392">Not separate resources.</text>
|
|
62
63
|
</g>
|
|
63
64
|
<g>
|
|
64
|
-
<rect class="node" x="568" y="
|
|
65
|
-
<text class="technical" x="584" y="
|
|
66
|
-
<text class="name" x="584" y="
|
|
67
|
-
<text class="technical" x="584" y="
|
|
65
|
+
<rect class="node" x="568" y="104" width="240" height="88" rx="6"/>
|
|
66
|
+
<text class="technical" x="584" y="128">internal/clone-tab.ts</text>
|
|
67
|
+
<text class="name" x="584" y="152">Clone conversations</text>
|
|
68
|
+
<text class="technical" x="584" y="176">/clone-tab · /clone-worktree</text>
|
|
68
69
|
</g>
|
|
69
70
|
<g>
|
|
70
|
-
<rect class="node" x="568" y="
|
|
71
|
-
<text class="technical" x="584" y="
|
|
72
|
-
<text class="name" x="584" y="
|
|
73
|
-
<text class="technical" x="584" y="
|
|
71
|
+
<rect class="node" x="568" y="208" width="240" height="88" rx="6"/>
|
|
72
|
+
<text class="technical" x="584" y="232">internal/handoff.ts</text>
|
|
73
|
+
<text class="name" x="584" y="256">Hand off issues</text>
|
|
74
|
+
<text class="technical" x="584" y="280">/handoff-worktree · herdr_handoff</text>
|
|
74
75
|
</g>
|
|
75
76
|
<g>
|
|
76
|
-
<rect class="node" x="568" y="
|
|
77
|
-
<text class="technical" x="584" y="
|
|
78
|
-
<text class="name" x="584" y="
|
|
79
|
-
<text class="technical" x="584" y="
|
|
77
|
+
<rect class="node" x="568" y="312" width="240" height="88" rx="6"/>
|
|
78
|
+
<text class="technical" x="584" y="336">internal/btw.ts</text>
|
|
79
|
+
<text class="name" x="584" y="360">Ask and merge side questions</text>
|
|
80
|
+
<text class="technical" x="584" y="384">/btw · /btw merge</text>
|
|
80
81
|
</g>
|
|
81
82
|
<g>
|
|
82
|
-
<rect class="node" x="568" y="
|
|
83
|
-
<text class="technical" x="584" y="
|
|
84
|
-
<text class="name" x="584" y="
|
|
85
|
-
<text class="technical" x="584" y="
|
|
83
|
+
<rect class="node" x="568" y="416" width="240" height="88" rx="6"/>
|
|
84
|
+
<text class="technical" x="584" y="440">internal/rename.ts</text>
|
|
85
|
+
<text class="name" x="584" y="464">Name conversations</text>
|
|
86
|
+
<text class="technical" x="584" y="488">/rename · automatic titles</text>
|
|
87
|
+
</g>
|
|
88
|
+
<g>
|
|
89
|
+
<rect class="node" x="568" y="520" width="240" height="88" rx="6"/>
|
|
90
|
+
<text class="technical" x="584" y="544">internal/done.ts</text>
|
|
91
|
+
<text class="name" x="584" y="568">Finish worktrees</text>
|
|
92
|
+
<text class="technical" x="584" y="592">/done [--force]</text>
|
|
86
93
|
</g>
|
|
87
94
|
<g>
|
|
88
95
|
<rect class="node" x="1040" y="208" width="200" height="96" rx="6"/>
|
|
89
96
|
<text class="technical" x="1056" y="232">@henryqw/pi-herdr</text>
|
|
90
97
|
<text class="name" x="1056" y="256">Shared Herdr CLI library</text>
|
|
91
|
-
<text class="body" x="1056" y="280">Used by all
|
|
98
|
+
<text class="body" x="1056" y="280">Used by all five handlers.</text>
|
|
92
99
|
</g>
|
|
93
100
|
<g>
|
|
94
101
|
<rect class="node" x="1040" y="388" width="200" height="96" rx="6"/>
|
package/extensions/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import registerBtwExtension from "../internal/btw.ts";
|
|
3
3
|
import herdrCloneExtension from "../internal/clone-tab.ts";
|
|
4
4
|
import herdrDoneExtension from "../internal/done.ts";
|
|
5
|
+
import herdrHandoffExtension from "../internal/handoff.ts";
|
|
5
6
|
import herdrRenameExtension from "../internal/rename.ts";
|
|
6
7
|
|
|
7
8
|
export default async function herdrToolsExtension(pi: ExtensionAPI): Promise<void> {
|
|
@@ -9,6 +10,7 @@ export default async function herdrToolsExtension(pi: ExtensionAPI): Promise<voi
|
|
|
9
10
|
await registerBtwExtension(pi);
|
|
10
11
|
herdrCloneExtension(pi);
|
|
11
12
|
herdrDoneExtension(pi);
|
|
13
|
+
herdrHandoffExtension(pi);
|
|
12
14
|
registerDialogStatus(pi);
|
|
13
15
|
}
|
|
14
16
|
|
package/internal/clone-tab.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { stat, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
5
4
|
import {
|
|
6
5
|
CURRENT_SESSION_VERSION,
|
|
7
6
|
SessionManager,
|
|
@@ -12,35 +11,25 @@ import {
|
|
|
12
11
|
import {
|
|
13
12
|
createHerdrClient,
|
|
14
13
|
herdrCommandFailure,
|
|
15
|
-
startPiAgent,
|
|
16
14
|
withWorktreeLock,
|
|
17
15
|
type HerdrClient,
|
|
18
16
|
type HerdrExecResult,
|
|
19
17
|
} from "@henryqw/pi-herdr";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is missing.`);
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type SourceContext = {
|
|
18
|
+
import {
|
|
19
|
+
createWorktree,
|
|
20
|
+
errorMessage,
|
|
21
|
+
launchPiAgent,
|
|
22
|
+
parseCreatedTab,
|
|
23
|
+
prepareWorktreeLaunch,
|
|
24
|
+
requiredString,
|
|
25
|
+
resolveWorkspace,
|
|
26
|
+
worktreeParentWorkspace,
|
|
27
|
+
type HerdrWorkspace,
|
|
28
|
+
} from "./worktree.ts";
|
|
29
|
+
|
|
30
|
+
type SourceContext = HerdrWorkspace & {
|
|
38
31
|
sessionFile: string;
|
|
39
32
|
leafId: string;
|
|
40
|
-
workspaceId: string;
|
|
41
|
-
checkout: string | undefined;
|
|
42
|
-
repoRoot: string | undefined;
|
|
43
|
-
isLinkedWorktree: boolean;
|
|
44
33
|
/** False while Pi has not flushed the session yet (no conversation message on disk). */
|
|
45
34
|
persisted: boolean;
|
|
46
35
|
};
|
|
@@ -71,26 +60,7 @@ async function resolveSource(
|
|
|
71
60
|
persisted = false;
|
|
72
61
|
}
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
const pane = (paneResponse as { result?: { pane?: { pane_id?: unknown; workspace_id?: unknown } } }).result?.pane;
|
|
76
|
-
requiredString(pane?.pane_id, "Herdr pane response pane_id");
|
|
77
|
-
const workspaceId = requiredString(pane?.workspace_id, "Herdr pane response workspace_id");
|
|
78
|
-
const workspaceResponse = await herdr.json(["workspace", "get", workspaceId], { cwd: ctx.cwd });
|
|
79
|
-
const workspace = (workspaceResponse as { result?: { workspace?: WorkspaceInfo } }).result?.workspace;
|
|
80
|
-
if (requiredString(workspace?.workspace_id, "Herdr workspace response workspace_id") !== workspaceId) {
|
|
81
|
-
throw new Error(`Herdr workspace response did not match ${workspaceId}.`);
|
|
82
|
-
}
|
|
83
|
-
const checkout = workspace?.worktree == null
|
|
84
|
-
? undefined
|
|
85
|
-
: requiredString(workspace.worktree.checkout_path, "Herdr workspace response checkout_path");
|
|
86
|
-
const repoRoot = typeof workspace?.worktree?.repo_root === "string" && workspace.worktree.repo_root.trim()
|
|
87
|
-
? workspace.worktree.repo_root
|
|
88
|
-
: undefined;
|
|
89
|
-
const isLinkedWorktree = workspace?.worktree?.is_linked_worktree === true;
|
|
90
|
-
if (isLinkedWorktree && !repoRoot) {
|
|
91
|
-
throw new Error("Herdr workspace response is missing worktree.repo_root for a linked worktree.");
|
|
92
|
-
}
|
|
93
|
-
return { sessionFile, leafId, workspaceId, checkout, repoRoot, isLinkedWorktree, persisted };
|
|
63
|
+
return { sessionFile, leafId, persisted, ...await resolveWorkspace(herdr, ctx.cwd, requestedPaneId) };
|
|
94
64
|
}
|
|
95
65
|
|
|
96
66
|
// Mirror of SessionManager.createBranchedSession for sessions Pi has not
|
|
@@ -138,46 +108,6 @@ async function createBranchedClone(
|
|
|
138
108
|
throw new Error(`Pi clone session path is not a file: ${cloneFile}`);
|
|
139
109
|
}
|
|
140
110
|
|
|
141
|
-
// A worktree layout plugin (e.g. herdr-plus) can start its own agent in a new
|
|
142
|
-
// worktree's root pane right after creation. Give it a moment to appear so the
|
|
143
|
-
// clone can move to its own tab instead of failing with agent_pane_busy.
|
|
144
|
-
async function waitForRootPaneAgent(
|
|
145
|
-
herdr: HerdrClient<{ cwd: string }>,
|
|
146
|
-
ctx: ExtensionCommandContext,
|
|
147
|
-
paneId: string,
|
|
148
|
-
): Promise<string | undefined> {
|
|
149
|
-
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
150
|
-
const response = await herdr.json(["pane", "get", paneId], { cwd: ctx.cwd });
|
|
151
|
-
const agent = (response as { result?: { pane?: { agent?: unknown } } }).result?.pane?.agent;
|
|
152
|
-
if (typeof agent === "string" && agent.trim()) return agent;
|
|
153
|
-
await delay(250);
|
|
154
|
-
}
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseCreatedTab(createdTab: HerdrExecResult, workspaceId: string): { tabId: string; rootPaneId: string } {
|
|
159
|
-
let tabId: string | undefined;
|
|
160
|
-
let rootPaneId: string | undefined;
|
|
161
|
-
try {
|
|
162
|
-
const response: unknown = JSON.parse(createdTab.stdout);
|
|
163
|
-
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
164
|
-
throw new Error("Herdr tab create returned invalid JSON");
|
|
165
|
-
}
|
|
166
|
-
const result = (response as { result?: { tab?: { tab_id?: unknown }; root_pane?: { pane_id?: unknown } } }).result;
|
|
167
|
-
tabId = typeof result?.tab?.tab_id === "string" && result.tab.tab_id.trim() ? result.tab.tab_id : undefined;
|
|
168
|
-
rootPaneId = typeof result?.root_pane?.pane_id === "string" && result.root_pane.pane_id.trim()
|
|
169
|
-
? result.root_pane.pane_id
|
|
170
|
-
: undefined;
|
|
171
|
-
const missing = [!tabId && "tab_id", !rootPaneId && "root_pane.pane_id"].filter(Boolean).join(", ");
|
|
172
|
-
if (missing) throw new Error(`Herdr tab create response is missing ${missing}.`);
|
|
173
|
-
return { tabId: tabId!, rootPaneId: rootPaneId! };
|
|
174
|
-
} catch (error) {
|
|
175
|
-
const retained = [`workspace ${workspaceId}`, tabId && `tab ${tabId}`, rootPaneId && `root pane ${rootPaneId}`]
|
|
176
|
-
.filter(Boolean).join(", ");
|
|
177
|
-
throw new Error(`Herdr tab create response could not be parsed; retained ${retained}: ${errorMessage(error)}`, { cause: error });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
111
|
async function discardCloneOrAggregate(cloneFile: string, error: Error): Promise<never> {
|
|
182
112
|
try {
|
|
183
113
|
await unlink(cloneFile);
|
|
@@ -190,42 +120,7 @@ async function discardCloneOrAggregate(cloneFile: string, error: Error): Promise
|
|
|
190
120
|
throw error;
|
|
191
121
|
}
|
|
192
122
|
|
|
193
|
-
|
|
194
|
-
herdr: HerdrClient<{ cwd: string }>,
|
|
195
|
-
ctx: ExtensionCommandContext,
|
|
196
|
-
rootPaneId: string,
|
|
197
|
-
cloneFile: string,
|
|
198
|
-
retained: string,
|
|
199
|
-
onPaneBusy?: () => Promise<{ rootPaneId: string; retained: string }>,
|
|
200
|
-
): Promise<string> {
|
|
201
|
-
const agentName = `clone-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
202
|
-
try {
|
|
203
|
-
const result = await startPiAgent(herdr, {
|
|
204
|
-
name: agentName,
|
|
205
|
-
pane: rootPaneId,
|
|
206
|
-
args: ["--session", cloneFile],
|
|
207
|
-
options: { cwd: ctx.cwd },
|
|
208
|
-
onPaneBusy: onPaneBusy
|
|
209
|
-
? async () => {
|
|
210
|
-
const next = await onPaneBusy!();
|
|
211
|
-
rootPaneId = next.rootPaneId;
|
|
212
|
-
retained = next.retained;
|
|
213
|
-
return rootPaneId;
|
|
214
|
-
}
|
|
215
|
-
: undefined,
|
|
216
|
-
});
|
|
217
|
-
if (result.code === 0 && !result.killed) return agentName;
|
|
218
|
-
const startArgs = [
|
|
219
|
-
"agent", "start", agentName, "--kind", "pi", "--pane", rootPaneId, "--", "--session", cloneFile,
|
|
220
|
-
];
|
|
221
|
-
throw new Error(herdrCommandFailure(startArgs, result));
|
|
222
|
-
} catch (error) {
|
|
223
|
-
throw new Error(
|
|
224
|
-
`Clone launch could not be confirmed after starting agent ${agentName}; retained ${retained}: ${errorMessage(error)}`,
|
|
225
|
-
{ cause: error },
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
123
|
+
const cloneAgentName = () => `clone-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
229
124
|
|
|
230
125
|
export default function herdrCloneExtension(pi: ExtensionAPI): void {
|
|
231
126
|
const herdr = createHerdrClient<{ cwd: string }>(pi.exec.bind(pi));
|
|
@@ -254,7 +149,14 @@ export default function herdrCloneExtension(pi: ExtensionAPI): void {
|
|
|
254
149
|
);
|
|
255
150
|
}
|
|
256
151
|
const { tabId, rootPaneId } = tab;
|
|
257
|
-
const agentName =
|
|
152
|
+
const agentName = cloneAgentName();
|
|
153
|
+
await launchPiAgent(herdr, ctx.cwd, {
|
|
154
|
+
action: "Clone",
|
|
155
|
+
name: agentName,
|
|
156
|
+
pane: rootPaneId,
|
|
157
|
+
args: ["--session", cloneFile],
|
|
158
|
+
retained: () => `Herdr tab ${tabId}, root pane ${rootPaneId}, and session ${cloneFile}`,
|
|
159
|
+
});
|
|
258
160
|
|
|
259
161
|
try {
|
|
260
162
|
await herdr.run(["tab", "focus", tabId], { cwd: ctx.cwd });
|
|
@@ -277,115 +179,36 @@ export default function herdrCloneExtension(pi: ExtensionAPI): void {
|
|
|
277
179
|
handler: async (_args, ctx) => {
|
|
278
180
|
const source = await resolveSource("clone-worktree", herdr, ctx);
|
|
279
181
|
|
|
280
|
-
// Herdr only creates worktrees from the repo parent workspace; running
|
|
281
|
-
// /clone-worktree inside a linked worktree must retarget the parent.
|
|
282
|
-
let targetWorkspaceId = source.workspaceId;
|
|
283
|
-
if (source.isLinkedWorktree) {
|
|
284
|
-
const listing = await herdr.json(["workspace", "list"], { cwd: ctx.cwd });
|
|
285
|
-
const result = (listing as { result?: { workspaces?: WorkspaceInfo[] } }).result;
|
|
286
|
-
const parent = (Array.isArray(result?.workspaces) ? result.workspaces : []).find((entry) =>
|
|
287
|
-
entry.worktree != null && !entry.worktree.is_linked_worktree &&
|
|
288
|
-
entry.worktree.checkout_path === source.repoRoot);
|
|
289
|
-
targetWorkspaceId = typeof parent?.workspace_id === "string"
|
|
290
|
-
? parent.workspace_id
|
|
291
|
-
: requiredString(undefined, `Repo parent workspace for ${source.repoRoot} in herdr workspace list`);
|
|
292
|
-
}
|
|
293
182
|
// Create the worktree before the clone so the session header can be
|
|
294
|
-
// stamped with the fresh checkout cwd.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
let workspaceId: string | undefined;
|
|
303
|
-
let tabId: string | undefined;
|
|
304
|
-
let rootPaneId: string | undefined;
|
|
305
|
-
let checkoutPath: string | undefined;
|
|
306
|
-
try {
|
|
307
|
-
const response: unknown = JSON.parse(createdWorktree.stdout);
|
|
308
|
-
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
309
|
-
throw new Error("Herdr worktree create returned invalid JSON");
|
|
310
|
-
}
|
|
311
|
-
// Herdr nests checkout_path under workspace.worktree; the top-level
|
|
312
|
-
// result.worktree uses `path` instead.
|
|
313
|
-
const result = (response as {
|
|
314
|
-
result?: {
|
|
315
|
-
workspace?: { workspace_id?: unknown; worktree?: { checkout_path?: unknown } };
|
|
316
|
-
tab?: { tab_id?: unknown };
|
|
317
|
-
root_pane?: { pane_id?: unknown };
|
|
318
|
-
};
|
|
319
|
-
}).result;
|
|
320
|
-
// Collect every returned identifier before validating so recovery
|
|
321
|
-
// keeps all known IDs even when an earlier field is missing.
|
|
322
|
-
workspaceId = typeof result?.workspace?.workspace_id === "string" ? result.workspace.workspace_id : undefined;
|
|
323
|
-
tabId = typeof result?.tab?.tab_id === "string" ? result.tab.tab_id : undefined;
|
|
324
|
-
rootPaneId = typeof result?.root_pane?.pane_id === "string" ? result.root_pane.pane_id : undefined;
|
|
325
|
-
checkoutPath = typeof result?.workspace?.worktree?.checkout_path === "string" && result.workspace.worktree.checkout_path.trim()
|
|
326
|
-
? result.workspace.worktree.checkout_path
|
|
327
|
-
: undefined;
|
|
328
|
-
if (createdWorktree.killed) {
|
|
329
|
-
throw new Error("Herdr worktree create was killed before completion.");
|
|
330
|
-
}
|
|
331
|
-
const missing = [
|
|
332
|
-
[workspaceId, "workspace_id"],
|
|
333
|
-
[tabId, "tab_id"],
|
|
334
|
-
[rootPaneId, "root_pane.pane_id"],
|
|
335
|
-
[checkoutPath, "workspace.worktree.checkout_path"],
|
|
336
|
-
].filter(([value]) => !value).map(([, label]) => label);
|
|
337
|
-
if (missing.length > 0) {
|
|
338
|
-
throw new Error(`Herdr worktree create response is missing ${missing.join(", ")}.`);
|
|
339
|
-
}
|
|
340
|
-
} catch (error) {
|
|
341
|
-
const known = [
|
|
342
|
-
workspaceId && `workspace ${workspaceId}`,
|
|
343
|
-
tabId && `tab ${tabId}`,
|
|
344
|
-
rootPaneId && `root pane ${rootPaneId}`,
|
|
345
|
-
checkoutPath && `checkout ${checkoutPath}`,
|
|
346
|
-
].filter(Boolean).join(", ");
|
|
347
|
-
throw new Error(
|
|
348
|
-
`Clone could not be confirmed after creating a Herdr worktree${known ? ` (${known})` : ""}; Herdr may have retained a partial worktree workspace, inspect herdr workspace list: ${errorMessage(error)}`,
|
|
349
|
-
{ cause: error },
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// A layout plugin may have claimed the root pane; park the clone in its
|
|
354
|
-
// own tab so both agents coexist.
|
|
355
|
-
let targetTabId = tabId!;
|
|
356
|
-
let targetPaneId = rootPaneId!;
|
|
357
|
-
const createCloneTab = async (): Promise<void> => {
|
|
358
|
-
const cloneTabArgs = ["tab", "create", "--workspace", workspaceId!, "--cwd", checkoutPath!, "--no-focus"] as const;
|
|
359
|
-
const createdTab = await herdr.exec(cloneTabArgs, { cwd: ctx.cwd });
|
|
360
|
-
if (createdTab.code !== 0 || createdTab.killed) {
|
|
361
|
-
throw new Error(herdrCommandFailure(cloneTabArgs, createdTab));
|
|
362
|
-
}
|
|
363
|
-
({ tabId: targetTabId, rootPaneId: targetPaneId } = parseCreatedTab(createdTab, workspaceId!));
|
|
364
|
-
};
|
|
365
|
-
if (await waitForRootPaneAgent(herdr, ctx, rootPaneId!)) await createCloneTab();
|
|
183
|
+
// stamped with the fresh checkout cwd.
|
|
184
|
+
const worktreeCreateArgs = [
|
|
185
|
+
"worktree", "create", "--workspace", await worktreeParentWorkspace(herdr, ctx.cwd, source), "--no-focus",
|
|
186
|
+
];
|
|
187
|
+
const worktree = await createWorktree(herdr, ctx.cwd, worktreeCreateArgs, "Clone");
|
|
188
|
+
const { workspaceId, tabId, checkoutPath } = worktree;
|
|
189
|
+
const { target, onPaneBusy } = await prepareWorktreeLaunch(herdr, ctx.cwd, worktree);
|
|
366
190
|
|
|
367
191
|
let cloneFile: string;
|
|
368
192
|
try {
|
|
369
193
|
cloneFile = source.checkout
|
|
370
|
-
? await withWorktreeLock(source.checkout, () => createBranchedClone(ctx, source, checkoutPath
|
|
371
|
-
: await createBranchedClone(ctx, source, checkoutPath
|
|
194
|
+
? await withWorktreeLock(source.checkout, () => createBranchedClone(ctx, source, checkoutPath))
|
|
195
|
+
: await createBranchedClone(ctx, source, checkoutPath);
|
|
372
196
|
} catch (error) {
|
|
373
197
|
throw new Error(
|
|
374
198
|
`Clone session could not be created for Herdr worktree workspace ${workspaceId} (tab ${tabId}, checkout ${checkoutPath}); retained worktree without a clone session: ${errorMessage(error)}`,
|
|
375
199
|
{ cause: error },
|
|
376
200
|
);
|
|
377
201
|
}
|
|
378
|
-
const agentName =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
);
|
|
202
|
+
const agentName = cloneAgentName();
|
|
203
|
+
await launchPiAgent(herdr, ctx.cwd, {
|
|
204
|
+
action: "Clone",
|
|
205
|
+
name: agentName,
|
|
206
|
+
pane: target.paneId,
|
|
207
|
+
args: ["--session", cloneFile],
|
|
208
|
+
retained: () => `Herdr workspace ${workspaceId}, tab ${target.tabId}, root pane ${target.paneId}, and session ${cloneFile}`,
|
|
209
|
+
onPaneBusy,
|
|
210
|
+
});
|
|
211
|
+
const targetTabId = target.tabId;
|
|
389
212
|
|
|
390
213
|
try {
|
|
391
214
|
await herdr.run(["tab", "focus", targetTabId], { cwd: ctx.cwd });
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createHerdrClient, herdrCommandFailure } from "@henryqw/pi-herdr";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import {
|
|
5
|
+
createWorktree,
|
|
6
|
+
errorMessage,
|
|
7
|
+
launchPiAgent,
|
|
8
|
+
prepareWorktreeLaunch,
|
|
9
|
+
requiredString,
|
|
10
|
+
resolveWorkspace,
|
|
11
|
+
worktreeParentWorkspace,
|
|
12
|
+
} from "./worktree.ts";
|
|
13
|
+
|
|
14
|
+
const PROMPT_TIMEOUT_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
const nonEmpty = (description: string) => Type.String({ description, minLength: 1, pattern: "\\S" });
|
|
17
|
+
|
|
18
|
+
const HandoffParams = Type.Object({
|
|
19
|
+
branch: nonEmpty("New Git branch for the worktree, created from local main"),
|
|
20
|
+
label: nonEmpty("Short Herdr workspace label"),
|
|
21
|
+
// Herdr agent names must match this pattern; checking it here fails before any mutation.
|
|
22
|
+
agentName: Type.String({
|
|
23
|
+
description: "Unique Herdr agent name for the new Pi agent: lowercase letter, then up to 31 of a-z, 0-9, _ or -",
|
|
24
|
+
pattern: "^[a-z][a-z0-9_-]{0,31}$",
|
|
25
|
+
}),
|
|
26
|
+
prompt: nonEmpty("Self-contained task prompt for the new agent"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** Register `herdr_handoff` only inside Herdr, where it can run. */
|
|
30
|
+
export default function herdrHandoffExtension(pi: ExtensionAPI): void {
|
|
31
|
+
if (process.env.HERDR_ENV !== "1") return;
|
|
32
|
+
const herdr = createHerdrClient<{ cwd: string }>(pi.exec.bind(pi));
|
|
33
|
+
|
|
34
|
+
pi.registerTool({
|
|
35
|
+
name: "herdr_handoff",
|
|
36
|
+
label: "Herdr Handoff",
|
|
37
|
+
description: "Create a Herdr worktree from local main, start Pi in it, and submit a self-contained task prompt. Focus does not change.",
|
|
38
|
+
promptSnippet: "Hand off a separate task to a new Pi agent in a new Herdr worktree from local main",
|
|
39
|
+
promptGuidelines: [
|
|
40
|
+
"Use herdr_handoff only when the user asks to hand off a task to a new worktree, for example through /handoff-worktree.",
|
|
41
|
+
],
|
|
42
|
+
parameters: HandoffParams,
|
|
43
|
+
executionMode: "sequential",
|
|
44
|
+
|
|
45
|
+
async execute(_toolCallId, { branch, label, agentName, prompt }, signal, _onUpdate, ctx) {
|
|
46
|
+
const paneId = requiredString(process.env.HERDR_PANE_ID, "HERDR_PANE_ID");
|
|
47
|
+
const workspace = await resolveWorkspace(herdr, ctx.cwd, paneId);
|
|
48
|
+
const checkout = requiredString(workspace.checkout, `Git checkout of Herdr workspace ${workspace.workspaceId}`);
|
|
49
|
+
const git = (cwd: string, args: string[]) => pi.exec("git", args, { cwd });
|
|
50
|
+
const repository = async (cwd: string) => {
|
|
51
|
+
const result = await git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
52
|
+
if (result.code !== 0 || result.killed) throw new Error(`herdr_handoff: ${cwd} is not in a Git repository; nothing was created.`);
|
|
53
|
+
return result.stdout.trim();
|
|
54
|
+
};
|
|
55
|
+
// The worktree comes from the pane's workspace, so it must be the repository checked here.
|
|
56
|
+
if (await repository(ctx.cwd) !== await repository(checkout)) {
|
|
57
|
+
throw new Error(`herdr_handoff: ${ctx.cwd} and Herdr workspace ${workspace.workspaceId} (${checkout}) are different Git repositories; nothing was created.`);
|
|
58
|
+
}
|
|
59
|
+
const main = await git(ctx.cwd, ["rev-parse", "--verify", "--quiet", "refs/heads/main"]);
|
|
60
|
+
if (main.code !== 0 || main.killed) {
|
|
61
|
+
throw new Error(`herdr_handoff requires a local main branch in ${ctx.cwd}; nothing was created.`);
|
|
62
|
+
}
|
|
63
|
+
// Herdr checks out an existing branch instead of creating it from --base.
|
|
64
|
+
const existing = await git(ctx.cwd, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
65
|
+
if (existing.code !== 1 || existing.killed) {
|
|
66
|
+
throw new Error(`herdr_handoff: branch ${branch} already exists or could not be checked; choose a new branch. Nothing was created.`);
|
|
67
|
+
}
|
|
68
|
+
const parentId = await worktreeParentWorkspace(herdr, ctx.cwd, workspace);
|
|
69
|
+
|
|
70
|
+
signal?.throwIfAborted();
|
|
71
|
+
const worktree = await createWorktree(herdr, ctx.cwd, [
|
|
72
|
+
"worktree", "create", "--workspace", parentId,
|
|
73
|
+
"--branch", branch, "--base", "main", "--label", label, "--no-focus",
|
|
74
|
+
], "Handoff");
|
|
75
|
+
const { workspaceId, checkoutPath } = worktree;
|
|
76
|
+
const { target, onPaneBusy } = await prepareWorktreeLaunch(herdr, ctx.cwd, worktree).catch((error: unknown) => {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Handoff launch preparation failed; retained Herdr workspace ${workspaceId}, tab ${worktree.tabId}, root pane ${worktree.rootPaneId}, branch ${branch}, checkout ${checkoutPath}: ${errorMessage(error)}`,
|
|
79
|
+
{ cause: error },
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
const retained = () =>
|
|
83
|
+
`Herdr workspace ${workspaceId}, tab ${target.tabId}, pane ${target.paneId}, branch ${branch}, checkout ${checkoutPath}`;
|
|
84
|
+
if (signal?.aborted) throw new Error(`Handoff was cancelled before agent start; retained ${retained()}.`);
|
|
85
|
+
await launchPiAgent(herdr, ctx.cwd, { action: "Handoff", name: agentName, pane: target.paneId, args: [], retained, onPaneBusy });
|
|
86
|
+
|
|
87
|
+
const promptArgs = [
|
|
88
|
+
"agent", "prompt", agentName, prompt, "--wait", "--until", "working", "--timeout", String(PROMPT_TIMEOUT_MS),
|
|
89
|
+
];
|
|
90
|
+
if (signal?.aborted) throw new Error(`Handoff was cancelled before prompt delivery; retained ${retained()}, and agent ${agentName}.`);
|
|
91
|
+
const delivered = await herdr.exec(promptArgs, { cwd: ctx.cwd });
|
|
92
|
+
if (delivered.code !== 0 || delivered.killed) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Handoff prompt was not confirmed for agent ${agentName}; retained ${retained()}, and agent ${agentName}: ${herdrCommandFailure(promptArgs, delivered)}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const details = { workspaceId, tabId: target.tabId, paneId: target.paneId, branch, checkoutPath, agentName };
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: "text" as const,
|
|
102
|
+
text: `Handed off to agent ${agentName}, which is working in Herdr workspace ${workspaceId} (tab ${target.tabId}, pane ${target.paneId}, branch ${branch}, checkout ${checkoutPath}). Focus did not change.`,
|
|
103
|
+
}],
|
|
104
|
+
details,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { herdrCommandFailure, startPiAgent, type HerdrClient, type HerdrExecResult } from "@henryqw/pi-herdr";
|
|
3
|
+
|
|
4
|
+
type Herdr = HerdrClient<{ cwd: string }>;
|
|
5
|
+
|
|
6
|
+
type WorkspaceInfo = {
|
|
7
|
+
workspace_id?: unknown;
|
|
8
|
+
worktree?: {
|
|
9
|
+
checkout_path?: unknown;
|
|
10
|
+
repo_root?: unknown;
|
|
11
|
+
is_linked_worktree?: unknown;
|
|
12
|
+
} | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type HerdrWorkspace = {
|
|
16
|
+
workspaceId: string;
|
|
17
|
+
checkout: string | undefined;
|
|
18
|
+
repoRoot: string | undefined;
|
|
19
|
+
isLinkedWorktree: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CreatedWorktree = {
|
|
23
|
+
workspaceId: string;
|
|
24
|
+
tabId: string;
|
|
25
|
+
rootPaneId: string;
|
|
26
|
+
checkoutPath: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type LaunchTarget = { tabId: string; paneId: string };
|
|
30
|
+
|
|
31
|
+
export const errorMessage = (error: unknown) => error instanceof Error ? error.message : String(error);
|
|
32
|
+
|
|
33
|
+
export function requiredString(value: unknown, label: string): string {
|
|
34
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is missing.`);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Resolve the Herdr workspace that owns `paneId`, with its Git worktree identity. */
|
|
39
|
+
export async function resolveWorkspace(herdr: Herdr, cwd: string, paneId: string): Promise<HerdrWorkspace> {
|
|
40
|
+
const paneResponse = await herdr.json(["pane", "get", paneId], { cwd });
|
|
41
|
+
const pane = (paneResponse as { result?: { pane?: { pane_id?: unknown; workspace_id?: unknown } } }).result?.pane;
|
|
42
|
+
requiredString(pane?.pane_id, "Herdr pane response pane_id");
|
|
43
|
+
const workspaceId = requiredString(pane?.workspace_id, "Herdr pane response workspace_id");
|
|
44
|
+
const workspaceResponse = await herdr.json(["workspace", "get", workspaceId], { cwd });
|
|
45
|
+
const workspace = (workspaceResponse as { result?: { workspace?: WorkspaceInfo } }).result?.workspace;
|
|
46
|
+
if (requiredString(workspace?.workspace_id, "Herdr workspace response workspace_id") !== workspaceId) {
|
|
47
|
+
throw new Error(`Herdr workspace response did not match ${workspaceId}.`);
|
|
48
|
+
}
|
|
49
|
+
const checkout = workspace?.worktree == null
|
|
50
|
+
? undefined
|
|
51
|
+
: requiredString(workspace.worktree.checkout_path, "Herdr workspace response checkout_path");
|
|
52
|
+
const repoRoot = typeof workspace?.worktree?.repo_root === "string" && workspace.worktree.repo_root.trim()
|
|
53
|
+
? workspace.worktree.repo_root
|
|
54
|
+
: undefined;
|
|
55
|
+
const isLinkedWorktree = workspace?.worktree?.is_linked_worktree === true;
|
|
56
|
+
if (isLinkedWorktree && !repoRoot) {
|
|
57
|
+
throw new Error("Herdr workspace response is missing worktree.repo_root for a linked worktree.");
|
|
58
|
+
}
|
|
59
|
+
return { workspaceId, checkout, repoRoot, isLinkedWorktree };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Herdr only creates worktrees from the repo parent workspace; a linked
|
|
64
|
+
* worktree must retarget the parent.
|
|
65
|
+
*/
|
|
66
|
+
export async function worktreeParentWorkspace(herdr: Herdr, cwd: string, workspace: HerdrWorkspace): Promise<string> {
|
|
67
|
+
if (!workspace.isLinkedWorktree) return workspace.workspaceId;
|
|
68
|
+
const listing = await herdr.json(["workspace", "list"], { cwd });
|
|
69
|
+
const result = (listing as { result?: { workspaces?: WorkspaceInfo[] } }).result;
|
|
70
|
+
const parent = (Array.isArray(result?.workspaces) ? result.workspaces : []).find((entry) =>
|
|
71
|
+
entry.worktree != null && !entry.worktree.is_linked_worktree &&
|
|
72
|
+
entry.worktree.checkout_path === workspace.repoRoot);
|
|
73
|
+
return typeof parent?.workspace_id === "string"
|
|
74
|
+
? parent.workspace_id
|
|
75
|
+
: requiredString(undefined, `Repo parent workspace for ${workspace.repoRoot} in herdr workspace list`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run `herdr worktree create` and parse its response. A killed or incomplete
|
|
80
|
+
* create is ambiguous: Herdr may have retained partial worktree state, so the
|
|
81
|
+
* error names every returned identifier. `action` prefixes that error.
|
|
82
|
+
*/
|
|
83
|
+
export async function createWorktree(
|
|
84
|
+
herdr: Herdr,
|
|
85
|
+
cwd: string,
|
|
86
|
+
args: readonly string[],
|
|
87
|
+
action: string,
|
|
88
|
+
): Promise<CreatedWorktree> {
|
|
89
|
+
const createdWorktree = await herdr.exec(args, { cwd });
|
|
90
|
+
if (createdWorktree.code !== 0 && !createdWorktree.killed) {
|
|
91
|
+
throw new Error(herdrCommandFailure(args, createdWorktree));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let workspaceId: string | undefined;
|
|
95
|
+
let tabId: string | undefined;
|
|
96
|
+
let rootPaneId: string | undefined;
|
|
97
|
+
let checkoutPath: string | undefined;
|
|
98
|
+
try {
|
|
99
|
+
const response: unknown = JSON.parse(createdWorktree.stdout);
|
|
100
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
101
|
+
throw new Error("Herdr worktree create returned invalid JSON");
|
|
102
|
+
}
|
|
103
|
+
// Herdr nests checkout_path under workspace.worktree; the top-level
|
|
104
|
+
// result.worktree uses `path` instead.
|
|
105
|
+
const result = (response as {
|
|
106
|
+
result?: {
|
|
107
|
+
workspace?: { workspace_id?: unknown; worktree?: { checkout_path?: unknown } };
|
|
108
|
+
tab?: { tab_id?: unknown };
|
|
109
|
+
root_pane?: { pane_id?: unknown };
|
|
110
|
+
};
|
|
111
|
+
}).result;
|
|
112
|
+
// Collect every returned identifier before validating so recovery
|
|
113
|
+
// keeps all known IDs even when an earlier field is missing.
|
|
114
|
+
workspaceId = typeof result?.workspace?.workspace_id === "string" ? result.workspace.workspace_id : undefined;
|
|
115
|
+
tabId = typeof result?.tab?.tab_id === "string" ? result.tab.tab_id : undefined;
|
|
116
|
+
rootPaneId = typeof result?.root_pane?.pane_id === "string" ? result.root_pane.pane_id : undefined;
|
|
117
|
+
checkoutPath = typeof result?.workspace?.worktree?.checkout_path === "string" && result.workspace.worktree.checkout_path.trim()
|
|
118
|
+
? result.workspace.worktree.checkout_path
|
|
119
|
+
: undefined;
|
|
120
|
+
if (createdWorktree.killed) {
|
|
121
|
+
throw new Error("Herdr worktree create was killed before completion.");
|
|
122
|
+
}
|
|
123
|
+
const missing = [
|
|
124
|
+
[workspaceId, "workspace_id"],
|
|
125
|
+
[tabId, "tab_id"],
|
|
126
|
+
[rootPaneId, "root_pane.pane_id"],
|
|
127
|
+
[checkoutPath, "workspace.worktree.checkout_path"],
|
|
128
|
+
].filter(([value]) => !value).map(([, label]) => label);
|
|
129
|
+
if (missing.length > 0) {
|
|
130
|
+
throw new Error(`Herdr worktree create response is missing ${missing.join(", ")}.`);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const known = [
|
|
134
|
+
workspaceId && `workspace ${workspaceId}`,
|
|
135
|
+
tabId && `tab ${tabId}`,
|
|
136
|
+
rootPaneId && `root pane ${rootPaneId}`,
|
|
137
|
+
checkoutPath && `checkout ${checkoutPath}`,
|
|
138
|
+
].filter(Boolean).join(", ");
|
|
139
|
+
throw new Error(
|
|
140
|
+
`${action} could not be confirmed after creating a Herdr worktree${known ? ` (${known})` : ""}; Herdr may have retained a partial worktree workspace, inspect herdr workspace list: ${errorMessage(error)}`,
|
|
141
|
+
{ cause: error },
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return { workspaceId: workspaceId!, tabId: tabId!, rootPaneId: rootPaneId!, checkoutPath: checkoutPath! };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function parseCreatedTab(createdTab: HerdrExecResult, workspaceId: string): { tabId: string; rootPaneId: string } {
|
|
148
|
+
let tabId: string | undefined;
|
|
149
|
+
let rootPaneId: string | undefined;
|
|
150
|
+
try {
|
|
151
|
+
const response: unknown = JSON.parse(createdTab.stdout);
|
|
152
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
153
|
+
throw new Error("Herdr tab create returned invalid JSON");
|
|
154
|
+
}
|
|
155
|
+
const result = (response as { result?: { tab?: { tab_id?: unknown }; root_pane?: { pane_id?: unknown } } }).result;
|
|
156
|
+
tabId = typeof result?.tab?.tab_id === "string" && result.tab.tab_id.trim() ? result.tab.tab_id : undefined;
|
|
157
|
+
rootPaneId = typeof result?.root_pane?.pane_id === "string" && result.root_pane.pane_id.trim()
|
|
158
|
+
? result.root_pane.pane_id
|
|
159
|
+
: undefined;
|
|
160
|
+
const missing = [!tabId && "tab_id", !rootPaneId && "root_pane.pane_id"].filter(Boolean).join(", ");
|
|
161
|
+
if (missing) throw new Error(`Herdr tab create response is missing ${missing}.`);
|
|
162
|
+
return { tabId: tabId!, rootPaneId: rootPaneId! };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const retained = [`workspace ${workspaceId}`, tabId && `tab ${tabId}`, rootPaneId && `root pane ${rootPaneId}`]
|
|
165
|
+
.filter(Boolean).join(", ");
|
|
166
|
+
throw new Error(`Herdr tab create response could not be parsed; retained ${retained}: ${errorMessage(error)}`, { cause: error });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// A worktree layout plugin (e.g. herdr-plus) can start its own agent in a new
|
|
171
|
+
// worktree's root pane right after creation. Give it a moment to appear so the
|
|
172
|
+
// new agent can move to its own tab instead of failing with agent_pane_busy.
|
|
173
|
+
async function waitForRootPaneAgent(herdr: Herdr, cwd: string, paneId: string): Promise<string | undefined> {
|
|
174
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
175
|
+
const response = await herdr.json(["pane", "get", paneId], { cwd });
|
|
176
|
+
const agent = (response as { result?: { pane?: { agent?: unknown } } }).result?.pane?.agent;
|
|
177
|
+
if (typeof agent === "string" && agent.trim()) return agent;
|
|
178
|
+
await delay(250);
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Choose the pane for a new agent in a created worktree. A layout plugin may
|
|
185
|
+
* claim the root pane; park the agent in its own tab so both coexist. The
|
|
186
|
+
* returned target updates in place when `onPaneBusy` creates that tab later.
|
|
187
|
+
*/
|
|
188
|
+
export async function prepareWorktreeLaunch(
|
|
189
|
+
herdr: Herdr,
|
|
190
|
+
cwd: string,
|
|
191
|
+
worktree: CreatedWorktree,
|
|
192
|
+
): Promise<{ target: LaunchTarget; onPaneBusy?: () => Promise<string> }> {
|
|
193
|
+
const target: LaunchTarget = { tabId: worktree.tabId, paneId: worktree.rootPaneId };
|
|
194
|
+
const createTab = async (): Promise<string> => {
|
|
195
|
+
const tabArgs = ["tab", "create", "--workspace", worktree.workspaceId, "--cwd", worktree.checkoutPath, "--no-focus"] as const;
|
|
196
|
+
const createdTab = await herdr.exec(tabArgs, { cwd });
|
|
197
|
+
if (createdTab.code !== 0 || createdTab.killed) {
|
|
198
|
+
throw new Error(herdrCommandFailure(tabArgs, createdTab));
|
|
199
|
+
}
|
|
200
|
+
({ tabId: target.tabId, rootPaneId: target.paneId } = parseCreatedTab(createdTab, worktree.workspaceId));
|
|
201
|
+
return target.paneId;
|
|
202
|
+
};
|
|
203
|
+
if (await waitForRootPaneAgent(herdr, cwd, worktree.rootPaneId)) await createTab();
|
|
204
|
+
return { target, onPaneBusy: target.paneId === worktree.rootPaneId ? createTab : undefined };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Start a Pi agent and wrap every failure as an ambiguous launch that names the
|
|
209
|
+
* retained resources. `retained` is read at failure time so a busy-pane move
|
|
210
|
+
* reports the final target.
|
|
211
|
+
*/
|
|
212
|
+
export async function launchPiAgent(
|
|
213
|
+
herdr: Herdr,
|
|
214
|
+
cwd: string,
|
|
215
|
+
input: {
|
|
216
|
+
action: string;
|
|
217
|
+
name: string;
|
|
218
|
+
pane: string;
|
|
219
|
+
args: readonly string[];
|
|
220
|
+
retained: () => string;
|
|
221
|
+
onPaneBusy?: () => Promise<string>;
|
|
222
|
+
},
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
let pane = input.pane;
|
|
225
|
+
try {
|
|
226
|
+
const result = await startPiAgent(herdr, {
|
|
227
|
+
name: input.name,
|
|
228
|
+
pane,
|
|
229
|
+
args: input.args,
|
|
230
|
+
options: { cwd },
|
|
231
|
+
onPaneBusy: input.onPaneBusy
|
|
232
|
+
? async () => (pane = await input.onPaneBusy!())
|
|
233
|
+
: undefined,
|
|
234
|
+
});
|
|
235
|
+
if (result.code === 0 && !result.killed) return;
|
|
236
|
+
const startArgs = ["agent", "start", input.name, "--kind", "pi", "--pane", pane, "--", ...input.args];
|
|
237
|
+
throw new Error(herdrCommandFailure(startArgs, result));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`${input.action} launch could not be confirmed after starting agent ${input.name}; retained ${input.retained()}: ${errorMessage(error)}`,
|
|
241
|
+
{ cause: error },
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-herdr-tools",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Clone, name, and finish Pi conversations in Herdr, with mergeable side questions.",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Clone, hand off, name, and finish Pi conversations in Herdr, with mergeable side questions.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "herdr", "session", "clone", "side-thread", "title"],
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": { "node": ">=22.19.0" },
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"files": ["LICENSE", "extensions", "internal", "docs", "README.md"],
|
|
9
|
+
"files": ["LICENSE", "extensions", "internal", "prompts", "docs", "README.md"],
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test test/*.test.ts",
|
|
12
12
|
"typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/*.ts internal/*.ts test/*.test.ts",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"@earendil-works/pi-agent-core": "^1.1.0",
|
|
17
|
-
"@earendil-works/pi-coding-agent": "^1.1.0"
|
|
17
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
18
|
+
"typebox": "^1.3.15"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
21
|
"@henryqw/pi-config-store": "^3.0.0",
|
|
@@ -32,5 +33,5 @@
|
|
|
32
33
|
},
|
|
33
34
|
"bugs": { "url": "https://github.com/HenryQW/pi-harness/issues" },
|
|
34
35
|
"publishConfig": { "access": "public" },
|
|
35
|
-
"pi": { "extensions": ["./extensions/index.ts"] }
|
|
36
|
+
"pi": { "extensions": ["./extensions/index.ts"], "prompts": ["./prompts"] }
|
|
36
37
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Hand off a separate issue to a new Herdr worktree from main.
|
|
3
|
+
argument-hint: "<issue>"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Hand off this separate issue to a new Herdr worktree: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
If the issue is empty, stop and ask the user to run `/handoff-worktree <issue>`. Do not modify the current worktree.
|
|
9
|
+
|
|
10
|
+
1. Gather only the missing facts from the conversation and the repository. Ask one focused question only if the repository, expected outcome, or a critical constraint is unclear.
|
|
11
|
+
2. Write one self-contained prompt: problem, evidence, expected outcome, constraints, starting files, validation, and requested delivery. Exclude unrelated context, secrets, and stale runtime IDs.
|
|
12
|
+
3. Choose a concise branch, a short label, and a unique agent name.
|
|
13
|
+
4. Call `herdr_handoff` with them. Report its result. Do not wait for the new agent to finish.
|