pi-legion 3.1.0 → 5.0.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +382 -2
  2. package/CONTRIBUTING.md +4 -17
  3. package/README.md +127 -66
  4. package/agents/centurion.md +20 -16
  5. package/agents/scout.md +3 -1
  6. package/agents/worker.md +6 -8
  7. package/docs/contracts/declarative-phase-machine-v5.md +128 -0
  8. package/docs/contracts/display-liveness-v1.md +373 -0
  9. package/docs/contracts/drop-non-pi-kinds-v4.md +198 -0
  10. package/docs/contracts/esc-abort-v1.md +4 -4
  11. package/docs/contracts/esc-no-retire-v3.md +2 -2
  12. package/docs/contracts/launch-admission-queue-v1.md +839 -0
  13. package/docs/contracts/status-panel-v1.md +174 -0
  14. package/docs/contracts/subagent-renewal-v1.md +260 -0
  15. package/docs/headless-child-contract.md +6 -3
  16. package/docs/inprocess-supervisor.md +106 -27
  17. package/docs/legion-v2-contract.md +27 -2
  18. package/docs/performance-2026-10-19.md +199 -0
  19. package/docs/spawn-mode-visibility-v1.md +4 -1
  20. package/docs/vexillatio-contract.md +3 -1
  21. package/index.ts +1155 -120
  22. package/package.json +5 -3
  23. package/skills/pi-legion/SKILL.md +2 -3
  24. package/src/agents/agents.ts +310 -46
  25. package/src/agents/overrides.ts +3 -2
  26. package/src/agents/presets.ts +14 -40
  27. package/src/agents/settings.ts +32 -95
  28. package/src/agents/step-model.ts +7 -22
  29. package/src/api.ts +1 -35
  30. package/src/extension/child-guard.ts +25 -1
  31. package/src/extension/event-bus.ts +80 -8
  32. package/src/extension/legion-commands.ts +41 -17
  33. package/src/extension/legion-doctor.ts +1 -1
  34. package/src/extension/legion-stop.ts +39 -13
  35. package/src/extension/legion-tools.ts +87 -25
  36. package/src/extension/liveness.ts +200 -0
  37. package/src/extension/notify.ts +136 -4
  38. package/src/extension/playbook.ts +4 -4
  39. package/src/extension/recycle.ts +4 -0
  40. package/src/extension/runtime.ts +435 -16
  41. package/src/extension/slash.ts +0 -21
  42. package/src/extension/summary.ts +14 -5
  43. package/src/extension/timing.ts +8 -4
  44. package/src/legion/db.ts +165 -1
  45. package/src/legion/events.ts +81 -16
  46. package/src/legion/nodes.ts +13 -2
  47. package/src/legion/phase-conditions.ts +164 -0
  48. package/src/legion/phase-machine.ts +403 -0
  49. package/src/legion/rebuild.ts +7 -1
  50. package/src/legion/state-machine.ts +219 -115
  51. package/src/legion/usage.ts +58 -4
  52. package/src/runs/args.ts +32 -18
  53. package/src/runs/extension-inheritance.ts +220 -0
  54. package/src/runs/orchestrator.ts +1292 -96
  55. package/src/runs/progress-signal.ts +137 -0
  56. package/src/runs/store-lookup.ts +270 -0
  57. package/src/runs/worktree.ts +190 -21
  58. package/src/shared/ignored-kind.ts +57 -0
  59. package/src/shared/progress.ts +11 -0
  60. package/src/shared/session.ts +199 -19
  61. package/src/shared/types.ts +138 -69
  62. package/src/supervisor/admission-queue.ts +516 -0
  63. package/src/supervisor/child-context.ts +19 -0
  64. package/src/supervisor/inprocess-supervisor.ts +137 -38
  65. package/src/supervisor/proc-memory.ts +67 -0
  66. package/src/supervisor/routing-supervisor.ts +60 -2
  67. package/src/supervisor/rpc-supervisor.ts +2 -2
  68. package/src/tui/session-messages-file.ts +100 -0
  69. package/src/tui/status.ts +91 -18
  70. package/src/tui/tree-view.ts +84 -3
  71. package/src/tui/viz/layout.ts +54 -3
  72. package/src/tui/viz/model.ts +67 -14
  73. package/src/tui/viz/palette.ts +11 -3
  74. package/src/tui/viz/panels.ts +222 -8
  75. package/src/tui/viz/view.ts +110 -23
  76. package/agents/advisor.md +0 -110
  77. package/agents/designer.md +0 -87
  78. package/agents/planner.md +0 -53
  79. package/agents/prototype.md +0 -72
  80. package/agents/reviewer.md +0 -113
  81. package/src/backends/legacy/client.ts +0 -638
  82. package/src/backends/legacy/cursor-chat.ts +0 -348
  83. package/src/backends/legacy/runner.ts +0 -101
  84. package/src/backends/legacy/types.ts +0 -138
  85. package/src/runs/kind.ts +0 -260
package/CHANGELOG.md CHANGED
@@ -11,6 +11,382 @@ summary so you can see **2.x vs 0.16** without leaving the tree.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [5.0.0] — 2026-10-26
15
+
16
+ Major because upgrading **can break an existing role card**: unknown frontmatter
17
+ is now a hard error, and `spawnMode` / `placement` are gone. A card carrying
18
+ either fails to load with a message naming the offending field. That is the point
19
+ — the previous behaviour was to silently ignore them, so a setting that looked
20
+ active did nothing.
21
+
22
+ **Before upgrading**, remove `spawnMode:` and `placement:` from every card under
23
+ `~/.pi/agent/agents/` and `.pi/agents/`. Nothing else about a card needs to
24
+ change.
25
+
26
+ ### Breaking
27
+
28
+ - **`spawnMode` is gone from role cards (#290).** The backend is a property of
29
+ the launch, not of a role: a launch that needs a worktree runs over rpc, and
30
+ everything else runs in-process. Since only a `centurion` may hold a worktree
31
+ (#214), a centurion is the only role that can reach rpc. There is no global
32
+ default to configure because there is no longer a choice to make per role.
33
+ - **Unknown frontmatter is refused (#290).** A card with a field the runtime does
34
+ not honour is rejected at load, listing the bad keys and the full legal set. The
35
+ retiral of `placement` was previously "tolerated"; fourteen bundled and local
36
+ cards still carried it while it did nothing.
37
+ - **Five bundled roles removed (#301).** `planner`, `reviewer`, `designer`,
38
+ `prototype`, and `advisor` are gone; `centurion`, `worker`, and `scout` remain.
39
+ They existed because the phase machine keyed its rules on role *names* — a role
40
+ that merely existed carried workflow meaning. See the declarative machine below.
41
+ - **Children inherit every host extension by default (#280).** The old default was
42
+ `--no-extensions` plus whatever a card named, which meant a role silently lost
43
+ host capabilities (a provider extension, a guard) it never knew to name. Use
44
+ `excludeExtensions: [pi-lens]` on a card, or
45
+ `PI_LEGION_CHILD_INHERIT_EXTENSIONS=0` globally, to get the old isolation.
46
+ `inheritExtensions: false` loads only the card's own list.
47
+
48
+ ### Added
49
+
50
+ - **Declarative phase machine (#298).** A team declares its own phases on its
51
+ lead's card:
52
+ ```yaml
53
+ phases:
54
+ - id: recon
55
+ next:
56
+ - to: build
57
+ require: [contract_file]
58
+ - id: build
59
+ next:
60
+ - to: done
61
+ require: [children_settled]
62
+ ```
63
+ Omitted means the built-in `planning → implementing → reviewing → verifying`.
64
+ Conditions are a closed vocabulary (`contract_file`, `children_present`,
65
+ `children_settled`, `children_none_failed`, `verdict_ok`, `verdict_not_ok`,
66
+ `verification_passed`, `verification_failed`, `rework_available`,
67
+ `children_all_failed`, `always`) and **none of them names a role**, which is
68
+ what lets a team reshape its workflow without touching source.
69
+ - Conditions live on the **edge**, not the phase: `planning → implementing`
70
+ needs a contract while `planning → failed` must not, or a team could not fail
71
+ before it had one.
72
+ - `done` / `failed` / `aborted` are injected when a card omits them. A team that
73
+ cannot be aborted is a safety hole. Reaching them is still gated: `failed`
74
+ needs a terminal reason, `aborted` needs a parent retire, `done` stays
75
+ supervisor-only. `aborted` is deliberately exempt from phase-level
76
+ requirements — an emergency stop must not wait for "everything settled".
77
+ - An invalid machine is rejected at load, reporting every problem at once
78
+ (unknown condition, duplicate id, unknown target, self-edge, unreachable
79
+ phase, terminal start).
80
+ - **The tree shows a team's machine (#302).** Selecting a team root unfolds its
81
+ phases, the current one, and every transition with its conditions.
82
+ - **`legion_phase` refusals name the legal exits (#299).** A refused transition
83
+ reports `legal next phases from 'recon': build, done, failed, aborted`. Measured:
84
+ without it a model invented plausible-but-nonexistent transitions and burned a
85
+ turn being refused.
86
+
87
+ ### Fixed
88
+
89
+ - **In-process children enforce `toolBudget` / `turnBudget` (#295).** The parent
90
+ computed budgets into `SpawnInput.env`, which an rpc child receives as its real
91
+ environment — but an in-process child shares the *host's* `process.env`, so the
92
+ guard read the parent's (absent) values. A card with `toolBudget: {maxToolCalls:
93
+ 3}` made 12 tool calls and finished normally. Budgets now travel in the
94
+ async-local child context, the same channel already used for lineage identity,
95
+ with `process.env` kept as the rpc fallback.
96
+ - **A malformed budget is an error, not "no limit" (#293).** `{not json}` parsed to
97
+ `undefined` and `{"maxToolCalls": -3}` was accepted, so the guard would have
98
+ blocked the first call. Both now fail at load naming the file, field, and value.
99
+ - **rpc is reserved for a centurion, and in-process keeps its ledger budget
100
+ (#279).** In-process launches had been exempted from every legion budget
101
+ (`Number.MAX_SAFE_INTEGER`), which made "run it in-process" a way to escape
102
+ depth/spawn/ledger caps. Admission and reservation now share one code path, so a
103
+ launch cannot be admitted and then rejected.
104
+ - **Runtime handles are keyed by (ledger path, node id) (#283, #285, #287).** Keyed
105
+ by path alone, a second session sharing the ledger could close the parent's
106
+ database; the identity is now adopted per node.
107
+ - **Extension inheritance discovery (#280).** `pi list` reports package
108
+ *directories*, resolved the way pi's own loader does (`package.json`'s
109
+ `pi.extensions`, else `index.ts` / `index.js`). Single-file `--extension`
110
+ arguments and builtins cannot be discovered this way — name them explicitly.
111
+ - **One spelling for "unknown child" (#288).** The old message advised "pass the
112
+ same cwd used for launch", which stopped being actionable once cross-store
113
+ lookup landed.
114
+ - **Prompt surfaces match the code again (#303, #304).** The parent playbook told a
115
+ centurion to "dispatch N centurions" — an instruction the runtime refuses
116
+ (only the root may dispatch one). The roster also listed a centurion to
117
+ itself. `worker` kept `subagent` in its tool list while its text said not to
118
+ spawn, and the README described every child as an rpc process.
119
+
120
+ ### Notes
121
+
122
+ - `toolBudget` / `turnBudget` remain **optional and unused by the bundled cards**.
123
+ Time, depth, per-node child counts, active-node counts, and the memory floor are
124
+ the intended bounds; a tool budget is a scalpel for one stubborn role, not a
125
+ default.
126
+ - Child extension inheritance includes heavy extensions. pi-lens starts an LSP
127
+ stack (~1 GB per child, #43); add `excludeExtensions: [pi-lens]` where a fleet
128
+ does not need it.
129
+
130
+ ## [4.0.0] — 2026-10-24
131
+
132
+ - **Renewal for a budget casualty (#270).** An agent whose turn budget (`timeoutMs`)
133
+ runs out no longer just reports `failed`. When it measurably progressed and its
134
+ role allows renewal, the completion notice ends with a ready-to-paste
135
+ `subagent({ action: "continue", … })` plus the evidence (`progressed: 47 tool
136
+ results, 12 assistant messages, 6 files changed`) and the remaining budget
137
+ (`renewals: 1/2`). Renewal reuses the existing `continue` path, so the child
138
+ resumes from its own session file with its context intact. Two dispatches in one
139
+ day motivated this: one worker was cut off with nothing on disk, another finished
140
+ its code but ran out during verification.
141
+ - **Never automatic.** An auto-renewed stuck child would hold a concurrency slot
142
+ indefinitely; the notice hands over the decision instead.
143
+ - "Progressed" = ≥ 3 new `toolResult` rows or ≥ 1 changed file since the last
144
+ baseline. `assistant` messages are shown but never count — a model looping on
145
+ the same sentence grows that number without doing work.
146
+ - New agent frontmatter: `renewable` (default **true**; a timed-out `scout` means
147
+ the task was mis-scoped) and `maxRenewals` (default **2**; `0` disables, each
148
+ renewal is a fresh full budget). Both are validated in the team-override path
149
+ like every other numeric/boolean field.
150
+ - `wait`/`collect` timeouts never propose a renewal: those return while the child
151
+ is still alive. Only a real termination does, judged by `stopReason: "toolUse"`
152
+ / missing assistant row (F29) / the host's `terminated` wording — a clean
153
+ `stop` from an ordinary retry does not spend the budget.
154
+
155
+ - **Launch admission queue (#266).** A launch that exceeds a *concurrency* gate
156
+ (`maxChildrenPerNode`, `maxActive`, ledger `max_children_per_node` /
157
+ `max_active_nodes`) no longer fails with `BUDGET_EXCEEDED`. It is enqueued,
158
+ the caller gets `▶ scout-7 (scout) supervisor=inprocess queued(3)` and treats
159
+ the work as dispatched, and the child starts automatically when a slot frees.
160
+ Previously a 12-step fan-out against `maxChildrenPerNode: 8` ran 8 children and
161
+ reported 4 errors — and nothing ever retried them, because the parent was
162
+ already inside `wait` and the freed slot had no subscriber.
163
+ - New settings: `subagents.inprocess.maxQueuedLaunches` (default **32**) and
164
+ `subagents.inprocess.launchQueueTimeoutMs` (default **900000**). Both are
165
+ shared by the in-process and RPC paths — one scheduler, not two.
166
+ - Depth, spawn-per-session, memory, name-collision and config errors are still
167
+ refused synchronously, byte-identically to before: only gates that a slot
168
+ release can satisfy are queueable. `maxChildrenPerNode: 0` is a hard refusal
169
+ (0 means "no children", so waiting can never satisfy it).
170
+ - A queued launch reserves nothing: no ledger row, no child run directory, no
171
+ worktree, no session file. `timeout`/`overflow`/`stop` are reported by name;
172
+ `stop` dequeues without ever spawning, and a parent-turn abort flushes the
173
+ queue and says how many launches it dropped.
174
+ - `subagent({action:"status"})` shows `state=queued`, `collect`/`wait` block
175
+ until admission instead of reading the placeholder session path, and
176
+ `async: false` reports `↳ <name> waited Ns in the launch queue`.
177
+ - Two new process-only events (`launch_queued`, `launch_admitted`) and a
178
+ `legion-queue: N queued` status pill.
179
+
180
+ ## [4.0.0] — 2026-10-19
181
+
182
+ A major line — but not only the pi-only collapse. Relative to 3.1.0 this release
183
+ removes every non-`pi` agent kind and the archived Herdr backend behind it, along
184
+ with the settings and error codes that only its transport read. It also carries
185
+ everything else that landed on `master` while that removal was held back: a
186
+ launch admission queue that queues instead of refusing, one liveness verdict
187
+ shared by every display surface, a status panel built on pid / rss /
188
+ context-token data, and ten measured performance fixes.
189
+
190
+ ### Breaking
191
+
192
+ - **Kinds collapse to `pi`.** `AGENT_KINDS` is now `["pi"]`; the
193
+ `cursor` / `claude` / `codex` / `gemini` and other third-party CLI kinds are
194
+ gone, and with them the `cursorModel` / `nativeModelFor` / `planKindStart`
195
+ model translation and the "model dropped, runs on its own default" notice
196
+ (a `pi` child expresses every model, so the case no longer exists). `kind` is
197
+ still a legal frontmatter key, but `pi` is the only accepted value: a non-`pi`
198
+ value is ignored — one note on stderr per role (deduplicated, because the
199
+ catalog is re-read on several UI paths) naming the role and the original
200
+ value — and the role launches as `kind: pi`. The same applies to
201
+ `subagents.presets.<role>.kind` and `subagents.agentOverrides.<role>.kind`,
202
+ the three places a role's `kind` can come from. No legacy prefix aliases.
203
+ - **Herdr backend removed.** `src/backends/legacy/` and `test/legacy/` are
204
+ deleted, together with the deprecated `pi-legion/api` re-exports that only
205
+ served them (`createHerdrClient`, `parseHerdrResponse`, `mapHerdrErrorCode`,
206
+ `readPaneDiagnostic`, `createCommandRunner`, `resolveHerdrBin`, `HerdrClient`
207
+ and their types). `pi-legion@0.16.x` stays the pin for the Herdr-era
208
+ execution model; the `spawnMode: rpc` / `inprocess` backends are unaffected —
209
+ those are two ways of running **Pi**, not third-party transports.
210
+ - **Dead `herdr.*` settings removed.** `subagents.herdr.*` is no longer parsed
211
+ (a leftover block is silently ignored, as it has been ignored since 2.0.0),
212
+ the seven `herdr` constants are gone from `DEFAULTS`, and
213
+ `ErrorCodes.HERDR_UNAVAILABLE` / `ErrorCodes.PANE_BUSY` are removed.
214
+ `RunRecord.herdr` and `StatusEntry.herdrStatus` stay: they carry live
215
+ `{supervisor, node_id}` state, not backend selection.
216
+
217
+ ### Added
218
+
219
+ - **Launch admission queue (#266).** A launch that exceeds a *concurrency* gate
220
+ (`maxChildrenPerNode`, `maxActive`, ledger `max_children_per_node` /
221
+ `max_active_nodes`) no longer fails with `BUDGET_EXCEEDED`. It is enqueued,
222
+ the caller gets `▶ scout-7 (scout) supervisor=inprocess queued(3)` and treats
223
+ the work as dispatched, and the child starts automatically when a slot frees.
224
+ Previously a 12-step fan-out against `maxChildrenPerNode: 8` ran 8 children and
225
+ reported 4 errors — and nothing ever retried them, because the parent was
226
+ already inside `wait` and the freed slot had no subscriber.
227
+ - New settings: `subagents.inprocess.maxQueuedLaunches` (default **32**) and
228
+ `subagents.inprocess.launchQueueTimeoutMs` (default **900000**). Both are
229
+ shared by the in-process and RPC paths — one scheduler, not two.
230
+ - Depth, spawn-per-session, memory, name-collision and config errors are still
231
+ refused synchronously, byte-identically to before: only gates that a slot
232
+ release can satisfy are queueable. `maxChildrenPerNode: 0` is a hard refusal
233
+ (0 means "no children", so waiting can never satisfy it).
234
+ - A queued launch reserves nothing: no ledger row, no child run directory, no
235
+ worktree, no session file. `timeout`/`overflow`/`stop` are reported by name;
236
+ `stop` dequeues without ever spawning, and a parent-turn abort flushes the
237
+ queue and says how many launches it dropped.
238
+ - `subagent({action:"status"})` shows `state=queued`, `collect`/`wait` block
239
+ until admission instead of reading the placeholder session path, and
240
+ `async: false` reports `↳ <name> waited Ns in the launch queue`.
241
+ - Two new process-only events (`launch_queued`, `launch_admitted`) and a
242
+ `legion-queue: N queued` status pill.
243
+
244
+
245
+ - **One liveness verdict for every display surface (#liveness).** The list, the
246
+ text view and the railway chart each decided "is this agent still working?"
247
+ their own way, and the railway one compared two clocks that no single writer
248
+ owned: `eventOverrides` vetoed an event against `node.updatedAt`, but the
249
+ event row is stamped by this process's event bus while `updatedAt` is stamped
250
+ by the child's heartbeat — and only `isAlive` children are heartbeaten, so a
251
+ dead child's `updatedAt` freezes. The comparison ranked two clocks and the
252
+ losing side was the truthful one: a finished node was repainted `running`,
253
+ forever, because the frozen row never moved again. `src/extension/liveness.ts`
254
+ replaces all three with one `liveness(facts)` in TOTAL ORDER — `live` /
255
+ `dead` / `unknown` — with no clock, no fs and no timer of its own. Unknown or
256
+ unrecognized states now render `unknown` instead of being guessed into
257
+ `retired`, and `queued` is a first-class display state.
258
+ - Contract: `docs/contracts/display-liveness-v1.md` (frozen first, then
259
+ implemented; D1-a and D5-a record two amendments found during the work).
260
+
261
+ - **A status panel on the `/legion` board (#241, #245).** A fifth pane carries
262
+ the `/legion status` table onto the full-screen viewer — name · role · state ·
263
+ supervisor · ctx · rss · turns — graded down by absolute column count
264
+ (72/52/36/20) so a narrow board keeps name · state instead of a column of
265
+ stubs. A portrait layout gets a zero-area rect and Tab skips it rather than
266
+ painting a fifth pane that cannot fit. Only an `rpc` child owns a process, so
267
+ only it shows rss; an `inprocess` child shows `—` rather than a share of the
268
+ host figure it does not own, because a fabricated number would get used.
269
+ - `pid`, `spawnMode`, `contextTokens` and `rss` are optional on `VizNode` and
270
+ projected conditionally: a missing mode renders `unknown`, never a guessed
271
+ `rpc`. No new `legion.db` column and no schema-version bump — pid is
272
+ host-scoped and never persisted, so a rebuild from a `ChildRecord` invents
273
+ none.
274
+ - Context tokens are absolute only. Session JSONL carries no context window,
275
+ so no percentage is computed and none is displayed.
276
+ - `/legion status` also stopped dropping `spawnMode` and now supplies `owner`.
277
+
278
+ - **`/legion stop --force` (#179).** The TUI slash command can now hard-retire a
279
+ child, equivalent to the tool's `force: true`; `--force` / `-f` is listed in
280
+ the doctor help and the README. This closes the "optional follow-up" that the
281
+ docs had been carrying.
282
+
283
+ - **`vexillatio` accepts `tasks[]` on its own.** `task` was required, so a
284
+ `{tasks: [...]}` call was rejected by the host's typebox pre-validation before
285
+ Legion ever saw it, and callers had to send a dummy `task` and retry. It is
286
+ optional now; a `tasks[]`-only launch is legal.
287
+
288
+ ### Changed
289
+
290
+ - **WAL is now an enforced startup precondition (#236).** `openLegionDb` used to
291
+ discard the value returned by `PRAGMA journal_mode = WAL`. That PRAGMA is a
292
+ *query*: the returned mode is the ground truth, and SQLite is entitled to
293
+ answer with the mode it settled on rather than to fail — which is how a
294
+ connection ends up on a rollback journal while every reader of the code
295
+ believes WAL is live. The ledger's durability reasoning (single-writer
296
+ serialisation, readers that never block the writer, the shutdown checkpoint)
297
+ rests entirely on WAL, so the observed mode is now captured and verified: a
298
+ file database that does not report `wal` throws at startup, with the observed
299
+ mode in the message, instead of surfacing later as data loss. `:memory:` is
300
+ exempt deliberately, keyed on `PRAGMA database_list` reporting an empty
301
+ `file` — not on a database name, and not by accident.
302
+ - The M2-(g) concurrency acceptance now runs as `npm run test:wal-stress`:
303
+ 60 supervisor processes against one ledger, 9004 attempts, 9004 commits, 0
304
+ busy, p50 1.03 ms / p95 1.99 ms / p99 4.92 ms.
305
+
306
+ - **The worker role's turn budget is 45 minutes, not 30.** The worker does the
307
+ heaviest per-dispatch work — several files, its own verification run, and a
308
+ report — yet it shared centurion's budget while scout gets 10 minutes and
309
+ reviewer 15. Two dispatches in this repository were cut off mid-work, one
310
+ with nothing on disk and one that ran out while verifying. 45 rather than 60
311
+ keeps a genuinely stuck child from holding a concurrency slot for an hour.
312
+ Only `timeoutMs` changed.
313
+
314
+ ### Fixed
315
+
316
+ - **A poison event row no longer freezes the cross-process event poller
317
+ (#233).** `node_abandoned` was missing from the event-type closed set, so a
318
+ snapshot row of that type could not be decoded; the 250 ms poller then threw
319
+ on the same row forever, its cursor never advanced, and no later event was
320
+ ever delivered. The closed set now contains `node_abandoned` (15 types), and
321
+ the poller advances past a single poison row instead of freezing: the row is
322
+ reported once through `onError`, the cursor steps over it, and the remaining
323
+ events in that batch are still delivered.
324
+
325
+ - **A finished supervisor's pid is no longer read as its successor's (#250).**
326
+ `rss` probes re-read `/proc/<pid>` after the child had exited, and Linux
327
+ reuses pids; the panel could attribute a stranger's memory to a retired node.
328
+ The probe now closes the race it was silently losing.
329
+
330
+ - **The log view stays inside its rect on narrow panes (#251).** `drawLog` wrote
331
+ past its own `maxX` when the pane was squeezed, which corrupted the frame
332
+ beside it.
333
+
334
+ - **The live overlay no longer scans the whole tree per token (#261).** A
335
+ per-token `getTreeNodes()` call made a long child turn quadratic in the number
336
+ of nodes; the scan is gone and re-opening the node that is already open is
337
+ guarded.
338
+
339
+ - **Repository subdirectories are no longer mistaken for child worktrees
340
+ (#237 P10).** The probe treated any git-visible path below the repo root as an
341
+ existing worktree, so a dispatch from `src/` could reuse the wrong directory.
342
+ All git probes are now bounded: `GIT_PROBE_TIMEOUT_MS = 5_000` and
343
+ `GIT_WORKTREE_TIMEOUT_MS = 120_000`, so a hung `git` can no longer wedge a
344
+ launch forever.
345
+
346
+ - **`SubagentError` is exported explicitly again.** It had reached
347
+ `pi-legion/api` only as a side effect of the legacy client re-exporting it, so
348
+ removing the Herdr backend silently dropped it from the public surface. It is
349
+ a deliberate export now, pinned by a test.
350
+
351
+ ### Performance
352
+
353
+ Ten fixes measured against the baseline in `docs/performance-2026-10-19.md`,
354
+ plus the harness that measured them (#239). Numbers below are the before/after
355
+ from each fix's own commit.
356
+
357
+ - **`collect()` no longer waits out a fixed settle-quiet window (#232).** An
358
+ already-settled turn cost a flat ~2.5 s of dead time; that wait is dropped when
359
+ the turn is settled (2509 ms → 503 ms).
360
+ - **The settle polls back off instead of re-parsing every 500 ms (#232).** The
361
+ transient-connection wait re-parsed the whole session file on every tick. It
362
+ now keeps the last parse, re-parses only when the artifact size moved, and
363
+ backs off 500 ms → 4 s. On an unchanged file in a 900 s window: 1800
364
+ iterations / 1804 parses → 228 iterations / 5 parses. The blip tolerance is
365
+ unchanged — sleeps are clamped to the remaining budget and the loop still ends
366
+ exactly on the deadline.
367
+ - **The `/legion` log view reads the session incrementally (#234).** It seeks
368
+ past the cached byte offset and projects only the new JSONL lines, instead of
369
+ re-parsing from the top on every refresh.
370
+ - **`attachUsageTotals` is one batched query (#235/#248).** It accounted for
371
+ ~72% of the cost of a UI snapshot; it is now a chunked `IN` query instead of a
372
+ per-node round trip.
373
+ - **Heartbeats are one transaction (#236 P1).** N children meant N commits;
374
+ eight children now cost one (3.83 ms → 0.99 ms, 3.9×).
375
+ - **Prepared statements are cached per connection (#236 P2).** `getNode` ×400
376
+ drops 15.7 ms → 4.8 ms.
377
+ - **Session parsing projects its result instead of `structuredClone` (#237 P7).**
378
+ - **The DB event source seeds its cursor at attach instead of replaying the log
379
+ (#238).** Attaching used to block ~1027 ms on replay.
380
+ - **Widget events are coalesced into one snapshot and one paint per tick
381
+ (#238).** `legion-tools` emits N `mail_sent` events in one synchronous loop, so
382
+ every burst cost 2N scans; a burst now costs a constant 2 snapshots — 2575 ms
383
+ → 4.4 ms for 1000 events over a 120-node ledger. `applyTreeEvent` itself still
384
+ runs synchronously, per event, in arrival order: it is an incremental state
385
+ machine, not a render, so deferring or reordering it would change the state it
386
+ computes.
387
+ - **The messages rate-limit `COUNT` is covered by an index (#237 P9).** 50k rows
388
+ go from a `SCAN` to ~6.6 µs.
389
+
14
390
  ## [3.1.0] — 2026-10-19
15
391
 
16
392
  ### Added
@@ -436,12 +812,16 @@ First **Pi Legion** line on headless Pi RPC. Relative to the final Herdr-era
436
812
 
437
813
  Last line that runs children in Herdr panes. Not developed further on this
438
814
  branch — pin `pi-legion@0.16.x` if you still need that model. See
439
- [Herdr transition](README.md#herdr-transition-and-role-migration) in the README.
815
+ [Herdr transition](README.md#supported-kinds) in the README.
440
816
 
441
817
  Earlier 0.11–0.15 history (including the `pi-herdr-swarm` /
442
818
  `@zzjcool/pi-herdr-subagents` renames) lives in git tags and older Releases.
443
819
 
444
- [Unreleased]: https://github.com/zzjcool/pi-legion/compare/v3.0.2...HEAD
820
+ [Unreleased]: https://github.com/zzjcool/pi-legion/compare/v5.0.0...HEAD
821
+ [5.0.0]: https://github.com/zzjcool/pi-legion/compare/v4.0.0...v5.0.0
822
+ [4.0.0]: https://github.com/zzjcool/pi-legion/compare/v3.1.0...v4.0.0
823
+ [3.1.0]: https://github.com/zzjcool/pi-legion/compare/v3.0.3...v3.1.0
824
+ [3.0.3]: https://github.com/zzjcool/pi-legion/compare/v3.0.2...v3.0.3
445
825
  [3.0.2]: https://github.com/zzjcool/pi-legion/compare/v3.0.1...v3.0.2
446
826
  [3.0.1]: https://github.com/zzjcool/pi-legion/compare/v3.0.0...v3.0.1
447
827
  [3.0.0]: https://github.com/zzjcool/pi-legion/compare/v2.0.4...v3.0.0
package/CONTRIBUTING.md CHANGED
@@ -107,10 +107,10 @@ the top of that file). Do not fail CI on it.
107
107
  Prefer **TDD** for behaviour changes: a failing test in `test/unit` or
108
108
  `test/integration` first, then the production code. Integration tests should
109
109
  drive the orchestrator through `FakeSupervisor` (`test/helpers/fake-supervisor.ts`)
110
- so nesting / launch / collect stay covered without a live `pi`. The herdr-era
111
- helpers `createHerdrClient` / `createFakeRunner` remain as **deprecated legacy**
112
- exports (see `src/api.ts` and `test/legacy/`); use them only when touching the
113
- archived Herdr backend.
110
+ so nesting / launch / collect stay covered without a live `pi`. The old
111
+ herdr-era `createHerdrClient` / `createFakeRunner` helpers are **gone** as of
112
+ v4.0.0, together with `src/backends/legacy/` and `test/legacy/`; everything
113
+ runs as `kind: pi`, in-process or over `pi --mode rpc`.
114
114
 
115
115
  ## Mock LLM
116
116
 
@@ -161,19 +161,6 @@ Or all at once (RPC mainline **1–11** only):
161
161
  npm run test:docker
162
162
  ```
163
163
 
164
- **Legacy Herdr / Space / kind checks** live under `test/legacy/docker/` (need
165
- pi-legion **0.16.x** Herdr-era setup). They are **not** part of
166
- `npm run test:docker`:
167
-
168
- ```bash
169
- bash test/legacy/docker/verify-12-workspace.sh # Herdr Space pin (legacy)
170
- bash test/legacy/docker/verify-13-kinds.sh # kind/model matrix; live CodeBuddy when mounted
171
- ```
172
-
173
- `verify-13-kinds.sh` also bind-mounts host `codebuddy` / `cursor-agent` and
174
- (unless `LIVE_KIND_PROBES=0`) calls live CodeBuddy. The numbered 1–11 scripts
175
- under `test/docker/` stay offline/mock.
176
-
177
164
  Override paths with `HOST_PI_AGENT`, `DOCKER_PI_HOME`, `PI_DOCKER_IMAGE`.
178
165
 
179
166
  Each `inside-N-*.sh` is what actually runs **in** the container. Keep those