pi-crew 0.9.68 → 0.10.2

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 (253) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/analyst.md +1 -1
  5. package/agents/cold-verifier.md +3 -1
  6. package/agents/critic.md +1 -1
  7. package/agents/executor.md +1 -1
  8. package/agents/explorer.md +1 -1
  9. package/agents/planner.md +1 -1
  10. package/agents/reviewer.md +1 -1
  11. package/agents/security-reviewer.md +1 -1
  12. package/agents/test-engineer.md +1 -1
  13. package/agents/verifier.md +1 -1
  14. package/agents/writer.md +1 -1
  15. package/dist/index.mjs +68113 -60774
  16. package/docs/README.md +2 -0
  17. package/docs/actions-reference.md +31 -0
  18. package/docs/commands-reference.md +17 -6
  19. package/docs/resource-formats.md +13 -0
  20. package/package.json +4 -2
  21. package/schema.json +503 -91
  22. package/scripts/resource-sampler.mjs +36 -2
  23. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  24. package/skills/widget-rendering/SKILL.md +7 -7
  25. package/src/agents/agent-config.ts +2 -1
  26. package/src/agents/discover-agents.ts +23 -14
  27. package/src/config/config-merge.ts +183 -0
  28. package/src/config/config-validation.ts +687 -0
  29. package/src/config/config.ts +22 -864
  30. package/src/config/defaults.ts +43 -2
  31. package/src/config/drift-detector.ts +1 -1
  32. package/src/config/env-vars.ts +691 -0
  33. package/src/config/role-tools.ts +11 -9
  34. package/src/config/sanitize-project-config.ts +172 -0
  35. package/src/config/types.ts +49 -1
  36. package/src/extension/async-notifier.ts +25 -2
  37. package/src/extension/crew-cleanup.ts +13 -0
  38. package/src/extension/crew-vibes/config.ts +2 -1
  39. package/src/extension/crew-vibes/footer.ts +19 -0
  40. package/src/extension/crew-vibes/index.ts +11 -1
  41. package/src/extension/plan-orchestrate.ts +132 -0
  42. package/src/extension/register.ts +8 -0
  43. package/src/extension/registration/command-registration.ts +1 -0
  44. package/src/extension/registration/commands/dashboard.ts +158 -0
  45. package/src/extension/registration/commands/index.ts +35 -0
  46. package/src/extension/registration/commands/manage.ts +303 -0
  47. package/src/extension/registration/commands/run.ts +228 -0
  48. package/src/extension/registration/commands/shared.ts +639 -0
  49. package/src/extension/registration/commands/status.ts +60 -0
  50. package/src/extension/registration/commands.ts +13 -1224
  51. package/src/extension/registration/foreground-run-controller.ts +10 -2
  52. package/src/extension/registration/lifecycle-handlers.ts +178 -17
  53. package/src/extension/registration/runtime-cleanup.ts +23 -5
  54. package/src/extension/registration/subagent-tools.ts +218 -9
  55. package/src/extension/registration/team-tool.ts +5 -1
  56. package/src/extension/registration/ui.ts +5 -4
  57. package/src/extension/rpc-hmac.ts +5 -3
  58. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  59. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  60. package/src/extension/team-tool/api/task-claims.ts +109 -40
  61. package/src/extension/team-tool/cancel.ts +84 -50
  62. package/src/extension/team-tool/dispatch/index.ts +1 -0
  63. package/src/extension/team-tool/dispatch/run.ts +4 -1
  64. package/src/extension/team-tool/doctor.ts +103 -1
  65. package/src/extension/team-tool/orchestrate.ts +66 -1
  66. package/src/extension/team-tool/plans.ts +192 -0
  67. package/src/extension/team-tool/respond.ts +197 -65
  68. package/src/extension/team-tool/run-deadline.ts +35 -3
  69. package/src/extension/team-tool/run-intent.ts +63 -0
  70. package/src/extension/team-tool/run.ts +74 -20
  71. package/src/extension/team-tool/status.ts +84 -26
  72. package/src/extension/team-tool.ts +11 -2
  73. package/src/hooks/registry.ts +1 -6
  74. package/src/i18n.ts +9 -0
  75. package/src/prompt/prompt-runtime.ts +521 -2
  76. package/src/prompt/worker-events-channel.ts +173 -0
  77. package/src/runtime/README.md +8 -8
  78. package/src/runtime/async-runner.ts +7 -3
  79. package/src/runtime/background-runner.ts +42 -14
  80. package/src/runtime/broker/broker-issuer.ts +9 -2
  81. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  82. package/src/runtime/broker/crew-broker.ts +838 -10
  83. package/src/runtime/broker/wait-status-cache.ts +157 -0
  84. package/src/runtime/budget-enforcement.ts +281 -0
  85. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  86. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  87. package/src/runtime/child-pi/child-pi-streams.ts +21 -1
  88. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  89. package/src/runtime/child-pi/child-pi.ts +97 -201
  90. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  91. package/src/runtime/crew-agent-records.ts +259 -14
  92. package/src/runtime/delegate-spawn.ts +148 -0
  93. package/src/runtime/detached-run-results.ts +90 -0
  94. package/src/runtime/deterministic-ast.ts +2 -1
  95. package/src/runtime/dispatch-batch.ts +945 -0
  96. package/src/runtime/finalize-run.ts +557 -0
  97. package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
  98. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
  99. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  100. package/src/runtime/group-join.ts +11 -125
  101. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  102. package/src/runtime/merge-gate.ts +32 -10
  103. package/src/runtime/merge-loop.ts +130 -0
  104. package/src/runtime/model/model-budget-summary.ts +53 -0
  105. package/src/runtime/model/model-fallback.ts +36 -2
  106. package/src/runtime/model/pi-args.ts +10 -0
  107. package/src/runtime/model/provider-extensions.ts +10 -0
  108. package/src/runtime/orphan-worker-registry.ts +1 -1
  109. package/src/runtime/output/output-validator.ts +45 -0
  110. package/src/runtime/parent-guard.ts +3 -1
  111. package/src/runtime/peer-dep.ts +2 -1
  112. package/src/runtime/per-write-validator.ts +0 -5
  113. package/src/runtime/pi-spawn.ts +61 -15
  114. package/src/runtime/plan-approval.ts +125 -0
  115. package/src/runtime/plan-replan.ts +151 -0
  116. package/src/runtime/process-status.ts +16 -1
  117. package/src/runtime/recovery/checkpoint.ts +0 -18
  118. package/src/runtime/recovery/crash-recovery.ts +111 -46
  119. package/src/runtime/run-tracker.ts +77 -10
  120. package/src/runtime/scheduler-context.ts +98 -0
  121. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  122. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  123. package/src/runtime/scheduling/nested-slots.ts +70 -0
  124. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  125. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  126. package/src/runtime/settings-store.ts +219 -0
  127. package/src/runtime/spawn-policy.ts +217 -0
  128. package/src/runtime/stale-reconciler.ts +87 -6
  129. package/src/runtime/subagent-manager.ts +25 -1
  130. package/src/runtime/task-output-context.ts +230 -9
  131. package/src/runtime/task-packet.ts +23 -1
  132. package/src/runtime/task-runner/child-executor.ts +106 -7
  133. package/src/runtime/task-runner/post-execution.ts +125 -1
  134. package/src/runtime/task-runner/pre-execution.ts +39 -1
  135. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  136. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  137. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  138. package/src/runtime/task-runner/state-helpers.ts +26 -24
  139. package/src/runtime/task-runner.ts +11 -0
  140. package/src/runtime/team-runner.ts +132 -1673
  141. package/src/runtime/verification/spec-sandbox.ts +255 -0
  142. package/src/runtime/verification/verification-gates.ts +3 -2
  143. package/src/runtime/verification/verification-worktree.ts +2 -1
  144. package/src/runtime/workflow-phase-advance.ts +100 -0
  145. package/src/runtime/workspace-tree.ts +9 -0
  146. package/src/schema/config-schema.ts +66 -26
  147. package/src/schema/sensitive-config-paths.ts +64 -0
  148. package/src/schema/team-tool-schema.ts +13 -3
  149. package/src/state/README.md +4 -10
  150. package/src/state/atomic-write.ts +20 -3
  151. package/src/state/contracts.ts +38 -0
  152. package/src/state/coordination/mailbox.ts +12 -2
  153. package/src/state/event-log/cursor.ts +223 -0
  154. package/src/state/event-log/event-log-rotation.ts +12 -4
  155. package/src/state/event-log/event-log.ts +152 -369
  156. package/src/state/event-log/sequence-cache.ts +373 -0
  157. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  158. package/src/state/stores/active-run-registry.ts +3 -2
  159. package/src/state/stores/manifest-io.ts +237 -0
  160. package/src/state/stores/ownership-map.ts +162 -0
  161. package/src/state/stores/plan-store.ts +241 -0
  162. package/src/state/stores/run-cache.ts +0 -90
  163. package/src/state/stores/spec-store.ts +189 -0
  164. package/src/state/stores/state-store.ts +139 -232
  165. package/src/state/types.ts +199 -0
  166. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  167. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  168. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  169. package/src/ui/dock-footer.ts +49 -0
  170. package/src/ui/heartbeat-aggregator.ts +9 -1
  171. package/src/ui/inline-panel/agent-pane.ts +375 -0
  172. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  173. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  174. package/src/ui/inline-panel/crew-editor.ts +192 -0
  175. package/src/ui/inline-panel/index.ts +290 -0
  176. package/src/ui/inline-panel/panel-rows.ts +37 -0
  177. package/src/ui/inline-panel/panel-selection.ts +157 -0
  178. package/src/ui/inline-panel/panel-store.ts +111 -0
  179. package/src/ui/inline-panel/view-session-store.ts +36 -0
  180. package/src/ui/keybinding-map.ts +54 -13
  181. package/src/ui/pi-ui-compat.ts +9 -0
  182. package/src/ui/powerbar-publisher.ts +52 -1
  183. package/src/ui/run-dashboard.ts +31 -5
  184. package/src/ui/run-snapshot-cache.ts +57 -30
  185. package/src/ui/snapshot-types.ts +6 -1
  186. package/src/ui/widget/index.ts +176 -22
  187. package/src/ui/widget/task-list.ts +198 -0
  188. package/src/ui/widget/widget-formatters.ts +240 -4
  189. package/src/ui/widget/widget-renderer.ts +243 -38
  190. package/src/ui/widget/widget-types.ts +11 -0
  191. package/src/utils/child-process-shield.ts +106 -0
  192. package/src/utils/file-coalescer.ts +0 -4
  193. package/src/utils/fs-errno.ts +66 -0
  194. package/src/utils/fs-watch.ts +1 -1
  195. package/src/utils/internal-error.ts +3 -1
  196. package/src/utils/paths.ts +11 -3
  197. package/src/utils/redaction.ts +7 -0
  198. package/src/utils/safe-abort.ts +45 -0
  199. package/src/utils/task-name-generator.ts +1 -8
  200. package/src/workflows/discover-workflows.ts +20 -2
  201. package/src/workflows/validate-workflow.ts +7 -1
  202. package/src/workflows/workflow-config.ts +17 -0
  203. package/src/workflows/workflow-serializer.ts +3 -0
  204. package/src/worktree/worktree-manager.ts +22 -0
  205. package/workflows/default.workflow.md +36 -26
  206. package/workflows/strict-fast-fix.workflow.md +26 -0
  207. package/src/agents/agent-search.ts +0 -98
  208. package/src/benchmark/benchmark-runner.ts +0 -313
  209. package/src/benchmark/feedback-loop.ts +0 -73
  210. package/src/config/resilient-parser.ts +0 -117
  211. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  212. package/src/extension/result-watcher.ts +0 -139
  213. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  214. package/src/observability/metric-retention.ts +0 -64
  215. package/src/runtime/compaction/compaction-summary.ts +0 -278
  216. package/src/runtime/errors/crew-errors.ts +0 -162
  217. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  218. package/src/runtime/loop-gates.ts +0 -128
  219. package/src/runtime/metric-parser.ts +0 -36
  220. package/src/runtime/output/stream-preview.ts +0 -184
  221. package/src/runtime/output/tool-progress.ts +0 -278
  222. package/src/runtime/phase-tracker.ts +0 -385
  223. package/src/runtime/pipeline-runner.ts +0 -523
  224. package/src/runtime/process/process-lifecycle.ts +0 -491
  225. package/src/runtime/recovery/retry-runner.ts +0 -330
  226. package/src/runtime/run-drift.ts +0 -219
  227. package/src/runtime/task-quality.ts +0 -199
  228. package/src/runtime/task-runner/run-projection.ts +0 -128
  229. package/src/runtime/verification/post-checks.ts +0 -142
  230. package/src/state/coordination/schedule.ts +0 -166
  231. package/src/state/event-log/jsonl-writer.ts +0 -115
  232. package/src/state/hook-instinct-bridge.ts +0 -94
  233. package/src/state/hook-integrations.ts +0 -51
  234. package/src/state/session-state-map.ts +0 -51
  235. package/src/state/stores/blob-store.ts +0 -308
  236. package/src/state/stores/instinct-store.ts +0 -275
  237. package/src/state/stores/observation-store.ts +0 -176
  238. package/src/state/tiered-eval.ts +0 -480
  239. package/src/state/types-eval.ts +0 -58
  240. package/src/tools/safe-bash-extension.ts +0 -54
  241. package/src/tools/safe-bash.ts +0 -505
  242. package/src/ui/agent-management-overlay.ts +0 -160
  243. package/src/ui/crew-footer.ts +0 -102
  244. package/src/ui/crew-select-list.ts +0 -114
  245. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  246. package/src/ui/transcript-entries.ts +0 -256
  247. package/src/utils/conflict-detect.ts +0 -721
  248. package/src/utils/fingerprint.ts +0 -180
  249. package/src/utils/gh-protocol.ts +0 -556
  250. package/src/utils/project-detector.ts +0 -160
  251. package/src/utils/sse-parser.ts +0 -131
  252. package/src/workflows/cost-estimator.ts +0 -34
  253. package/src/workflows/intermediate-store.ts +0 -166
@@ -147,9 +147,16 @@ function readProcStatus(pid) {
147
147
 
148
148
  // ---------- fallback via ps (non-Linux) ----------
149
149
  function readPs(pid) {
150
- const res = spawnSync("ps", ["-o", "rss=,pcpu=", "-p", String(pid)], { encoding: "utf8" });
150
+ // state= makes zombie detection possible on BSD ps (macOS): a dead child
151
+ // stays listed by ps until reaped — without the state check, watch-parent
152
+ // (R4) never sees the watched PID die and live-warn (proc_died) never fires
153
+ // (CI incident: resource-sampler-audit.test.ts on macos-latest). Linux ps
154
+ // also supports state=. Z/X (and empty) = dead.
155
+ const res = spawnSync("ps", ["-o", "rss=,pcpu=,state=", "-p", String(pid)], { encoding: "utf8" });
151
156
  if (res.status !== 0 || !res.stdout.trim()) return null;
152
157
  const parts = res.stdout.trim().split(/\s+/);
158
+ const state = parts.length >= 3 ? parts[2] : "";
159
+ if (state.startsWith("Z") || state.startsWith("X")) return null;
153
160
  return {
154
161
  pid,
155
162
  ppid: 0,
@@ -163,7 +170,34 @@ function readPs(pid) {
163
170
  // ---------- child discovery ----------
164
171
  function findDescendants(rootPid) {
165
172
  // BFS over /proc to find all PIDs whose ppid chain leads to rootPid.
166
- if (!existsSync("/proc")) return [rootPid];
173
+ // Non-Linux (macOS/BSD): no /proc — build the ppid map from `ps -eo pid=,ppid=`
174
+ // instead (R3 on macos-latest CI: /proc-less platforms previously returned
175
+ // [rootPid] only, so descendants were never sampled).
176
+ if (!existsSync("/proc")) {
177
+ try {
178
+ const res = spawnSync("ps", ["-eo", "pid=,ppid="], { encoding: "utf8" });
179
+ if (res.status !== 0 || !res.stdout.trim()) return [rootPid];
180
+ const ppidOf = new Map();
181
+ for (const line of res.stdout.trim().split("\n")) {
182
+ const parts = line.trim().split(/\s+/);
183
+ if (parts.length >= 2) ppidOf.set(Number.parseInt(parts[0], 10), Number.parseInt(parts[1], 10));
184
+ }
185
+ const result = new Set([rootPid]);
186
+ let grew = true;
187
+ while (grew) {
188
+ grew = false;
189
+ for (const [pid, ppid] of ppidOf) {
190
+ if (result.has(ppid) && !result.has(pid)) {
191
+ result.add(pid);
192
+ grew = true;
193
+ }
194
+ }
195
+ }
196
+ return [...result];
197
+ } catch {
198
+ return [rootPid];
199
+ }
200
+ }
167
201
  const all = [];
168
202
  try {
169
203
  for (const name of readdirSync("/proc")) {
@@ -62,6 +62,32 @@ Use observable checks:
62
62
  - compatibility requirements such as Windows paths or Pi CLI flags;
63
63
  - rollback notes.
64
64
 
65
+ ## Spec Pairs (pi-crew v0.10.1+, ADR-6)
66
+
67
+ When the target runs pi-crew, author the **SpecRecord + TaskPacket pair** instead of prose-only acceptance:
68
+
69
+ 1. **SpecRecord** (workspace `state/specs/<id>.json`): `requirements[]` with
70
+ `must|should|could` priority + stable ids; `acceptance[]` entries each tied to
71
+ a `requirementId`. Machine-checkable acceptances carry `command`,
72
+ `expectedDigest` (sha-256 hex of stdout) or `expectedExitCode`, and
73
+ `idempotent: true` — only idempotent commands are ever re-run.
74
+ 2. **PROVENANCE — critical**: specs you (an agent/skill path) write are persisted
75
+ `generated` and NEVER re-executed by the orchestrator. `manual`+`trusted`
76
+ (the only specs strict mode re-runs) are minted exclusively by USER-facing
77
+ import actions — a worker cannot author a command the root re-executes.
78
+ 3. **Wire the workflow**: add `specRefs: [<spec ids>]` to steps held to the spec;
79
+ `specStrict: true` in frontmatter opts the whole workflow into strict mode
80
+ (requires a `verifier` role step — the run rejects at start otherwise).
81
+ 4. **Executor footer contract**: workers must END results with
82
+
83
+ ```text
84
+ SPEC-EVIDENCE:
85
+ <acceptanceId>: <one-line evidence>
86
+ ```
87
+
88
+ Non-strict = mechanical coverage only (`unverified` badge on gaps, never
89
+ blocks). Strict = coverage AND machine-check; failures fail the run.
90
+
65
91
  ## Enforcement — Requirements to Task Packet Gate
66
92
 
67
93
  **Before dispatching workers, verify task packet has:**
@@ -32,16 +32,16 @@ In-memory map from `live-agent-manager.ts`. Provides:
32
32
 
33
33
  **When NOT used:** After `evictStaleLiveAgentHandles()` removes a handle, widget falls back to agent records on disk.
34
34
 
35
- ### 2. Snapshot cache (500ms TTL)
35
+ ### 2. Snapshot cache (1500ms TTL)
36
36
 
37
- `RunSnapshotCache` from `run-snapshot-cache.ts` caches parsed manifests and agents for 500ms. Reduces disk reads during rapid refresh.
37
+ `RunSnapshotCache` from `run-snapshot-cache.ts` caches parsed manifests and agents for 1500ms. Reduces disk reads during rapid refresh.
38
38
 
39
39
  **When used:** As the fallback when no live handle exists. Prevents excessive disk reads on every render tick.
40
40
 
41
41
  **Invalidation:** Cache is invalidated when:
42
42
  - `invalidate()` is called on a specific run
43
43
  - An empty result is returned (forces refresh on next tick)
44
- - TTL expires (500ms)
44
+ - TTL expires (1500ms)
45
45
 
46
46
  ### 3. `agents.json` on disk (durables, lowest priority)
47
47
 
@@ -156,7 +156,7 @@ Every render cycle (`renderTick` / `requestAnimationFrame`) must complete in <16
156
156
 
157
157
  ### TTL interactions
158
158
 
159
- - Snapshot cache TTL = 500ms
159
+ - Snapshot cache TTL = 1500ms
160
160
  - Preload interval must be < TTL to avoid render-time gaps
161
161
  - If preload interval ≥ TTL, the cache always has fresh data for render
162
162
 
@@ -248,7 +248,7 @@ If ANY answer is NO → Stop. Fix widget rendering issues before proceeding.
248
248
  ## Anti-patterns
249
249
 
250
250
  - **Blocking render with fs calls**: Every `readFileSync`, `readdirSync`, `fs.statSync` in the render path causes frame drops. Preload everything async.
251
- - **Stale cache in hot path**: If snapshot cache TTL is too long, widget shows outdated state. Keep TTL at 500ms or less.
251
+ - **Stale cache in hot path**: If snapshot cache TTL is too long, widget shows outdated state. Keep TTL at 1500ms or less (the code default; lower only with cause).
252
252
  - **No invalidation on empty**: When `readCrewAgents` returns `[]` (no agents yet), the cache must be invalidated on next tick to prevent showing empty for too long.
253
253
  - **Expired handles accumulating**: Without `evictStaleLiveAgentHandles`, the Map grows indefinitely. Call it on every refresh.
254
254
  - **Widget showing stale health warnings**: Completed/cancelled/failed runs should not show health warnings. Filter by status.
@@ -258,7 +258,7 @@ If ANY answer is NO → Stop. Fix widget rendering issues before proceeding.
258
258
  ## Source patterns
259
259
 
260
260
  - `src/ui/crew-widget.ts` — render, refresh, activeWidgetRuns, evictStaleLiveAgentHandles, agentActivity, describeLiveActivity
261
- - `src/ui/run-snapshot-cache.ts` — SnapshotCache, get, refreshIfStale, TTL=500ms
261
+ - `src/ui/run-snapshot-cache.ts` — SnapshotCache, get, refreshIfStale, TTL=1500ms
262
262
  - `src/runtime/crew-agent-records.ts` — readCrewAgents, agents.json
263
263
  - `src/runtime/process-status.ts` — hasStaleAsyncProcess, isDisplayActiveRun
264
264
  - `src/runtime/background-runner.ts` — active run filtering with async PID check
@@ -275,7 +275,7 @@ Render-path performance for the widget is non-negotiable. Treat every `render(wi
275
275
  - **Prefer `snapshotCache.get(runId)`** on render paths. If a synchronous fallback is genuinely unavoidable, classify it as first-load/rare and document why it can't be preloaded.
276
276
  - **Keep panes pure.** Dashboard panes must accept a snapshot/model and format strings only. Never call `fs.readFileSync`, `fs.readdirSync`, `fs.statSync`, network APIs, or large JSON parsing from pane render methods.
277
277
  - **Stay non-blocking at 60fps.** Each render cycle must complete in under ~16ms. Anything that can't finish synchronously (reads, fetches, directory scans) belongs in the async preload path, not `renderTick()`.
278
- - **Respect the snapshot-cache TTL of ≤500ms.** Keep the `RunSnapshotCache` TTL at 500ms or less so the widget never shows stale state. Watch TTL interactions: the preload interval must be shorter than the cache TTL, otherwise render-time refresh gaps appear.
278
+ - **Respect the snapshot-cache TTL of ≤1500ms.** Keep the `RunSnapshotCache` TTL at 1500ms or less (the code default; lower only with cause) so the widget never shows stale state. Watch TTL interactions: the preload interval must be shorter than the cache TTL, otherwise render-time refresh gaps appear.
279
279
  - **Guard session switches.** On a session switch, cancel timers and ensure in-flight async preloads cannot update a now-stale session's UI.
280
280
  - **Filter stale warnings by terminal status.** Do not surface health warnings for completed/failed/cancelled runs.
281
281
 
@@ -1,3 +1,4 @@
1
+ import { getCrewEnv } from "../config/env-vars.ts";
1
2
  import type { RoleToolConfig } from "../config/role-tools.ts";
2
3
  import { getToolConfig, isScratchpadEnabledForRole } from "../config/role-tools.ts";
3
4
 
@@ -196,7 +197,7 @@ export function resolveToolPolicy(agent: AgentConfig, role?: string): ResolvedTo
196
197
  * live-session filterActiveTools) via the unified policy.
197
198
  */
198
199
  function shouldDemoteBashForScratchpad(role: string | undefined, agent: AgentConfig): boolean {
199
- if (process.env.PI_CREW_SCRATCHPAD_DEMOTE_BASH !== "1") return false;
200
+ if (getCrewEnv("PI_CREW_SCRATCHPAD_DEMOTE_BASH") !== "1") return false;
200
201
  if (!role) return false;
201
202
  return isScratchpadEnabledForRole(role, { scratchpad: agent.scratchpad });
202
203
  }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { type LoadedPiTeamsConfig, loadConfig } from "../config/config.ts";
4
+ import { getCrewEnv } from "../config/env-vars.ts";
4
5
  import { discoverProviderExtensionPaths } from "../runtime/model/provider-extensions.ts";
5
6
  import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
6
7
  import { logInternalError } from "../utils/internal-error.ts";
@@ -136,19 +137,13 @@ function logSecurityEvent(event: SecurityEvent): void {
136
137
  }
137
138
 
138
139
  /**
139
- * Get recent security events (for debugging/testing).
140
+ * Get recent security events. Production reader: the team doctor report
141
+ * surfaces a compact summary (R7-13); tests assert on the full list.
140
142
  */
141
143
  export function getSecurityEventLog(): readonly SecurityEvent[] {
142
144
  return securityEventLog;
143
145
  }
144
146
 
145
- /**
146
- * Clear security event log (for testing).
147
- */
148
- export function clearSecurityEventLog(): void {
149
- securityEventLog.length = 0;
150
- }
151
-
152
147
  /**
153
148
  * Security check: throws if the agent name is protected.
154
149
  *
@@ -373,7 +368,11 @@ export function sanitizeAgentSystemPrompt(content: string, source: ResourceSourc
373
368
  * emitted the `contextMode: fork` warn-only notice. Avoids spam when
374
369
  * the discovery cache reloads or the same agent is parsed multiple
375
370
  * times in a session. Exported for test reset.
371
+ * R5-L5: FIFO cap so the set stays bounded in long-lived sessions
372
+ * (Set preserves insertion order; bounded naturally <50, cap is a
373
+ * safety net).
376
374
  */
375
+ const MAX_WARNED_FORK_AGENTS = 128;
377
376
  const warnedForkAgents = new Set<string>();
378
377
  export function __test_resetForkWarnings(): void {
379
378
  warnedForkAgents.clear();
@@ -406,14 +405,19 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
406
405
  // so the agent will behave as `fresh` regardless of the setting.
407
406
  // We warn (not throw) so existing configs that predate live-session
408
407
  // keep working. Deduped per-filePath so we don't spam on cache
409
- // reload. Use console.warn (no logger hook here yet; a future
410
- // refactor could pipe through the same log channel as
411
- // logInternalError below).
408
+ // reload. User-facing config notice (kept on console.warn deliberately
409
+ // so the user sees it in the interactive session; internal failures
410
+ // below use logInternalError).
412
411
  if (contextMode === "fork" && !warnedForkAgents.has(filePath)) {
413
412
  console.warn(
414
413
  "contextMode: 'fork' is only effective in live-session runtime; current default child-process will behave as 'fresh'. See docs/runtime-flow.md.",
415
414
  );
416
415
  warnedForkAgents.add(filePath);
416
+ // R5-L5: FIFO eviction — drop the oldest entry past the cap.
417
+ if (warnedForkAgents.size > MAX_WARNED_FORK_AGENTS) {
418
+ const oldest = warnedForkAgents.values().next().value;
419
+ if (oldest !== undefined) warnedForkAgents.delete(oldest);
420
+ }
417
421
  }
418
422
 
419
423
  return {
@@ -442,7 +446,7 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
442
446
  // code. Bypass only when PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS=1 is
443
447
  // explicitly set. buildPiWorkerArgs also enforces this as
444
448
  // defense-in-depth.
445
- ...((source === "project" || source === "project-pi") && process.env.PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS !== "1"
449
+ ...((source === "project" || source === "project-pi") && getCrewEnv("PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS") !== "1"
446
450
  ? { extensions: [], excludeExtensions: [] }
447
451
  : {
448
452
  extensions: frontmatter.extensions === "" ? [] : parseCsv(frontmatter.extensions),
@@ -494,7 +498,12 @@ function readAgentDir(dir: string, source: ResourceSource): AgentConfig[] {
494
498
  try {
495
499
  const stat = fs.statSync(fullPath);
496
500
  if (stat.size > MAX_AGENT_FILE_BYTES) {
497
- console.warn(`[pi-crew] Skipping oversized agent file (${stat.size} > ${MAX_AGENT_FILE_BYTES} bytes): ${fullPath}`);
501
+ logInternalError(
502
+ "discover-agents",
503
+ new Error(`Skipping oversized agent file (${stat.size} > ${MAX_AGENT_FILE_BYTES} bytes): ${fullPath}`),
504
+ undefined,
505
+ "warn",
506
+ );
498
507
  return undefined;
499
508
  }
500
509
  } catch {
@@ -536,7 +545,7 @@ function applyAgentOverrides(agents: AgentConfig[], cwd: string, loadedConfig?:
536
545
  // user-trusted config knob (provider extensions like pi-commandcode-provider)
537
546
  // and applies to builtin / user agents only.
538
547
  const isUntrustedProject = (agent: AgentConfig) =>
539
- (agent.source === "project" || agent.source === "project-pi") && process.env.PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS !== "1";
548
+ (agent.source === "project" || agent.source === "project-pi") && getCrewEnv("PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS") !== "1";
540
549
  const withGlobalExtensions = (agent: AgentConfig): AgentConfig => {
541
550
  if (isUntrustedProject(agent)) return agent;
542
551
  return deduped.length > 0 || agent.extensions !== undefined
@@ -0,0 +1,183 @@
1
+ import { DANGEROUS_OBJECT_KEYS } from "./config-validation.ts";
2
+ import type { AgentOverrideConfig, PiTeamsConfig } from "./types.ts";
3
+
4
+ function withoutUndefined<T extends Record<string, unknown>>(value: T): Partial<T> {
5
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as Partial<T>;
6
+ }
7
+
8
+ export function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfig {
9
+ const warnings: string[] = [];
10
+ const merged: PiTeamsConfig = {
11
+ ...base,
12
+ ...withoutUndefined(override as Record<string, unknown>),
13
+ };
14
+ if (base.autonomous || override.autonomous) {
15
+ merged.autonomous = {
16
+ ...(base.autonomous ?? {}),
17
+ ...withoutUndefined((override.autonomous ?? {}) as Record<string, unknown>),
18
+ };
19
+ }
20
+ if (base.broker || override.broker) {
21
+ // WP-2/R2 fix (B1 battery 2026-08-18): `broker` was previously merged by
22
+ // the top-level spread only — a user-side broker object (defaults or a
23
+ // user config file with ANY broker block) replaced the project config's
24
+ // block WHOLESALE, silently dropping keys like waitMethodsEnabled that
25
+ // the user side did not set (enabled:true survived only because it
26
+ // matched DEFAULT_BROKER). Per-key user-wins, same as runtime/ui/….
27
+ merged.broker = {
28
+ ...(base.broker ?? {}),
29
+ ...withoutUndefined((override.broker ?? {}) as Record<string, unknown>),
30
+ };
31
+ }
32
+ if (base.nesting || override.nesting) {
33
+ // ADR-5: per-key user-wins deep merge — a partial user-side nesting block
34
+ // (e.g. just maxSlots) must not erase DEFAULT_NESTING.enabled=false.
35
+ merged.nesting = {
36
+ ...(base.nesting ?? {}),
37
+ ...withoutUndefined((override.nesting ?? {}) as Record<string, unknown>),
38
+ };
39
+ }
40
+ if (base.limits || override.limits) {
41
+ merged.limits = {
42
+ ...(base.limits ?? {}),
43
+ ...withoutUndefined((override.limits ?? {}) as Record<string, unknown>),
44
+ };
45
+ }
46
+ if (base.runtime || override.runtime) {
47
+ merged.runtime = {
48
+ ...(base.runtime ?? {}),
49
+ ...withoutUndefined((override.runtime ?? {}) as Record<string, unknown>),
50
+ };
51
+ // F19-1 (Round 19 parity): deep-merge modelFallback like
52
+ // reliability.retryPolicy so a partial override cannot erase base fields
53
+ // (user-wins precedence per key). Assigned only when a side defines it —
54
+ // unlike retryPolicy we avoid a stray `modelFallback: undefined` key so
55
+ // the merged runtime shape stays byte-identical for configs without it.
56
+ if (base.runtime?.modelFallback || override.runtime?.modelFallback) {
57
+ merged.runtime.modelFallback = {
58
+ ...(base.runtime?.modelFallback ?? {}),
59
+ ...withoutUndefined((override.runtime?.modelFallback ?? {}) as Record<string, unknown>),
60
+ };
61
+ }
62
+ }
63
+ if (base.control || override.control) {
64
+ merged.control = {
65
+ ...(base.control ?? {}),
66
+ ...withoutUndefined((override.control ?? {}) as Record<string, unknown>),
67
+ };
68
+ }
69
+ if (base.worktree || override.worktree) {
70
+ merged.worktree = {
71
+ ...(base.worktree ?? {}),
72
+ ...withoutUndefined((override.worktree ?? {}) as Record<string, unknown>),
73
+ };
74
+ }
75
+ if (base.ui || override.ui) {
76
+ merged.ui = {
77
+ ...(base.ui ?? {}),
78
+ ...withoutUndefined((override.ui ?? {}) as Record<string, unknown>),
79
+ };
80
+ }
81
+ if (base.agents || override.agents) {
82
+ merged.agents = {
83
+ ...(base.agents ?? {}),
84
+ ...withoutUndefined((override.agents ?? {}) as Record<string, unknown>),
85
+ overrides: {
86
+ ...(base.agents?.overrides ?? {}),
87
+ ...(withoutUndefined((override.agents?.overrides ?? {}) as Record<string, unknown>) as Record<string, AgentOverrideConfig>),
88
+ },
89
+ };
90
+ }
91
+ if (base.tools || override.tools) {
92
+ merged.tools = {
93
+ ...(base.tools ?? {}),
94
+ ...withoutUndefined((override.tools ?? {}) as Record<string, unknown>),
95
+ };
96
+ }
97
+ if (base.telemetry || override.telemetry) {
98
+ merged.telemetry = {
99
+ ...(base.telemetry ?? {}),
100
+ ...withoutUndefined((override.telemetry ?? {}) as Record<string, unknown>),
101
+ };
102
+ }
103
+ if (base.policy || override.policy) {
104
+ merged.policy = {
105
+ ...(base.policy ?? {}),
106
+ ...withoutUndefined((override.policy ?? {}) as Record<string, unknown>),
107
+ };
108
+ }
109
+ if (base.notifications || override.notifications) {
110
+ merged.notifications = {
111
+ ...(base.notifications ?? {}),
112
+ ...withoutUndefined((override.notifications ?? {}) as Record<string, unknown>),
113
+ };
114
+ }
115
+ if (base.observability || override.observability) {
116
+ merged.observability = {
117
+ ...(base.observability ?? {}),
118
+ ...withoutUndefined((override.observability ?? {}) as Record<string, unknown>),
119
+ };
120
+ }
121
+ if (base.reliability || override.reliability) {
122
+ merged.reliability = {
123
+ ...(base.reliability ?? {}),
124
+ ...withoutUndefined((override.reliability ?? {}) as Record<string, unknown>),
125
+ retryPolicy:
126
+ base.reliability?.retryPolicy || override.reliability?.retryPolicy
127
+ ? {
128
+ ...(base.reliability?.retryPolicy ?? {}),
129
+ ...withoutUndefined((override.reliability?.retryPolicy ?? {}) as Record<string, unknown>),
130
+ }
131
+ : undefined,
132
+ };
133
+ }
134
+ if (base.otlp || override.otlp) {
135
+ merged.otlp = {
136
+ ...(base.otlp ?? {}),
137
+ ...withoutUndefined((override.otlp ?? {}) as Record<string, unknown>),
138
+ headers: {
139
+ ...(base.otlp?.headers ?? {}),
140
+ ...(override.otlp?.headers ?? {}),
141
+ },
142
+ };
143
+ if (Object.keys(merged.otlp.headers ?? {}).length === 0) delete merged.otlp.headers;
144
+ // Validate OTLP headers for injection attacks:
145
+ // - Check top-level keys for dangerous prototype pollution patterns
146
+ // - Block ALL control characters except tab (0x09) to prevent header
147
+ // injection via CR/LF/zero-byte/etc.
148
+ // BUG (Round 28, CRLF injection): the previous range
149
+ // /[\x00-\x08\x0b\x0c\x0e-\x1f]/ left THREE chars unblocked: tab (0x09,
150
+ // intentionally allowed), LF (0x0A) AND CR (0x0D). The comment claimed to
151
+ // "prevent header injection via CR/LF" but CR was never matched, and LF
152
+ // was explicitly allowed — both are CRLF injection vectors that can split
153
+ // HTTP headers. Fix: block 0x00-0x08 and 0x0A-0x1F, allowing only tab.
154
+ const invalidHeaders: string[] = [];
155
+ for (const [k, v] of Object.entries(merged.otlp.headers ?? {})) {
156
+ // Check top-level key for dangerous names (only top-level keys are checked)
157
+ const checkKey = (key: string): boolean => {
158
+ const lowerKey = key.toLowerCase();
159
+ if (DANGEROUS_OBJECT_KEYS.has(lowerKey)) return true;
160
+ return false;
161
+ };
162
+ if (checkKey(k)) {
163
+ invalidHeaders.push(k);
164
+ continue;
165
+ }
166
+ // Block any control characters except tab (0x09) in values.
167
+ // Round 28 fix: /[\x00-\x08\x0a-\x1f]/ blocks LF (0x0A) and CR (0x0D) too.
168
+ const valStr = String(v);
169
+ if (/[\x00-\x08\x0a-\x1f]/.test(valStr)) {
170
+ invalidHeaders.push(k);
171
+ }
172
+ }
173
+ if (invalidHeaders.length > 0) {
174
+ delete merged.otlp.headers;
175
+ warnings.push(`OTLP headers blocked due to invalid characters: ${invalidHeaders.join(", ")}`);
176
+ }
177
+ }
178
+ if (merged.agents?.overrides && Object.keys(merged.agents.overrides).length === 0) delete merged.agents.overrides;
179
+ return merged;
180
+ }
181
+
182
+ /** @internal — direct-test seam for Phase 2.2 extraction target (refactor-plan step 1.9c). */
183
+ export const __test__mergeConfig = mergeConfig;