@335g/pi-herdr-fleet 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +419 -0
- package/README.md +442 -0
- package/approvals.ts +748 -0
- package/audit.ts +153 -0
- package/clean.ts +256 -0
- package/fork.ts +216 -0
- package/herdr-client.ts +300 -0
- package/index.ts +420 -0
- package/package.json +45 -0
- package/recipes.ts +172 -0
- package/review.ts +515 -0
- package/runs.ts +437 -0
- package/scopes.ts +134 -0
- package/worktree.ts +573 -0
package/README.md
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# pi-herdr-fleet
|
|
2
|
+
|
|
3
|
+
[日本語](./README.ja.md)
|
|
4
|
+
|
|
5
|
+
An approval broker between [herdr](https://herdr.dev) panes and Pi. herdr knows which panes are
|
|
6
|
+
waiting on a human; `/fleet` shows that list as an overlay on the pane you are already looking at,
|
|
7
|
+
and answers a blocked agent without switching panes. The same command saves and restores tab
|
|
8
|
+
layouts, creates worktrees with the untracked development environment carried over, and forks one:
|
|
9
|
+
a worktree, a Pi session in it, and a task handed to that session.
|
|
10
|
+
|
|
11
|
+
Phase 3a adds `/fleet fork`. Review (3b) and the merge gate (3c) are still to come.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
The extension only exists inside a herdr-managed pane, in interactive mode. If `HERDR_ENV=1`,
|
|
16
|
+
`HERDR_SOCKET_PATH` and `HERDR_PANE_ID` are not all set, it registers nothing at all; if pi is not
|
|
17
|
+
in TUI mode it starts nothing, because RPC and print modes have no pane herdr can display and no
|
|
18
|
+
terminal to draw an overlay in.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pi install npm:@335g/pi-herdr-fleet
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`pi install` writes to the user settings (`~/.pi/agent/settings.json`). Add `-l` to write to the
|
|
27
|
+
project settings (`.pi/settings.json`) instead.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
- `/fleet` — open the list of panes waiting on approval
|
|
32
|
+
- `ctrl+shift+a` — the same list, without typing a command
|
|
33
|
+
- `/fleet recipe save <name>` — store the current tab's layout
|
|
34
|
+
- `/fleet recipe apply <name> [--start]` — restore it as a new tab
|
|
35
|
+
- `/fleet recipe ls` — list the stored recipes
|
|
36
|
+
- `/fleet worktree create <branch> [--base <ref>] [--label <text>]` — make a worktree and carry the
|
|
37
|
+
development environment into it
|
|
38
|
+
- `/fleet fork <branch> --task "<text>" [--base <ref>] [--scope implementation] [--no-install]
|
|
39
|
+
[--no-start]` — fork a worktree, start a Pi session in it, and hand it the task
|
|
40
|
+
- `/fleet review <branch> --task "<text>" [--base <ref>]` — start a read-only reviewer inside that
|
|
41
|
+
worktree, with the diff and the author's own session
|
|
42
|
+
- `/fleet status` — every recorded run: branch, scope, state and verdict
|
|
43
|
+
- `/fleet merge <branch> [--force]` — merge an approved branch into the main checkout
|
|
44
|
+
- `/fleet clean <branch> [--force]` — remove a merged run's worktree, branch and panes
|
|
45
|
+
- `fleet_fork` / `fleet_review` / `fleet_verdict` / `fleet_status` / `fleet_merge` / `fleet_clean` —
|
|
46
|
+
the same from an agent, as tools rather than a command line
|
|
47
|
+
|
|
48
|
+
| Key | Action |
|
|
49
|
+
|-----|--------|
|
|
50
|
+
| `↑` `↓` | move the selection |
|
|
51
|
+
| `1`-`9` | open that row |
|
|
52
|
+
| `Enter` | open the selected row |
|
|
53
|
+
| `Esc` | close the overlay |
|
|
54
|
+
|
|
55
|
+
In the detail view:
|
|
56
|
+
|
|
57
|
+
| Key | Action |
|
|
58
|
+
|-----|--------|
|
|
59
|
+
| `Enter` | send what you typed, then Enter (`pane.send_input`) |
|
|
60
|
+
| `ctrl+k` | send what you typed as raw keys (`pane.send_keys`) |
|
|
61
|
+
| `PageUp` `PageDown` | scroll the question |
|
|
62
|
+
| `Esc` | back to the list |
|
|
63
|
+
|
|
64
|
+
## Answering a blocked pane
|
|
65
|
+
|
|
66
|
+
An approval dialog is usually not a question you can answer with a sentence: it may be a numbered
|
|
67
|
+
list, a yes/no, or a full-screen picker. So there are two routes out of the detail view, and which
|
|
68
|
+
one you take is your choice rather than a guess the extension makes from your text.
|
|
69
|
+
|
|
70
|
+
- **Text** (`Enter`) submits what you typed, the way the pane's own input would.
|
|
71
|
+
- **Raw keys** (`ctrl+k`) writes keystrokes instead. The input is split on whitespace, so
|
|
72
|
+
`esc 1` sends `esc` then `1`, `up up enter` walks a menu, and an empty input is a bare `Enter`
|
|
73
|
+
— the common case for a confirmation dialog.
|
|
74
|
+
|
|
75
|
+
Both routes go through the pane surface (`pane.send_input` / `pane.send_keys`), not the agent
|
|
76
|
+
surface. That is not a shortcut: `agent.prompt` **refuses any pane herdr reports as blocked**
|
|
77
|
+
(`agent_blocked`) — which is every pane this overlay can answer — and `agent.send_keys` refuses an
|
|
78
|
+
agent reported through `pane.report_agent` (`agent_not_ready`), which is how hooks and plugins report
|
|
79
|
+
state. Answering an approval dialog is intentional raw input into that pane.
|
|
80
|
+
|
|
81
|
+
Two things never happen:
|
|
82
|
+
|
|
83
|
+
- **A failure is never retried.** herdr's timeout is not proof the input never reached the agent,
|
|
84
|
+
so a send that fails is reported in the overlay with its reason and the row stays in the list.
|
|
85
|
+
Retrying is your decision, not the extension's.
|
|
86
|
+
- **The row is not removed on a successful write.** A write that succeeded is not proof the agent
|
|
87
|
+
moved on. Rows disappear only when herdr reports the pane is no longer blocked.
|
|
88
|
+
|
|
89
|
+
This pane is never listed. Answering itself would deadlock.
|
|
90
|
+
|
|
91
|
+
## Recipes
|
|
92
|
+
|
|
93
|
+
A recipe is herdr's own tab layout: `/fleet recipe save dev` exports the current tab and writes the
|
|
94
|
+
`LayoutNode` tree to `.pi/herdr-fleet/recipes/dev.json` (relative to the directory pi runs in). The
|
|
95
|
+
tree is stored as-is except for `pane_id`, which is dropped because a closed pane's id is never
|
|
96
|
+
reused.
|
|
97
|
+
|
|
98
|
+
`/fleet recipe apply dev` builds that tree as a **new tab** in the current workspace, labelled with
|
|
99
|
+
the recipe name. It never replaces the tab you are in: the saved tree has no source tab id, and
|
|
100
|
+
replacing the current tab would kill the session that ran the command. Applying is layout only by
|
|
101
|
+
default, so a saved pane comes back as a plain shell in the saved `cwd`. `--start` also replays the
|
|
102
|
+
saved launch commands.
|
|
103
|
+
|
|
104
|
+
Recipe names are restricted to one safe filename segment (`[A-Za-z0-9][A-Za-z0-9._-]*`); a name is a
|
|
105
|
+
path, so `../` in one would be a write outside the project.
|
|
106
|
+
|
|
107
|
+
## Worktrees and the environment
|
|
108
|
+
|
|
109
|
+
`/fleet worktree create <branch>` calls `herdr worktree create`, then copies the untracked
|
|
110
|
+
development environment into the new checkout:
|
|
111
|
+
|
|
112
|
+
- Every `.env*` file in the source root (`.env`, `.env.local`, `.env.example`, `.envrc`, ...) is
|
|
113
|
+
copied in. **Nothing is overwritten**: an `.env.example` that git already put in the worktree stays
|
|
114
|
+
as it is.
|
|
115
|
+
- `direnv allow` is run for the new worktree **only if the source `.envrc` was already allowed**.
|
|
116
|
+
That is checked with `direnv status --json` and `state.foundRC.allowed === 0`. Allowing an
|
|
117
|
+
unallowed `.envrc` in a new location is a trust grant — it would let the worktree the extension
|
|
118
|
+
just created execute code the user never approved — so it is mirrored, never introduced.
|
|
119
|
+
- Without direnv, or without an `.envrc` in the source, the copy happens and a warning is reported.
|
|
120
|
+
- A failure at any point in this step is a warning, not a failure: the worktree exists and is
|
|
121
|
+
reported as created.
|
|
122
|
+
|
|
123
|
+
This exists because a worktree gets only what git tracks. Without the copy, the Pi started in a
|
|
124
|
+
fresh worktree dies on startup with `No API key found`.
|
|
125
|
+
|
|
126
|
+
## Forking work
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
/fleet fork feat/x --task "Add retries to the uploader"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The same fork is available as a tool, and **the tool is the primary path**: the loop this extension
|
|
133
|
+
exists for is driven by an agent, and a command alone would put a human in the middle of every step.
|
|
134
|
+
The command stays for the times a human does want to type it; both call the same function in
|
|
135
|
+
`fork.ts`, so there is one order of operations to keep right.
|
|
136
|
+
|
|
137
|
+
`fleet_fork`:
|
|
138
|
+
|
|
139
|
+
| Argument | Type | Default | Meaning |
|
|
140
|
+
|---|---|---|---|
|
|
141
|
+
| `branch` | string | required | Branch name for the new worktree |
|
|
142
|
+
| `task` | string | required | The task the forked session works on |
|
|
143
|
+
| `base` | string | HEAD | Ref to branch from |
|
|
144
|
+
| `scope` | enum | `implementation` | What kind of session to fork |
|
|
145
|
+
| `install` | boolean | true | Install dependencies when the worktree has a lockfile |
|
|
146
|
+
| `start` | boolean | true | Open a pane and start Pi in it |
|
|
147
|
+
|
|
148
|
+
It returns the worktree path, branch, workspace, pane and agent name, and — only when there are any —
|
|
149
|
+
the environment warnings. A tool result becomes one entry in the conversation, so it stays short.
|
|
150
|
+
|
|
151
|
+
Five steps, in this order:
|
|
152
|
+
|
|
153
|
+
1. A worktree on a new branch, with `.env*`/`.envrc` carried over as above.
|
|
154
|
+
2. **Prepare** — if the checkout has a lockfile, dependencies are installed in it. The lockfile
|
|
155
|
+
picks the installer: `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `bun.lockb`/`bun.lock` → bun,
|
|
156
|
+
`package-lock.json` → npm. No lockfile means no install. The command is typed into the new
|
|
157
|
+
pane's shell and herdr waits for it to finish, so the install output lands on a screen you can
|
|
158
|
+
switch to instead of inside the session that asked for it. `--no-install` skips this step.
|
|
159
|
+
3. A pane in the worktree's workspace (`pane.split` with `--cwd <worktree>` and `--no-focus`), then
|
|
160
|
+
a Pi session in it. The agent name comes from the branch, normalised to herdr's
|
|
161
|
+
`[a-z][a-z0-9_-]{0,31}` and cut at 32 characters.
|
|
162
|
+
4. The task, as **one message**, through `pane.send_input`. Not `agent.prompt`: that refuses any
|
|
163
|
+
pane herdr reports as blocked, which is the same layer of problem the approval broker hit, and
|
|
164
|
+
typing a prompt into a session is pane work.
|
|
165
|
+
5. A notification with the worktree path, branch, workspace, pane and agent name.
|
|
166
|
+
|
|
167
|
+
`--no-start` stops after step 1: a worktree and its environment, with nothing running in it.
|
|
168
|
+
|
|
169
|
+
### The task is the whole brief
|
|
170
|
+
|
|
171
|
+
**No conversation history is passed.** The forked session gets the task text, the worktree and
|
|
172
|
+
branch, the constraints, and what "done" means. A discussion is not a brief: anything decided in
|
|
173
|
+
the session that forked it has to be written into the task, and anything left open has to be asked
|
|
174
|
+
again. That prompt lives in `scopes.ts`, which holds one seed per scope: `implementation` for a
|
|
175
|
+
fork, `review` for a reviewer. Only a scope that can be built from a task and a worktree is offered
|
|
176
|
+
as `fleet_fork`'s `scope` argument; `review` needs material gathered from an existing worktree, so it
|
|
177
|
+
has its own tool.
|
|
178
|
+
|
|
179
|
+
That is also why the tool's argument descriptions say so: the model writing the `task` is the one
|
|
180
|
+
who has to write the brief.
|
|
181
|
+
|
|
182
|
+
`branch` and `task` are rejected when they are empty. The tool's caller is a model, so a field it
|
|
183
|
+
filled in with nothing is the shape a missing argument usually takes; the schema catches an argument
|
|
184
|
+
that is not there at all, and this catches the empty string the schema cannot express.
|
|
185
|
+
|
|
186
|
+
The implementation scope tells the session to:
|
|
187
|
+
|
|
188
|
+
- work only inside this worktree, and not touch other checkouts
|
|
189
|
+
- commit on this branch; not create worktrees, start agents, or push
|
|
190
|
+
- ask instead of guessing when the task does not decide something
|
|
191
|
+
- finish with a commit on the branch and a short report, not a diff
|
|
192
|
+
|
|
193
|
+
A failed install is a warning rather than a failure: the worktree and the pane exist, and the
|
|
194
|
+
notification says what happened. Environment warnings are reported the same way.
|
|
195
|
+
|
|
196
|
+
### Why the fork waits
|
|
197
|
+
|
|
198
|
+
Two things a real pane does that the API does not read like:
|
|
199
|
+
|
|
200
|
+
- `agent.start` answers `agent_pane_busy` while the pane's shell is still being recognised — which
|
|
201
|
+
is the normal case here, because the install just ran in that pane. The fork retries it.
|
|
202
|
+
- herdr reports the agent ready about three seconds before the agent accepts input. A prompt sent
|
|
203
|
+
inside that window lands in the editor and is never submitted; the trailing Enter is simply lost.
|
|
204
|
+
The fork waits for herdr to report a settled agent before sending the task.
|
|
205
|
+
|
|
206
|
+
Both were measured against herdr and Pi, and both are the reason the steps are ordered this way.
|
|
207
|
+
|
|
208
|
+
## Reviewing a fork
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
/fleet review feat/x --task "Add retries to the uploader"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The reviewer is a second Pi session, in a new pane **inside the implementation worktree** — not in a
|
|
215
|
+
second worktree, because git refuses to check out one branch in two places. It is told to be
|
|
216
|
+
read-only: a review that edits the thing under review is not a review. The agent name is the
|
|
217
|
+
branch's, with `-review` appended, because an agent name can only be taken once.
|
|
218
|
+
|
|
219
|
+
`fleet_review`:
|
|
220
|
+
|
|
221
|
+
| Argument | Type | Default | Meaning |
|
|
222
|
+
|---|---|---|---|
|
|
223
|
+
| `branch` | string | required | The branch the implementation session worked on |
|
|
224
|
+
| `task` | string | required | The task that session was given |
|
|
225
|
+
| `base` | string | the main checkout's HEAD | Ref the change is measured from |
|
|
226
|
+
|
|
227
|
+
A fork's job is to pass as little as possible, because the work is a task. A review's job is the
|
|
228
|
+
opposite: the reviewer has the worktree but no way to know what was asked or what the author already
|
|
229
|
+
knew was unfinished. So the seed carries four things:
|
|
230
|
+
|
|
231
|
+
- `git diff <base>...HEAD`, run in the worktree. The diff is cut at 60000 characters, with the cut
|
|
232
|
+
spelled out in the seed: a reviewer that silently sees half a change is worse than one that runs
|
|
233
|
+
`git diff` itself.
|
|
234
|
+
- the task, so the change is judged against the brief rather than against taste.
|
|
235
|
+
- the author's own session, read from the path herdr reports for that pane (`agent_session.value` in
|
|
236
|
+
`session.snapshot`). Only the assistant's `text` parts are taken — not thinking, not tool calls,
|
|
237
|
+
which are the diff by another route.
|
|
238
|
+
- the worktree and branch, and the sandbox rules.
|
|
239
|
+
|
|
240
|
+
The session is a JSONL file that reaches megabytes, so the excerpt is capped at **300 lines and
|
|
241
|
+
20000 characters**, and the caps are applied from the end: the report is the newest message, and the
|
|
242
|
+
reasoning around the last commits matters more than the opening. Only the tail of the file is read
|
|
243
|
+
(4 MB), and a session that was cut says so in the seed. The last message is the author's report;
|
|
244
|
+
the ones before it are how it got there.
|
|
245
|
+
|
|
246
|
+
The seed ends the review with a `fleet_verdict` tool call, not a line of text: the reviewer is told
|
|
247
|
+
to record `approve` or `request-changes` and one finding per problem, and that the merge gate reads
|
|
248
|
+
that call and nothing else. The reviewer is started with `-e <this extension>`, so the tool exists in
|
|
249
|
+
its session even when the extension is not installed, and so a review during development runs the
|
|
250
|
+
code in the worktree rather than an older installed copy.
|
|
251
|
+
|
|
252
|
+
A review of a branch whose worktree is not open in a workspace is refused: there is nowhere to put
|
|
253
|
+
the reviewer. So is a review with no task. When no Pi session is still running in the worktree there
|
|
254
|
+
is no session to read, and the seed says so rather than looking like an author who wrote nothing.
|
|
255
|
+
|
|
256
|
+
## Verdicts and merging
|
|
257
|
+
|
|
258
|
+
A fork, a review and a verdict are recorded in one file per branch:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
<main checkout>/.pi/herdr-fleet/runs/<branch>.json # `/` in the branch becomes `-`
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`fleet_fork` writes it, `fleet_review` adds the reviewer's pane, and `fleet_verdict` adds the
|
|
265
|
+
verdict. Keeping it in a file rather than in the reviewer's live session is the point: a pane can be
|
|
266
|
+
closed, and a gate that opens when a pane disappears is not a gate.
|
|
267
|
+
|
|
268
|
+
`fleet_verdict`:
|
|
269
|
+
|
|
270
|
+
| Argument | Type | Meaning |
|
|
271
|
+
|---|---|---|
|
|
272
|
+
| `verdict` | `approve` / `request-changes` | Whether the change is ready |
|
|
273
|
+
| `findings` | `{ path, line?, note }[]` | One entry per problem |
|
|
274
|
+
|
|
275
|
+
Only the pane the run recorded as the reviewer may call it. The extension is loaded in every Pi
|
|
276
|
+
session, so without that check any session could write a verdict. On `request-changes` the findings
|
|
277
|
+
are sent back to the implementation session with `pane.send_input` when that session is still
|
|
278
|
+
running; nothing is started in its place.
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
/fleet status
|
|
282
|
+
/fleet merge feat/x
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`/fleet status` prints one line per run — branch, scope, state, verdict — where the state is
|
|
286
|
+
`working`, `unreviewed`, `approve`, `request-changes`, `merged` or `cleaned` (`merged` when the branch
|
|
287
|
+
is already in the main checkout's history, `cleaned` once `/fleet clean` has removed its worktree,
|
|
288
|
+
branch and panes).
|
|
289
|
+
|
|
290
|
+
`/fleet merge` runs `git merge --no-edit` in the main checkout, and refuses unless the verdict is
|
|
291
|
+
`approve` (or `--force` is given) and the tracked files are clean. Untracked files do not block it,
|
|
292
|
+
because the run records themselves live under `.pi/`. The worktree is left in place: cleanup is its
|
|
293
|
+
own operation.
|
|
294
|
+
|
|
295
|
+
`/fleet clean` is that operation: it closes the panes the run recorded, removes the worktree through
|
|
296
|
+
herdr's `worktree.remove`, and deletes the branch with `git branch -d` in the main checkout. It
|
|
297
|
+
refuses a run whose branch is not in main's history unless `--force` is given. The run record and the
|
|
298
|
+
session JSONL are kept — the record only gains a `cleanedAt`.
|
|
299
|
+
|
|
300
|
+
All three are tools as well — `fleet_status` (no arguments), `fleet_merge` (`branch`, optional
|
|
301
|
+
`force`) and `fleet_clean` (`branch`, optional `force`) — and the commands are thin wrappers over the
|
|
302
|
+
same `statusRuns`, `mergeRun` and `cleanRun` the tools call. The loop closes without a human at the
|
|
303
|
+
keyboard: an agent can fork, review, merge, and clean up.
|
|
304
|
+
|
|
305
|
+
## Notifications
|
|
306
|
+
|
|
307
|
+
A pane that newly becomes blocked raises a pi notification. To turn that off:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
// ~/.pi/agent/pi-herdr-fleet.json
|
|
311
|
+
{ "notify": false }
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Nothing else in the file is read.
|
|
315
|
+
|
|
316
|
+
## How it works
|
|
317
|
+
|
|
318
|
+
herdr is the source of truth. The extension holds no state it cannot rebuild:
|
|
319
|
+
|
|
320
|
+
- The list is built from `session.snapshot` and corrected by `pane.agent_status_changed`. A pane
|
|
321
|
+
that behaves unexpectedly is resolved by asking herdr again, not by guessing locally.
|
|
322
|
+
- The question shown in the detail view is read with `agent.read` (`detection` first, `visible`
|
|
323
|
+
when herdr returns nothing), once, when the pane becomes blocked.
|
|
324
|
+
- Every request has a timeout, and every failure — a missing method on an older herdr, a closed
|
|
325
|
+
socket, a slow server — comes back as a value rather than an exception. The overlay shows the
|
|
326
|
+
reason instead of breaking the session.
|
|
327
|
+
- The event stream reconnects with exponential backoff. On recovery it resubscribes and re-reads
|
|
328
|
+
`session.snapshot`, so a state the extension derived from the old stream is replaced by herdr's.
|
|
329
|
+
|
|
330
|
+
## Limitations
|
|
331
|
+
|
|
332
|
+
- One subscription connection is needed per pane set. herdr scopes `pane.agent_status_changed` to a
|
|
333
|
+
single `pane_id` and rejects a second `events.subscribe` on an already-subscribed connection, so
|
|
334
|
+
a new pane means a new connection. Nothing is accumulated: the set is re-derived from the
|
|
335
|
+
snapshot and compared.
|
|
336
|
+
- The fork loop is closed: a fork, a review and a verdict are recorded per branch, `/fleet status`
|
|
337
|
+
lists them, `/fleet merge` refuses a branch that has no `approve`, and `/fleet clean` removes a
|
|
338
|
+
merged run's worktree, branch and panes. A run record and its session JSONL are never deleted: the
|
|
339
|
+
record only gains a `cleanedAt`.
|
|
340
|
+
- A reviewer is started with `-e <this extension>`: the code it runs is the code that started it, not
|
|
341
|
+
an installed copy.
|
|
342
|
+
- `worktree.create` cannot branch from a linked worktree, so `/fleet fork` from inside one is created
|
|
343
|
+
from the main checkout, pinned to the caller's HEAD. Uncommitted changes there stay behind; the
|
|
344
|
+
command warns when there are any.
|
|
345
|
+
- Neither `/fleet worktree create` nor `/fleet fork` moves your focus: the new workspace is built in
|
|
346
|
+
the background.
|
|
347
|
+
- A recipe records one tab. There is no way to save a whole workspace, and no way to restore into
|
|
348
|
+
the tab a recipe was saved from.
|
|
349
|
+
- `direnv` is the only environment manager recognised. A mise/asdf-style setup arrives through
|
|
350
|
+
`.envrc` or not at all.
|
|
351
|
+
|
|
352
|
+
## Development
|
|
353
|
+
|
|
354
|
+
```sh
|
|
355
|
+
node packages/pi-herdr-fleet/selfcheck.ts
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Runs the transport and the broker against a fake herdr server on a temporary socket: the
|
|
359
|
+
request/error/timeout degradation paths, subscription and reconnect/resync, that a refused
|
|
360
|
+
subscription set is rebuilt instead of retried forever, that only other panes are listed, that
|
|
361
|
+
leaving `blocked` drops a row, that the first snapshot does not notify and a re-snapshot does not
|
|
362
|
+
re-announce, that a failed send keeps the row, and the registration guard.
|
|
363
|
+
It also renders the overlay and checks that every line has the same width and the box is closed.
|
|
364
|
+
|
|
365
|
+
Recipes and the environment copy are checked against real temporary directories: that a recipe
|
|
366
|
+
keeps `cwd`/`env`/`command` but no `pane_id`, that `--start` decides whether commands are replayed,
|
|
367
|
+
that a name cannot escape the recipe directory, and — with a stubbed `direnv` — that an unallowed
|
|
368
|
+
source `.envrc` is never allowed in the new worktree while an allowed one is.
|
|
369
|
+
|
|
370
|
+
The fork's own pieces are checked the same way: that the lockfile and only the lockfile picks the
|
|
371
|
+
installer, that a checkout with no lockfile is not installed into, that `--no-install` types
|
|
372
|
+
nothing, that the install marker cannot match the command echo (the pane echoes what is typed, so a
|
|
373
|
+
literal marker would match before the install ran), that a non-zero exit is a reported failure, that
|
|
374
|
+
a busy pane is retried while an unfixable `agent.start` failure is not, and that the agent is waited
|
|
375
|
+
for before anything is typed into it. The seed and the agent name are pure functions, so the task,
|
|
376
|
+
the worktree and the branch are checked against the string they produce.
|
|
377
|
+
|
|
378
|
+
The tool is checked through its own surface: that `branch` and `task` are required by the schema,
|
|
379
|
+
that the scope enum comes from the registry, that an empty `branch`/`task` or an unknown scope never
|
|
380
|
+
reaches herdr, that a call from a non-TUI session is refused, that a failure throws (a returned
|
|
381
|
+
value never sets the error flag), that the result names the fork without describing a clean
|
|
382
|
+
environment, and that an environment warning does reach the model.
|
|
383
|
+
|
|
384
|
+
### Acceptance
|
|
385
|
+
|
|
386
|
+
```sh
|
|
387
|
+
packages/pi-herdr-fleet/acceptance.sh
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
End-to-end against a real herdr server, real panes, real direnv and a real Pi TUI. Run it from
|
|
391
|
+
inside a herdr pane. The broker never lists its own pane, so this needs three panes, and the script
|
|
392
|
+
builds them: a **subject** shell reported as `blocked` through `pane.report_agent`, an **observer**
|
|
393
|
+
Pi running this extension in a different pane, and the caller's pane. It then checks the
|
|
394
|
+
notification, the overlay listing, the question in the detail view, both answer routes (text and
|
|
395
|
+
raw keys), and that `Esc` closes the overlay. Only the panes it created are closed.
|
|
396
|
+
|
|
397
|
+
Reporting the subject's state instead of waiting for a real agent keeps the run deterministic: no
|
|
398
|
+
model has to answer, yet the whole path — socket, subscription, overlay, key delivery into the
|
|
399
|
+
subject's `read` — is exercised for real.
|
|
400
|
+
|
|
401
|
+
```sh
|
|
402
|
+
packages/pi-herdr-fleet/acceptance-fork.sh
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
End-to-end for `/fleet fork`, against the same real stack plus real git and real npm. A fork creates
|
|
406
|
+
real worktrees, so this script does not touch this repository: it builds its own git repository in a
|
|
407
|
+
temp directory, with a base commit that has no lockfile, and its own workspace for the observer Pi.
|
|
408
|
+
It removes every worktree, workspace, pane and direnv trust entry it created, and never reads or
|
|
409
|
+
closes anything else.
|
|
410
|
+
|
|
411
|
+
It checks, across four forks: that the worktree, the environment copy, the direnv trust, the
|
|
412
|
+
install, the pane, the Pi and the seed all happen, that the forked session actually does the task
|
|
413
|
+
and commits it, that a checkout without a lockfile is not installed into, that `--no-install`
|
|
414
|
+
ignores a lockfile, and that `--no-start` leaves a worktree with nothing running in it.
|
|
415
|
+
|
|
416
|
+
Then it checks the tool path, which is the one an agent actually uses and the one a command-line
|
|
417
|
+
test cannot reach: the observer's agent is told to call `fleet_fork`, and the worktree, the pane, the
|
|
418
|
+
tool result in the observer's conversation and the seed in the forked session are all checked. Two
|
|
419
|
+
refusals follow — an empty `task`, which the tool's own validation catches, and a call with no `task`
|
|
420
|
+
at all, which the schema catches before the tool runs. Neither may leave a worktree behind.
|
|
421
|
+
|
|
422
|
+
Unlike `acceptance.sh`, this needs a working model for two reasons: the forked session has to do the
|
|
423
|
+
task, and the observer has to decide to call the tool. The script warns when no API key is in the
|
|
424
|
+
environment.
|
|
425
|
+
|
|
426
|
+
A review follows: `/fleet review` over the first fork's branch, which puts a second Pi in the author's
|
|
427
|
+
worktree. The checks are on the reviewer's own session file — the task, the diff, the verdict shape,
|
|
428
|
+
and a fragment of the author's report, which is the one thing no `git` command produces — and then on
|
|
429
|
+
the reviewer's last reply, which has to end with a verdict. The worktree has to be clean afterwards,
|
|
430
|
+
because the reviewer was told to be read-only.
|
|
431
|
+
|
|
432
|
+
Last, a fork started from a linked worktree: a worktree is created from the main checkout, with a
|
|
433
|
+
commit that exists only in the linked one proving the fork point was pinned to the caller's HEAD, and
|
|
434
|
+
its uncommitted change left behind. Both are reported as warnings.
|
|
435
|
+
|
|
436
|
+
There is no `tsconfig.json` in this repo, so the type check is explicit:
|
|
437
|
+
|
|
438
|
+
```sh
|
|
439
|
+
npx tsc --noEmit --target es2022 --module nodenext --moduleResolution nodenext \
|
|
440
|
+
--strict --skipLibCheck --allowImportingTsExtensions --types node \
|
|
441
|
+
packages/pi-herdr-fleet/index.ts
|
|
442
|
+
```
|