pi-crew 0.11.24 → 0.11.26

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 (112) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +2 -2
  3. package/agents/designer.md +23 -0
  4. package/agents/orchestrator.md +17 -0
  5. package/agents/verifier.md +1 -1
  6. package/dist/index.mjs +568 -559
  7. package/{workflows/chain.workflow.md → docs/chain-workflow.md} +8 -0
  8. package/docs/dynamic-workflows.md +23 -0
  9. package/docs/publishing.md +34 -5
  10. package/docs/troubleshooting.md +50 -2
  11. package/docs/usage.md +7 -2
  12. package/install.mjs +2 -1
  13. package/package.json +4 -2
  14. package/skills/read-only-explorer/SKILL.md +15 -5
  15. package/skills/real-test-pi-crew/SKILL.md +7 -7
  16. package/src/config/config-validation.ts +58 -0
  17. package/src/config/config.ts +90 -5
  18. package/src/config/env-vars.ts +14 -7
  19. package/src/config/role-tools.ts +7 -5
  20. package/src/config/types.ts +4 -1
  21. package/src/extension/crew-cleanup.ts +6 -0
  22. package/src/extension/project-init.ts +19 -1
  23. package/src/extension/register.ts +68 -30
  24. package/src/extension/registration/artifact-cleanup.ts +52 -11
  25. package/src/extension/registration/commands/manage.ts +9 -2
  26. package/src/extension/registration/hook-registration.ts +3 -0
  27. package/src/extension/registration/lazy-configurers.ts +2 -0
  28. package/src/extension/registration/lifecycle-handlers.ts +79 -18
  29. package/src/extension/registration/render-loop.ts +27 -2
  30. package/src/extension/registration/tool-loop-guard.ts +5 -3
  31. package/src/extension/registration/verify-gate-tool.ts +7 -3
  32. package/src/extension/team-tool/api/read.ts +12 -3
  33. package/src/extension/team-tool/cancel.ts +11 -0
  34. package/src/extension/team-tool/config-patch.ts +56 -11
  35. package/src/extension/team-tool/destructive-gate.ts +72 -12
  36. package/src/extension/team-tool/dispatch/manage.ts +9 -1
  37. package/src/extension/team-tool/handle-schedule.ts +38 -4
  38. package/src/extension/team-tool/handle-settings.ts +76 -6
  39. package/src/extension/team-tool/health-monitor.ts +14 -2
  40. package/src/extension/team-tool/lifecycle-actions.ts +28 -10
  41. package/src/extension/team-tool/respond.ts +12 -5
  42. package/src/extension/team-tool/run-deadline.ts +29 -2
  43. package/src/extension/team-tool/run.ts +11 -0
  44. package/src/extension/team-tool/status.ts +64 -8
  45. package/src/extension/team-tool/workflow-manage.ts +5 -1
  46. package/src/extension/team-tool.ts +3 -0
  47. package/src/i18n.ts +18 -14
  48. package/src/prompt/inbox-poll.ts +14 -2
  49. package/src/prompt/message-tool.ts +1 -1
  50. package/src/prompt/prompt-runtime.ts +25 -3
  51. package/src/prompt/scratchpad-lifecycle.ts +14 -13
  52. package/src/prompt/surface-worker.ts +35 -9
  53. package/src/runtime/broker/crew-broker.ts +113 -111
  54. package/src/runtime/broker/delegate/delegate-abort.ts +118 -0
  55. package/src/runtime/broker/delegate/delegate-event.ts +3 -1
  56. package/src/runtime/broker/delegate/delegate-finalize.ts +179 -0
  57. package/src/runtime/broker/protocol/wait-resolve.ts +71 -5
  58. package/src/runtime/budget-enforcement.ts +26 -6
  59. package/src/runtime/chain-parser.ts +5 -3
  60. package/src/runtime/chain-runner.ts +41 -22
  61. package/src/runtime/child-pi/mock-fixtures.ts +12 -5
  62. package/src/runtime/compaction/tool-output-pruner.ts +8 -3
  63. package/src/runtime/delegate-spawn.ts +61 -8
  64. package/src/runtime/dispatch-batch.ts +10 -4
  65. package/src/runtime/effectiveness.ts +31 -3
  66. package/src/runtime/goal-workflow/plan-templates.ts +2 -2
  67. package/src/runtime/live-session/live-agent-control.ts +89 -6
  68. package/src/runtime/live-session/live-agent-manager.ts +94 -10
  69. package/src/runtime/live-session/live-session-runtime.ts +9 -2
  70. package/src/runtime/model/model-fallback.ts +6 -9
  71. package/src/runtime/model/pi-args.ts +3 -3
  72. package/src/runtime/model/runtime-policy.ts +4 -2
  73. package/src/runtime/model/runtime-resolver.ts +6 -2
  74. package/src/runtime/process/orphan-reaper.ts +143 -0
  75. package/src/runtime/process-status.ts +38 -16
  76. package/src/runtime/resilient-edit.ts +170 -93
  77. package/src/runtime/role-permission.ts +3 -1
  78. package/src/runtime/run-tracker.ts +9 -1
  79. package/src/runtime/sanitize/worker-text.ts +98 -0
  80. package/src/runtime/scheduling/run-coalesced-task-group.ts +82 -28
  81. package/src/runtime/spawn-policy.ts +190 -8
  82. package/src/runtime/stale-reconciler.ts +36 -1
  83. package/src/runtime/subagent-manager.ts +29 -0
  84. package/src/runtime/surface/herdr-provider.ts +31 -1
  85. package/src/runtime/task-output-context.ts +58 -11
  86. package/src/runtime/task-runner/post-execution.ts +150 -0
  87. package/src/runtime/task-runner/prompt-builder.ts +41 -8
  88. package/src/runtime/task-runner.ts +16 -3
  89. package/src/runtime/verification/classifier-pre-gate.ts +27 -6
  90. package/src/runtime/verification/verify-gate.ts +153 -30
  91. package/src/state/atomic-write.ts +73 -4
  92. package/src/state/contracts.ts +7 -0
  93. package/src/state/coordination/mailbox.ts +40 -11
  94. package/src/state/event-log/event-log-rotation.ts +28 -6
  95. package/src/state/event-log/poison-marker.ts +16 -1
  96. package/src/state/stores/artifact-store.ts +181 -6
  97. package/src/state/stores/manifest-io.ts +126 -8
  98. package/src/state/stores/state-store.ts +30 -31
  99. package/src/state/types.ts +13 -0
  100. package/src/state/usage.ts +41 -0
  101. package/src/ui/run-dashboard.ts +3 -3
  102. package/src/ui/settings-overlay.ts +19 -1
  103. package/src/utils/internal-error.ts +5 -1
  104. package/src/utils/timer-limits.ts +30 -0
  105. package/src/utils/token-counter.ts +47 -0
  106. package/src/workflows/discover-workflows.ts +9 -1
  107. package/src/worktree/worktree-manager.ts +128 -18
  108. package/workflows/default.workflow.md +1 -1
  109. package/workflows/fast-fix.workflow.md +1 -1
  110. package/workflows/plan-execute.workflow.md +1 -1
  111. package/workflows/review.workflow.md +1 -1
  112. package/workflows/strict-fast-fix.workflow.md +1 -1
@@ -6,6 +6,14 @@ topology: dynamic
6
6
 
7
7
  # Chain Workflow - Sequential execution with context passing
8
8
 
9
+ > **This file is DOCUMENTATION, not a runnable workflow (R28-M1 / wave-6 M3-24).**
10
+ > It lives in `docs/` because the framework discovers every
11
+ > `workflows/*.workflow.md` as a workflow resource; as a doc it parsed to 10
12
+ > phantom steps (its prose headings) and the advertised `chain` workflow could
13
+ > never run. Chain execution is implemented by the chain runner
14
+ > (`src/runtime/chain-runner.ts` + `src/extension/team-tool/chain-dispatch.ts`);
15
+ > there is no `chain` workflow resource.
16
+
9
17
  **Source:** pi-boomerang integration plan
10
18
  **Syntax:** `step1 -> step2 -> step3`
11
19
  **Version:** 1.0.0
@@ -275,6 +275,29 @@ script can reach `process`/`require` directly or via constructor walking. The
275
275
  (e.g. `require('child'+'_process')`, `globalThis.process.mainModule.require`).
276
276
  The real boundary is commit-review + the path-allowlist, not the content check.
277
277
 
278
+ ### Project-sourced workflows are DEFAULT-DENY (F-01)
279
+
280
+ `.dwf.ts` files that resolve to a **project** source (`.crew/workflows/`,
281
+ `<proj>/.pi/teams/workflows/`) are refused at run time unless you explicitly opt
282
+ in with the env var `PI_CREW_TRUST_PROJECT_DWF=1`
283
+ (`src/runtime/goal-workflow/dynamic-workflow-runner.ts:189`). The rejected run
284
+ emits a `dwf.trust_denied` event and throws before the script is loaded:
285
+
286
+ ```text
287
+ Project dynamic workflow requires explicit trust. Set PI_CREW_TRUST_PROJECT_DWF=1
288
+ ```
289
+
290
+ The gate compares against the literal string `"1"` — `true`, `yes`, or `on` all
291
+ FAIL closed. Cloning a repo and running its workflow is otherwise arbitrary code
292
+ execution, hence the opt-in.
293
+
294
+ **Scope of the gate (honest):** only `source === "project"` is gated. **Builtin**
295
+ workflows and **user-scope** workflows (`~/.pi/agent/extensions/pi-crew/workflows/`)
296
+ proceed with **no** trust prompt — they run as ordinary trusted local code. The
297
+ `PI_CREW_TRUST_PROJECT_DWF` gate is therefore not a blanket guarantee that every
298
+ `.dwf.ts` you execute was reviewed: it protects against *repo-supplied* authoring,
299
+ not against user-scope authoring.
300
+
278
301
  ## Determinism (round-13 P0-2)
279
302
 
280
303
  Dynamic workflow scripts must be **deterministic** — the runner rejects
@@ -24,8 +24,8 @@ Before publishing to npm:
24
24
  npm run check
25
25
  ```
26
26
 
27
- For a fast pre-publish smoke (97 broker/UI/config tests in ~20s, instead
28
- of the full ~6,500-test `npm run check` which takes minutes):
27
+ For a fast pre-publish smoke (124 broker/UI/config tests in ~15s, instead
28
+ of the full `npm run check` unit+integration suite, which takes minutes):
29
29
 
30
30
  ```bash
31
31
  npm run test:critical && npm run typecheck && npm run build:bundle
@@ -94,7 +94,7 @@ lockfile drift):
94
94
  - **push to `refs/heads/main`** — `check:conflict-markers` + `typecheck`,
95
95
  then the **FULL unit suite** (`npm run test:unit`, the same gate CI
96
96
  runs). Any failure blocks the push. This closes the 2026-10 gap where
97
- local pushes ran only `test:critical` (~120 tests) while CI ran the full
97
+ local pushes ran only `test:critical` (124 tests) while CI ran the full
98
98
  suite — three waves shipped red that way.
99
99
  - **push to any other ref** — fast gates only (`check:conflict-markers` +
100
100
  `typecheck`).
@@ -128,8 +128,37 @@ The package exports:
128
128
  ./schema.json
129
129
  ```
130
130
 
131
- Use this for editor validation of:
131
+ Use this for editor validation of the **current** config path
132
+ (`src/config/config.ts:208`; the `~/.pi/agent/extensions/pi-crew/config.json`
133
+ path is legacy/read-only-for-migration — see `docs/usage.md`):
132
134
 
133
135
  ```text
134
- ~/.pi/agent/extensions/pi-crew/config.json
136
+ ~/.pi/agent/pi-crew.json
135
137
  ```
138
+
139
+ ## Known tarball / consumer-side notes (R26/R29 census, 2026-10-19)
140
+
141
+ Verified with `npm pack --dry-run`. These are **declared defects**, not hidden:
142
+
143
+ - **`skills/research/scripts/__pycache__/safe_io.cpython-312.pyc` ships** in the
144
+ tarball (11.5 kB): the `files` entry `"skills/**"` plus the missing
145
+ `.npmignore` rule means compiled Python bytecode is published and it embeds
146
+ build-machine paths. Fix = add `__pycache__` to a `.npmignore` / tighten
147
+ `files`.
148
+ - **Dev tooling ships to consumers:** `scripts/analyze-run.mjs` (64.5 kB),
149
+ `scripts/resource-sampler.mjs`, `scripts/watch-agent-transcript.mjs`. Harmless
150
+ bulk; consider moving them behind a dev-only entry.
151
+ - **Consumer-side git hooks reference scripts that are NOT published:**
152
+ `scripts/install-hooks.mjs` (published) sets `core.hooksPath=.githooks`, and
153
+ `.githooks/pre-commit` runs `node scripts/check-lockfile-sync.mjs` — but
154
+ `files[]` ships neither `scripts/check-lockfile-sync.mjs` nor
155
+ `scripts/test-runner.mjs`, so an installed-from-npm consumer's hook fails.
156
+ - **Peer range on `pi-coding-agent` is unbounded** (`>=1.0.4`, no ceiling) while
157
+ the `pi-tui` peer IS bounded (`<2.0.0`); pi-crew consumes pi-tui symbols
158
+ re-exported through `pi-coding-agent`, so a future 2.x installs clean and fails
159
+ at load (R49-F2 — bounding it is prospective work).
160
+ - **Committed `dist/` must match the tag:** the `--committed-hash` gate reads the
161
+ committed blob (`dist/index.mjs` — the ONLY tracked dist artifact) from the git
162
+ object DB, so every commit that touches `src/` needs
163
+ `npm run build:bundle && git add -f dist/index.mjs` rerun and committed
164
+ (`dist/` is `.gitignore`d, hence `-f`).
@@ -18,8 +18,11 @@ run/process status. Start here.
18
18
  **Symptom:** `team action='run'` returns a `blocked` status with a message like
19
19
  "Child worker execution is disabled".
20
20
 
21
- **Cause:** Worker execution is off. pi-crew refuses to create no-op scaffold
22
- subagents by default until you opt in.
21
+ **Cause:** Worker execution is off. When `executeWorkers` is **explicitly**
22
+ `false` (or `PI_CREW_EXECUTE_WORKERS=0`), pi-crew refuses to create no-op
23
+ scaffold subagents and returns `blocked`. Note the default is the **opposite**:
24
+ child-worker execution is enabled by default
25
+ (`src/extension/team-tool/run.ts:205`).
23
26
 
24
27
  **Fix — pick one:**
25
28
  - Set in your config (`~/.pi/agent/pi-crew.json`): `"executeWorkers": true`
@@ -149,6 +152,51 @@ code + a help hint inline. Common ones:
149
152
  | E012 | RunStale | run reconciled as stale | see "Stale async process" above |
150
153
  | E013 | ModelOutOfScope | requested model not in pi's `enabledModels` allowlist | pick a model listed in `enabledModels` (settings.json) or disable `reliability.scopeModels` |
151
154
 
155
+ ## Doctor says OK but the run is dead (doctor blind spots, R48)
156
+
157
+ `team action='doctor'` is a **liveness sanity check, not a watchdog**. The
158
+ deep-review health/status/doctor census (round 48) found these classes are
159
+ **NOT** detected — status/health can print `running` for a dead host:
160
+
161
+ | Class | Why it is missed | What to do |
162
+ |---|---|---|
163
+ | Host process dead, manifest frozen `running` | orphan detection keys on updatedAt/magic-string heuristics (`src/runtime/process-status.ts`) | check the host pid yourself; `team action='cancel'` then re-run |
164
+ | Disk full (`ENOSPC`) | no free-space probe in doctor | `df -h` the state root / TMPDIR |
165
+ | Bad state-file permissions (`EACCES`) | reads may classify as unreadable, not as a doctor failure | `ls -l <crewRoot>/state/runs/*` |
166
+ | Orphaned worker processes (reparented to PID 1) | only reaped when a run is reconciled | `ps -ef \| grep ' pi '`, then `team action='cleanup'` |
167
+ | Broken `node_modules` / missing `pi` resolve | spawn succeeds, worker dies instantly | run `pi --version` in the run cwd |
168
+
169
+ Only two classes are reliably caught: stale broker socket and orphan mux pane.
170
+ Treat a green `doctor` as "no *known* problem", not as "the run is alive".
171
+
172
+ ## Debug logging: `PI_CREW_DEBUG` / `PI_TEAMS_DEBUG` are TRUTHY, not "=1" (R45-F4)
173
+
174
+ Internal debug logging (`logInternalError(..., "debug")`, `src/utils/internal-error.ts:10`)
175
+ is gated on a raw truthiness check, so **any non-empty value enables it** —
176
+ including `PI_CREW_DEBUG=0`, `PI_TEAMS_DEBUG=false`, and `PI_TEAMS_DEBUG=off`.
177
+ To actually silence debug output, **unset** the variable (do not set it to `0`).
178
+
179
+ ## Mux surfaces (tmux / herdr) — known sharp edges (R42)
180
+
181
+ `runtime.surface.mode: 'tmux' | 'herdr'` (default `auto`) runs tier-1 workers in
182
+ real mux panes. Live-probed known limits:
183
+
184
+ - **Panic characters in commands:** tmux `send-keys -l` swallows a trailing `\n`,
185
+ while herdr `pane.send_text` treats `\n` as Enter — a boot line containing a
186
+ newline would be split/executed early. pi-crew's own callers pass
187
+ shell-escaped single-line paths, so this is latent, not live.
188
+ - **Pane/tab ids are not regex-validated before `-t`:** a malformed id in a
189
+ manifest (`kill-pane -t "-x"`) can act on the *wrong* pane (today every path is
190
+ liveness-gated, so it is defence-in-depth, not an open hole).
191
+ - **`capture-pane -p` trims trailing spaces** (cosmetic; only feeds doctor
192
+ liveness).
193
+ - **No tmux minimum-version gate:** old tmux silently falls back to a two-strike
194
+ headless degrade.
195
+ - **herdr is an external socket daemon** — pi-crew never spawns it, and the
196
+ server-wide subscription (no pane filter) amplifies traffic.
197
+ - Pane ids are unique only within one mux server lifetime; a stale manifest can
198
+ name an id now owned by an unrelated pane.
199
+
152
200
  ## Still stuck
153
201
 
154
202
  - `team action='explain' runId=…` — structured per-task analysis (why, files,
package/docs/usage.md CHANGED
@@ -42,7 +42,7 @@ Supported fields:
42
42
  "requirePlanApproval": false
43
43
  },
44
44
  "ui": {
45
- "widgetPlacement": "aboveEditor",
45
+ "widgetPlacement": "bottom",
46
46
  "widgetMaxLines": 8,
47
47
  "powerbar": true,
48
48
  "dashboardPlacement": "right",
@@ -239,6 +239,12 @@ Dirty worktrees are preserved unless `force: true` is provided.
239
239
 
240
240
  ## Slash commands
241
241
 
242
+ > **`/team-cleanup <runId>` does not exist** (R27-D1). The registered commands
243
+ > are `/team-manager` (and its alias `/team-cleanup-menu`) — runId-targeted
244
+ > cleanup is the `cleanup` action on the `team` tool (`team action='cleanup'
245
+ > runId=…`). Only `team-cleanup-menu` is registered
246
+ > (`src/extension/registration/commands/manage.ts:99-107`).
247
+
242
248
  ```text
243
249
  /teams
244
250
  /team-run default "Implement login with tests"
@@ -251,7 +257,6 @@ Dirty worktrees are preserved unless `force: true` is provided.
251
257
  /team-events team_...
252
258
  /team-artifacts team_...
253
259
  /team-worktrees team_...
254
- /team-cleanup team_...
255
260
  /team-forget team_... --confirm
256
261
  /team-export team_...
257
262
  /team-import .crew/artifacts/team_.../export/run-export.json # or .pi/teams/artifacts/... on legacy layout
package/install.mjs CHANGED
@@ -13,7 +13,8 @@ const legacyConfigPath = path.join(agentDir, "extensions", "pi-crew", "config.js
13
13
  * Default config written to `~/.pi/agent/pi-crew.json` on fresh installs.
14
14
  *
15
15
  * WI-1b.2 (G17): this object is intentionally an embedded literal — install.mjs
16
- * runs under bare `node` as the `pi-crew` bin (engines floor is >=22.0.0; .ts
16
+ * runs under bare `node` as the `pi-crew` bin (engines floor is >=22.19.0 — the
17
+ * pre-R29-F3 comment said >=22.0.0; .ts
17
18
  * imports would require >=22.18 or `--experimental-strip-types`, which the npm
18
19
  * bin shim cannot pass). Runtime jiti/esbuild loading was rejected as a
19
20
  * postinstall failure surface; build-time codegen would need package.json /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.11.24",
3
+ "version": "0.11.26",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -57,6 +57,7 @@
57
57
  "docs/troubleshooting.md",
58
58
  "docs/usage.md",
59
59
  "docs/architecture.md",
60
+ "docs/chain-workflow.md",
60
61
  "docs/dynamic-workflows.md",
61
62
  "docs/resource-formats.md",
62
63
  "docs/publishing.md",
@@ -116,6 +117,7 @@
116
117
  "smoke:pi": "pi install .",
117
118
  "smoke:release": "node scripts/release-smoke.mjs",
118
119
  "prepack": "node scripts/clean-strip-types.mjs",
120
+ "prepublishOnly": "npm run ci",
119
121
  "test:fast": "npm run test:critical && npm run test:bundle",
120
122
  "check:wc-gate": "node scripts/wc-gate.mjs"
121
123
  },
@@ -136,7 +138,7 @@
136
138
  "peerDependencies": {
137
139
  "@earendil-works/pi-agent-core": ">=1.0.4",
138
140
  "@earendil-works/pi-ai": ">=1.0.4",
139
- "@earendil-works/pi-coding-agent": ">=1.0.4",
141
+ "@earendil-works/pi-coding-agent": ">=1.0.4 <2.0.0",
140
142
  "@earendil-works/pi-tui": ">=1.0.4 <2.0.0",
141
143
  "@sinclair/typebox": "*"
142
144
  },
@@ -17,11 +17,21 @@ triggers:
17
17
 
18
18
  Use this skill for explorer, analyst, reviewer, and source-audit roles. These roles must inspect code without modifying it.
19
19
 
20
+ > **Tool availability (enforced — R27-D6).** `explorer` (and `librarian`) ship
21
+ > **without a `bash` tool**: `src/config/role-tools.ts:25-29` sets
22
+ > `tools: ["read", "grep", "find", "ls", "glob", "ask"]` +
23
+ > `excludeTools: ["edit", "write", "bash", "web"]`. The shell syntax below
24
+ > (`rg …`, `find …`, `git status`) is therefore **reference syntax for the
25
+ > intent**, not runnable in an explorer session. Map each example to the tool you
26
+ > actually have: `rg` → `grep`, `find`/shell globbing → `find`/`glob`,
27
+ > `ls` → `ls`, `cat`/`sed -n` → `read` (with offset/limit). `analyst`/`reviewer`
28
+ > roles keep `bash` (`role-tools.ts:33-35`, `:64-68`) — the restriction is per-role.
29
+
20
30
  ## Core Contract
21
31
 
22
32
  1. **Do not edit files** — no write, no edit, no delete
23
33
  2. **Do not write generated artifacts** outside the run artifact directory
24
- 3. **Prefer read-only commands**: `read`, `rg`, `find`, `ls`, `git status`
34
+ 3. **Prefer read-only tools**: `read`, `grep`, `find`, `ls`, `glob` (explorer has no shell — see the note above)
25
35
  4. **Record exact files inspected** — include path and relevant line numbers
26
36
  5. **Distinguish direct evidence from inference** — don't guess
27
37
  6. **If implementation is needed, recommend** — don't modify code
@@ -242,10 +252,10 @@ If ANY answer is NO → Stop. Adhere to read-only contract.
242
252
 
243
253
  ## Source patterns
244
254
 
245
- - `src/runtime/task-runner.ts` — task execution pipeline
246
- - `src/runtime/child-pi.ts` — worker spawning
247
- - `src/runtime/live-agent-manager.ts` — live agent lifecycle
248
- - `src/state/event-log.ts` — event logging system
255
+ - `src/runtime/task-runner.ts` + `src/runtime/task-runner/` — task execution pipeline
256
+ - `src/runtime/child-pi/` — worker spawning
257
+ - `src/runtime/live-session/live-agent-manager.ts` — live agent lifecycle
258
+ - `src/state/event-log/` — event logging system
249
259
  - `src/extension/team-tool/` — API and tool handling
250
260
  - `src/ui/` — widget and TUI rendering
251
261
 
@@ -275,7 +275,7 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
275
275
 
276
276
  ---
277
277
 
278
- ## Tier 1 — Critical unit tests (~19s, 120 tests, the only suite you need for broker/UI changes)
278
+ ## Tier 1 — Critical unit tests (~13s, 124 tests, the only suite you need for broker/UI changes)
279
279
 
280
280
  **What**: run the curated 15-file fast subset.
281
281
 
@@ -287,7 +287,7 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
287
287
  time npm run test:critical
288
288
  ```
289
289
 
290
- Expected output: `# tests 120 # pass 120 # fail 0 # cancelled 0 # duration_ms ~19965`. (Count was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 since 2026-09-20, **120 @ 2026-10-07** (+`child-executor-heartbeat-pulse.test.ts` → 15 files) — verify with the actual run; the skill's hard-coded numbers drift between releases.) **Since RR-021 round-2 the runner itself fails loudly on `# cancelled N>0`** (dual-reporter TAP scrape — the round-1 unref incident shipped green with 2 cancelled tests), and **round-3 added the missing-file pre-check**: an explicitly-listed file that does not exist now exits 1 naming the path instead of being silently skipped by node:test.
290
+ Expected output: `# tests 124 # pass 124 # fail 0 # cancelled 0 # duration_ms ~12800`. (Count was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 since 2026-09-20, 120 @ 2026-10-07, **124 @ 2026-10-18** (+`child-executor-heartbeat-pulse.test.ts` → 15 files) — verify with the actual run; the skill's hard-coded numbers drift between releases.) **Since RR-021 round-2 the runner itself fails loudly on `# cancelled N>0`** (dual-reporter TAP scrape — the round-1 unref incident shipped green with 2 cancelled tests), and **round-3 added the missing-file pre-check**: an explicitly-listed file that does not exist now exits 1 naming the path instead of being silently skipped by node:test.
291
291
 
292
292
  **References**:
293
293
 
@@ -327,7 +327,7 @@ PI_CREW_BROKER=0 npm run test:critical
327
327
  PI_CREW_BROKER=1 npm run test:critical
328
328
  ```
329
329
 
330
- All three must show `# pass 120 # fail 0`. Measured times in this session (2026-08-11): ~26s for default, ~26s for `PI_CREW_BROKER=0`, ~26s for `PI_CREW_BROKER=1` (varies ±1-2s run-to-run).
330
+ All three must show `# pass 124 # fail 0`. Measured times in this session (2026-08-11): ~26s for default, ~26s for `PI_CREW_BROKER=0`, ~26s for `PI_CREW_BROKER=1` (varies ±1-2s run-to-run).
331
331
 
332
332
  **References**:
333
333
 
@@ -609,7 +609,7 @@ If the two md5s match → session is on the latest code. If not → user must `/
609
609
 
610
610
  **What**: drive the team tool + subagent tools through a spread of actions from the parent Pi session to prove the full surface works end-to-end, not just one smoke run.
611
611
 
612
- **Why this exists**: Tier 7 proves one team run completes. But pi-crew has **55 `team` actions across 5 domain dispatchers** (`RUN` 10: run/parallel/plan/plans/orchestrate/resume/retry/wait/steer/goal · `STATUS` 16: status/list/get/events/artifacts/summary/graph/search/health/worktrees/checkpoint/cache/explain/onboard/recommend/help · `CONTROL` 7 · `MANAGE` 16 · `AUTOMATE` 6 — `src/schema/team-tool-schema.ts:391-437`) plus the subagent tools (`Agent`, `crew_agent`, `get_subagent_result`, `steer_subagent` — with `crew_agent_result`/`crew_agent_steer` aliases), the worker-side tools (`ask`, `delegate`, `message`), and the `team-settings` config surface, dispatched through several code paths (sync run, async run, chain, parallel, direct subagent). A schema or registration regression can break *some* paths while others still pass. The battery catches path-specific breakage.
612
+ **Why this exists**: Tier 7 proves one team run completes. But pi-crew has **57 `team` actions across 5 domain dispatchers** (`RUN` 10: run/parallel/plan/plans/orchestrate/resume/retry/wait/steer/goal · `STATUS` 17: status/list/get/events/artifacts/breakdown/summary/graph/search/health/worktrees/checkpoint/cache/explain/onboard/recommend/help · `CONTROL` 7 · `MANAGE` 17 · `AUTOMATE` 6 — `src/schema/team-tool-schema.ts:437-489`) plus the subagent tools (`Agent`, `crew_agent`, `get_subagent_result`, `steer_subagent` — with `crew_agent_result`/`crew_agent_steer` aliases), the worker-side tools (`ask`, `delegate`, `message`), and the `team-settings` config surface, dispatched through several code paths (sync run, async run, chain, parallel, direct subagent). A schema or registration regression can break *some* paths while others still pass. The battery catches path-specific breakage.
613
613
 
614
614
  **When required**: any change to `src/schema/team-tool-schema.ts`, `src/extension/registration/team-tool.ts`, `src/extension/team-tool/*.ts` (handler dispatch), or the subagent-tool registration. Optional but cheap for any change — the read-only actions are free.
615
615
 
@@ -1383,7 +1383,7 @@ Use this to answer "đủ full tính năng chưa?" without re-deriving. Every us
1383
1383
 
1384
1384
  | Feature | Code entry | Verify via |
1385
1385
  |---|---|---|
1386
- | Team tool — 56 actions / 5 domains | `src/schema/team-tool-schema.ts:391-437`, dispatch in `src/extension/team-tool/` | 9a (read-only) + 9b/9c/9d/9e/9f theo domain |
1386
+ | Team tool — 57 actions / 5 domains | `src/schema/team-tool-schema.ts:437-489`, dispatch in `src/extension/team-tool/` | 9a (read-only) + 9b/9c/9d/9e/9f theo domain |
1387
1387
  | Runtime mode `child-process` (default) | `src/runtime/child-pi/` | 9b sync run + T7 |
1388
1388
  | Runtime mode `scaffold` (dry-run) | `src/runtime/task-runner/pre-execution.ts:176` | 9b `action='plan'`/`'plans'` (preview không spawn) hoặc run với `runtime.mode='scaffold'` |
1389
1389
  | Runtime mode `live-session` (experimental) | `src/runtime/live-session/` | Run với `runtime.mode='live-session'` + irc tool xuất hiện trong worker (`src/runtime/custom-tools/irc-tool.ts`) |
@@ -1464,7 +1464,7 @@ The skill does NOT need to be updated for every commit — only when the cited l
1464
1464
  ## Quick reference — exact commands
1465
1465
 
1466
1466
  ```bash
1467
- # Tier 1 (critical unit, ~19s, 120 tests)
1467
+ # Tier 1 (critical unit, ~13s, 124 tests)
1468
1468
  npm run test:critical
1469
1469
  # Tier 2 (3-path proof, broker changes only)
1470
1470
  PI_CREW_BROKER=0 npm run test:critical
@@ -1548,7 +1548,7 @@ PY
1548
1548
 
1549
1549
  Before claiming "tested":
1550
1550
 
1551
- - [ ] Tier 1: `test:critical` fresh-run, all pass (<25s). Count varies by release — was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 @ 2026-09-20, **120 @ 2026-10-07**; record the actual count in the report.
1551
+ - [ ] Tier 1: `test:critical` fresh-run, all pass (<25s). Count varies by release — was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 @ 2026-09-20, 120 @ 2026-10-07, **124 @ 2026-10-18**; record the actual count in the report.
1552
1552
  - [ ] Tier 2: 3-path proof all pass — **required if you touched `src/config/defaults.ts` or `src/extension/registration/lifecycle-handlers.ts`**
1553
1553
  - [ ] Tier 3: `npm run typecheck` exit 0, `npm run build:bundle` exit 0
1554
1554
  - [ ] Tier 4: bundle md5 matches what the session loaded (or user has `/quit`-ed + reopened)
@@ -110,6 +110,64 @@ export const DANGEROUS_OBJECT_KEYS = new Set([
110
110
  "__lookupSetter__",
111
111
  ]);
112
112
 
113
+ /**
114
+ * Case-normalized view of DANGEROUS_OBJECT_KEYS. The canonical set keeps the
115
+ * real member spellings (toString, valueOf, …), but key matching must be
116
+ * lowercase on BOTH sides — `"valueOf".toLowerCase()` is `"valueof"`, which
117
+ * would never match a camelCase entry.
118
+ */
119
+ const DANGEROUS_KEYS_LOWER = new Set([...DANGEROUS_OBJECT_KEYS].map((key) => key.toLowerCase()));
120
+
121
+ /**
122
+ * True when `key` collides with a reserved Object.prototype member.
123
+ * Case-insensitive to catch `__Proto__`, `CONSTRUCTOR`, `TOSTRING`, etc.
124
+ */
125
+ export function isDangerousKey(key: string): boolean {
126
+ return DANGEROUS_KEYS_LOWER.has(key.toLowerCase());
127
+ }
128
+
129
+ /**
130
+ * True when ANY dot-separated segment of a dotted config path collides with a
131
+ * reserved Object.prototype member. This is the guard for dotted-path walkers
132
+ * (setNested/unsetPath): it catches the NESTED pollution vectors
133
+ * `__proto__.polluted` and `constructor.prototype.x` that single-key checks
134
+ * miss, because the walk hits `obj["__proto__"]` (the [[Prototype]] getter)
135
+ * long before the final segment is written.
136
+ */
137
+ export function isDangerousPath(dottedPath: string): boolean {
138
+ return dottedPath.split(".").some((segment) => segment.length > 0 && isDangerousKey(segment));
139
+ }
140
+
141
+ /**
142
+ * Deeply strip dangerous prototype-pollution keys from an arbitrary value
143
+ * (objects, arrays, primitives pass through). Returns a fresh plain-object
144
+ * tree that is safe to merge/assign at a parse boundary.
145
+ *
146
+ * M1-1 shared boundary contract: every boundary that accepts a user-supplied
147
+ * config patch, dotted key, or translation registration (team-settings
148
+ * set/unset, updateConfig unsetPaths, config/autonomy patch parsing, i18n
149
+ * addTranslations) routes through isDangerousPath/isDangerousKey/
150
+ * stripDangerousKeysDeep here — ONE canonical drop rule. This is
151
+ * BOUNDARY-LEVEL defense only; it must not grow handler-specific predicates:
152
+ * carrier keys of tool payloads (`args`, `scope`, `unset`, …) are
153
+ * intentionally NOT dangerous keys — never add them (RT24-F1 lesson: mirrored
154
+ * handler predicates drifted and over-blocked reads).
155
+ *
156
+ * Case-insensitive superset of the retired per-module strippers (which covered
157
+ * only `__proto__`/`constructor`/`prototype` case-sensitively): this drops the
158
+ * full DANGEROUS_OBJECT_KEYS set at every level.
159
+ */
160
+ export function stripDangerousKeysDeep<T>(value: T): T {
161
+ if (value === null || typeof value !== "object") return value;
162
+ if (Array.isArray(value)) return value.map((item) => stripDangerousKeysDeep(item)) as T;
163
+ const safe: Record<string, unknown> = {};
164
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
165
+ if (isDangerousKey(key)) continue;
166
+ safe[key] = stripDangerousKeysDeep(child);
167
+ }
168
+ return safe as T;
169
+ }
170
+
113
171
  /**
114
172
  * Strips dangerous Object.prototype keys from an object.
115
173
  * Returns a new object built with Object.create(null) to prevent
@@ -6,7 +6,7 @@ import { withFileLockSync } from "../state/coordination/locks.ts";
6
6
  import { logInternalError } from "../utils/internal-error.ts";
7
7
  import { isJunkPathLiteral, projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
8
8
  import { mergeConfig } from "./config-merge.ts";
9
- import { parseConfig, parseConfigWithWarnings } from "./config-validation.ts";
9
+ import { isDangerousKey, isDangerousPath, parseConfig, parseConfigWithWarnings } from "./config-validation.ts";
10
10
  import { DEFAULT_BROKER, DEFAULT_NESTING, DEFAULT_PERSISTENCE, resolveBrokerEnvOverride } from "./defaults.ts";
11
11
  import { getCrewEnv, getCrewEnvBool } from "./env-vars.ts";
12
12
  import { sanitizeProjectConfig } from "./sanitize-project-config.ts";
@@ -258,6 +258,13 @@ function resolvePersistenceEnvOverrideAndDefaults(parsed: PersistenceConfig | un
258
258
  }
259
259
 
260
260
  function unsetPath(record: Record<string, unknown>, dottedPath: string): void {
261
+ // R24-H2 (M1-1): walking a raw dotted key through `target["__proto__"]`
262
+ // resolves to Object.prototype via the [[Prototype]] getter, so a key like
263
+ // `__proto__.toString` DELETES Object.prototype members. Refuse dangerous
264
+ // segments at the walker itself (shared proto-safe helpers in
265
+ // config-validation.ts) — this covers
266
+ // every unsetPaths consumer (team-settings unset, team `config` action).
267
+ if (isDangerousPath(dottedPath)) return;
261
268
  const parts = dottedPath.split(".").filter(Boolean);
262
269
  if (parts.length === 0) return;
263
270
  let target: Record<string, unknown> = record;
@@ -305,6 +312,66 @@ function measureJsonDepth(root: unknown): number {
305
312
  return maxDepth;
306
313
  }
307
314
 
315
+ /**
316
+ * M3-13 (R45-F1): `parseConfig` is a strict WHITELIST builder (see
317
+ * config-validation.ts) — it returns an object literal of known keys only, so
318
+ * merging a patch into `parseConfig(current)` and writing the result back
319
+ * silently DELETES every unknown on-disk key (typos, forward-compat keys
320
+ * written by a newer version, keys owned by another tool). updateConfig used
321
+ * to do exactly that on every write.
322
+ *
323
+ * This helper re-attaches the raw on-disk shape underneath the normalized
324
+ * merge result: `parsed` wins for every key/value it defines (so mergeConfig's
325
+ * per-key semantics and its sanitizers keep applying to known keys), while raw
326
+ * keys that the whitelist dropped are carried through untouched.
327
+ *
328
+ * Dangerous prototype keys are NEVER carried through (M1-1/R24-H1: the parse
329
+ * boundary drops `__proto__`/`constructor`/`prototype` at every depth).
330
+ *
331
+ * @param parsed normalized merge result (what the whitelist understands)
332
+ * @param raw raw on-disk record, already with `unsetPaths` applied
333
+ * @returns a new record shaped like the on-disk file, preserving key order
334
+ */
335
+ function preserveUnknownKeys(parsed: Record<string, unknown>, raw: Record<string, unknown>): Record<string, unknown> {
336
+ const target = cloneRawRecord(raw, 0);
337
+ return overlayKnownKeys(target, parsed, 0);
338
+ }
339
+
340
+ const MAX_PRESERVED_DEPTH = 100;
341
+
342
+ function isPlainRecordObject(value: unknown): value is Record<string, unknown> {
343
+ return value !== null && typeof value === "object" && !Array.isArray(value);
344
+ }
345
+
346
+ /** Depth-bounded clone that keeps raw key order and skips dangerous keys. */
347
+ function cloneRawRecord(source: Record<string, unknown>, depth: number): Record<string, unknown> {
348
+ const out: Record<string, unknown> = {};
349
+ if (depth > MAX_PRESERVED_DEPTH) return out;
350
+ for (const [key, value] of Object.entries(source)) {
351
+ if (isDangerousKey(key)) continue;
352
+ out[key] = isPlainRecordObject(value) ? cloneRawRecord(value, depth + 1) : value;
353
+ }
354
+ return out;
355
+ }
356
+
357
+ /** Overlay `source` (normalized) onto `target` (raw clone), recursing into shared objects. */
358
+ function overlayKnownKeys(target: Record<string, unknown>, source: Record<string, unknown>, depth: number): Record<string, unknown> {
359
+ if (depth > MAX_PRESERVED_DEPTH) return target;
360
+ for (const [key, value] of Object.entries(source)) {
361
+ // `undefined` means "not set in this layer" (mergeConfig's withoutUndefined
362
+ // contract) — never write it over a preserved raw value.
363
+ if (value === undefined) continue;
364
+ if (isDangerousKey(key)) continue;
365
+ const existing = target[key];
366
+ if (isPlainRecordObject(value) && isPlainRecordObject(existing)) {
367
+ overlayKnownKeys(existing, value, depth + 1);
368
+ continue;
369
+ }
370
+ target[key] = value;
371
+ }
372
+ return target;
373
+ }
374
+
308
375
  function readConfigRecord(filePath: string): Record<string, unknown> {
309
376
  if (!fs.existsSync(filePath)) return {};
310
377
  // Defense-in-depth: reject config files larger than 10 MB before parsing.
@@ -462,7 +529,11 @@ export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions
462
529
  const message = error instanceof Error ? error.message : String(error);
463
530
  throw new Error(`Could not update pi-crew config: ${message}`);
464
531
  }
465
- let merged = mergeConfig(parseConfig(current), patch);
532
+ const normalizedCurrent = parseConfig(current);
533
+ // Known-key semantics (per-key merge + otlp header sanitization) stay in
534
+ // mergeConfig — see M3-13 below for why the WRITE target is rebuilt from
535
+ // the raw record instead of from this whitelist-normalized result.
536
+ let merged = mergeConfig(normalizedCurrent, patch);
466
537
  if (options.unsetPaths?.length) {
467
538
  const raw = JSON.parse(JSON.stringify(merged)) as Record<string, unknown>;
468
539
  for (const unset of options.unsetPaths) unsetPath(raw, unset);
@@ -472,12 +543,26 @@ export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions
472
543
  // (e.g. `team action='config'` with an empty patch — read-only path).
473
544
  // Both sides are parseConfig-normalized, so JSON.stringify key order is
474
545
  // deterministic (same construction path); no key sorting needed.
475
- const normalizedCurrent = parseConfig(current);
476
- if (JSON.stringify(merged) === JSON.stringify(normalizedCurrent)) {
546
+ const normalizedUnchanged = JSON.stringify(merged) === JSON.stringify(normalizedCurrent);
547
+ // M3-13 (R45-F1): parseConfig is a whitelist, so writing `merged` erased
548
+ // every unknown on-disk key (typos, forward-compat keys). Rebuild the
549
+ // written shape from the RAW record: known keys take the merged value,
550
+ // unknown keys survive untouched. `unsetPaths` are applied to the raw
551
+ // base too, so an explicit unset of an unknown key still removes it.
552
+ const rawBase = cloneRawRecord(current, 0);
553
+ for (const unset of options.unsetPaths ?? []) unsetPath(rawBase, unset);
554
+ const toWrite = preserveUnknownKeys(merged as unknown as Record<string, unknown>, rawBase);
555
+ // An unsetPaths entry may name a key the whitelist cannot see (an unknown
556
+ // key), in which case the normalized comparison above reports "unchanged"
557
+ // even though the raw record must change. Only that case overrides the
558
+ // skip: if applying the unsets left the raw base byte-identical, nothing
559
+ // outside the whitelist moved and the legacy skip stands.
560
+ const unsetsChangedRaw = JSON.stringify(rawBase) !== JSON.stringify(current);
561
+ if (normalizedUnchanged && !unsetsChangedRaw) {
477
562
  return { path: filePath, config: merged, written: false }; // unchanged — skip write
478
563
  }
479
564
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
480
- atomicWriteFile(filePath, `${JSON.stringify(merged, null, 2)}\n`);
565
+ atomicWriteFile(filePath, `${JSON.stringify(toWrite, null, 2)}\n`);
481
566
  // (F16) Invalidate the loadConfig cache after a write — the next
482
567
  // caller must see the new value, not a 0-2s stale snapshot.
483
568
  invalidateConfigCache();
@@ -277,7 +277,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
277
277
  },
278
278
  PI_CREW_DEBUG: {
279
279
  name: "PI_CREW_DEBUG",
280
- doc: "truthiness enables background debug logging (background-runner.ts:63)",
280
+ doc: "truthiness enables background debug logging (background-runner.ts:63); RAW truthiness — '0'/'false' still ENABLE, unset to disable (R45-F4)",
281
281
  },
282
282
  PI_CREW_DEBUG_BUDGET: {
283
283
  name: "PI_CREW_DEBUG_BUDGET",
@@ -371,7 +371,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
371
371
  },
372
372
  PI_CREW_TRUST_PROJECT_DWF: {
373
373
  name: "PI_CREW_TRUST_PROJECT_DWF",
374
- doc: "'1' allows project-sourced .dwf.ts workflows (dynamic-workflow-runner.ts:154)",
374
+ doc: "'1' allows project-sourced .dwf.ts workflows (dynamic-workflow-runner.ts:189)",
375
375
  },
376
376
  PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS: {
377
377
  name: "PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS",
@@ -380,7 +380,7 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
380
380
  },
381
381
  PI_TEAMS_DEBUG: {
382
382
  name: "PI_TEAMS_DEBUG",
383
- doc: "truthiness gates debug-severity internal-error logging (internal-error.ts:4)",
383
+ doc: "truthiness gates debug-severity internal-error logging (internal-error.ts:10); RAW truthiness — '0'/'false' still ENABLE, unset to disable (R45-F4)",
384
384
  },
385
385
  PI_TEAMS_MOCK_CHILD_PI: {
386
386
  name: "PI_TEAMS_MOCK_CHILD_PI",
@@ -766,18 +766,25 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
766
766
  * Raw crew-env lookup. Returns the raw string (undefined when unset) with
767
767
  * mirror-alias fallback applied per the entry's `mirrorPrecedence`. This is
768
768
  * the behavior-preserving replacement for `process.env.PI_CREW_X` reads.
769
+ *
770
+ * R26-F1 (deep-review round 26): `env` is an OPTIONAL source override. Reads
771
+ * that receive an env object as a parameter (`isRootSession(env)`,
772
+ * `currentCrewDepth(env)`, …) must still route through the registry — passing
773
+ * the object here keeps the registry invariant intact for those sites instead
774
+ * of leaving them as gate-blind `env.PI_CREW_X` dot reads. Defaults to
775
+ * `process.env`, so existing call sites are unchanged.
769
776
  */
770
- export function getCrewEnv(name: string): string | undefined {
777
+ export function getCrewEnv(name: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
771
778
  const spec = CREW_ENV_VARS[name];
772
779
  if (!spec?.mirror || spec.mirrorPrecedence === "or") {
773
- return process.env[name];
780
+ return env[name];
774
781
  }
775
782
  const crewName = name.startsWith("PI_TEAMS_") ? spec.mirror : name;
776
783
  const teamsName = name.startsWith("PI_TEAMS_") ? name : spec.mirror;
777
784
  if (spec.mirrorPrecedence === "teams") {
778
- return process.env[teamsName] ?? process.env[crewName];
785
+ return env[teamsName] ?? env[crewName];
779
786
  }
780
- return process.env[crewName] ?? process.env[teamsName];
787
+ return env[crewName] ?? env[teamsName];
781
788
  }
782
789
 
783
790
  /**
@@ -27,9 +27,11 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
27
27
  excludeTools: ["edit", "write", "bash", "web"],
28
28
  },
29
29
 
30
- // Analyst - Read and analyze, limited execution
30
+ // Analyst - Read and analyze, limited execution. (ask_question removed
31
+ // 2026-10-18: the host tool is `ask` — excluding a nonexistent name was a
32
+ // dead entry; the live `ask` tool stays allowed for analyst.)
31
33
  analyst: {
32
- excludeTools: ["edit", "write", "ask_question"],
34
+ excludeTools: ["edit", "write"],
33
35
  },
34
36
 
35
37
  // Planner - Read-only planning; emits plans as TEXT (runner persists result).
@@ -41,7 +43,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
41
43
  // default/implementation workflows.
42
44
  planner: {
43
45
  tools: ["read", "grep", "find", "ls", "glob", "ask"],
44
- excludeTools: ["edit", "write", "bash", "web", "ask_question"],
46
+ excludeTools: ["edit", "write", "bash", "web"],
45
47
  },
46
48
 
47
49
  // Critic - Read-only plan/design critique (F2: was missing from the map,
@@ -67,7 +69,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
67
69
  // Writer - Documentation focused
68
70
  writer: {
69
71
  tools: ["read", "edit", "write", "ls", "ask"],
70
- excludeTools: ["bash", "web", "ask_question"],
72
+ excludeTools: ["bash", "web"],
71
73
  },
72
74
 
73
75
  // Security Reviewer - Strict restrictions
@@ -76,7 +78,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
76
78
  // resolved at runtime (returned {}), silently dropping enforcement.
77
79
  "security-reviewer": {
78
80
  tools: ["read", "grep", "find", "ask"],
79
- excludeTools: ["edit", "write", "bash", "web", "ask_question"],
81
+ excludeTools: ["edit", "write", "bash", "web"],
80
82
  },
81
83
 
82
84
  // Verifier - Runs tests (needs bash) but must NOT edit source (F4: moved