pi-legion 0.16.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/CONTRIBUTING.md +109 -18
  3. package/README.md +389 -630
  4. package/agents/advisor.md +7 -5
  5. package/agents/centurion.md +53 -0
  6. package/agents/designer.md +33 -33
  7. package/agents/planner.md +1 -2
  8. package/agents/prototype.md +2 -2
  9. package/agents/reviewer.md +66 -31
  10. package/agents/scout.md +6 -2
  11. package/agents/worker.md +11 -3
  12. package/docs/contracts/esc-abort-v1.md +131 -0
  13. package/docs/contracts/esc-cascade-v2.md +101 -0
  14. package/docs/design.md +73 -37
  15. package/docs/legion-v2-contract.md +461 -0
  16. package/docs/legion-v2-delivery.md +62 -0
  17. package/docs/retire-hardening-contract.md +125 -0
  18. package/docs/subagents-teams.md +76 -0
  19. package/docs/tui-viewer-contract.md +276 -0
  20. package/docs/vexillatio-contract.md +162 -0
  21. package/index.ts +1568 -288
  22. package/package.json +19 -12
  23. package/skills/pi-legion/SKILL.md +86 -94
  24. package/src/agents/agents.ts +66 -16
  25. package/src/agents/overrides.ts +2 -1
  26. package/src/agents/paths.ts +1 -1
  27. package/src/agents/settings.ts +268 -20
  28. package/src/api.ts +49 -48
  29. package/src/{herdr → backends/legacy}/client.ts +14 -69
  30. package/src/{shared → backends/legacy}/cursor-chat.ts +2 -2
  31. package/src/{herdr → backends/legacy}/runner.ts +22 -9
  32. package/src/backends/legacy/types.ts +138 -0
  33. package/src/extension/auto-doctor.ts +144 -0
  34. package/src/extension/budget.ts +2 -2
  35. package/src/extension/child-guard.ts +214 -201
  36. package/src/extension/event-bus.ts +263 -0
  37. package/src/extension/legion-commands.ts +773 -0
  38. package/src/extension/legion-doctor.ts +975 -0
  39. package/src/extension/legion-status.ts +172 -0
  40. package/src/extension/legion-stop.ts +150 -0
  41. package/src/extension/legion-tools.ts +816 -0
  42. package/src/extension/notify.ts +24 -19
  43. package/src/extension/playbook.ts +20 -89
  44. package/src/extension/recycle.ts +2 -7
  45. package/src/extension/runtime.ts +736 -107
  46. package/src/extension/scout-guard.ts +492 -0
  47. package/src/extension/slash.ts +89 -438
  48. package/src/extension/spawn-hints.ts +79 -0
  49. package/src/extension/summary.ts +17 -58
  50. package/src/legion/budget.ts +165 -0
  51. package/src/legion/db.ts +242 -0
  52. package/src/legion/events.ts +150 -0
  53. package/src/legion/mail.ts +412 -0
  54. package/src/legion/nodes.ts +461 -0
  55. package/src/legion/rebuild.ts +533 -0
  56. package/src/legion/state-machine.ts +313 -0
  57. package/src/legion/usage.ts +101 -0
  58. package/src/runs/acceptance.ts +82 -25
  59. package/src/runs/args.ts +117 -15
  60. package/src/runs/hygiene.ts +335 -0
  61. package/src/runs/orchestrator.ts +808 -1681
  62. package/src/runs/owner.ts +61 -0
  63. package/src/runs/provider-pin.ts +59 -0
  64. package/src/runs/provision.ts +135 -0
  65. package/src/runs/role-tool-gates.ts +103 -0
  66. package/src/runs/store.ts +44 -17
  67. package/src/runs/verify-queue.ts +195 -0
  68. package/src/shared/name.ts +29 -0
  69. package/src/shared/output-cap.ts +57 -0
  70. package/src/shared/progress.ts +57 -135
  71. package/src/shared/session.ts +280 -173
  72. package/src/shared/types.ts +115 -208
  73. package/src/supervisor/child-memory.ts +140 -0
  74. package/src/supervisor/proc-tree.ts +103 -0
  75. package/src/supervisor/rpc-supervisor.ts +1033 -0
  76. package/src/supervisor/types.ts +153 -0
  77. package/src/supervisor/ui-proxy.ts +226 -0
  78. package/src/tui/chat-items.ts +330 -0
  79. package/src/tui/live-overlay.ts +874 -0
  80. package/src/tui/status.ts +553 -27
  81. package/src/tui/tree-view.ts +808 -0
  82. package/src/tui/viz/canvas.ts +119 -0
  83. package/src/tui/viz/layout.ts +111 -0
  84. package/src/tui/viz/model.ts +123 -0
  85. package/src/tui/viz/palette.ts +188 -0
  86. package/src/tui/viz/panels.ts +532 -0
  87. package/src/tui/viz/railway.ts +313 -0
  88. package/src/tui/viz/rate.ts +211 -0
  89. package/src/tui/viz/view.ts +228 -0
  90. package/src/extension/parent-label.ts +0 -115
  91. package/src/profiles/classify.ts +0 -375
  92. package/src/profiles/profiles.ts +0 -730
  93. package/src/runs/layout.ts +0 -258
package/README.md CHANGED
@@ -1,715 +1,474 @@
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**
17
+ >
18
+ > **What changed in 2.x?** See [CHANGELOG.md](CHANGELOG.md) (Keep a Changelog). Published notes also land on [GitHub Releases](https://github.com/zzjcool/pi-legion/releases).
25
19
 
26
20
  ## Contents
27
21
 
28
- - [Requirements](#requirements)
29
- - [Install](#install)
30
- - [What the plugin does for you](#what-the-plugin-does-for-you)
22
+ - [Architecture](#architecture)
31
23
  - [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)
24
+ - [Legion tools and phases](#legion-tools-and-phases)
25
+ - [Three-layer visualization](#three-layer-visualization)
26
+ - [Settings and budget gates](#settings-and-budget-gates)
27
+ - [Scout guard](#scout-guard)
28
+ - [Team 配置](#team-配置)
29
+ - [Parent/child context conventions](#parentchild-context-conventions)
30
+ - [Delivery and resume](#delivery-and-resume)
31
+ - [Herdr transition and role migration](#herdr-transition-and-role-migration)
32
+ - [Changelog](CHANGELOG.md)
43
33
  - [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
34
 
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.
35
+ ## Architecture
57
36
 
58
- ## Install
59
-
60
- ```bash
61
- pi install npm:pi-legion
37
+ ```text
38
+ User (CEO) — root Pi session (TUI)
39
+ ├─ pi-legion extension
40
+ │ ├─ LegionSupervisor (root)
41
+ │ │ ├─ RpcClient × N → headless `pi --mode rpc` child processes
42
+ │ │ ├─ mail postman (inbox delivery) + four-phase state machine
43
+ │ │ └─ orphan scan + child lifecycle / usage accounting
44
+ │ ├─ `legion.db` (one shared SQLite/WAL tree ledger)
45
+ │ └─ LegionEventBus → resident tree | `/legion` full view | live overlay
46
+ └─ centurion child (RpcClient; cwd = its subtree worktree)
47
+ └─ pi-legion in child mode
48
+ ├─ LegionSupervisor for this squad
49
+ ├─ `legion_mail` / `legion_tree` / `legion_phase` tools
50
+ ├─ worker / scout / reviewer … children (`worktree: false`)
51
+ └─ nested centurions → recursive squads
62
52
  ```
63
53
 
64
- From git (picks up commits that are not on npm yet):
54
+ Transport, state, decision-making, and display are separate layers:
65
55
 
66
- ```bash
67
- pi install git:github.com/zzjcool/pi-legion
68
- ```
56
+ - **Transport:** each child is a headless Pi RPC process managed by a
57
+ `LegionSupervisor`; a centurion can supervise another squad recursively.
58
+ - **State:** all nodes in the tree share the `legion.db` SQLite ledger in WAL
59
+ mode. Per-run `run.json` records and session files remain the resume source
60
+ for each child.
61
+ - **Decision:** centurions and squad members do the LLM work. The supervisor
62
+ owns machine state and validates phase transitions.
63
+ - **Display:** the event bus feeds the resident tree, the `/legion` full view,
64
+ and an on-demand live overlay.
65
+
66
+ ## Quick start
69
67
 
70
- From a local checkout:
68
+ Install and load the extension in Pi:
71
69
 
72
70
  ```bash
73
- pi install /path/to/pi-legion
71
+ pi install npm:pi-legion
74
72
  ```
75
73
 
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.
74
+ Ask a `centurion` to lead a recursive squad, with a goal and concrete
75
+ acceptance checklist in its task:
113
76
 
114
77
  ```text
115
78
  subagent({
116
- agent: "reviewer",
117
- task: "Review src/foo.ts for regressions. Write findings to /tmp/review.md."
79
+ agent: "centurion",
80
+ 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
81
  })
119
82
  ```
120
83
 
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:
84
+ The centurion plans the work, writes the squad contract, dispatches ordinary
85
+ roles such as `worker`, `scout`, and `reviewer`, and coordinates the squad
86
+ through its phases. For a single leaf task, dispatch an ordinary role directly:
124
87
 
125
88
  ```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
89
+ subagent({ agent: "scout", task: "Map the export code path and report relevant files and risks." })
128
90
  ```
129
91
 
130
- Parallel fan-out — one tool call, then return control:
92
+ No manual child-process launch is needed. Use `/legion` to inspect the tree;
93
+ see [Three-layer visualization](#three-layer-visualization) for navigation.
94
+ `/legion status` prints that tree in the chat, and `/legion status <name>`
95
+ prints one node — see [Status](#status-legion-status).
96
+
97
+ The `subagent` control actions include event-driven `wait` and graceful `stop`:
98
+ `subagent({ action: "wait", name: "worker-0" })` returns as soon as the child
99
+ settles (or the parent turn is interrupted with Esc). Aborting a parent turn
100
+ with Esc gracefully retires its tracked children, including those being waited
101
+ on or collected; the cascade is silent. Launch stays async by default. Under
102
+ `pi --print` or JSON mode the process would otherwise exit at the end of the
103
+ parent turn and retire those children before they write an assistant message.
104
+ That shutdown waits for each live async child to settle, bounded by the
105
+ child's turn timeout, and only then retires whatever is still running. A
106
+ child killed by that shutdown is recorded as aborted because the parent
107
+ session shut down, not because the model produced no reply. To include the
108
+ child's result in the same print turn, call `wait` or `collect` before the
109
+ turn ends, or launch with `async: false`. To stop children explicitly, use
110
+ `subagent({ action: "stop", name: "worker-0" })` or
111
+ `subagent({ action: "stop", all: true })`; this runs the normal graceful-retire
112
+ sequence for each selected child.
113
+
114
+ ### Stopping async subagents yourself
115
+
116
+ Async children keep running after the main turn ends, so Esc-abort no longer
117
+ reaches them. Three user controls run the same graceful `stop` (a stopped
118
+ parent retires its own subtree on shutdown):
119
+
120
+ - **`/legion stop`** stops every live subagent; **`/legion stop <name>`** stops
121
+ one (`all` is an alias for no name). Names complete from the live children;
122
+ nested agents complete as `parent/name` and are reported as owned by their
123
+ parent (stop the parent to retire them).
124
+ - **Esc** while the main agent is idle, the editor is empty, no dialog or
125
+ overlay is open and subagents are live asks "Stop N running subagents?".
126
+ In every other state Esc keeps pi's normal behaviour.
127
+ - **`x`** on the selected live agent in the `/legion` view or its live
128
+ overlay asks `y` to confirm in place, then stops it.
129
+
130
+ While the main agent is idle and subagents are live, the railway header shows
131
+ a dim `esc stop all · /legion stop [name]` hint (shortened on narrow terminals,
132
+ hidden while the main agent is working).
133
+
134
+ ### Status (`/legion status`)
135
+
136
+ The railway and `/legion` view are TUI surfaces. `/legion status` is the chat
137
+ snapshot of the same tree:
138
+
139
+ - `/legion status` prints every node with the same lines as `legion_tree`
140
+ (`id role=… status=… phase=… model=… depth=…`), plus a count of each status.
141
+ Live jobs that are not in the tree yet are listed under that summary.
142
+ - `/legion status <name>` prints one node with the same text as
143
+ `subagent({ action: "status", name })`. Living children lead with
144
+ `alive · quiet Ns` (session JSONL mtime); after ~30s of silence the line
145
+ becomes `alive · quiet Ns (living)` so 「活着但安静」is obvious. Run-store
146
+ execution/acceptance fields are labeled snapshots, and collect-deadline /
147
+ transient-connection reasons are omitted from the snapshot line while the
148
+ child is alive so they do not steal attention from quiet age. A ledger-only
149
+ node still uses the same renderer with the fields the tree has.
150
+ - Quiet age is not a heartbeat: there is no child-side progress ping. Minutes
151
+ of silence can still mean the model is reasoning (especially high-thinking
152
+ turns). Quiet alone does not mean the child is stuck; pair it with `alive`.
153
+ - Both forms work outside the TUI. A short name that matches more than one
154
+ node needs the full node id (`/legion status <node-id>`).
155
+
156
+ ## Legion tools and phases
157
+
158
+ Child nodes receive these tree-aware tools:
131
159
 
132
160
  ```text
133
- subagent({ tasks: [
134
- { agent: "worker", task: "<task A>" },
135
- { agent: "worker", task: "<task B>" },
136
- { agent: "reviewer", task: "<task C>" },
137
- ] })
161
+ legion_mail({ to, subject, body, kind?, urgency? })
162
+ legion_tree({ scope?: "children" | "squad" | "subtree" | "tree", depth? })
163
+ legion_phase({ to, note?, evidence? })
138
164
  ```
139
165
 
140
- Steer a running agent (current task abandoned, context kept):
141
-
142
- ```text
143
- subagent({ action: "steer", name: "reviewer-0", message: "Stop the API review; focus on the migration script instead." })
144
- ```
145
-
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.
148
-
149
- ## Tool reference
150
-
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):
166
+ - **`legion_mail`** sends asynchronous messages over permitted tree edges:
167
+ parent, children, or same-parent siblings. `to` may be a short name, node ID,
168
+ `parent`, `children`, or `squad`. Cross-squad direct mail and bypassing the
169
+ parent are refused; route cross-squad requests through the relevant
170
+ centurions. Keep large artifacts in the shared worktree and mail a file
171
+ pointer. The body has a 4 KB soft limit and a 32 KB hard limit.
172
+ - **`legion_tree`** is a read-only view of machine-recorded node state. It does
173
+ not interrupt the nodes being observed.
174
+ - **`legion_phase`** is available to centurions. It requests a state transition;
175
+ the supervisor checks the transition's invariants and records the result.
176
+
177
+ A squad moves through **planning → implementing → reviewing → verifying**.
178
+ Planning produces a non-empty `.pi-legion/contract.md` with file ownership
179
+ and acceptance commands. During reviewing, an independent reviewer supplies
180
+ the judgment; in verifying, the supervisor runs the contract's `legion-verify`
181
+ commands without an LLM. Passing verification advances the squad to `done`.
182
+
183
+ ## Three-layer visualization
184
+
185
+ 1. **Railway strip above the input:** one git-graph lane per live subagent
186
+ (max 4, 3 below 80 columns; oldest/shallowest first, `+N` for the rest).
187
+ When an agent ends its lane merges back and lower lanes shift up. The
188
+ header shows the live count and tok/s (trailing 60s average). Hidden when
189
+ no subagent is live. Set `PI_LEGION_STRIP=tree` for the classic roster.
190
+ 2. **`/legion` full view:** galaxy graph of the agent tree, stacked tok/s
191
+ chart by agent type (peak marker, now/avg/peak/total, per-role share), and
192
+ a scrollable `log --graph` history. Tab switches panes, arrows scroll or
193
+ move the selection, Enter selects an agent (its own rate line, tok/s,
194
+ tokens and share are overlaid), `o` opens its live overlay, `x` stops it
195
+ (confirm with `y`), Esc clears the
196
+ selection, `q`/Esc closes. Layouts: wide (≥140 cols), stacked, and portrait
197
+ (<80 cols, or tall and <100 cols). `/legion --tree` opens the classic tree
198
+ (`j`/`k`, Enter/Space fold, `/` search, PageUp/PageDown).
199
+ 3. **Live overlay:** press `o` on the selected node, use a component-routed
200
+ mouse click in the strip or classic tree, or open it directly with `/legion <name>`.
201
+ The overlay subscribes to that node's live feed only while open; Esc closes
202
+ it (returning to `/legion` when opened from there) and unsubscribes; `x`
203
+ stops the agent after a `y` confirmation. OSC 8 links are visual anchors only, not overlay links.
204
+
205
+ ## Settings and budget gates
206
+
207
+ Add the v2 `legion` block to `~/.pi/agent/settings.json` or the project
208
+ `.pi/settings.json`, merging it with any existing `subagents` keys:
200
209
 
201
210
  ```json
202
211
  {
203
212
  "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
- ```
214
-
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:
223
-
224
- ```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
232
- ```
233
-
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" }
213
+ "legion": {
214
+ "maxDepth": 4,
215
+ "maxChildrenPerNode": 8,
216
+ "maxActiveNodes": 30,
217
+ "phaseTimeouts": {
218
+ "planning": 900000,
219
+ "implementing": 3600000,
220
+ "reviewing": 1800000,
221
+ "verifying": 900000
222
+ },
223
+ "verifyCommandTimeoutMs": 600000,
224
+ "mailRatePer5Min": 10,
225
+ "usagePollMs": 30000,
226
+ "orphanStaleMs": 90000
352
227
  }
353
228
  }
354
229
  }
355
230
  ```
356
231
 
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.
232
+ The three launch budget gates are enforced against the tree ledger:
368
233
 
369
- While disabled:
234
+ | Gate | Setting | Default | Limit |
235
+ | --- | --- | ---: | --- |
236
+ | Depth | `maxDepth` | 4 | Maximum tree depth; the hard cap is 4 (`MAX_NESTED_PATH_ENTRIES`). |
237
+ | Fan-out | `maxChildrenPerNode` | 8 | Direct children for one node. |
238
+ | Concurrency | `maxActiveNodes` | 30 | Whole-tree `running` + `starting` nodes. |
370
239
 
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.
240
+ The effective tree depth cap is bounded by the operator override,
241
+ `subagents.legion.maxDepth`, and the hard cap of 4. **INCL semantics:** a
242
+ launched role's `maxSubagentDepth = R` counts generations in that role's
243
+ subtree, including itself; when launched by a parent at depth `d`, it inherits
244
+ `C_child = min(C_parent, d + R, 4)` (`R = 1` makes it a leaf), so it can tighten
245
+ the inherited cap but cannot widen it.
379
246
 
380
- ## Settings
247
+ ### Scout guard
381
248
 
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.
249
+ `subagents.scoutGuard` watches the main session. After `threshold` consecutive
250
+ read-like tool calls with no `subagent` launch or `vexillatio` call in
251
+ between, it injects one follow-up reminder and then waits out `cooldownMs`.
252
+ It is on by default. Set it in `~/.pi/agent/settings.json` or the project
253
+ `.pi/settings.json`; a project file overrides the same fields from the user
254
+ file.
390
255
 
391
256
  ```json
392
257
  {
393
258
  "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
259
+ "scoutGuard": {
260
+ "enabled": true,
261
+ "threshold": 8,
262
+ "cooldownMs": 120000
418
263
  }
419
264
  }
420
265
  }
421
266
  ```
422
267
 
423
- | Key | Meaning |
268
+ | Field | Default | Meaning |
269
+ | --- | ---: | --- |
270
+ | `enabled` | `true` | Count and remind. `false` skips the guard until it is turned back on. |
271
+ | `threshold` | `8` | Consecutive read-like calls before one reminder. Positive integer. |
272
+ | `cooldownMs` | `120000` | Quiet period after a reminder, in milliseconds. Integer ≥ 0. |
273
+
274
+ `/scout-guard` turns the guard on for this session and `/no-scout-guard`
275
+ turns it off. Both override `enabled` in memory only; settings files stay
276
+ unchanged. Child sessions do not register these commands. Behavior in full
277
+ is in [docs/design.md §6.4.1](docs/design.md#641-scout-guard-subagentsscoutguard).
278
+
279
+ ### Team 配置
280
+
281
+ Use `subagents.team` to select the default named team from `subagents.teams`.
282
+ The `PI_SUBAGENTS_TEAM` environment variable takes precedence over
283
+ `subagents.team`; if neither selects a named team, the full discovered roster
284
+ is used. Team definitions have a description and an ordered `members` list;
285
+ each member can be a role-name string or an object with an `agent` name and
286
+ per-member overrides. Object member overrides (for example `model` or
287
+ `thinking`) take precedence over those fields in the role card's frontmatter;
288
+ omitted override fields continue to use their normal role/configuration
289
+ values.
290
+
291
+ See [docs/subagents-teams.md](docs/subagents-teams.md) for four editable
292
+ frontend/backend × premium/value team templates. The model IDs in that template
293
+ are placeholders for your provider, not required defaults.
294
+
295
+ ### Parent/child context conventions
296
+
297
+ Settings can provide `subagents.parentContext` and `subagents.childContext`
298
+ inline or through `@path` references. If either setting is omitted, Legion
299
+ looks for `subagents-parent-context.md` or `subagents-child-context.md`
300
+ respectively in the directory containing that settings file. These are
301
+ convention-based fallbacks: an explicit setting takes precedence, and setting
302
+ either field to `false` disables its context. The convention filenames are
303
+ `PARENT_CONTEXT_CONVENTION` and `CHILD_CONTEXT_CONVENTION` in
304
+ [`src/agents/settings.ts`](src/agents/settings.ts#L30-L31); missing convention
305
+ files simply contribute no context. The repository examples are
306
+ [`examples/subagents-parent-context.md`](examples/subagents-parent-context.md)
307
+ and [`examples/subagents-child-context.md`](examples/subagents-child-context.md).
308
+
309
+ ### Self-diagnosis (`/legion doctor`)
310
+
311
+ When something about subagents misbehaves (model not found, spawn exit, memory
312
+ gate, missing lens tools, orphan temps, …), ask Legion rather than guessing —
313
+ same idea as `pi list` / `pi auth check`.
314
+
315
+ - `/legion doctor` — full report in the chat (works outside TUI too).
316
+ - `/legion doctor [section]` — one of: `summary`, `extensions`, `models`,
317
+ `spawn`, `memory`, `tools`, `storage`, `runtime`, `next`.
318
+ - `subagent({ action: "diagnose" })` — same full report as tool output so the
319
+ main agent can call it without waiting for you. The tool has no section
320
+ field; filter with `/legion doctor [section]`.
321
+
322
+ The report is structured for the model: each section ends in **Issues** /
323
+ **Next steps** with copy-pasteable fixes (env exports, role frontmatter,
324
+ `/legion stop`, …). Spawn failures that look like a missing model also append
325
+ a short Hint automatically. **Gated auto-doctor:** on model-not-found,
326
+ `INSUFFICIENT_MEMORY`, extension load failures, and child exit during
327
+ startup / first prompt, the tool error itself includes a focused doctor
328
+ snippet (relevant sections + Next steps) so the main agent can self-fix
329
+ without anyone typing `/legion doctor`. Full `/legion doctor` remains for
330
+ manual deep dives.
331
+
332
+ Coverage:
333
+
334
+ | Section | What it checks |
424
335
  | --- | --- |
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.
336
+ | extensions | Host `packages`, resolved `--extension` paths, `--no-extensions` / inherit knobs, per-role effective extension list |
337
+ | models | Host vs child model lists, which package restores providers, team/roster gaps |
338
+ | spawn | Sample child argv, recent spawn failures + stderr, nested-spawn flags |
339
+ | memory | Free RAM, memory gate, child V8 flags, verify concurrency slots in use |
340
+ | tools | `/legion` slash list (status, team, agents, summary, on/off, scout-guard, doctor, stop), idle-Esc / strip hints, tool names |
341
+ | storage | `legion.db` sizes, orphan `rpc-child-*` temps, effective hygiene limits (default vs env), last sweep marker plus a hint when it is missing or stale |
342
+ | runtime | Live children, tree statuses, acceptance-ish / active nodes |
343
+
344
+ ### Child extensions
345
+
346
+ Children start with `--no-extensions`, so they do not load the host's
347
+ discovered extensions (settings `packages`, `~/.pi/agent/extensions`, project
348
+ extensions). Each extension a child loads is a memory cost multiplied by every
349
+ concurrent child; an LSP stack is ~1 GB (#43). A child still loads pi-legion
350
+ itself (guard and nested spawning) and every path listed in the role's
351
+ `extensions` / `subagentOnlyExtensions`.
352
+
353
+ | Knob | Effect |
354
+ | --- | --- |
355
+ | role frontmatter `inheritExtensions: true` | That role loads all host extensions, as before. |
356
+ | `PI_LEGION_CHILD_INHERIT_EXTENSIONS=1` | Every child loads all host extensions, as before. |
357
+ | `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. |
493
358
 
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.
359
+ Bundled planner, reviewer, and worker do not list `lens_*` tools. pi-lens starts an LSP stack (~1 GB per child, #43) and is not part of the default `--no-extensions` child. To give a role those tools, add the tool names to its `tools:` list and point `extensions:` / `subagentOnlyExtensions` (or `PI_LEGION_CHILD_EXTENSIONS`) at the pi-lens extension file. `/legion doctor` reports `ROLE_MISSING_LENS` when a role lists the tools without that path.
500
360
 
501
- ## Custom agents
361
+ ### Extra agent directories
502
362
 
503
- A role is a markdown file with YAML frontmatter + a system prompt body:
363
+ PATH-style directories scanned for agent `*.md` definitions. They load as
364
+ user-level, **below** the real user agents dir in precedence (a same-name file
365
+ in `~/.pi/agent/agents` still wins). Useful for hermetic installs (Nix store,
366
+ container, read-only mount).
504
367
 
505
- ```markdown
368
+ | Env | Effect |
369
+ | --- | --- |
370
+ | `PI_LEGION_EXTRA_AGENT_DIRS` | Canonical name. Colon-/semicolon-separated absolute dirs (`path.delimiter`). |
371
+ | `PI_HERDR_SUBAGENTS_EXTRA_AGENT_DIRS` | Deprecated compatibility alias (kept for at least one minor version). If **both** are set, `PI_LEGION_EXTRA_AGENT_DIRS` wins and the legacy value is ignored (one stderr warning). |
372
+
373
+ ### Child memory controls
374
+
375
+ | Env | Default | Effect |
376
+ | --- | ---: | --- |
377
+ | `PI_LEGION_CHILD_SEMI_SPACE_MB` | 1 | Child pi `--max-semi-space-size` (via `NODE_OPTIONS`). `0` = off. |
378
+ | `PI_LEGION_CHILD_MAX_OLD_SPACE_MB` | 1024 | Soft child heap cap `--max-old-space-size`. `0` = off. A host `NODE_OPTIONS` value wins. |
379
+ | `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. |
380
+ | `PI_LEGION_CHILD_RESERVE_MB` | 160 | Per-child reservation for children still starting, so a parallel burst cannot all pass one reading. |
381
+ | `PI_LEGION_VERIFY_CONCURRENCY` | 2 | Machine-wide limit on concurrent acceptance / legion-verify commands, using slot lock files. `0` = unlimited. |
382
+ | `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. |
383
+ | `PI_LEGION_VERIFY_LOCK_DIR` | `$TMPDIR/pi-legion-verify-<uid>` | Slot directory. Set it to scope the limit more narrowly. |
384
+
385
+ The child restores the host `NODE_OPTIONS` when its extension loads, so bash
386
+ commands it runs (`tsc`, `npm test`) keep the default heap.
387
+
388
+ ### Storage hygiene
389
+
390
+ Each root pi session runs one deferred, best-effort sweep of `.pi-subagents/`
391
+ at startup. A step only touches state whose owner process is gone: runs and
392
+ `rpc-child-*` temp dirs record the owner pid (`owner.json` / `.owner`). Set any
393
+ limit to `0` to disable that step.
394
+
395
+ | Env | Default | Effect |
396
+ | --- | ---: | --- |
397
+ | `PI_LEGION_HYGIENE` | on | `0`/`off` disables the whole sweep. |
398
+ | `PI_LEGION_RUN_RETENTION_DAYS` | 14 | Delete run dirs idle this long. Runs with a `worktrees/` entry are kept as resume credentials. |
399
+ | `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). |
400
+ | `PI_LEGION_STALE_TEMP_HOURS` | 24 | Age threshold for legacy `rpc-child-*` dirs without an owner marker. |
401
+ | `PI_LEGION_EVENT_RETENTION_DAYS` | 30 | Drop ledger events of settled/failed nodes older than this (`orphan_detected`, `budget_refused`, `node_abandoned` are kept). |
402
+ | `PI_LEGION_EVENT_MAX_ROWS` | 200000 | Soft cap on the events table. Oldest terminal-node rows are deleted first. |
403
+
404
+ The live `legion.db` gets event retention and a `wal_checkpoint(TRUNCATE)` on
405
+ session shutdown. A large delete is followed by `VACUUM`, which is skipped when
406
+ another connection holds the db.
407
+
408
+ ## Delivery and resume
409
+
410
+ A centurion owns a Git worktree for its subtree; its worker/scout/reviewer
411
+ children work in that subtree, and a nested centurion gets a nested worktree.
412
+ Each subtree is delivered as a squash MR to its parent level. MRs move upward
413
+ through the tree until the root reviews and merges into the default branch.
414
+
415
+ Resuming follows the tree's saved sessions and node records. If a supervisor
416
+ process ends, its child processes do not become detached workers; resuming the
417
+ parent restores the squad, while already-settled children are not restarted.
418
+ Keep the run records and session files while the tree may need to resume.
419
+
420
+ ## Herdr transition and role migration
421
+
422
+ Version **0.16.x is the final Herdr-era line**. Pi Legion 2.x is RPC-only:
423
+ child execution uses headless `pi --mode rpc` processes, and the legacy backend
424
+ is archived under `src/backends/legacy/`, not offered as a v2 runtime option.
425
+ Pin to `pi-legion` 0.16.x if a workflow must stay on the former Herdr-based
426
+ execution model. A short breaking-change summary lives in
427
+ [CHANGELOG.md](CHANGELOG.md#200---2026-10-14).
428
+
429
+ Pi Legion 2.x supports `kind: pi` only. Migrate a custom role that uses another
430
+ CLI kind by changing its frontmatter, for example:
431
+
432
+ ```yaml
506
433
  ---
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
434
+ name: search
514
435
  kind: pi
515
- placement: split-down
516
- steer: true
517
- onBlocked: forward
518
436
  ---
519
-
520
- You are a read-only researcher. Facts with paths. No edits.
521
437
  ```
522
438
 
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.
439
+ A non-Pi kind is rejected by v2. If that role must continue to run through
440
+ another CLI, keep it on the 0.16.x line or replace it with a Pi role.
640
441
 
641
442
  ## Development
642
443
 
643
444
  ```bash
644
- git clone https://github.com/zzjcool/pi-legion.git
645
- cd pi-legion
646
445
  npm install
647
446
  npm run typecheck
648
- npm test # unit
649
- npm run test:integration # fake herdr, no live binary
447
+ npm test # unit tests
448
+ npm run test:integration # integration tests
449
+ npm run test:all # typecheck + unit + integration tests
650
450
  ```
651
451
 
652
- Live tests (`npm run test:live`) need a real herdr on `PATH` and
653
- `HERDR_ENV=1`. They are skipped otherwise.
452
+ ### Mock LLM (offline pi / legion smoke)
654
453
 
655
- Container checks (host Pi config bind-mounted, package at `/plugin`) live in
656
- `test/docker/`. Each numbered script is one feature:
454
+ A scriptable OpenAI-compatible server lives under [`test/mock-llm/`](test/mock-llm/README.md).
455
+ It registers with Pi like any local gateway (`models.json` → `api: "openai-completions"`),
456
+ so spawn / stop / doctor / verify paths can run without real API keys:
657
457
 
658
458
  ```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
459
+ npm run mock-llm # http://127.0.0.1:8765/v1 scenario=echo
460
+ npm run test:mock-llm # unit + integration smoke for the mock
461
+ MOCK_LLM_SCENARIO=spawn-scout npm run mock-llm
672
462
  ```
673
463
 
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
464
+ Use `writeMockPiHome()` from `test/mock-llm` to generate an isolated
465
+ `PI_CODING_AGENT_DIR` with dummy `apiKey: "mock"` only — never copy host
466
+ `auth.json` into fixtures.
467
+
468
+ See [CONTRIBUTING.md](CONTRIBUTING.md#project-workflow) for the project
469
+ workflow: issues, pull requests, the merge gate, and npm releases.
470
+
471
+ Version history for consumers: [CHANGELOG.md](CHANGELOG.md). Tag-by-tag
472
+ release bodies: [GitHub Releases](https://github.com/zzjcool/pi-legion/releases).
714
473
 
715
474
  MIT. See [LICENSE](LICENSE).