pi-legion 0.16.0 → 2.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 (89) hide show
  1. package/CONTRIBUTING.md +58 -0
  2. package/README.md +313 -640
  3. package/agents/advisor.md +7 -5
  4. package/agents/centurion.md +53 -0
  5. package/agents/designer.md +33 -33
  6. package/agents/planner.md +1 -2
  7. package/agents/prototype.md +2 -2
  8. package/agents/reviewer.md +67 -31
  9. package/agents/scout.md +6 -2
  10. package/agents/worker.md +12 -3
  11. package/docs/contracts/esc-abort-v1.md +131 -0
  12. package/docs/contracts/esc-cascade-v2.md +100 -0
  13. package/docs/design.md +44 -25
  14. package/docs/legion-v2-contract.md +461 -0
  15. package/docs/legion-v2-delivery.md +62 -0
  16. package/docs/retire-hardening-contract.md +125 -0
  17. package/docs/subagents-teams.md +76 -0
  18. package/docs/tui-viewer-contract.md +276 -0
  19. package/docs/vexillatio-contract.md +162 -0
  20. package/index.ts +1462 -286
  21. package/package.json +12 -9
  22. package/skills/pi-legion/SKILL.md +74 -96
  23. package/src/agents/agents.ts +6 -11
  24. package/src/agents/overrides.ts +2 -1
  25. package/src/agents/paths.ts +1 -1
  26. package/src/agents/settings.ts +268 -20
  27. package/src/api.ts +47 -48
  28. package/src/{herdr → backends/legacy}/client.ts +14 -69
  29. package/src/{shared → backends/legacy}/cursor-chat.ts +2 -2
  30. package/src/{herdr → backends/legacy}/runner.ts +22 -9
  31. package/src/backends/legacy/types.ts +138 -0
  32. package/src/extension/auto-doctor.ts +144 -0
  33. package/src/extension/budget.ts +2 -2
  34. package/src/extension/child-guard.ts +214 -201
  35. package/src/extension/event-bus.ts +263 -0
  36. package/src/extension/legion-commands.ts +719 -0
  37. package/src/extension/legion-doctor.ts +888 -0
  38. package/src/extension/legion-stop.ts +150 -0
  39. package/src/extension/legion-tools.ts +816 -0
  40. package/src/extension/notify.ts +24 -19
  41. package/src/extension/playbook.ts +20 -89
  42. package/src/extension/recycle.ts +2 -7
  43. package/src/extension/runtime.ts +655 -105
  44. package/src/extension/scout-guard.ts +483 -0
  45. package/src/extension/slash.ts +89 -438
  46. package/src/extension/spawn-hints.ts +79 -0
  47. package/src/extension/summary.ts +17 -58
  48. package/src/legion/budget.ts +165 -0
  49. package/src/legion/db.ts +242 -0
  50. package/src/legion/events.ts +150 -0
  51. package/src/legion/mail.ts +412 -0
  52. package/src/legion/nodes.ts +461 -0
  53. package/src/legion/rebuild.ts +533 -0
  54. package/src/legion/state-machine.ts +313 -0
  55. package/src/legion/usage.ts +101 -0
  56. package/src/runs/acceptance.ts +82 -25
  57. package/src/runs/args.ts +51 -5
  58. package/src/runs/hygiene.ts +335 -0
  59. package/src/runs/orchestrator.ts +790 -1683
  60. package/src/runs/owner.ts +61 -0
  61. package/src/runs/provision.ts +135 -0
  62. package/src/runs/store.ts +44 -17
  63. package/src/runs/verify-queue.ts +195 -0
  64. package/src/shared/name.ts +29 -0
  65. package/src/shared/output-cap.ts +57 -0
  66. package/src/shared/progress.ts +57 -135
  67. package/src/shared/session.ts +243 -171
  68. package/src/shared/types.ts +107 -208
  69. package/src/supervisor/child-memory.ts +140 -0
  70. package/src/supervisor/proc-tree.ts +103 -0
  71. package/src/supervisor/rpc-supervisor.ts +1033 -0
  72. package/src/supervisor/types.ts +153 -0
  73. package/src/supervisor/ui-proxy.ts +226 -0
  74. package/src/tui/chat-items.ts +330 -0
  75. package/src/tui/live-overlay.ts +874 -0
  76. package/src/tui/status.ts +553 -27
  77. package/src/tui/tree-view.ts +808 -0
  78. package/src/tui/viz/canvas.ts +119 -0
  79. package/src/tui/viz/layout.ts +111 -0
  80. package/src/tui/viz/model.ts +123 -0
  81. package/src/tui/viz/palette.ts +188 -0
  82. package/src/tui/viz/panels.ts +530 -0
  83. package/src/tui/viz/railway.ts +313 -0
  84. package/src/tui/viz/rate.ts +211 -0
  85. package/src/tui/viz/view.ts +228 -0
  86. package/src/extension/parent-label.ts +0 -115
  87. package/src/profiles/classify.ts +0 -375
  88. package/src/profiles/profiles.ts +0 -730
  89. package/src/runs/layout.ts +0 -258
package/README.md CHANGED
@@ -1,715 +1,388 @@
1
1
  ---
2
2
  name: pi-legion
3
3
  npm: "pi-legion"
4
- description: "Delegate work to child Pi agents running in Herdr panes: visible, steerable, resumable, with accurate success/failure accounting. Use when a task would benefit from parallel workers, fresh-context adversarial review, or long-running background subtasks that must survive the parent. Do not use for trivial single-file edits, running tests, or answering simple questions."
4
+ description: "Pi Legion v2: recursive centurion trees on headless Pi RPC; shared SQLite WAL legion.db. Four-stage planning/implementing/reviewing/verifying state machine, parent/child/sibling legion_mail, three-layer /legion UI, upward MR trees, squad resume."
5
5
  ---
6
6
 
7
7
  # pi-legion
8
8
 
9
- A [Pi](https://github.com/badlogic/pi-mono) extension that delegates work to
10
- **child Pi agents in [Herdr](https://herdr.dev) panes**.
11
-
12
- A herdr subagent is a **supervised live process**, not a one-shot function call.
13
- It stays resident across turns, can be steered mid-flight, resumed after its
14
- pane is gone, and stays visible to you. The parent session never has to invent
15
- `herdr pane split` / `herdr agent start` — one `subagent` tool call is the
16
- whole launch path.
9
+ A Pi extension for building recursive, resumable agent teams. A `centurion`
10
+ leads each squad; Pi RPC runs its children as headless processes, while a
11
+ shared SQLite ledger records the whole tree.
17
12
 
18
13
  > **Name note.** This package was formerly published as
19
- > `@zzjcool/pi-herdr-subagents` (through 0.14.3; renamed after an npm
20
- > name collision) and then as `pi-herdr-swarm` (0.15.0; retired when the
21
- > name stopped matching the architecture — this is a command tree, not a
22
- > swarm). It is now `pi-legion`, first published as 0.16.0 (continuing
23
- > the version line). To migrate: `pi uninstall` the old package, then
24
- > `pi install npm:pi-legion`.
14
+ > `@zzjcool/pi-herdr-subagents` (through 0.14.3; renamed after an npm name
15
+ > collision), then as `pi-herdr-swarm` (0.15.0), before becoming `pi-legion`
16
+ > in 0.16.0. **2.0.0: the legion era — recursive centurion tree on pi RPC transport; herdr pane execution archived to legacy**
25
17
 
26
18
  ## Contents
27
19
 
28
- - [Requirements](#requirements)
29
- - [Install](#install)
30
- - [What the plugin does for you](#what-the-plugin-does-for-you)
20
+ - [Architecture](#architecture)
31
21
  - [Quick start](#quick-start)
32
- - [Tool reference](#tool-reference)
33
- - [Bundled roles](#bundled-roles)
34
- - [Presets (kind+model+thinking)](#presets-kindmodelthinking)
35
- - [Model profiles (cheap / medium / strong)](#model-profiles-cheap--medium--strong)
36
- - [Settings](#settings)
37
- - [Master switch](#master-switch)
38
- - [Teams](#teams)
39
- - [Custom agents](#custom-agents)
40
- - [Isolation](#isolation)
41
- - [Success and failure](#success-and-failure)
42
- - [Programmatic API](#programmatic-api)
22
+ - [Legion tools and phases](#legion-tools-and-phases)
23
+ - [Three-layer visualization](#three-layer-visualization)
24
+ - [Settings and budget gates](#settings-and-budget-gates)
25
+ - [Team 配置](#team-配置)
26
+ - [Parent/child context conventions](#parentchild-context-conventions)
27
+ - [Delivery and resume](#delivery-and-resume)
28
+ - [Herdr transition and role migration](#herdr-transition-and-role-migration)
43
29
  - [Development](#development)
44
- - [Known limitations](#known-limitations)
45
-
46
- ## Requirements
47
-
48
- - **Pi** (the coding agent that loads this package as an extension)
49
- - **Herdr**, with the parent session running **inside** a Herdr pane
50
- (`HERDR_ENV=1`). The plugin talks to herdr to create tabs/panes and start
51
- child agents; it will not launch anything from a plain terminal Pi.
52
- - Node.js 22+ if you are developing the package itself
53
30
 
54
- You do **not** need to run `test "$HERDR_ENV" = 1` or `herdr --help` yourself.
55
- The parent model must not either — the plugin blocks that ritual and checks
56
- herdr on every launch.
31
+ ## Architecture
57
32
 
58
- ## Install
59
-
60
- ```bash
61
- pi install npm:pi-legion
33
+ ```text
34
+ User (CEO) — root Pi session (TUI)
35
+ ├─ pi-legion extension
36
+ │ ├─ LegionSupervisor (root)
37
+ │ │ ├─ RpcClient × N → headless `pi --mode rpc` child processes
38
+ │ │ ├─ mail postman (inbox delivery) + four-phase state machine
39
+ │ │ └─ orphan scan + child lifecycle / usage accounting
40
+ │ ├─ `legion.db` (one shared SQLite/WAL tree ledger)
41
+ │ └─ LegionEventBus → resident tree | `/legion` full view | live overlay
42
+ └─ centurion child (RpcClient; cwd = its subtree worktree)
43
+ └─ pi-legion in child mode
44
+ ├─ LegionSupervisor for this squad
45
+ ├─ `legion_mail` / `legion_tree` / `legion_phase` tools
46
+ ├─ worker / scout / reviewer … children (`worktree: false`)
47
+ └─ nested centurions → recursive squads
62
48
  ```
63
49
 
64
- From git (picks up commits that are not on npm yet):
50
+ Transport, state, decision-making, and display are separate layers:
65
51
 
66
- ```bash
67
- pi install git:github.com/zzjcool/pi-legion
68
- ```
52
+ - **Transport:** each child is a headless Pi RPC process managed by a
53
+ `LegionSupervisor`; a centurion can supervise another squad recursively.
54
+ - **State:** all nodes in the tree share the `legion.db` SQLite ledger in WAL
55
+ mode. Per-run `run.json` records and session files remain the resume source
56
+ for each child.
57
+ - **Decision:** centurions and squad members do the LLM work. The supervisor
58
+ owns machine state and validates phase transitions.
59
+ - **Display:** the event bus feeds the resident tree, the `/legion` full view,
60
+ and an on-demand live overlay.
69
61
 
70
- From a local checkout:
62
+ ## Quick start
63
+
64
+ Install and load the extension in Pi:
71
65
 
72
66
  ```bash
73
- pi install /path/to/pi-legion
67
+ pi install npm:pi-legion
74
68
  ```
75
69
 
76
- Then **reload the parent session** (`/reload`, or restart Pi). After install,
77
- seven roles (`scout`, `planner`, `worker`, `reviewer`, `advisor`, `designer`,
78
- `prototype`) are available
79
- with no extra files to copy — see [Bundled roles](#bundled-roles).
80
-
81
- To update a git install: `pi update`. To switch from a path/git install to npm
82
- once a release is published, remove the old package and `pi install npm:pi-legion`.
83
-
84
- ## What the plugin does for you
85
-
86
- These used to be “please remember this in the prompt.” They are now
87
- **scripts**. Neither the parent model nor the child has to cooperate for them
88
- to hold.
89
-
90
- | Concern | What happens |
91
- | --- | --- |
92
- | Launch | One `subagent` call creates the type tab, splits a pane, starts `pi`, and begins watching. Same agent type shares one tab (each child is a pane). |
93
- | Status | Running children are painted **above the parent input** with model (`:thinking` when set), kind (if not `pi`), turn / in-flight tool, and worktree branch. Elapsed time keeps ticking after the tool call returns. Non-pi kinds (cursor, claude, …) fill the same slots from `agent get` labels / pane title when jsonl is absent. |
94
- | Completion | When a turn finishes, the plugin injects `Background task completed: **name**` into the parent. If the parent is idle it wakes immediately; if it is still in a tool loop, the notice waits (`followUp`) instead of steering mid-turn. Every finish is shown in the transcript: a completed child renders as one truncated row (`✓ name · success · acceptance · size`, clamped to the live pane width), failures/stops keep the full block, and `ctrl+o` expands a collapsed row. |
95
- | Recycle | Terminal children get `ctrl+d`, then pane close (and the type tab when it is empty). You do not `retire` or close panes. `blocked` is the exception — the pane stays. |
96
- | Child must not ping the parent | Every child Pi loads a **child-guard** extension. `herdr agent prompt/wait/send-keys/start`, pane split, tab create/close, and reading someone else’s pane are blocked. |
97
- | Task-card boilerplate | A frozen appendix is appended to every child task: no wakeup, no nested agents, end with `{"ok": true\|false, "reason": "..."}`. |
98
- | Read-only roles | `acceptance.role: read-only` (scout, reviewer, …) may still have `bash` for `rg` / `git log` / `ls`, but writes (`rm`, `git commit`, `echo > file`, `npm install`, `sed -i`, …) are blocked. |
99
- | Acceptance | `{"ok": true}` is only **attested**. If a required criterion lists `evidence: [verification-output]` **and** carries an explicit `command`, collect then runs that command in the child’s cwd and promotes the result to **verified** or rejects it. Without a `command` the result stays attested with the criterion surfaced as pending — the parent confirms it from the child’s pasted output; nothing is guessed. |
100
- | Tool approval (`blocked`) | `onBlocked: auto-approve` (**default**; a subagent pane has no human, so an approval dialog is a deadlock) silently approves a blocked child via `send-keys y` and keeps watching. `forward` pops a **confirm** in the parent TUI (Yes → `send-keys y`, No → deny; no TUI → a notify is queued). `notify` only notifies. The default is also what cursor children launch with: `--force` (Run Everything) unless the agent sets `forward`/`notify` — set `subagents.defaultOnBlocked` in settings.json to change the default globally. |
101
- | Late `collect` / `retire` | If watch already collected, `collect` returns the cached snapshot. If the pane is already gone, `retire` is a no-op that points at the session file. |
102
-
103
- What is still a **judgment** (and must stay one):
104
-
105
- - whether to spawn a child, which role, and what the task should say
106
- - what to do with the completion (accept, `steer`, `continue`, ignore)
107
- - whether a semantic checklist item like “no secrets in the diff” actually holds
108
-
109
- ## Quick start
110
-
111
- Use the `subagent` tool. **Do not run herdr CLI or env checks first.** Do not
112
- tell the child to prompt the parent.
70
+ Ask a `centurion` to lead a recursive squad, with a goal and concrete
71
+ acceptance checklist in its task:
113
72
 
114
73
  ```text
115
74
  subagent({
116
- agent: "reviewer",
117
- task: "Review src/foo.ts for regressions. Write findings to /tmp/review.md."
75
+ agent: "centurion",
76
+ task: "Goal: implement the account export flow. Acceptance: contract is recorded; implementation is reviewed; the verification commands pass; return a squash MR and verdict."
118
77
  })
119
78
  ```
120
79
 
121
- The frozen appendix (verdict JSON, no wakeup) is added automatically. You can
122
- still write a detailed task card; just do not include “when done, `herdr agent
123
- prompt` the parent.” Isolation is the parent’s call:
80
+ The centurion plans the work, writes the squad contract, dispatches ordinary
81
+ roles such as `worker`, `scout`, and `reviewer`, and coordinates the squad
82
+ through its phases. For a single leaf task, dispatch an ordinary role directly:
124
83
 
125
84
  ```text
126
- subagent({ agent: "worker", task: "<implement>", worktree: true }) // own branch, child opens MR
127
- subagent({ agent: "worker", task: "<hotfix>", worktree: false }) // edit this checkout
85
+ subagent({ agent: "scout", task: "Map the export code path and report relevant files and risks." })
128
86
  ```
129
87
 
130
- Parallel fan-out — one tool call, then return control:
88
+ No manual child-process launch is needed. Use `/legion` to inspect the tree;
89
+ see [Three-layer visualization](#three-layer-visualization) for navigation.
131
90
 
132
- ```text
133
- subagent({ tasks: [
134
- { agent: "worker", task: "<task A>" },
135
- { agent: "worker", task: "<task B>" },
136
- { agent: "reviewer", task: "<task C>" },
137
- ] })
138
- ```
91
+ The `subagent` control actions include event-driven `wait` and graceful `stop`:
92
+ `subagent({ action: "wait", name: "worker-0" })` returns as soon as the child
93
+ settles (or the parent turn is interrupted with Esc). Aborting a parent turn
94
+ with Esc gracefully retires its tracked children, including those being waited
95
+ on or collected; the cascade is silent. To stop children explicitly, use
96
+ `subagent({ action: "stop", name: "worker-0" })` or
97
+ `subagent({ action: "stop", all: true })`; this runs the normal graceful-retire
98
+ sequence for each selected child.
139
99
 
140
- Steer a running agent (current task abandoned, context kept):
100
+ ### Stopping async subagents yourself
141
101
 
142
- ```text
143
- subagent({ action: "steer", name: "reviewer-0", message: "Stop the API review; focus on the migration script instead." })
144
- ```
102
+ Async children keep running after the main turn ends, so Esc-abort no longer
103
+ reaches them. Three user controls run the same graceful `stop` (a stopped
104
+ parent retires its own subtree on shutdown):
145
105
 
146
- Pass `async: false` only when **this turn** must have the result before it
147
- ends. The default is async: launch, return control, get a completion later.
106
+ - **`/legion stop`** stops every live subagent; **`/legion stop <name>`** stops
107
+ one (`all` is an alias for no name). Names complete from the live children;
108
+ nested agents complete as `parent/name` and are reported as owned by their
109
+ parent (stop the parent to retire them).
110
+ - **Esc** while the main agent is idle, the editor is empty, no dialog or
111
+ overlay is open and subagents are live asks "Stop N running subagents?".
112
+ In every other state Esc keeps pi's normal behaviour.
113
+ - **`x`** on the selected live agent in the `/legion` view or its live
114
+ overlay asks `y` to confirm in place, then stops it.
148
115
 
149
- ## Tool reference
116
+ While the main agent is idle and subagents are live, the railway header shows
117
+ a dim `esc stop all · /legion stop [name]` hint (shortened on narrow terminals,
118
+ hidden while the main agent is working).
150
119
 
151
- | `action` | Purpose |
152
- | --- | --- |
153
- | *(omitted)* / `launch` | Start one child (`agent` + `task`) or several (`tasks[]`) |
154
- | `steer` | Prompt a live child; it drops the current turn and takes the new message |
155
- | `continue` | Prompt a live child without abandoning the current turn |
156
- | `resume` | Relaunch from the session file if the pane is already gone |
157
- | `status` | Show the persisted record (state, pane, session path, last execution) |
158
- | `collect` | Wait for the current turn (or return the cache if watch already did) |
159
- | `list` | Roles the parent can spawn, with provenance (`builtin` / `user` / `project`) |
160
- | `retire` | Close the pane. After auto-recycle this is a documented no-op |
161
-
162
- Useful launch fields: `model`, `preset`, `cwd`, `placement` (`split-down` / `split-right` /
163
- `new-tab`), `worktree` (`true` = isolated branch + child opens an MR; `false` =
164
- edit the current checkout; omit = role default), `agentScope` (`user` /
165
- `project` / `both`), `async`.
166
-
167
- Handles look like `reviewer-0`. Every loaded role (builtin plus
168
- `~/.pi/agent/agents` and project `.pi/agents`) is injected into the parent
169
- system prompt each turn — that is how the model knows to call `search` without
170
- `action=list` first. `status` / `collect` take `name`.
171
-
172
- ## Bundled roles
173
-
174
- Five role definitions ship inside the package and load with **zero setup**.
175
- They are anchored to the package itself (not to `~/.pi/agent/agents`), because
176
- Pi’s package resource conventions have no `agents/` slot — a manifest entry
177
- alone would be silently ignored.
178
-
179
- | Role | Purpose | Acceptance |
180
- | --- | --- | --- |
181
- | `scout` | Read-only reconnaissance: map code, conventions, environment. Facts only. | attested, **read-only** bash |
182
- | `planner` | Turn a task into a parallelisable, verifiable plan; freeze interfaces. | attested |
183
- | `worker` | Implement a frozen plan on an isolated worktree; open an MR; run tests; self-report a verdict. | **verified** via the project’s own verifier |
184
- | `reviewer` | Read-only adversarial review; findings with path + severity. | attested, **read-only** bash |
185
- | `advisor` | Decision counsel and final arbitration on contested options (A-vs-B rulings). Grok via cursor. | attested |
186
- | `designer` | Front-end UI/visual/interaction design: design tokens, full-state matrices, and the component implementation. UI layer only — business logic stays with `worker`. | **verified** via build |
187
- | `prototype` | Single-file clickable HTML prototypes to validate an interaction flow before real implementation. | attested, writer |
188
-
189
- Bundled roles do **not** pin a vendor model. A child uses the parent session’s
190
- model unless you set `subagents.defaultModel`,
191
- `subagents.agentOverrides.<name>.model`, assign a named
192
- [preset](#presets-kindmodelthinking), pass `model` on the tool call, or
193
- [load a model profile](#model-profiles-cheap--medium--strong).
194
-
195
- ## Presets (kind+model+thinking)
196
-
197
- Named bundles in settings, referenced from agent frontmatter (`preset: strong`),
198
- from `agentOverrides.<name>.preset`, or from the tool’s `preset` param (single
199
- launch and `tasks[]` both accept it):
200
-
201
- ```json
202
- {
203
- "subagents": {
204
- "presets": {
205
- "cheap": { "kind": "pi", "model": "cb/deepseek-v4.1-flash", "thinking": "low" },
206
- "coder": { "kind": "pi", "model": "cb/deepseek-v4.1-flash", "thinking": "max" },
207
- "medium": { "kind": "pi", "model": "cb/glm-5.3", "thinking": "high" },
208
- "strong": { "kind": "pi", "model": "cb/kimi-k3", "thinking": "max" },
209
- "strongest": { "kind": "cursor", "model": "grok-4.6", "thinking": "xhigh" }
210
- }
211
- }
212
- }
213
- ```
120
+ ## Legion tools and phases
214
121
 
215
- One calibrated mapping (2026-09 snapshot — re-verify against current
216
- benchmarks before adopting) aligns each role with what its model is measured
217
- best at, not with price-tier folklore: cheap flash for scouting, a coder-tier
218
- flash for implementation (same-card benchmarks had it out-coding flagships at
219
- 1/20 the price), a reasoning-strong model for adversarial review, and grok for
220
- decision arbitration.
221
-
222
- Model precedence, strongest first:
122
+ Child nodes receive these tree-aware tools:
223
123
 
224
124
  ```text
225
- per-run tool `model` (beats the preset's MODEL, but the preset's KIND still applies)
226
- → preset kind / model / thinking
227
- → agentOverridesByProvider.<provider>.<name>
228
- → agentOverrides.<name>
229
- → agent frontmatter `model`
230
- → subagents.defaultModel
231
- → the dispatching (parent) session model
125
+ legion_mail({ to, subject, body, kind?, urgency? })
126
+ legion_tree({ scope?: "children" | "squad" | "subtree" | "tree", depth? })
127
+ legion_phase({ to, note?, evidence? })
232
128
  ```
233
129
 
234
- A preset deliberately beats `agentOverrides` — that is the point of the level:
235
- profiles pin roles via `agentOverrides` (`loadCatalog` folds them into
236
- `agent.model` before anything resolves), so a frontmatter-adjacent preset would
237
- be unreachable. Two guarantees:
238
-
239
- - **Atomic.** The preset's `kind` and the model the child is *actually launched
240
- with* are checked together. Whichever level supplied the model — the preset
241
- itself, a per-run tool `model`, `agentOverrides`, `defaultModel`, the parent
242
- session — a pair the kind cannot accept is refused loudly instead of being
243
- silently dropped at start (`kind: cursor` with a pi-shaped `provider/id` is
244
- the canonical case).
245
- - **Loud.** Referencing an undefined preset is an error naming the defined
246
- presets — never a silent fallback to the parent model.
247
-
248
- Agents with no `preset` resolve exactly as before.
249
-
250
- Precedence, lowest to highest — later layers override earlier ones by name:
251
-
252
- ```text
253
- builtin (shipped) < extra dirs < ~/.pi/agent/agents < <project>/.pi/agents
254
- ```
255
-
256
- Shadow any bundled role by putting a same-name `*.md` in `~/.pi/agent/agents`
257
- or `<repo>/.pi/agents`.
258
-
259
- - `subagents.disableBuiltins: true` drops the bundled layer entirely.
260
- - `PI_HERDR_SUBAGENTS_EXTRA_AGENT_DIRS` (PATH-style) injects extra read-only
261
- role directories — Nix store, container, read-only mount.
262
- - `subagent({ action: "list" })` shows every role with its provenance tag.
263
-
264
- ## Model profiles (cheap / medium / strong)
265
-
266
- Same workflow as pi-subagents: classify a provider’s models into three
267
- capability tiers, then bind roles to those tiers so you do not have to change
268
- the parent session model every time.
269
-
270
- | Tier | Roles |
271
- | --- | --- |
272
- | cheap | `scout`, `prototype` |
273
- | medium | `planner` |
274
- | strong | `worker`, `reviewer`, `designer` |
275
-
276
- (`advisor` is cursor-kind and carries its own model, so it never takes a pi-tier override.)
277
-
278
- ```text
279
- /subagents-refresh-provider-models <provider>
280
- /subagents-generate-profiles <provider>
281
- /subagents-load-profile <provider>.quota
282
- ```
283
-
284
- That writes two profiles (`<provider>.quota` leans cheaper,
285
- `<provider>.quality` leans stronger) under
286
- `~/.pi/agent/profiles/pi-legion/` and, on load, copies
287
- `agentOverrides` into `~/.pi/agent/settings.json`. Project `.pi/settings.json`
288
- still wins on overlapping keys.
289
-
290
- Other commands: `/subagents-profiles` lists saved profiles;
291
- `/subagents-check-profile <name>` re-checks each assigned model against the
292
- current registry (and a live probe unless you pass `--no-probe`). Refresh and
293
- generate accept `--force` and `--no-probe`.
294
-
295
- `/subagents-agents [user|project|both]` lists the roles available to the
296
- `subagent` tool in the CURRENT session — the human-facing version of the
297
- roster injected into the model's system prompt, plus what a launch would
298
- actually resolve:
299
-
300
- ```text
301
- builtin (7)
302
- advisor
303
- 决策顾问:……
304
- kind=cursor · alias: ask, consult, oracle · model: (agent CLI default)
305
- file: /pkg/agents/advisor.md
306
- scout
307
- 侦察员:……
308
- model: cb/glm-5.3 (agentOverrides)
309
- file: ~/.pi/agent/agents/scout.md
310
- project (1)
311
- deploy
312
- deploys things
313
- model: cb/kimi-k3 (the parent session model)
314
- file: <repo>/.pi/agents/deploy.md
315
-
316
- Directories
317
- builtin: /pkg/agents
318
- user: ~/.pi/agent/agents
319
- project: /repo/.pi/agents
320
- ```
321
-
322
- Each role shows its layer (`builtin` / `user` / `project`), the model a launch
323
- would actually resolve — with the same provenance labels and refusal checks
324
- the launch path uses (`the agent's frontmatter`, `agentOverrides`, `the
325
- preset`, `the parent session model`, …; `✗ outside model scope`, `✗ cannot
326
- be used with kind 'cursor'`, or `dropped at launch` for a model the target
327
- CLI cannot express) — plus kind, aliases, and the definition file, so a role
328
- that is not doing what you expect can be traced to the exact file that
329
- defines it in one step. The optional scope argument mirrors the tool's
330
- `agentScope` (`user` by default; `project` skips the user layers, the bundled
331
- roles always load); it re-reads agent files on every invocation, so edits
332
- show up immediately.
333
-
334
- `/subagents-summary` renders a Markdown table of the current parent pane's
335
- subagent runs, including role outcomes, turns, token usage, cost, and cumulative
336
- agent-time. Use `/subagents-summary --all` to include runs from every parent
337
- pane in the current working directory, or `/subagents-summary <name>` for one
338
- child's detailed state, timing, usage, and session path. Child names are
339
- available through command completion.
340
-
341
- you can still hand-write the same mapping:
342
-
343
- ```json
344
- {
345
- "subagents": {
346
- "agentOverrides": {
347
- "scout": { "model": "your-provider/fast-model" },
348
- "planner": { "model": "your-provider/mid-model" },
349
- "worker": { "model": "your-provider/strong-coder" },
350
- "reviewer": { "model": "your-provider/strong-reasoner" },
351
- "designer": { "model": "your-provider/strong-model" }
352
- }
353
- }
354
- }
355
- ```
356
-
357
- ## Master switch
358
-
359
- Two levels, both temporary-by-design:
360
-
361
- - **Session** — `/subagents-toggle` flips the switch for the current session
362
- only. No files change; `/reload` restores whatever the settings say.
363
- `/subagents-toggle` (no args) shows the current state; `on`/`off` are
364
- explicit; the status line also warns how many children are still running.
365
- - **Project / user settings** — `"subagents": { "enabled": false }` in
366
- `<repo>/.pi/settings.json` (project wins over `~/.pi/agent/settings.json`)
367
- persists the off state across sessions.
368
-
369
- While disabled:
370
-
371
- - New launches are refused with a pointer to `/subagents-toggle on`.
372
- - The system-prompt roster/playbook injection stops (next turn), so the model
373
- is not coached toward a tool that refuses.
374
- - The bash guard stops blocking manual `herdr` commands — you are free to
375
- drive herdr by hand.
376
- - **Existing children are untouched**: they keep running and notify normally,
377
- and `status`/`collect`/`wait`/`resume`/`steer` keep working, so a disable
378
- mid-flight stays observable.
379
-
380
- ## Settings
381
-
382
- Read from `~/.pi/agent/settings.json` (user) and `<project>/.pi/settings.json`
383
- (project). Project wins on overlap.
384
-
385
- > **herdr sidebar**: while async children are live, the parent pane is annotated
386
- > `idle=⏳ N subagents (…)` via `pane report-metadata --state-label` (TTL'd, self
387
- > clearing). herdr's own integration reports the parent `idle` the moment its
388
- > turn settles — the label is the visible "waiting on subagents" cue in the
389
- > sidebar. No setting; automatic when running inside herdr.
130
+ - **`legion_mail`** sends asynchronous messages over permitted tree edges:
131
+ parent, children, or same-parent siblings. `to` may be a short name, node ID,
132
+ `parent`, `children`, or `squad`. Cross-squad direct mail and bypassing the
133
+ parent are refused; route cross-squad requests through the relevant
134
+ centurions. Keep large artifacts in the shared worktree and mail a file
135
+ pointer. The body has a 4 KB soft limit and a 32 KB hard limit.
136
+ - **`legion_tree`** is a read-only view of machine-recorded node state. It does
137
+ not interrupt the nodes being observed.
138
+ - **`legion_phase`** is available to centurions. It requests a state transition;
139
+ the supervisor checks the transition's invariants and records the result.
140
+
141
+ A squad moves through **planning → implementing → reviewing → verifying**.
142
+ Planning produces a non-empty `.pi-legion/contract.md` with file ownership
143
+ and acceptance commands. During reviewing, an independent reviewer supplies
144
+ the judgment; in verifying, the supervisor runs the contract's `legion-verify`
145
+ commands without an LLM. Passing verification advances the squad to `done`.
146
+
147
+ ## Three-layer visualization
148
+
149
+ 1. **Railway strip above the input:** one git-graph lane per live subagent
150
+ (max 4, 3 below 80 columns; oldest/shallowest first, `+N` for the rest).
151
+ When an agent ends its lane merges back and lower lanes shift up. The
152
+ header shows the live count and tok/s (trailing 60s average). Hidden when
153
+ no subagent is live. Set `PI_LEGION_STRIP=tree` for the classic roster.
154
+ 2. **`/legion` full view:** galaxy graph of the agent tree, stacked tok/s
155
+ chart by agent type (peak marker, now/avg/peak/total, per-role share), and
156
+ a scrollable `log --graph` history. Tab switches panes, arrows scroll or
157
+ move the selection, Enter selects an agent (its own rate line, tok/s,
158
+ tokens and share are overlaid), `o` opens its live overlay, `x` stops it
159
+ (confirm with `y`), Esc clears the
160
+ selection, `q`/Esc closes. Layouts: wide (≥140 cols), stacked, and portrait
161
+ (<80 cols, or tall and <100 cols). `/legion --tree` opens the classic tree
162
+ (`j`/`k`, Enter/Space fold, `/` search, PageUp/PageDown).
163
+ 3. **Live overlay:** press `o` on the selected node, use a component-routed
164
+ mouse click in the strip or classic tree, or open it directly with `/legion <name>`.
165
+ The overlay subscribes to that node's live feed only while open; Esc closes
166
+ it (returning to `/legion` when opened from there) and unsubscribes; `x`
167
+ stops the agent after a `y` confirmation. OSC 8 links are visual anchors only, not overlay links.
168
+
169
+ ## Settings and budget gates
170
+
171
+ Add the v2 `legion` block to `~/.pi/agent/settings.json` or the project
172
+ `.pi/settings.json`, merging it with any existing `subagents` keys:
390
173
 
391
174
  ```json
392
175
  {
393
176
  "subagents": {
394
- "defaultModel": "provider/id",
395
- "defaultOnBlocked": "auto-approve",
396
- "team": "frontend",
397
- "teams": {
398
- "frontend": {
399
- "description": "UI work",
400
- "members": ["designer", "prototype", { "agent": "worker", "model": "provider/strong" }]
401
- }
402
- },
403
- "disableBuiltins": false,
404
- "maxSubagentSpawnsPerSession": 8,
405
- "agentOverrides": {
406
- "worker": { "model": "provider/id", "thinking": "medium" }
407
- },
408
- "agentOverridesByProvider": {
409
- "cb": { "worker": { "model": "cb/glm-5.3" } }
410
- },
411
- "modelScope": { "allow": ["cb/*", "openai/*"] },
412
- "herdr": {
413
- "defaultPlacement": "split-down",
414
- "maxConcurrentAgents": 6,
415
- "startRetries": 40,
416
- "startRetryBackoffMs": 150,
417
- "sessionRetentionDays": 7
177
+ "legion": {
178
+ "maxDepth": 4,
179
+ "maxChildrenPerNode": 8,
180
+ "maxActiveNodes": 30,
181
+ "phaseTimeouts": {
182
+ "planning": 900000,
183
+ "implementing": 3600000,
184
+ "reviewing": 1800000,
185
+ "verifying": 900000
186
+ },
187
+ "verifyCommandTimeoutMs": 600000,
188
+ "mailRatePer5Min": 10,
189
+ "usagePollMs": 30000,
190
+ "orphanStaleMs": 90000
418
191
  }
419
192
  }
420
193
  }
421
194
  ```
422
195
 
423
- | Key | Meaning |
196
+ The three launch budget gates are enforced against the tree ledger:
197
+
198
+ | Gate | Setting | Default | Limit |
199
+ | --- | --- | ---: | --- |
200
+ | Depth | `maxDepth` | 4 | Maximum tree depth; the hard cap is 4 (`MAX_NESTED_PATH_ENTRIES`). |
201
+ | Fan-out | `maxChildrenPerNode` | 8 | Direct children for one node. |
202
+ | Concurrency | `maxActiveNodes` | 30 | Whole-tree `running` + `starting` nodes. |
203
+
204
+ The effective tree depth cap is bounded by the operator override,
205
+ `subagents.legion.maxDepth`, and the hard cap of 4. **INCL semantics:** a
206
+ launched role's `maxSubagentDepth = R` counts generations in that role's
207
+ subtree, including itself; when launched by a parent at depth `d`, it inherits
208
+ `C_child = min(C_parent, d + R, 4)` (`R = 1` makes it a leaf), so it can tighten
209
+ the inherited cap but cannot widen it.
210
+
211
+ ### Team 配置
212
+
213
+ Use `subagents.team` to select the default named team from `subagents.teams`.
214
+ The `PI_SUBAGENTS_TEAM` environment variable takes precedence over
215
+ `subagents.team`; if neither selects a named team, the full discovered roster
216
+ is used. Team definitions have a description and an ordered `members` list;
217
+ each member can be a role-name string or an object with an `agent` name and
218
+ per-member overrides. Object member overrides (for example `model` or
219
+ `thinking`) take precedence over those fields in the role card's frontmatter;
220
+ omitted override fields continue to use their normal role/configuration
221
+ values.
222
+
223
+ See [docs/subagents-teams.md](docs/subagents-teams.md) for four editable
224
+ frontend/backend × premium/value team templates. The model IDs in that template
225
+ are placeholders for your provider, not required defaults.
226
+
227
+ ### Parent/child context conventions
228
+
229
+ Settings can provide `subagents.parentContext` and `subagents.childContext`
230
+ inline or through `@path` references. If either setting is omitted, Legion
231
+ looks for `subagents-parent-context.md` or `subagents-child-context.md`
232
+ respectively in the directory containing that settings file. These are
233
+ convention-based fallbacks: an explicit setting takes precedence, and setting
234
+ either field to `false` disables its context. The convention filenames are
235
+ `PARENT_CONTEXT_CONVENTION` and `CHILD_CONTEXT_CONVENTION` in
236
+ [`src/agents/settings.ts`](src/agents/settings.ts#L30-L31); missing convention
237
+ files simply contribute no context. The repository examples are
238
+ [`examples/subagents-parent-context.md`](examples/subagents-parent-context.md)
239
+ and [`examples/subagents-child-context.md`](examples/subagents-child-context.md).
240
+
241
+ ### Self-diagnosis (`/legion doctor`)
242
+
243
+ When something about subagents misbehaves (model not found, spawn exit, memory
244
+ gate, missing lens tools, orphan temps, …), ask Legion rather than guessing —
245
+ same idea as `pi list` / `pi auth check`.
246
+
247
+ - `/legion doctor` — full report in the chat (works outside TUI too).
248
+ - `/legion doctor <section>` — one of: `summary`, `extensions`, `models`,
249
+ `spawn`, `memory`, `tools`, `storage`, `runtime`, `next`.
250
+ - `subagent({ action: "diagnose" })` — same full report as tool output so the
251
+ main agent can call it without waiting for you.
252
+
253
+ The report is structured for the model: each section ends in **Issues** /
254
+ **Next steps** with copy-pasteable fixes (env exports, role frontmatter,
255
+ `/legion stop`, …). Spawn failures that look like a missing model also append
256
+ a short Hint automatically. **Gated auto-doctor:** on model-not-found,
257
+ `INSUFFICIENT_MEMORY`, extension load failures, and child exit during
258
+ startup / first prompt, the tool error itself includes a focused doctor
259
+ snippet (relevant sections + Next steps) so the main agent can self-fix
260
+ without anyone typing `/legion doctor`. Full `/legion doctor` remains for
261
+ manual deep dives.
262
+
263
+ Coverage:
264
+
265
+ | Section | What it checks |
424
266
  | --- | --- |
425
- | `defaultModel` | Fallback model for roles that do not pin one |
426
- | `defaultOnBlocked` | Fallback `onBlocked` for roles that do not set one (default `auto-approve`; `forward` restores a human confirm in the parent TUI) |
427
- | `team` | Project/user-selected team name; `PI_SUBAGENTS_TEAM` takes precedence |
428
- | `teams` | Named role lists; project entries with the same name replace the user entry |
429
- | `presets` | Named kind+model+thinking bundles, referenced via `preset:` — they beat `agentOverrides`, lose to the tool `model` |
430
- | `agentOverrides` | Per-role field overlay (`model`, `thinking`, `preset`, `tools`, `disabled`, …) |
431
- | `agentOverridesByProvider` | Same overlay, keyed by the **parent** provider id |
432
- | `enabled` | Master switch: `false` disables launches, roster injection, and the bash guard (see [Master switch](#master-switch)) |
433
- | `parentContext` | Extra parent-side context appended after the playbook + roster every turn. Inline markdown or `@path` file reference(s) (string or array); if omitted, uses the same-directory `subagents-parent-context.md` convention file |
434
- | `childContext` | Extra context injected into every child's system prompt (pi: appended after the role prompt; cursor and other kinds: rides along in the task prompt). Same value forms and precedence as `parentContext`; if omitted, uses `subagents-child-context.md` beside that settings file |
435
- | `modelScope.allow` | Glob list of `provider/id` the parent may assign. Explicit tool `model` is an error if it misses; inherited is a warning |
436
- | `disableBuiltins` | Do not load the five shipped roles |
437
- | `maxSubagentSpawnsPerSession` | Hard cap on how many children this session may start |
438
- | `herdr.maxConcurrentAgents` | Layout / density hint (default 6) |
439
- | `herdr.startRetries` | `agent_pane_busy` retries (default 40 × 150ms+) |
440
-
441
- ### Context convention files
442
-
443
- Context can live beside settings instead of in `settings.json`: place
444
- `subagents-parent-context.md` and/or `subagents-child-context.md` next to the
445
- settings file (`~/.pi/agent/` for user settings, `<repo>/.pi/` for project
446
- settings). When a key is omitted, its convention file is read automatically;
447
- a missing, unreadable, or whitespace-only convention file is silently ignored.
448
-
449
- Within one settings layer, explicit inline text or `@path` references (including
450
- arrays) take precedence. `false` disables that layer's context and suppresses
451
- its convention file; otherwise the convention file is the default. Without any
452
- resolved context, that layer contributes no value. Existing cross-layer
453
- precedence is unchanged: a project context value—including one from a project
454
- convention file—replaces the user context wholesale, while a layer with no
455
- value leaves the other layer's context intact. Sharing these named files with
456
- a project means machines do not need absolute context paths in `settings.json`.
457
-
458
- Run records and child session jsonl live under `<cwd>/.pi-subagents/`. That
459
- directory is the resume credential; do not delete it while you still want to
460
- `resume` a child.
461
-
462
- ## Teams
463
-
464
- Teams select a named subset of discovered roles without changing the role files:
465
-
466
- ```json
467
- {
468
- "subagents": {
469
- "teams": {
470
- "frontend": {
471
- "description": "UI work",
472
- "members": [
473
- "designer",
474
- "prototype",
475
- { "agent": "worker", "model": "provider/strong" }
476
- ]
477
- },
478
- "full+": { "members": ["*", { "agent": "worker", "model": "provider/strong" }] }
479
- },
480
- "team": "frontend"
481
- }
482
- }
483
- ```
484
-
485
- Use `/subagents-team` to show the active team, `/subagents-team list` to list
486
- teams, `/subagents-team use <name>` to select one in project settings, and add
487
- `--global` to write user settings. `/subagents-team create <name> a,b,c`
488
- creates a team. The selection priority is
489
- `PI_SUBAGENTS_TEAM` > `subagents.team` > `default`; whitespace-only environment
490
- values are ignored. `default` means all discovered roles and returns the
491
- unfiltered catalog. A missing selected team also falls back to all roles with a
492
- warning.
493
-
494
- A string member names a role, and `"*"` expands to every discovered role once,
495
- preserving first-seen order. An object member reuses the normal
496
- `agentOverrides` fields (including `model`, `thinking`, `kind`, and `disabled`);
497
- its fields are applied after earlier members, so later objects win. The active
498
- team name is injected into child pane/tab processes as `PI_SUBAGENTS_TEAM` for
499
- non-default teams, so nested children inherit the same selection.
500
-
501
- ## Custom agents
502
-
503
- A role is a markdown file with YAML frontmatter + a system prompt body:
504
-
505
- ```markdown
267
+ | extensions | Host `packages`, resolved `--extension` paths, `--no-extensions` / inherit knobs, per-role effective extension list |
268
+ | models | Host vs child model lists, which package restores providers, team/roster gaps |
269
+ | spawn | Sample child argv, recent spawn failures + stderr, nested-spawn flags |
270
+ | memory | Free RAM, memory gate, child V8 flags, verify concurrency slots in use |
271
+ | tools | `/legion` commands, stop / idle-Esc / strip hints, tool names |
272
+ | storage | `legion.db` sizes, orphan `rpc-child-*` temps, last hygiene sweep marker |
273
+ | runtime | Live children, tree statuses, acceptance-ish / active nodes |
274
+
275
+ ### Child extensions
276
+ ### Child extensions
277
+
278
+ Children start with `--no-extensions`, so they do not load the host's
279
+ discovered extensions (settings `packages`, `~/.pi/agent/extensions`, project
280
+ extensions). Each extension a child loads is a memory cost multiplied by every
281
+ concurrent child; an LSP stack is ~1 GB (#43). A child still loads pi-legion
282
+ itself (guard and nested spawning) and every path listed in the role's
283
+ `extensions` / `subagentOnlyExtensions`.
284
+
285
+ | Knob | Effect |
286
+ | --- | --- |
287
+ | role frontmatter `inheritExtensions: true` | That role loads all host extensions, as before. |
288
+ | `PI_LEGION_CHILD_INHERIT_EXTENSIONS=1` | Every child loads all host extensions, as before. |
289
+ | `PI_LEGION_CHILD_EXTENSIONS=/a.ts,/b.ts` | Extra extension paths every child loads explicitly, e.g. a provider extension the child's model needs. |
290
+
291
+ ### Child memory controls
292
+
293
+ | Env | Default | Effect |
294
+ | --- | ---: | --- |
295
+ | `PI_LEGION_CHILD_SEMI_SPACE_MB` | 1 | Child pi `--max-semi-space-size` (via `NODE_OPTIONS`). `0` = off. |
296
+ | `PI_LEGION_CHILD_MAX_OLD_SPACE_MB` | 1024 | Soft child heap cap `--max-old-space-size`. `0` = off. A host `NODE_OPTIONS` value wins. |
297
+ | `PI_LEGION_MIN_FREE_MEM_MB` | 1024 | Memory gate: a spawn is refused (`INSUFFICIENT_MEMORY`, plus a UI notice) when available memory minus reservations falls below this. `0` = off. |
298
+ | `PI_LEGION_CHILD_RESERVE_MB` | 160 | Per-child reservation for children still starting, so a parallel burst cannot all pass one reading. |
299
+ | `PI_LEGION_VERIFY_CONCURRENCY` | 2 | Machine-wide limit on concurrent acceptance / legion-verify commands, using slot lock files. `0` = unlimited. |
300
+ | `PI_LEGION_VERIFY_QUEUE_MAX_MS` | 1800000 | Overall queue-wait cap. After it expires the command runs without a slot. Queue wait does not count toward the command timeout. |
301
+ | `PI_LEGION_VERIFY_LOCK_DIR` | `$TMPDIR/pi-legion-verify-<uid>` | Slot directory. Set it to scope the limit more narrowly. |
302
+
303
+ The child restores the host `NODE_OPTIONS` when its extension loads, so bash
304
+ commands it runs (`tsc`, `npm test`) keep the default heap.
305
+
306
+ ### Storage hygiene
307
+
308
+ Each root pi session runs one deferred, best-effort sweep of `.pi-subagents/`
309
+ at startup. A step only touches state whose owner process is gone: runs and
310
+ `rpc-child-*` temp dirs record the owner pid (`owner.json` / `.owner`). Set any
311
+ limit to `0` to disable that step.
312
+
313
+ | Env | Default | Effect |
314
+ | --- | ---: | --- |
315
+ | `PI_LEGION_HYGIENE` | on | `0`/`off` disables the whole sweep. |
316
+ | `PI_LEGION_RUN_RETENTION_DAYS` | 14 | Delete run dirs idle this long. Runs with a `worktrees/` entry are kept as resume credentials. |
317
+ | `PI_LEGION_STALE_NODE_HOURS` | 72 | Non-root ledger nodes still `starting`/`running`/`blocked` this long after their owner died are marked `failed` (`node_abandoned` event). |
318
+ | `PI_LEGION_STALE_TEMP_HOURS` | 24 | Age threshold for legacy `rpc-child-*` dirs without an owner marker. |
319
+ | `PI_LEGION_EVENT_RETENTION_DAYS` | 30 | Drop ledger events of settled/failed nodes older than this (`orphan_detected`, `budget_refused`, `node_abandoned` are kept). |
320
+ | `PI_LEGION_EVENT_MAX_ROWS` | 200000 | Soft cap on the events table. Oldest terminal-node rows are deleted first. |
321
+
322
+ The live `legion.db` gets event retention and a `wal_checkpoint(TRUNCATE)` on
323
+ session shutdown. A large delete is followed by `VACUUM`, which is skipped when
324
+ another connection holds the db.
325
+
326
+ ## Delivery and resume
327
+
328
+ A centurion owns a Git worktree for its subtree; its worker/scout/reviewer
329
+ children work in that subtree, and a nested centurion gets a nested worktree.
330
+ Each subtree is delivered as a squash MR to its parent level. MRs move upward
331
+ through the tree until the root reviews and merges into the default branch.
332
+
333
+ Resuming follows the tree's saved sessions and node records. If a supervisor
334
+ process ends, its child processes do not become detached workers; resuming the
335
+ parent restores the squad, while already-settled children are not restarted.
336
+ Keep the run records and session files while the tree may need to resume.
337
+
338
+ ## Herdr transition and role migration
339
+
340
+ Version **0.16.x is the final Herdr-era line**. Pi Legion 2.x is RPC-only:
341
+ child execution uses headless `pi --mode rpc` processes, and the legacy backend
342
+ is archived under `src/backends/legacy/`, not offered as a v2 runtime option.
343
+ Pin to `pi-legion` 0.16.x if a workflow must stay on the former Herdr-based
344
+ execution model.
345
+
346
+ Pi Legion 2.x supports `kind: pi` only. Migrate a custom role that uses another
347
+ CLI kind by changing its frontmatter, for example:
348
+
349
+ ```yaml
506
350
  ---
507
- name: researcher
508
- description: Read-only paper/code survey
509
- tools: read, grep, find, ls, bash
510
- timeoutMs: 600000
511
- acceptance:
512
- level: attested
513
- role: read-only
351
+ name: search
514
352
  kind: pi
515
- placement: split-down
516
- steer: true
517
- onBlocked: forward
518
353
  ---
519
-
520
- You are a read-only researcher. Facts with paths. No edits.
521
354
  ```
522
355
 
523
- Put it in `~/.pi/agent/agents/researcher.md` (user) or
524
- `<repo>/.pi/agents/researcher.md` (project). Required fields: `name`,
525
- `description`. Useful optional fields: `model`, `preset`, `thinking`, `tools`, `skills`,
526
- `timeoutMs`, `placement`, `onBlocked` (`auto-approve` default / `forward` / `notify`),
527
- `acceptance.role` (`read-only` / `writer`), `acceptance.criteria` with
528
- `evidence: [verification-output]` (and optional `command:`) if collect should
529
- run a check, `worktree`, `toolBudget` / `turnBudget` / `toolTimeoutMs`,
530
- `fallbackModels`, `alias`, `completionGuard`, `allowNestedSubagents`.
531
-
532
- If `subagent({ action: "list" })` prints `⚠ not enforced yet: …`, that key is
533
- parsed but inert. A listed key that does nothing is worse than an unknown key
534
- — if you see that warning, the field is decoration. The shipped roles currently
535
- report none.
536
-
537
- ## Isolation
538
-
539
- Herdr panes are **one trust domain**. A child can in principle `herdr pane
540
- read` any pane in the session (measured: F23). We now **enforce** a subset of
541
- that discipline; the rest is still on you.
542
-
543
- **Enforced in the child process**
544
-
545
- - `herdr agent prompt|wait|send-keys|start`
546
- - `herdr pane split`, `herdr tab create|close`
547
- - `herdr pane read|close` of a pane that is not this child’s
548
- - for `acceptance.role: read-only`: filesystem writes, `git` mutations,
549
- package-manager installs, `sed -i`, redirects onto files
550
- - `toolBudget.maxToolCalls` / `turnBudget.maxTurns` (further tool calls are
551
- blocked); `toolTimeoutMs` wraps bash with GNU `timeout`
552
- - `worktree: true` → `git worktree add -b` under the run dir (named
553
- `pi-subagent/<name>-…` branch). The pane’s cwd is that tree. The **parent**
554
- decides per launch (`subagent({ worktree: true|false })`); omitting it uses
555
- the role default. Writer roles (`acceptance.role: writer`, including bundled
556
- `worker`) default this on so concurrent parent Pis do not share a dirty
557
- checkout; the child commits on the branch and opens an MR. `worktree: false`
558
- opts out. Retire leaves the tree on disk. Requires a git repository.
559
- - child tabs are pinned to the **parent herdr Space**
560
- (`HERDR_WORKSPACE_ID` → `tab create --workspace`). A focused Space elsewhere
561
- cannot steal the child, and adopt will not reuse a same-label tab in another
562
- Space.
563
-
564
- **Still not a sandbox**
565
-
566
- - `HERDR_SOCKET_PATH` cannot be overridden per pane (F24)
567
- - a true isolated session would break the visible/steerable model (F25)
568
- - never put secrets on screen in any pane while children run
569
- - owner tokens: recycle only panes the tree registered; skip + warn otherwise
570
-
571
- If a task genuinely needs isolation, run it **outside** herdr (plain `pi`, no
572
- `HERDR_ENV`), rather than pretending panes are sandboxed.
573
-
574
- ## Success and failure
575
-
576
- **herdr `agent_status` cannot tell you whether an agent succeeded.** Success,
577
- model error, and kill all report `done`. The plugin derives outcome from the
578
- child’s session jsonl (`stopReason` of the last turn) and from the
579
- machine-readable verdict.
580
-
581
- Two orthogonal dimensions — never conflate them:
582
-
583
- - **Lifecycle** (`launching → working → awaiting → blocked → retired/exited`):
584
- is the process alive?
585
- - **Execution** (`success / failed / aborted / truncated / running`): did the
586
- task succeed?
587
-
588
- `state: retired` + `execution.status: failed` is a normal combination.
589
-
590
- | Session signal | Execution |
591
- | --- | --- |
592
- | Last turn `stopReason: "stop"` | success |
593
- | `stopReason: "error"` without “aborted” | failed |
594
- | `stopReason: "error"` + “This operation was aborted” | aborted |
595
- | Last user prompt with **no** assistant message | aborted (killed mid-turn) |
596
- | Last assistant `stopReason: "toolUse"` and the agent is gone | aborted |
597
- | Last assistant `toolUse` and herdr says `blocked` | not terminal — confirm / wait |
598
- | `stopReason: "length"` | truncated |
599
- | No messages | unknown |
600
-
601
- On top of that:
602
-
603
- - **Only the last turn decides execution.** A tool error in turn 1 followed by
604
- a clean turn 2 is overall success. `toolErrors` is diagnostic only.
605
- - **Self-report is attested, not verified.** `{"ok": true}` is the agent
606
- marking its own homework. Worker’s `verification-output` criterion is the
607
- exception: when it carries an explicit `command`, the plugin runs it itself,
608
- with a timeout; without one, the criterion is surfaced as pending for the
609
- parent to confirm from the pasted output — the plugin never guesses a
610
- project-unaware command. `completionGuard:
611
- true` rejects a successful turn that forgot the `{"ok":…}` verdict.
612
- - **Recycle destroys live evidence** (`agent get` → `agent_not_found`). The
613
- plugin collects and snapshots `execution` into `.pi-subagents/run.json`
614
- **before** closing the pane. Resume uses the session file, not the pane.
615
-
616
- ## Programmatic API
617
-
618
- Hosts that are not the Pi `subagent` tool can drive the same machinery:
619
-
620
- ```ts
621
- import {
622
- createHerdrClient,
623
- Orchestrator,
624
- loadAgentsFromDir,
625
- } from "pi-legion/api";
626
-
627
- const client = createHerdrClient();
628
- const [scout] = loadAgentsFromDir("/path/to/agents", "user");
629
- const orch = new Orchestrator({
630
- client,
631
- runDir: "/tmp/run",
632
- cwd: process.cwd(),
633
- });
634
- const handle = await orch.launch({ agent: scout, task: "survey the repo" });
635
- const result = await orch.collect(handle.name);
636
- ```
637
-
638
- Everything re-exported from `./api` is covered by the test suite. Do not import
639
- from `src/` directly.
356
+ A non-Pi kind is rejected by v2. If that role must continue to run through
357
+ another CLI, keep it on the 0.16.x line or replace it with a Pi role.
640
358
 
641
359
  ## Development
642
360
 
643
361
  ```bash
644
- git clone https://github.com/zzjcool/pi-legion.git
645
- cd pi-legion
646
362
  npm install
647
363
  npm run typecheck
648
- npm test # unit
649
- npm run test:integration # fake herdr, no live binary
364
+ npm test # unit tests
365
+ npm run test:integration # integration tests
366
+ npm run test:all # typecheck + unit + integration tests
650
367
  ```
651
368
 
652
- Live tests (`npm run test:live`) need a real herdr on `PATH` and
653
- `HERDR_ENV=1`. They are skipped otherwise.
369
+ ### Mock LLM (offline pi / legion smoke)
654
370
 
655
- Container checks (host Pi config bind-mounted, package at `/plugin`) live in
656
- `test/docker/`. Each numbered script is one feature:
371
+ A scriptable OpenAI-compatible server lives under [`test/mock-llm/`](test/mock-llm/README.md).
372
+ It registers with Pi like any local gateway (`models.json` → `api: "openai-completions"`),
373
+ so spawn / stop / doctor / verify paths can run without real API keys:
657
374
 
658
375
  ```bash
659
- bash test/docker/verify-1-child-guard.sh
660
- bash test/docker/verify-2-task-appendix.sh
661
- bash test/docker/verify-3-readonly.sh
662
- bash test/docker/verify-4-acceptance.sh
663
- bash test/docker/verify-5-blocked.sh
664
- bash test/docker/verify-6-cache.sh
665
- bash test/docker/verify-7-budget.sh
666
- bash test/docker/verify-8-verify-command.sh
667
- bash test/docker/verify-9-worktree.sh
668
- bash test/docker/verify-10-fallback.sh
669
- bash test/docker/verify-11-nested-alias.sh
670
- bash test/docker/verify-12-workspace.sh
671
- bash test/docker/verify-13-kinds.sh # kinds + models; live CodeBuddy/Cursor when mounted
376
+ npm run mock-llm # http://127.0.0.1:8765/v1 scenario=echo
377
+ npm run test:mock-llm # unit + integration smoke for the mock
378
+ MOCK_LLM_SCENARIO=spawn-scout npm run mock-llm
672
379
  ```
673
380
 
674
- They expect image `pi-herdr-sandbox:latest` and copy `~/.pi/agent/{settings,models,auth}.json`
675
- into a disposable home so the container uses your providers without writing
676
- back. See [CONTRIBUTING.md](CONTRIBUTING.md).
677
-
678
- The measured design (findings F1–F46, why session jsonl is the source of
679
- truth, why recycle is unconditional) is [docs/design.md](docs/design.md).
680
- Historical “this field is parsed but inert” notes in that file may predate
681
- the runtime table above — trust this README and `action=list`’s
682
- `unenforcedFields` for what is actually wired.
683
-
684
- ## Known limitations
685
-
686
- - **Not a sandbox** (F23–F25): child-guard covers bash/herdr dispatch and
687
- read-only writes; it does not give you process isolation. The read-only
688
- bash rules are a **text heuristic** over the command line, so they miss
689
- indirect writes — `node -e "require('fs').writeFileSync(…)"`,
690
- `python3 -c "open('f','w')…"` and `perl -i` all pass, as does any child whose
691
- agent definition omits `acceptance.role` (the guard is keyed off it). Treat it
692
- as a guard-rail against accidents, not a boundary against intent.
693
- - **`agent_status` has no success/failure semantics** (F26).
694
- - **Non-pi kinds degrade** (F7): same Herdr control plane (`start` → `prompt` → `wait`), but no usage and outcome is `unknown` unless the pane text includes a verdict JSON that is not just the echoed launch prompt. Cursor starts with `--trust --force` (Run Everything) unless the agent opts back into a human gate via `onBlocked: forward`/`notify`, so workspace-trust and shell-approval dialogs cannot block the first prompt. Resume depends on each CLI.
695
- - **A model the kind cannot express**: `--model` is built per CLI (pi wants
696
- `provider/id`, cursor wants slugs like `cursor-grok-4.6-high`). A model that
697
- cannot be expressed is omitted rather than mangled. Whether that is refused or
698
- allowed depends on how it was chosen — an **explicit** choice (frontmatter,
699
- `agentOverrides`, a preset, a `model` param) is refused before any pane is
700
- created, while an **inherited** one (`subagents.defaultModel`, the parent
701
- session's model) is dropped on purpose and reported as `modelDropped`, because
702
- the parent's pi model simply means nothing to another CLI.
703
- - **`blocked` is screen-heuristic**: false positives exist; the confirm is
704
- paired with the collect timeout as a backstop.
705
- - **Verification** runs `criterion.command` when a required criterion asks
706
- for `verification-output` and carries one. A criterion without a `command`
707
- stays attested and is surfaced as pending for the parent — semantic `must`
708
- strings are never NLP-parsed (F32 / F44) and no default command is guessed.
709
- Other evidence types stay a checklist.
710
- - **Panes are scarce**: cap fan-out (`maxSubagentSpawnsPerSession` /
711
- `herdr.maxConcurrentAgents`). Warm-pane pooling is not worth it (~0.9s).
712
-
713
- ## License
381
+ Use `writeMockPiHome()` from `test/mock-llm` to generate an isolated
382
+ `PI_CODING_AGENT_DIR` with dummy `apiKey: "mock"` only — never copy host
383
+ `auth.json` into fixtures.
384
+
385
+ See [CONTRIBUTING.md](CONTRIBUTING.md#project-workflow) for the project
386
+ workflow: issues, pull requests, the merge gate, and npm releases.
714
387
 
715
388
  MIT. See [LICENSE](LICENSE).