@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.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
+ ```