pi-legion 2.0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,117 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Published GitHub Release notes remain available at
9
+ https://github.com/zzjcool/pi-legion/releases — this file is the in-repo
10
+ summary so you can see **2.x vs 0.16** without leaving the tree.
11
+
12
+ ## [Unreleased]
13
+
14
+ ### Fixed
15
+
16
+ - Dispatched children now receive `--provider` pinned from the parent's model
17
+ registry, so a bare model id (`workbuddy/…`) that also exists under a static
18
+ auth-less provider in `models.json` can no longer hijack the child start
19
+ (`No API key found for …`) (#123).
20
+ - Scout-guard now counts first-class `grep` / `find` / `ls` tool calls as reads (#87).
21
+ - Doctor diagnose help no longer repeats itself; storage section shows hygiene sweep state (#86).
22
+ - Bundled roles no longer advertise unloaded `lens_*` tools (#75).
23
+ - Non-interactive `pi --print` waits for async children before cascade retire (#77).
24
+ - Roles load `builtin:codemode`; doctor diagnose shows a dedicated section (#89 / #90 → #93).
25
+ - Doctor role-tool→extension gates are data-driven / extensible (#97 / #99).
26
+ - Launch honors `name=` so `/legion status <name>` matches the railway label (#101 / #104).
27
+ - Galaxy legend shows failed/retired glyphs only when those states are present (#102 / #103).
28
+ - Live child status leads with `alive · quiet Ns`; long quiet emphasizes living over a stale execution snapshot (#81 / #92 / #105).
29
+ - npm `package.json` description/keywords aligned with Legion 2.x RPC (no Herdr panes) (#106 / #111).
30
+
31
+ ### Added
32
+
33
+ - `/legion status` human-readable summary; doctor slash list covers team / agents / on|off / summary (#76).
34
+ - README documents scout-guard settings and `/scout-guard` / `/no-scout-guard` (#74 / #71).
35
+ - Scout-guard mock-llm e2e: notify ×1, cooldown, and dispatch clear (#108 / #113).
36
+
37
+ ### Changed
38
+
39
+ - Prefer `PI_LEGION_EXTRA_AGENT_DIRS` over legacy `PI_HERDR_SUBAGENTS_*` (alias; new name wins if both set) (#109 / #114).
40
+ - CONTRIBUTING/CI docs aligned with 2.x RPC; verify-12/13 paths point at `test/legacy/docker/` (#95 / #100, #107 / #112).
41
+ - README: remove duplicate Child extensions heading (#94 / #98).
42
+ - `design.md` historical banner; landed F38–F46 marked ✅ / Historical (#80 / #83 → #91).
43
+ - CI PR job runs `test:integration` (#79 / #88).
44
+
45
+ ## [2.0.0] - 2026-10-14
46
+
47
+ First **Pi Legion** line on headless Pi RPC. Relative to the final Herdr-era
48
+ **0.16.x** packages, this is a breaking transport and packaging change.
49
+
50
+ ### Breaking
51
+
52
+ - **RPC-only transport:** children run as headless `pi --mode rpc` processes
53
+ under `LegionSupervisor`. Herdr pane execution is archived under
54
+ `src/backends/legacy/` and is **not** a v2 runtime option.
55
+ - **Kinds:** only `kind: pi` is supported. Migrate non-Pi roles or stay on
56
+ `pi-legion@0.16.x`.
57
+ - **Shared tree ledger:** squad state lives in SQLite WAL `legion.db` (phases,
58
+ mail, resume), not pane-centric session bookkeeping alone.
59
+ - Bundled agent playbooks aligned with local RPC practice (#66).
60
+
61
+ ### Added
62
+
63
+ - Recursive centurion trees, four-phase
64
+ planning / implementing / reviewing / verifying machine, `legion_mail`,
65
+ three-layer `/legion` UI (resident tree, full view, live overlay), railway
66
+ strip, tok/s / galaxy / log panels.
67
+ - Scout-guard: remind the main session after a streak of read-class tools (#64).
68
+ - Storage hygiene sweep on startup: prune idle runs, orphan temp dirs, abandoned
69
+ nodes, event retention, WAL checkpoint (#37 / #38 / #40 → #47).
70
+ - `/legion doctor` (+ diagnose) self-check; auto-doctor snippets on gated spawn
71
+ failures (#52).
72
+ - Child memory gate (`NODE_OPTIONS` V8 flags, pre-spawn gate) and tree-wide
73
+ verify concurrency limit (#50).
74
+ - Children default to `--no-extensions` with explicit/role escape hatches (#43 / #48).
75
+ - User stop controls for async children (`/legion stop`, idle Esc, `x` in view) (#41).
76
+ - Mock LLM server for offline pi / legion smoke (`test/mock-llm`, #54 / #56).
77
+ - GitHub Actions CI (typecheck + unit + integration) and stage-only npm publish
78
+ on tag (#65 / #68).
79
+ - Project workflow docs in CONTRIBUTING (#69).
80
+
81
+ ### Changed
82
+
83
+ - Slash commands consolidated under `/legion` (#57).
84
+ - Status bar shows current-turn elapsed beside lifetime total (#62).
85
+ - tok/s uses output tokens / generationMs (decode throughput) (#61).
86
+
87
+ ### Fixed
88
+
89
+ - Railway: pin live spawn verticals when the strip scrolls left (#67); retire /
90
+ downward spawn connector regressions (#46).
91
+ - Live worker no longer marked retired on a transient Connection blip (#63).
92
+ - Session JSONL streamed + incrementally parsed (no full-file `readFileSync`) (#36 / #45).
93
+ - Per-stream child output byte cap; kill on overflow (#35 / #44).
94
+ - Bun SQLite `.get()` null treated as miss on root insert (#53).
95
+ - Vexillatio sibling naming scoped to the current parent (#42 / #51).
96
+
97
+ ### Security / robustness (design.md F38–F46, already on master)
98
+
99
+ - Model-scope globs use non-RegExp matching (ReDoS).
100
+ - Session create uses `writeFileSync` with `wx` (no fd leak / TOCTOU).
101
+ - Session decode swallows scalar JSON lines as torn.
102
+ - `tasks[]` / blank task validated before launch.
103
+ - Per-agent `timeoutMs`, pending criteria, nested YAML acceptance parsing;
104
+ `UNENFORCED_FIELDS` empty.
105
+
106
+ ## [0.16.x] - Herdr-era (final)
107
+
108
+ Last line that runs children in Herdr panes. Not developed further on this
109
+ branch — pin `pi-legion@0.16.x` if you still need that model. See
110
+ [Herdr transition](README.md#herdr-transition-and-role-migration) in the README.
111
+
112
+ Earlier 0.11–0.15 history (including the `pi-herdr-swarm` /
113
+ `@zzjcool/pi-herdr-subagents` renames) lives in git tags and older Releases.
114
+
115
+ [Unreleased]: https://github.com/zzjcool/pi-legion/compare/v2.0.0...HEAD
116
+ [2.0.0]: https://github.com/zzjcool/pi-legion/releases/tag/v2.0.0
117
+ [0.16.x]: https://github.com/zzjcool/pi-legion/releases
package/CONTRIBUTING.md CHANGED
@@ -23,8 +23,14 @@ directly to `master`. Implementers do not merge their own pull requests.
23
23
  ```bash
24
24
  npm run typecheck
25
25
  npm test
26
+ npm run test:integration # or npm run test:all
26
27
  ```
27
28
 
29
+ The required GitHub Actions check `ci` runs typecheck, unit, integration, and
30
+ mock-llm. Run `npm run test:integration` (or `npm run test:all`) locally
31
+ before you push, especially when the change touches `src/runs/`, `src/legion/`,
32
+ or `src/extension/runtime.ts`.
33
+
28
34
  Add the unit tests the change needs. Prefer **TDD** (see [Checks](#checks)):
29
35
  a failing test in `test/unit` or `test/integration` first, then the production
30
36
  code.
@@ -59,20 +65,36 @@ there is no compile step to ship).
59
65
 
60
66
  ## Checks
61
67
 
68
+ The required GitHub Actions check `ci` (`.github/workflows/ci.yml`, on pull
69
+ requests and pushes to `master`) runs, in order:
70
+
71
+ 1. `npm run typecheck`
72
+ 2. `npm test` (unit)
73
+ 3. `npm run test:integration`
74
+ 4. `npm run test:mock-llm`
75
+
76
+ A failing integration step fails `ci`, so nesting and orchestrator regressions
77
+ are blocked before merge. `test:live` and `test:docker` stay out of that job.
78
+
62
79
  ```bash
63
80
  npm run typecheck
64
81
  npm test # test/unit/*.test.ts
65
- npm run test:integration # fake herdr, no live binary
82
+ npm run test:integration # FakeSupervisor / nested fakes; no live `pi` binary
66
83
  npm run test:all # typecheck + unit + integration
84
+ npm run test:mock-llm # also part of the required ci check
67
85
  ```
68
86
 
69
- `npm run test:live` talks to a real herdr. It skips itself unless `HERDR_ENV=1`
70
- and `herdr` is on `PATH`. Do not fail CI on it.
87
+ `npm run test:live` is a **pi RPC live** smoke (`test/live/rpc-live.test.ts`).
88
+ It skips itself when the `pi` binary is missing from `PATH` (see the probe at
89
+ the top of that file). Do not fail CI on it.
71
90
 
72
91
  Prefer **TDD** for behaviour changes: a failing test in `test/unit` or
73
92
  `test/integration` first, then the production code. Integration tests should
74
- go through `createHerdrClient(createFakeRunner(fake))` so argv construction
75
- and JSON parsing stay covered.
93
+ drive the orchestrator through `FakeSupervisor` (`test/helpers/fake-supervisor.ts`)
94
+ so nesting / launch / collect stay covered without a live `pi`. The herdr-era
95
+ helpers `createHerdrClient` / `createFakeRunner` remain as **deprecated legacy**
96
+ exports (see `src/api.ts` and `test/legacy/`); use them only when touching the
97
+ archived Herdr backend.
76
98
 
77
99
  ## Mock LLM
78
100
 
@@ -91,16 +113,20 @@ dummy key is always the literal `mock`.
91
113
 
92
114
  ## Docker verification
93
115
 
94
- `test/docker/` runs the package inside `pi-herdr-sandbox:latest` with a copy
95
- of the **host** Pi config (`~/.pi/agent/settings.json`, `models.json`,
96
- `auth.json` if present). The copy is disposable (`/tmp/pi-herdr-docker-home`
97
- by default); the container cannot write back to your real `~/.pi`.
116
+ `test/docker/` runs the package inside the RPC / pi-legion sandbox image.
117
+ Default image and staging path names are **historical** (`pi-herdr-sandbox:latest`,
118
+ `/tmp/pi-herdr-docker-home`; override with `PI_DOCKER_IMAGE` / `DOCKER_PI_HOME`).
119
+ Content is the 2.x RPC sandbox, not a live herdr pane host. The scripts copy
120
+ the **host** Pi config (`~/.pi/agent/settings.json`, `models.json`, `auth.json`
121
+ if present) into that disposable home; the container cannot write back to your
122
+ real `~/.pi`.
98
123
 
99
124
  ```bash
100
- # image must already exist
125
+ # image must already exist (historical default name; do not rename a local tag
126
+ # just to match docs — override with PI_DOCKER_IMAGE if yours differs)
101
127
  docker image inspect pi-herdr-sandbox:latest >/dev/null
102
128
 
103
- bash test/docker/verify-1-child-guard.sh # child herdr interceptor
129
+ bash test/docker/verify-1-child-guard.sh # RPC child-guard / ledger block
104
130
  bash test/docker/verify-2-task-appendix.sh # frozen task appendix
105
131
  bash test/docker/verify-3-readonly.sh # read-only bash denylist
106
132
  bash test/docker/verify-4-acceptance.sh # collect → verified
@@ -111,19 +137,26 @@ bash test/docker/verify-8-verify-command.sh # criterion.command + timeout
111
137
  bash test/docker/verify-9-worktree.sh # git worktree cwd
112
138
  bash test/docker/verify-10-fallback.sh # fallbackModels
113
139
  bash test/docker/verify-11-nested-alias.sh # nested + alias + completionGuard
114
- bash test/docker/verify-12-workspace.sh # pi install + child pinned to parent Space
115
- bash test/docker/verify-13-kinds.sh # kind/model matrix; live CodeBuddy when mounted
116
140
  ```
117
141
 
118
- Or all at once:
142
+ Or all at once (RPC mainline **1–11** only):
119
143
 
120
144
  ```bash
121
145
  npm run test:docker
122
146
  ```
123
147
 
124
- `verify-13-kinds.sh` is **not** in `npm run test:docker`: it bind-mounts host
125
- `codebuddy` / `cursor-agent` and (unless `LIVE_KIND_PROBES=0`) calls live
126
- CodeBuddy. The numbered 1–12 scripts stay offline/mock.
148
+ **Legacy Herdr / Space / kind checks** live under `test/legacy/docker/` (need
149
+ pi-legion **0.16.x** Herdr-era setup). They are **not** part of
150
+ `npm run test:docker`:
151
+
152
+ ```bash
153
+ bash test/legacy/docker/verify-12-workspace.sh # Herdr Space pin (legacy)
154
+ bash test/legacy/docker/verify-13-kinds.sh # kind/model matrix; live CodeBuddy when mounted
155
+ ```
156
+
157
+ `verify-13-kinds.sh` also bind-mounts host `codebuddy` / `cursor-agent` and
158
+ (unless `LIVE_KIND_PROBES=0`) calls live CodeBuddy. The numbered 1–11 scripts
159
+ under `test/docker/` stay offline/mock.
127
160
 
128
161
  Override paths with `HOST_PI_AGENT`, `DOCKER_PI_HOME`, `PI_DOCKER_IMAGE`.
129
162
 
@@ -145,7 +178,7 @@ them by hand.
145
178
  | `src/runs/acceptance.ts` | `verification-output` command |
146
179
  | `test/mock-llm/` | Scriptable OpenAI mock for offline pi / legion |
147
180
  | `agents/*.md` | bundled roles |
148
- | `docs/design.md` | measured herdr findings (F1–F46) |
181
+ | `docs/design.md` | design notes / F1–F46 (incl. historical herdr-era measurements; see file banner) |
149
182
  | `README.md` | what is actually wired today |
150
183
 
151
184
  Do not import from `src/` in downstream packages — use
package/README.md CHANGED
@@ -14,6 +14,8 @@ shared SQLite ledger records the whole tree.
14
14
  > `@zzjcool/pi-herdr-subagents` (through 0.14.3; renamed after an npm name
15
15
  > collision), then as `pi-herdr-swarm` (0.15.0), before becoming `pi-legion`
16
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).
17
19
 
18
20
  ## Contents
19
21
 
@@ -22,10 +24,12 @@ shared SQLite ledger records the whole tree.
22
24
  - [Legion tools and phases](#legion-tools-and-phases)
23
25
  - [Three-layer visualization](#three-layer-visualization)
24
26
  - [Settings and budget gates](#settings-and-budget-gates)
27
+ - [Scout guard](#scout-guard)
25
28
  - [Team 配置](#team-配置)
26
29
  - [Parent/child context conventions](#parentchild-context-conventions)
27
30
  - [Delivery and resume](#delivery-and-resume)
28
31
  - [Herdr transition and role migration](#herdr-transition-and-role-migration)
32
+ - [Changelog](CHANGELOG.md)
29
33
  - [Development](#development)
30
34
 
31
35
  ## Architecture
@@ -87,12 +91,22 @@ subagent({ agent: "scout", task: "Map the export code path and report relevant f
87
91
 
88
92
  No manual child-process launch is needed. Use `/legion` to inspect the tree;
89
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).
90
96
 
91
97
  The `subagent` control actions include event-driven `wait` and graceful `stop`:
92
98
  `subagent({ action: "wait", name: "worker-0" })` returns as soon as the child
93
99
  settles (or the parent turn is interrupted with Esc). Aborting a parent turn
94
100
  with Esc gracefully retires its tracked children, including those being waited
95
- on or collected; the cascade is silent. To stop children explicitly, use
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
96
110
  `subagent({ action: "stop", name: "worker-0" })` or
97
111
  `subagent({ action: "stop", all: true })`; this runs the normal graceful-retire
98
112
  sequence for each selected child.
@@ -117,6 +131,28 @@ While the main agent is idle and subagents are live, the railway header shows
117
131
  a dim `esc stop all · /legion stop [name]` hint (shortened on narrow terminals,
118
132
  hidden while the main agent is working).
119
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
+
120
156
  ## Legion tools and phases
121
157
 
122
158
  Child nodes receive these tree-aware tools:
@@ -208,6 +244,38 @@ subtree, including itself; when launched by a parent at depth `d`, it inherits
208
244
  `C_child = min(C_parent, d + R, 4)` (`R = 1` makes it a leaf), so it can tighten
209
245
  the inherited cap but cannot widen it.
210
246
 
247
+ ### Scout guard
248
+
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.
255
+
256
+ ```json
257
+ {
258
+ "subagents": {
259
+ "scoutGuard": {
260
+ "enabled": true,
261
+ "threshold": 8,
262
+ "cooldownMs": 120000
263
+ }
264
+ }
265
+ }
266
+ ```
267
+
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
+
211
279
  ### Team 配置
212
280
 
213
281
  Use `subagents.team` to select the default named team from `subagents.teams`.
@@ -245,10 +313,11 @@ gate, missing lens tools, orphan temps, …), ask Legion rather than guessing
245
313
  same idea as `pi list` / `pi auth check`.
246
314
 
247
315
  - `/legion doctor` — full report in the chat (works outside TUI too).
248
- - `/legion doctor <section>` — one of: `summary`, `extensions`, `models`,
316
+ - `/legion doctor [section]` — one of: `summary`, `extensions`, `models`,
249
317
  `spawn`, `memory`, `tools`, `storage`, `runtime`, `next`.
250
318
  - `subagent({ action: "diagnose" })` — same full report as tool output so the
251
- main agent can call it without waiting for you.
319
+ main agent can call it without waiting for you. The tool has no section
320
+ field; filter with `/legion doctor [section]`.
252
321
 
253
322
  The report is structured for the model: each section ends in **Issues** /
254
323
  **Next steps** with copy-pasteable fixes (env exports, role frontmatter,
@@ -268,11 +337,10 @@ Coverage:
268
337
  | models | Host vs child model lists, which package restores providers, team/roster gaps |
269
338
  | spawn | Sample child argv, recent spawn failures + stderr, nested-spawn flags |
270
339
  | 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 |
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 |
273
342
  | runtime | Live children, tree statuses, acceptance-ish / active nodes |
274
343
 
275
- ### Child extensions
276
344
  ### Child extensions
277
345
 
278
346
  Children start with `--no-extensions`, so they do not load the host's
@@ -288,6 +356,20 @@ itself (guard and nested spawning) and every path listed in the role's
288
356
  | `PI_LEGION_CHILD_INHERIT_EXTENSIONS=1` | Every child loads all host extensions, as before. |
289
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. |
290
358
 
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.
360
+
361
+ ### Extra agent directories
362
+
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).
367
+
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
+
291
373
  ### Child memory controls
292
374
 
293
375
  | Env | Default | Effect |
@@ -341,7 +423,8 @@ Version **0.16.x is the final Herdr-era line**. Pi Legion 2.x is RPC-only:
341
423
  child execution uses headless `pi --mode rpc` processes, and the legacy backend
342
424
  is archived under `src/backends/legacy/`, not offered as a v2 runtime option.
343
425
  Pin to `pi-legion` 0.16.x if a workflow must stay on the former Herdr-based
344
- execution model.
426
+ execution model. A short breaking-change summary lives in
427
+ [CHANGELOG.md](CHANGELOG.md#200---2026-10-14).
345
428
 
346
429
  Pi Legion 2.x supports `kind: pi` only. Migrate a custom role that uses another
347
430
  CLI kind by changing its frontmatter, for example:
@@ -385,4 +468,7 @@ Use `writeMockPiHome()` from `test/mock-llm` to generate an isolated
385
468
  See [CONTRIBUTING.md](CONTRIBUTING.md#project-workflow) for the project
386
469
  workflow: issues, pull requests, the merge gate, and npm releases.
387
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).
473
+
388
474
  MIT. See [LICENSE](LICENSE).
package/agents/planner.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: planner
3
3
  description: 规划员:把任务拆成可并行/可验收的实施计划,冻结接口,指定文件归属
4
- tools: read, grep, find, ls, bash, codemode, lens_diagnostics, symbol_search, project_report, module_report, read_symbol, read_enclosing, pi_lens_activate_tools
4
+ tools: read, grep, find, ls, bash, codemode
5
5
  systemPromptMode: replace
6
6
  inheritProjectContext: true
7
7
  inheritSkills: false
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: reviewer
3
3
  description: 审查员:只读对抗式审查一个指定范围的改动——对照需求查漏、查正确性与测试覆盖,输出带路径与严重级别的发现
4
- tools: read, grep, find, ls, bash, codemode, lens_diagnostics, symbol_search, project_report, module_report, read_symbol, read_enclosing, pi_lens_activate_tools
4
+ tools: read, grep, find, ls, bash, codemode
5
5
  systemPromptMode: replace
6
6
  inheritProjectContext: true
7
7
  inheritSkills: false
@@ -31,9 +31,8 @@ maxSubagentDepth: 1
31
31
 
32
32
  ## 工具纪律(读代码的正确姿势)
33
33
 
34
- - 你有 pi-lens 工具:看模块结构用 `module_report`,读单个函数/类的完整源码用 `read_symbol`(比 grep 片段读快且省 token),找符号位置用 `symbol_search`,改完的类型/错误检查用 `lens_diagnostics`。
35
34
  - `grep`/`find` 工具底层就是 ripgrep/fd,优先于 bash 管道。只在需要管道(如 `git show X | grep`)时才用 bash。
36
- - **禁止对同一文件碎片化 grep 超过 3 次**——超过说明该用 `module_report`/`read_symbol` 整体看了。
35
+ - **禁止对同一文件碎片化 grep 超过 3 次**——超过就用 `read` 把相关段落整段读完。
37
36
  - 多个独立的读操作(批量读文件、批量检查)用 `codemode` 的 `Promise.allSettled` 并行发,不要串行。
38
37
 
39
38
  ## 先确定范围
package/agents/worker.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: worker
3
3
  description: 实现者:按冻结计划写代码、跑测试,完成后自报 verdict
4
- tools: read, edit, write, grep, find, ls, bash, subagent, codemode, lens_diagnostics, symbol_search, project_report, module_report, read_symbol, read_enclosing, pi_lens_activate_tools
4
+ tools: read, edit, write, grep, find, ls, bash, subagent, codemode
5
5
  systemPromptMode: replace
6
6
  inheritProjectContext: true
7
7
  inheritSkills: false
@@ -27,9 +27,8 @@ maxSubagentDepth: 3
27
27
 
28
28
  ## 工具纪律(读代码的正确姿势)
29
29
 
30
- - 你有 pi-lens 工具:看模块结构用 `module_report`,读单个函数/类的完整源码用 `read_symbol`(比 grep 片段读快且省 token),找符号位置用 `symbol_search`,改完后对涉及文件跑 `lens_diagnostics` 查类型/错误。
31
30
  - `grep`/`find` 工具底层就是 ripgrep/fd,优先于 bash 管道。只在需要管道(如 `git diff | grep`)时才用 bash。
32
- - **禁止对同一文件碎片化 grep 超过 3 次**——超过说明该用 `module_report`/`read_symbol` 整体看了。
31
+ - **禁止对同一文件碎片化 grep 超过 3 次**——超过就用 `read` 把相关段落整段读完。
33
32
  - 多个独立的读操作(批量读文件、批量检查)用 `codemode` 的 `Promise.allSettled` 并行发,不要串行。
34
33
 
35
34
  ## 职责边界
@@ -57,7 +57,8 @@ vexillatio 工具 execute 也要把 signal 传进来(找到它的 registerTool
57
57
  ## 语义边界(不变)
58
58
 
59
59
  - **stop action 不变**:显式停止仍走全链。
60
- - **主 pi 正常退出(quit)不变**:session_shutdown 已级联。
60
+ - **主 pi 正常退出(quit)不变**:交互 TUI 的 session_shutdown 仍立即级联 retire。
61
+ - **非交互 print/JSON(#72)**:`ctx.mode` 为 `print` 或 `json` 时,session_shutdown 先等仍活着的 async 子 agent 结算(上限为各子的 turn timeout),再 retire 尚未结束的。避免 `--print` 在首包前把刚 spawn 的子进程记成 `no assistant message for the last prompt`。父会话关机导致的无 assistant abort 原因改为 parent session shutdown,与模型自己没写出 assistant 区分。
61
62
  - **SIGKILL/崩溃不覆盖**:孤儿扫描 + resume 语义不变(这是既定接受的风险)。
62
63
  - **children 的 children**:retire 链本身递归(孙进程 sweep 在 rpc-supervisor 的 retireChild 里),无需额外处理。
63
64
  - **blocked children**:一并 retire(stop 语义一致)。
package/docs/design.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # pi-legion 设计文档
2
2
 
3
+ > **⚠️ 历史实验笔记(非现行契约)**
4
+ > 本文记录 herdr / pane 时代的实测与设计推演。**运行时契约以 [README](../README.md) 与 [CONTRIBUTING](../CONTRIBUTING.md) 为准**(Pi Legion 2.x = headless Pi RPC + `legion.db`;herdr pane 执行已归档到 `src/backends/legacy/`)。
5
+ > 下文 F 条中标 **✅ landed** 的是已在当前 master 落地的修复;未标注的早期结论仍有参考价值,但不要当作「今日仍坏」的缺口清单。
6
+
3
7
  > 基于 10 组实测实验(exp1–exp10)得出的设计。所有结论都标注了实验依据,
4
8
  > 未经验证的假设明确标记为「待验证」。
5
9
 
@@ -20,7 +24,7 @@
20
24
 
21
25
  ### 实现状态(相对本文档)
22
26
 
23
- 下面 F 条目里不少写的是「当时的缺口」。当前运行时已经钉死、以 [README](../README.md) 为准:
27
+ 下面 F 条目里不少写的是「当时的缺口」(文首 banner 已说明:契约以 README + CONTRIBUTING 为准)。当前运行时已经钉死:
24
28
 
25
29
  - 子进程强制加载 child-guard:拦 `herdr agent prompt|wait|send-keys|start`、外 pane read、只读角色写操作
26
30
  - 任务卡自动附录(禁止 wakeup、必须 `{"ok":…}`)
@@ -654,10 +658,11 @@ system prompt 不再注入 playbook/roster;bash 派发 guard 放行手动 herd
654
658
  ### 6.4.1 Scout guard `subagents.scoutGuard`
655
659
 
656
660
  只在主会话生效(子进程带 `PI_SUBAGENT_ROLE`,不计数、不注入)。连续 N 次
657
- 读类工具调用(`read`,以及可判定为只读的 bash:`cat`/`head`/`tail`/`less`/
658
- `rg`/`grep`/`find`/`ls`/`stat`/`wc`/`file`/`jq`、`git show|log|diff|status|blame`、
659
- 不带 `-i` 的 `sed`)且中间没有成功的 `subagent` / `vexillatio` 派发时,异步
660
- 注入一条 follow-up 提醒,不挡住当前 `tool_call`。
661
+ 读类工具调用(first-class `read`/`grep`/`find`/`ls`,以及可判定为只读的 bash:
662
+ `cat`/`head`/`tail`/`less`/`rg`/`grep`/`find`/`ls`/`stat`/`wc`/`file`/`jq`、
663
+ `git show|log|diff|status|blame`、不带 `-i` 的 `sed`)且中间没有成功的
664
+ `subagent` / `vexillatio` 派发时,异步注入一条 follow-up 提醒,不挡住当前
665
+ `tool_call`。
661
666
 
662
667
  ```json
663
668
  { "subagents": { "scoutGuard": { "enabled": true, "threshold": 8, "cooldownMs": 120000 } } }
@@ -1132,12 +1137,24 @@ index.ts(132,4): error TS7006: Parameter 'signal' implicitly has an 'any' type.
1132
1137
 
1133
1138
  ---
1134
1139
 
1135
- ## 16. 交付前安全/健壮性审计(F38–F45)
1140
+ ## 16. 交付前安全/健壮性审计(F38–F46)— Historical findings(✅ landed)
1136
1141
 
1137
1142
  对全部模块做了对抗式输入扫描(畸形会话文件、路径穿越、恶意配置、并发写)。
1138
- 以下 8 条是真缺陷,其余区域已验证健壮。
1143
+ 以下条目**当时**是真缺陷;对照当前 master 均已落地(见各条 ✅),勿再当 open gap。
1139
1144
 
1140
- ### F38 — ReDoS:glob → RegExp 的嵌套量词(安全)
1145
+ | # | 当时缺口 | master 落点(摘要) |
1146
+ | --- | --- | --- |
1147
+ | F38 | glob→RegExp ReDoS | `matchesScopePattern` 双指针 glob,**不用 RegExp** |
1148
+ | F39 | `openSync`/`closeSync` fd 泄漏 | session 预创建改 `writeFileSync(..., { flag: "wx" })` |
1149
+ | F40 | `JSON.parse` 标量拖垮会话解析 | `decodeEvent` 非对象计入 `tornLines` |
1150
+ | F41 | `tasks[]` 条目未校验 | `firstInvalidStep` / 启动前校验 agent+task |
1151
+ | F42 | 空白 `task` 被 `Boolean` 放行 | 存在性按 `trim()` |
1152
+ | F43 | 角色 `timeoutMs` 被忽略 | launch 记入 `timeoutByName`,follow/wait 使用 |
1153
+ | F44 | `acceptance.criteria` 静默丢弃 | `pendingCriteria` + command 才升 `verified` |
1154
+ | F45 | 未生效配置看起来生效 | `UNENFORCED_FIELDS` 现为空;list 不再假警告 |
1155
+ | F46 | 嵌套 YAML acceptance 未解析 | `parseAcceptance` + `parseIndentedBlock` |
1156
+
1157
+ ### F38 — ✅ landed — ReDoS:glob → RegExp 的嵌套量词(安全)
1141
1158
 
1142
1159
  `modelScope.allow` 的 glob 被翻译成 `.*`,于是 `*a*a*a…b` 变成 `.*a.*a.*a…b`——
1143
1160
  嵌套量词在**不匹配**的输入上指数回溯。
@@ -1152,7 +1169,7 @@ index.ts(132,4): error TS7006: Parameter 'signal' implicitly has an 'any' type.
1152
1169
  **修法**:不用正则,改用双指针贪心 glob 匹配(只回溯最近一个 `*`)。
1153
1170
  修复后 n=6400 仅 2ms。
1154
1171
 
1155
- ### F39 — fd 泄漏:`openSync`/`closeSync` 无保护
1172
+ ### F39 — ✅ landed — fd 泄漏:`openSync`/`closeSync` 无保护
1156
1173
 
1157
1174
  ```ts
1158
1175
  const fd = fs.openSync(file, "w", 0o600);
@@ -1165,7 +1182,7 @@ launcher 为**每个 child** 创建一个 session 文件,所以泄漏会随 fa
1165
1182
  **修法**:`fs.writeFileSync(file, "", { flag: "wx", mode: 0o600 })`——
1166
1183
  由 Node 自己管理描述符,并且 `wx` 顺带消除了 `existsSync` 检查留下的 TOCTOU 窗口。
1167
1184
 
1168
- ### F40 — `JSON.parse("null")` 拖垮整个会话解析
1185
+ ### F40 — ✅ landed — `JSON.parse("null")` 拖垮整个会话解析
1169
1186
 
1170
1187
  `JSON.parse` 对标量是成功的:`null` / `42` / `true` / `"x"`。
1171
1188
  随后 `event.type` 在 `null` 上抛异常,**整个 `parseSessionText` 崩溃**,
@@ -1173,7 +1190,7 @@ launcher 为**每个 child** 创建一个 session 文件,所以泄漏会随 fa
1173
1190
 
1174
1191
  **修法**:非对象、非数组的标量按损坏行计入 `tornLines`,继续解析其余行。
1175
1192
 
1176
- ### F41 — `tasks[]` 的条目未校验
1193
+ ### F41 — ✅ landed — `tasks[]` 的条目未校验
1177
1194
 
1178
1195
  单任务路径用 `Boolean(params.agent && params.task)` 把关,但数组路径直接放行:
1179
1196
 
@@ -1184,12 +1201,12 @@ tasks:[{task:"do it"}] → 放行,agent 名为 undefined
1184
1201
 
1185
1202
  **修法**:每个 step 在分配任何资源之前校验 `agent` 与 `task`。
1186
1203
 
1187
- ### F42 — `Boolean(" ")` 为真:纯空白 task 被放行
1204
+ ### F42 — ✅ landed — `Boolean(" ")` 为真:纯空白 task 被放行
1188
1205
 
1189
1206
  即使单任务路径,`task: " "` 也会通过,生成一个毫无意义的 `Task:` prompt。
1190
1207
  **修法**:存在性按 `trim()` 后的内容判定,而非 `Boolean()`。
1191
1208
 
1192
- ### F43 — `timeoutMs` 被解析却从不生效
1209
+ ### F43 — ✅ landed — `timeoutMs` 被解析却从不生效
1193
1210
 
1194
1211
  5 个自带角色都声明了预算,运行时却全部忽略,永远用全局默认 15 分钟:
1195
1212
 
@@ -1201,7 +1218,7 @@ DEFAULTS.turnTimeoutMs: 900000 (15min) ← 实际生效
1201
1218
 
1202
1219
  **修法**:`collect` 采用 `agent.timeoutMs`(按 child 记录,逐个子 agent 生效)。
1203
1220
 
1204
- ### F44 — `acceptance.criteria` 被静默丢弃
1221
+ ### F44 — ✅ landed — `acceptance.criteria` 被静默丢弃
1205
1222
 
1206
1223
  角色可以声明 `must: "测试通过"` 这类**语义**验收条件,代码解析后就不管了。
1207
1224
  调用方无法知道「没有任何东西验证过它」。
@@ -1212,7 +1229,7 @@ DEFAULTS.turnTimeoutMs: 900000 (15min) ← 实际生效
1212
1229
  **修法**:criteria 作为**待验证清单**回传调用方(`pendingCriteria`),
1213
1230
  且 `level` 保持 `attested`——**`verified` 只留给真正被人核对过的证据**。
1214
1231
 
1215
- ### F45 — 配置字段「看起来生效、实际无效」
1232
+ ### F45 — ✅ landed — 配置字段「看起来生效、实际无效」
1216
1233
 
1217
1234
  自带角色携带 `onBlocked` / `allowNestedSubagents` / `fallbackModels` / `toolTimeoutMs`,
1218
1235
  全部被解析、全部不生效。其中 `allowNestedSubagents: false` 最危险——
@@ -1227,7 +1244,7 @@ DEFAULTS.turnTimeoutMs: 900000 (15min) ← 实际生效
1227
1244
  **原则**:**一个被接受却不做事的键,比一个未知的键更危险**——
1228
1245
  用户会相信它已经在保护自己。
1229
1246
 
1230
- ### F46 — 嵌套 YAML 块从未被解析:自带角色的 acceptance 全部丢失
1247
+ ### F46 — ✅ landed — 嵌套 YAML 块从未被解析:自带角色的 acceptance 全部丢失
1231
1248
 
1232
1249
  frontmatter 解析器是**扁平的**(只处理 `key: value`);一个嵌套块会以
1233
1250
  **剥掉公共缩进后的原始文本**返回:
@@ -121,7 +121,7 @@ const VexillatioParams = Type.Object({
121
121
 
122
122
  **既有测试文件除 §3 明确授权的 `test/unit/child-mode.test.ts` 两处断言数字修订外一律不修改**;§4 要求的新增测试全部落在两个新文件里。
123
123
 
124
- **断言影响总则(REV-17.5)**:契约导致的注册行为变化若使某既有断言失效,属于本契约的影响面;授权范围以本 §3 该行所列为准,本行已全量扫描(grep `pi.tools\|tools.length\|tools.find\|tools.get\|herdrSubagents(` 全测试目录)并一次列尽。后续若再发现同类冲突,上报照旧。
124
+ **断言影响总则(REV-17.5)**:契约导致的注册行为变化若使某既有断言失效,属于本契约的影响面;授权范围以本 §3 该行所列为准,本行已全量扫描(grep `pi.tools\|tools.length\|tools.find\|tools.get\|piLegion(` 全测试目录)并一次列尽。后续若再发现同类冲突,上报照旧。
125
125
 
126
126
  ## 4. 验收清单(可机器检查)
127
127