pi-crew 0.9.50 → 0.9.52

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 (609) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +216 -20
  3. package/dist/build-meta.json +2222 -1761
  4. package/dist/index.mjs +18636 -17603
  5. package/docs/actions-reference.md +431 -1
  6. package/package.json +19 -9
  7. package/schema.json +41 -0
  8. package/scripts/check-bundle-size.mjs +38 -0
  9. package/scripts/test-runner.mjs +11 -1
  10. package/skills/distill-software/SKILL.md +24 -1
  11. package/dist/index.mjs.map +0 -7
  12. package/docs/FEATURE_INTAKE.md +0 -126
  13. package/docs/HARNESS.md +0 -87
  14. package/docs/HARNESS_BACKLOG.md +0 -89
  15. package/docs/PROJECT_REVIEW.md +0 -270
  16. package/docs/PROJECT_REVIEW_FIXES.md +0 -342
  17. package/docs/PROJECT_REVIEW_ROUND4.md +0 -156
  18. package/docs/PROJECT_REVIEW_ROUND5.md +0 -86
  19. package/docs/REVIEW-FINDINGS-2026-06 +0 -357
  20. package/docs/REVIEW-FINDINGS-2026-07-CORE.md +0 -139
  21. package/docs/REVIEW-FINDINGS-2026-07.md +0 -125
  22. package/docs/TEST_MATRIX.md +0 -51
  23. package/docs/archive/README.md +0 -91
  24. package/docs/archive/followup-review-round3-2026-05-12.md +0 -342
  25. package/docs/archive/followup-review-round4-2026-05-13.md +0 -107
  26. package/docs/archive/pi-crew-bugs.md +0 -961
  27. package/docs/archive/pi-crew-test-final.md +0 -120
  28. package/docs/archive/pi-crew-test-results.md +0 -260
  29. package/docs/archive/pi-crew-test-round2.md +0 -136
  30. package/docs/archive/pi-crew-test-round4.md +0 -100
  31. package/docs/archive/pi-crew-test-round5.md +0 -70
  32. package/docs/archive/pi-crew-test-round6.md +0 -110
  33. package/docs/archive/pi-crew-v0.5.10-audit-fix-plan.md +0 -60
  34. package/docs/archive/pi-crew-v0.5.11-audit-fix-plan.md +0 -92
  35. package/docs/archive/pi-crew-v0.5.12-audit-fix-plan.md +0 -76
  36. package/docs/archive/pi-crew-v0.5.13-audit-fix-plan.md +0 -75
  37. package/docs/archive/pi-crew-v0.5.14-audit-fix-plan.md +0 -75
  38. package/docs/archive/pi-crew-v0.5.16-audit-fix-plan.md +0 -35
  39. package/docs/archive/pi-crew-v0.5.17-audit-fix-plan.md +0 -80
  40. package/docs/archive/pi-crew-v0.5.5-audit-fix-plan.md +0 -133
  41. package/docs/archive/pi-crew-v0.5.9-audit-fix-plan.md +0 -88
  42. package/docs/archive/pi-mono-opportunities.md +0 -969
  43. package/docs/archive/pi-mono-review.md +0 -291
  44. package/docs/archive/pi-subagent4-comparison.md +0 -261
  45. package/docs/archive/pi-subagents3-deep-analysis.md +0 -508
  46. package/docs/bugs/bug-021-notification-badge-counter-misleading.md +0 -293
  47. package/docs/bugs/bug-023-chain-windows-path-resolution.md +0 -106
  48. package/docs/bugs/bug-quota-display-truncation.md +0 -223
  49. package/docs/bugs/cross-session-notification-leakage.md +0 -82
  50. package/docs/code-review-2026-05-11.md +0 -592
  51. package/docs/coding-agent-optimization.md +0 -268
  52. package/docs/comparison-pi-subagents-vs-pi-crew.md +0 -303
  53. package/docs/decisions/0001-durable-state.md +0 -41
  54. package/docs/decisions/0002-child-process-for-async.md +0 -42
  55. package/docs/decisions/0003-depth-guard.md +0 -36
  56. package/docs/decisions/0004-execfile-over-exec.md +0 -34
  57. package/docs/decisions/0005-no-parameter-properties.md +0 -49
  58. package/docs/decisions/0006-publish-bundled-esm.md +0 -63
  59. package/docs/decisions/0007-active-run-binary-index.md +0 -54
  60. package/docs/decisions/0008-child-pi-warm-pool.md +0 -61
  61. package/docs/decisions/2026-07-21-broker-phase4-default-on.md +0 -77
  62. package/docs/decisions/2026-07-21-broker-windows-perms.md +0 -91
  63. package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +0 -99
  64. package/docs/decisions/2026-07-24-oidc-trusted-publishing.md +0 -112
  65. package/docs/decisions/README.md +0 -26
  66. package/docs/deep-review-report.md +0 -384
  67. package/docs/distillation/cybersecurity-patterns.md +0 -294
  68. package/docs/feature-analysis-subagent4.md +0 -305
  69. package/docs/fix-plan-disabletools-exit-null.md +0 -219
  70. package/docs/fixes/BATCH_A_H1_H2.md +0 -86
  71. package/docs/fixes/bug-006-foreground-cancel-concurrent.md +0 -78
  72. package/docs/fixes/bug-007-async-notifier-stale-ctx.md +0 -112
  73. package/docs/fixes/bug-008-child-process-silent-timeout.md +0 -100
  74. package/docs/fixes/bug-009-executor-yield-limit-needs-attention.md +0 -75
  75. package/docs/fixes/bug-010-child-process-api-key-filtered.md +0 -109
  76. package/docs/fixes/bug-011-spawn-pi-enoent.md +0 -92
  77. package/docs/fixes/bug-012-essential-env-stripped.md +0 -89
  78. package/docs/fixes/bug-013-background-runner-death.md +0 -84
  79. package/docs/fixes/bug-014-infinite-retry-loop-needs-attention.md +0 -82
  80. package/docs/fixes/bug-015-background-runner-sigterm.md +0 -65
  81. package/docs/fixes/bug-017-background-runner-session-shutdown.md +0 -66
  82. package/docs/fixes/bug-017-background-runner-sigkill-double-fork.md +0 -28
  83. package/docs/fixes/bug-018-child-pi-worker-stdin-hang.md +0 -61
  84. package/docs/fixes/bug-019-phantom-runs-temp-workspace.md +0 -52
  85. package/docs/fixes/bug-020-infinite-retry-loop-needs-attention.md +0 -47
  86. package/docs/fixes/v0.9.10/locks-fix-verify.md +0 -3
  87. package/docs/fixes/v0.9.10/smoke-test.md +0 -12
  88. package/docs/followup-plan-2026-05-12.md +0 -463
  89. package/docs/followup-review-2026-05-12.md +0 -297
  90. package/docs/goals.md +0 -59
  91. package/docs/implementation-plan-top3.md +0 -333
  92. package/docs/issue-29-analysis.md +0 -189
  93. package/docs/live-mailbox-runtime.md +0 -36
  94. package/docs/migration/atomic-write-v2-migration.md +0 -299
  95. package/docs/next-upgrade-roadmap.md +0 -808
  96. package/docs/oh-my-pi-research.md +0 -509
  97. package/docs/optimization-plan-2026-07.md +0 -366
  98. package/docs/optimization-plan.md +0 -644
  99. package/docs/patterns/command-agent-skill.md +0 -71
  100. package/docs/perf/baseline-2026-05.md +0 -113
  101. package/docs/perf/final-report-2026-05.md +0 -206
  102. package/docs/perf/optimization-plan-2026-07-verified.md +0 -396
  103. package/docs/perf/performance-audit-report-2026-07.md +0 -948
  104. package/docs/perf/performance-review-2026-07.md +0 -287
  105. package/docs/perf/sprint-1-report.md +0 -71
  106. package/docs/perf/sprint-2-report.md +0 -81
  107. package/docs/perf/sprint-2.5-report.md +0 -53
  108. package/docs/perf/sprint-3-report.md +0 -36
  109. package/docs/perf/sprint-4-report.md +0 -47
  110. package/docs/perf/sprint-5-report.md +0 -51
  111. package/docs/perf/sprint-6-report.md +0 -94
  112. package/docs/perf/sprint-7-report.md +0 -74
  113. package/docs/perf/upgrade-plan-2026-05.md +0 -147
  114. package/docs/performance-optimization-execution-plan.md +0 -520
  115. package/docs/phase4-triage.md +0 -58
  116. package/docs/pi-crew-investigation-report.md +0 -411
  117. package/docs/product/README.md +0 -31
  118. package/docs/product/platform.md +0 -27
  119. package/docs/product/runtime-safety.md +0 -37
  120. package/docs/product/team-run.md +0 -39
  121. package/docs/product/team-tool.md +0 -37
  122. package/docs/runtime-analysis-child-vs-live.md +0 -171
  123. package/docs/runtime-migration-in-process-analysis.md +0 -250
  124. package/docs/skills/REFERENCE.md +0 -138
  125. package/docs/stories/README.md +0 -32
  126. package/docs/stories/US-DEPS-major-upgrade.md +0 -62
  127. package/docs/stories/backlog.md +0 -36
  128. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +0 -1270
  129. package/docs/templates/decision.md +0 -27
  130. package/docs/templates/story.md +0 -44
  131. package/docs/templates/validation-report.md +0 -32
  132. package/docs/ui-optimization-plan.md +0 -447
  133. package/src/adapters/claude-adapter.ts +0 -23
  134. package/src/adapters/codex-adapter.ts +0 -21
  135. package/src/adapters/cursor-adapter.ts +0 -17
  136. package/src/adapters/export-util.ts +0 -143
  137. package/src/adapters/index.ts +0 -15
  138. package/src/adapters/registry.ts +0 -18
  139. package/src/adapters/types.ts +0 -23
  140. package/src/agents/agent-config.ts +0 -194
  141. package/src/agents/agent-search.ts +0 -98
  142. package/src/agents/agent-serializer.ts +0 -38
  143. package/src/agents/discover-agents.ts +0 -623
  144. package/src/benchmark/benchmark-runner.ts +0 -313
  145. package/src/benchmark/feedback-loop.ts +0 -73
  146. package/src/config/config.ts +0 -1269
  147. package/src/config/defaults.ts +0 -193
  148. package/src/config/drift-detector.ts +0 -290
  149. package/src/config/markers.ts +0 -330
  150. package/src/config/resilient-parser.ts +0 -117
  151. package/src/config/role-tools.ts +0 -118
  152. package/src/config/suggestions.ts +0 -75
  153. package/src/config/types.ts +0 -278
  154. package/src/errors.ts +0 -194
  155. package/src/extension/action-suggestions.ts +0 -117
  156. package/src/extension/async-notifier.ts +0 -179
  157. package/src/extension/autonomous-policy.ts +0 -209
  158. package/src/extension/command-completions.ts +0 -120
  159. package/src/extension/context-status-injection.ts +0 -185
  160. package/src/extension/crew-autocomplete.ts +0 -131
  161. package/src/extension/crew-cleanup.ts +0 -168
  162. package/src/extension/crew-input-router.ts +0 -93
  163. package/src/extension/crew-shortcuts.ts +0 -87
  164. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  165. package/src/extension/crew-vibes/config.ts +0 -195
  166. package/src/extension/crew-vibes/figures.ts +0 -101
  167. package/src/extension/crew-vibes/font-detect.ts +0 -71
  168. package/src/extension/crew-vibes/footer.ts +0 -292
  169. package/src/extension/crew-vibes/index.ts +0 -432
  170. package/src/extension/crew-vibes/provider-usage.ts +0 -396
  171. package/src/extension/crew-vibes/render.ts +0 -206
  172. package/src/extension/crew-vibes/speed.ts +0 -286
  173. package/src/extension/cross-extension-rpc.ts +0 -350
  174. package/src/extension/help.ts +0 -62
  175. package/src/extension/import-index.ts +0 -74
  176. package/src/extension/knowledge-injection.ts +0 -402
  177. package/src/extension/management.ts +0 -571
  178. package/src/extension/message-renderers.ts +0 -108
  179. package/src/extension/notification-router.ts +0 -152
  180. package/src/extension/notification-sink.ts +0 -54
  181. package/src/extension/pi-api.ts +0 -56
  182. package/src/extension/plan-orchestrate.ts +0 -302
  183. package/src/extension/project-init.ts +0 -170
  184. package/src/extension/register.ts +0 -113
  185. package/src/extension/registration/artifact-cleanup.ts +0 -20
  186. package/src/extension/registration/command-registration.ts +0 -58
  187. package/src/extension/registration/command-utils.ts +0 -58
  188. package/src/extension/registration/commands.ts +0 -1228
  189. package/src/extension/registration/compaction-guard.ts +0 -333
  190. package/src/extension/registration/context-builder.ts +0 -135
  191. package/src/extension/registration/crash-recovery-cache.ts +0 -54
  192. package/src/extension/registration/foreground-run-controller.ts +0 -238
  193. package/src/extension/registration/hook-registration.ts +0 -119
  194. package/src/extension/registration/lazy-configurers.ts +0 -124
  195. package/src/extension/registration/lifecycle-handlers.ts +0 -923
  196. package/src/extension/registration/lifecycle.ts +0 -259
  197. package/src/extension/registration/observability.ts +0 -322
  198. package/src/extension/registration/registration-types.ts +0 -158
  199. package/src/extension/registration/runtime-cleanup.ts +0 -230
  200. package/src/extension/registration/subagent-helpers.ts +0 -125
  201. package/src/extension/registration/subagent-manager-setup.ts +0 -330
  202. package/src/extension/registration/subagent-tools.ts +0 -456
  203. package/src/extension/registration/team-tool.ts +0 -220
  204. package/src/extension/registration/tool-registration.ts +0 -50
  205. package/src/extension/registration/ui.ts +0 -172
  206. package/src/extension/registration/viewers.ts +0 -121
  207. package/src/extension/registration/wire-cross-extension.ts +0 -37
  208. package/src/extension/result-watcher.ts +0 -139
  209. package/src/extension/rpc-hmac.ts +0 -256
  210. package/src/extension/run-bundle-schema.ts +0 -104
  211. package/src/extension/run-export.ts +0 -97
  212. package/src/extension/run-import.ts +0 -168
  213. package/src/extension/run-index.ts +0 -131
  214. package/src/extension/run-maintenance.ts +0 -190
  215. package/src/extension/session-summary.ts +0 -18
  216. package/src/extension/team-manager-command.ts +0 -153
  217. package/src/extension/team-onboard.ts +0 -177
  218. package/src/extension/team-recommendation.ts +0 -288
  219. package/src/extension/team-tool/anchor.ts +0 -172
  220. package/src/extension/team-tool/api.ts +0 -1244
  221. package/src/extension/team-tool/auto-summarize.ts +0 -142
  222. package/src/extension/team-tool/cache-control.ts +0 -19
  223. package/src/extension/team-tool/cancel.ts +0 -337
  224. package/src/extension/team-tool/chain-dispatch.ts +0 -95
  225. package/src/extension/team-tool/chain-executor.ts +0 -379
  226. package/src/extension/team-tool/config-patch.ts +0 -50
  227. package/src/extension/team-tool/context.ts +0 -149
  228. package/src/extension/team-tool/destructive-gate.ts +0 -52
  229. package/src/extension/team-tool/doctor.ts +0 -431
  230. package/src/extension/team-tool/explain.ts +0 -282
  231. package/src/extension/team-tool/failure-patterns.ts +0 -118
  232. package/src/extension/team-tool/goal-wrap.ts +0 -330
  233. package/src/extension/team-tool/goal.ts +0 -556
  234. package/src/extension/team-tool/handle-schedule.ts +0 -332
  235. package/src/extension/team-tool/handle-settings.ts +0 -520
  236. package/src/extension/team-tool/health-monitor.ts +0 -481
  237. package/src/extension/team-tool/inspect.ts +0 -88
  238. package/src/extension/team-tool/intent-policy.ts +0 -45
  239. package/src/extension/team-tool/lifecycle-actions.ts +0 -615
  240. package/src/extension/team-tool/orchestrate.ts +0 -98
  241. package/src/extension/team-tool/parallel-dispatch.ts +0 -183
  242. package/src/extension/team-tool/plan.ts +0 -50
  243. package/src/extension/team-tool/respond.ts +0 -150
  244. package/src/extension/team-tool/run-not-found.ts +0 -49
  245. package/src/extension/team-tool/run.ts +0 -1035
  246. package/src/extension/team-tool/status.ts +0 -260
  247. package/src/extension/team-tool/workflow-manage.ts +0 -261
  248. package/src/extension/team-tool-types.ts +0 -26
  249. package/src/extension/team-tool.ts +0 -1116
  250. package/src/extension/tool-result.ts +0 -16
  251. package/src/extension/validate-resources.ts +0 -108
  252. package/src/hooks/registry.ts +0 -200
  253. package/src/hooks/types.ts +0 -69
  254. package/src/i18n.ts +0 -212
  255. package/src/observability/correlation.ts +0 -50
  256. package/src/observability/event-bus.ts +0 -86
  257. package/src/observability/event-to-metric.ts +0 -170
  258. package/src/observability/exporters/adapter.ts +0 -27
  259. package/src/observability/exporters/otlp-exporter.ts +0 -356
  260. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  261. package/src/observability/metric-registry.ts +0 -100
  262. package/src/observability/metric-retention.ts +0 -64
  263. package/src/observability/metric-sink.ts +0 -99
  264. package/src/observability/metrics-primitives.ts +0 -219
  265. package/src/plugins/plugin-define.ts +0 -6
  266. package/src/plugins/plugin-registry.ts +0 -32
  267. package/src/plugins/plugins/index.ts +0 -3
  268. package/src/plugins/plugins/nextjs.ts +0 -19
  269. package/src/plugins/plugins/vite.ts +0 -10
  270. package/src/plugins/plugins/vitest.ts +0 -9
  271. package/src/prompt/prompt-runtime.ts +0 -380
  272. package/src/runtime/adaptive-plan.ts +0 -563
  273. package/src/runtime/agent-control.ts +0 -215
  274. package/src/runtime/agent-memory.ts +0 -81
  275. package/src/runtime/agent-observability.ts +0 -122
  276. package/src/runtime/anchor-manager.ts +0 -475
  277. package/src/runtime/async-marker.ts +0 -32
  278. package/src/runtime/async-runner.ts +0 -381
  279. package/src/runtime/attention-events.ts +0 -28
  280. package/src/runtime/auto-summarize.ts +0 -344
  281. package/src/runtime/background-runner.ts +0 -837
  282. package/src/runtime/batch-barrier.ts +0 -147
  283. package/src/runtime/broker-issuer.ts +0 -37
  284. package/src/runtime/cancellation-token.ts +0 -99
  285. package/src/runtime/cancellation.ts +0 -99
  286. package/src/runtime/capability-inventory.ts +0 -137
  287. package/src/runtime/chain-parser.ts +0 -237
  288. package/src/runtime/chain-runner.ts +0 -606
  289. package/src/runtime/checkpoint.ts +0 -291
  290. package/src/runtime/child-pi-constants.ts +0 -42
  291. package/src/runtime/child-pi-kill.ts +0 -180
  292. package/src/runtime/child-pi-pool.ts +0 -68
  293. package/src/runtime/child-pi-spawn.ts +0 -287
  294. package/src/runtime/child-pi-steering.ts +0 -128
  295. package/src/runtime/child-pi-streams.ts +0 -296
  296. package/src/runtime/child-pi-transcript.ts +0 -169
  297. package/src/runtime/child-pi.ts +0 -1105
  298. package/src/runtime/coalesce-tasks.ts +0 -268
  299. package/src/runtime/code-summary.ts +0 -292
  300. package/src/runtime/command-trace.ts +0 -105
  301. package/src/runtime/compact-pipeline.ts +0 -56
  302. package/src/runtime/compact-stages/ansi-strip-stage.ts +0 -25
  303. package/src/runtime/compact-stages/blank-collapse-stage.ts +0 -31
  304. package/src/runtime/compact-stages/deduplicate-stage.ts +0 -34
  305. package/src/runtime/compact-stages/head-snap-stage.ts +0 -57
  306. package/src/runtime/compact-stages/index.ts +0 -23
  307. package/src/runtime/compact-stages/tail-capture-stage.ts +0 -77
  308. package/src/runtime/compact-stages/truncation-stage.ts +0 -74
  309. package/src/runtime/compaction-summary.ts +0 -278
  310. package/src/runtime/completion-guard.ts +0 -207
  311. package/src/runtime/concurrency.ts +0 -58
  312. package/src/runtime/crash-classification.ts +0 -229
  313. package/src/runtime/crash-recovery.ts +0 -577
  314. package/src/runtime/crew-agent-records.ts +0 -593
  315. package/src/runtime/crew-agent-runtime.ts +0 -62
  316. package/src/runtime/crew-broker-child.ts +0 -88
  317. package/src/runtime/crew-broker-client.ts +0 -673
  318. package/src/runtime/crew-broker-tokens.ts +0 -84
  319. package/src/runtime/crew-broker.ts +0 -1276
  320. package/src/runtime/crew-hooks.ts +0 -227
  321. package/src/runtime/cross-extension-rpc.ts +0 -143
  322. package/src/runtime/custom-tools/irc-tool.ts +0 -282
  323. package/src/runtime/custom-tools/submit-result-tool.ts +0 -102
  324. package/src/runtime/deadletter.ts +0 -47
  325. package/src/runtime/delivery-coordinator.ts +0 -211
  326. package/src/runtime/delta-conflict.ts +0 -348
  327. package/src/runtime/deterministic-ast.ts +0 -161
  328. package/src/runtime/diagnostic-export.ts +0 -171
  329. package/src/runtime/direct-run.ts +0 -45
  330. package/src/runtime/dwf-state-store.ts +0 -102
  331. package/src/runtime/dynamic-workflow-context.ts +0 -1016
  332. package/src/runtime/dynamic-workflow-runner.ts +0 -310
  333. package/src/runtime/effectiveness.ts +0 -90
  334. package/src/runtime/errors/crew-errors.ts +0 -162
  335. package/src/runtime/event-stream-bridge.ts +0 -98
  336. package/src/runtime/foreground-control.ts +0 -193
  337. package/src/runtime/foreground-watchdog.ts +0 -132
  338. package/src/runtime/global-worker-cap.ts +0 -96
  339. package/src/runtime/goal-achievement.ts +0 -148
  340. package/src/runtime/goal-evaluator.ts +0 -361
  341. package/src/runtime/goal-loop-runner.ts +0 -828
  342. package/src/runtime/goal-state-store.ts +0 -219
  343. package/src/runtime/green-contract.ts +0 -55
  344. package/src/runtime/group-join.ts +0 -267
  345. package/src/runtime/handoff-manager.ts +0 -598
  346. package/src/runtime/heartbeat-gradient.ts +0 -36
  347. package/src/runtime/heartbeat-watcher.ts +0 -239
  348. package/src/runtime/hidden-handoff.ts +0 -407
  349. package/src/runtime/important-line-classifier.ts +0 -140
  350. package/src/runtime/intercom-bridge.ts +0 -187
  351. package/src/runtime/iteration-hooks.ts +0 -305
  352. package/src/runtime/live-agent-control.ts +0 -136
  353. package/src/runtime/live-agent-manager.ts +0 -704
  354. package/src/runtime/live-control-realtime.ts +0 -56
  355. package/src/runtime/live-extension-bridge.ts +0 -148
  356. package/src/runtime/live-irc.ts +0 -97
  357. package/src/runtime/live-session-health.ts +0 -108
  358. package/src/runtime/live-session-runtime.ts +0 -1139
  359. package/src/runtime/loop-gates.ts +0 -128
  360. package/src/runtime/manifest-cache.ts +0 -375
  361. package/src/runtime/mcp-proxy.ts +0 -105
  362. package/src/runtime/metric-parser.ts +0 -36
  363. package/src/runtime/model-fallback.ts +0 -409
  364. package/src/runtime/model-resolver.ts +0 -126
  365. package/src/runtime/model-scope.ts +0 -158
  366. package/src/runtime/orphan-worker-registry.ts +0 -465
  367. package/src/runtime/output-validator.ts +0 -202
  368. package/src/runtime/overflow-recovery.ts +0 -206
  369. package/src/runtime/parallel-research.ts +0 -68
  370. package/src/runtime/parallel-utils.ts +0 -160
  371. package/src/runtime/parent-guard.ts +0 -134
  372. package/src/runtime/path-overlap.ts +0 -150
  373. package/src/runtime/peer-dep.ts +0 -292
  374. package/src/runtime/per-write-validator.ts +0 -181
  375. package/src/runtime/phase-progress.ts +0 -217
  376. package/src/runtime/phase-tracker.ts +0 -385
  377. package/src/runtime/pi-args.ts +0 -655
  378. package/src/runtime/pi-json-output.ts +0 -170
  379. package/src/runtime/pi-spawn.ts +0 -273
  380. package/src/runtime/pipeline-runner.ts +0 -523
  381. package/src/runtime/plan-templates.ts +0 -201
  382. package/src/runtime/policy-engine.ts +0 -115
  383. package/src/runtime/post-checks.ts +0 -142
  384. package/src/runtime/post-exit-stdio-guard.ts +0 -104
  385. package/src/runtime/process-lifecycle.ts +0 -491
  386. package/src/runtime/process-status.ts +0 -154
  387. package/src/runtime/progress-event-coalescer.ts +0 -44
  388. package/src/runtime/progress-tracker.ts +0 -124
  389. package/src/runtime/prose-compressor.ts +0 -162
  390. package/src/runtime/recovery-recipes.ts +0 -203
  391. package/src/runtime/replace.ts +0 -570
  392. package/src/runtime/resilient-edit.ts +0 -153
  393. package/src/runtime/result-extractor.ts +0 -213
  394. package/src/runtime/retry-executor.ts +0 -100
  395. package/src/runtime/retry-runner.ts +0 -336
  396. package/src/runtime/role-permission.ts +0 -59
  397. package/src/runtime/run-coalesced-task-group.ts +0 -308
  398. package/src/runtime/run-drift.ts +0 -219
  399. package/src/runtime/run-tracker.ts +0 -116
  400. package/src/runtime/runtime-policy.ts +0 -29
  401. package/src/runtime/runtime-resolver.ts +0 -172
  402. package/src/runtime/runtime-warmup.ts +0 -160
  403. package/src/runtime/scheduler.ts +0 -353
  404. package/src/runtime/semaphore.ts +0 -141
  405. package/src/runtime/sensitive-paths.ts +0 -93
  406. package/src/runtime/session-resources.ts +0 -25
  407. package/src/runtime/session-snapshot.ts +0 -59
  408. package/src/runtime/session-usage.ts +0 -79
  409. package/src/runtime/settings-store.ts +0 -155
  410. package/src/runtime/sidechain-output.ts +0 -35
  411. package/src/runtime/single-agent-compose.ts +0 -84
  412. package/src/runtime/skill-effectiveness.ts +0 -524
  413. package/src/runtime/skill-instructions.ts +0 -391
  414. package/src/runtime/stale-reconciler.ts +0 -680
  415. package/src/runtime/stream-preview.ts +0 -184
  416. package/src/runtime/streaming-output.ts +0 -50
  417. package/src/runtime/subagent-manager.ts +0 -561
  418. package/src/runtime/subprocess-tool-registry.ts +0 -70
  419. package/src/runtime/supervisor-contact.ts +0 -64
  420. package/src/runtime/task-display.ts +0 -52
  421. package/src/runtime/task-graph-scheduler.ts +0 -227
  422. package/src/runtime/task-graph.ts +0 -290
  423. package/src/runtime/task-health.ts +0 -83
  424. package/src/runtime/task-id.ts +0 -161
  425. package/src/runtime/task-output-context.ts +0 -602
  426. package/src/runtime/task-packet.ts +0 -268
  427. package/src/runtime/task-quality.ts +0 -199
  428. package/src/runtime/task-runner/capabilities.ts +0 -78
  429. package/src/runtime/task-runner/context-retrieval.ts +0 -163
  430. package/src/runtime/task-runner/live-executor.ts +0 -227
  431. package/src/runtime/task-runner/output-splitter.ts +0 -152
  432. package/src/runtime/task-runner/progress.ts +0 -157
  433. package/src/runtime/task-runner/prompt-builder.ts +0 -278
  434. package/src/runtime/task-runner/prompt-pipeline.ts +0 -88
  435. package/src/runtime/task-runner/result-utils.ts +0 -16
  436. package/src/runtime/task-runner/retrieval-orchestrator.ts +0 -310
  437. package/src/runtime/task-runner/run-projection.ts +0 -125
  438. package/src/runtime/task-runner/state-helpers.ts +0 -176
  439. package/src/runtime/task-runner/tail-read.ts +0 -34
  440. package/src/runtime/task-runner.ts +0 -1420
  441. package/src/runtime/team-runner-artifacts.ts +0 -13
  442. package/src/runtime/team-runner.ts +0 -1993
  443. package/src/runtime/tool-output-pruner.ts +0 -333
  444. package/src/runtime/tool-progress.ts +0 -278
  445. package/src/runtime/usage-tracker.ts +0 -73
  446. package/src/runtime/verification-gates.ts +0 -571
  447. package/src/runtime/verification-integrity.ts +0 -105
  448. package/src/runtime/verification-worktree.ts +0 -186
  449. package/src/runtime/worker-heartbeat.ts +0 -25
  450. package/src/runtime/worker-startup.ts +0 -83
  451. package/src/runtime/workflow-state.ts +0 -195
  452. package/src/runtime/workspace-lock.ts +0 -443
  453. package/src/runtime/workspace-tree.ts +0 -312
  454. package/src/runtime/yield-handler.ts +0 -224
  455. package/src/runtime/zombie-scanner.ts +0 -298
  456. package/src/schema/config-schema.ts +0 -327
  457. package/src/schema/team-tool-schema.ts +0 -415
  458. package/src/schema/validation-types.ts +0 -159
  459. package/src/skills/discover-skills.ts +0 -197
  460. package/src/skills/skill-templates.ts +0 -456
  461. package/src/skills/validate.ts +0 -285
  462. package/src/state/active-run-registry.ts +0 -439
  463. package/src/state/artifact-store.ts +0 -176
  464. package/src/state/atomic-write.ts +0 -903
  465. package/src/state/blob-store.ts +0 -308
  466. package/src/state/contracts.ts +0 -158
  467. package/src/state/crew-init.ts +0 -269
  468. package/src/state/decision-ledger.ts +0 -345
  469. package/src/state/event-log-rotation.ts +0 -342
  470. package/src/state/event-log.ts +0 -1311
  471. package/src/state/event-reconstructor.ts +0 -229
  472. package/src/state/gitignore-manager.ts +0 -46
  473. package/src/state/health-store.ts +0 -79
  474. package/src/state/hook-instinct-bridge.ts +0 -94
  475. package/src/state/hook-integrations.ts +0 -51
  476. package/src/state/instinct-store.ts +0 -275
  477. package/src/state/jsonl-writer.ts +0 -107
  478. package/src/state/locks.ts +0 -562
  479. package/src/state/mailbox.ts +0 -916
  480. package/src/state/observation-store.ts +0 -176
  481. package/src/state/run-cache.ts +0 -193
  482. package/src/state/run-graph.ts +0 -183
  483. package/src/state/run-metrics.ts +0 -165
  484. package/src/state/schedule.ts +0 -166
  485. package/src/state/session-state-map.ts +0 -51
  486. package/src/state/state-store.ts +0 -958
  487. package/src/state/task-claims.ts +0 -61
  488. package/src/state/tiered-eval.ts +0 -480
  489. package/src/state/types-eval.ts +0 -58
  490. package/src/state/types.ts +0 -440
  491. package/src/state/usage.ts +0 -138
  492. package/src/state/worker-atomic-writer.ts +0 -194
  493. package/src/subagents/async-entry.ts +0 -1
  494. package/src/subagents/index.ts +0 -3
  495. package/src/subagents/live/control.ts +0 -1
  496. package/src/subagents/live/manager.ts +0 -1
  497. package/src/subagents/live/realtime.ts +0 -1
  498. package/src/subagents/live/session-runtime.ts +0 -1
  499. package/src/subagents/manager.ts +0 -1
  500. package/src/subagents/spawn.ts +0 -1
  501. package/src/teams/discover-teams.ts +0 -187
  502. package/src/teams/team-config.ts +0 -27
  503. package/src/teams/team-serializer.ts +0 -38
  504. package/src/tools/safe-bash-extension.ts +0 -54
  505. package/src/tools/safe-bash.ts +0 -505
  506. package/src/types/diff.d.ts +0 -18
  507. package/src/types/new-api-types.ts +0 -35
  508. package/src/ui/agent-management-overlay.ts +0 -160
  509. package/src/ui/card-colors.ts +0 -139
  510. package/src/ui/crew-footer.ts +0 -102
  511. package/src/ui/crew-select-list.ts +0 -111
  512. package/src/ui/dashboard-panes/agents-pane.ts +0 -164
  513. package/src/ui/dashboard-panes/cancellation-pane.ts +0 -66
  514. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  515. package/src/ui/dashboard-panes/health-pane.ts +0 -31
  516. package/src/ui/dashboard-panes/mailbox-pane.ts +0 -35
  517. package/src/ui/dashboard-panes/metrics-pane.ts +0 -37
  518. package/src/ui/dashboard-panes/progress-pane.ts +0 -38
  519. package/src/ui/dashboard-panes/transcript-pane.ts +0 -10
  520. package/src/ui/deploy-bundled-themes.ts +0 -71
  521. package/src/ui/dwf-phase-display.ts +0 -151
  522. package/src/ui/dynamic-border.ts +0 -35
  523. package/src/ui/heartbeat-aggregator.ts +0 -74
  524. package/src/ui/key-utils.ts +0 -42
  525. package/src/ui/keybinding-map.ts +0 -248
  526. package/src/ui/layout-primitives.ts +0 -106
  527. package/src/ui/live-conversation-overlay.ts +0 -183
  528. package/src/ui/live-duration.ts +0 -55
  529. package/src/ui/live-run-sidebar.ts +0 -301
  530. package/src/ui/loaders.ts +0 -176
  531. package/src/ui/mascot.ts +0 -436
  532. package/src/ui/overlays/agent-picker-overlay.ts +0 -65
  533. package/src/ui/overlays/confirm-overlay.ts +0 -60
  534. package/src/ui/overlays/help-overlay.ts +0 -177
  535. package/src/ui/overlays/mailbox-compose-overlay.ts +0 -181
  536. package/src/ui/overlays/mailbox-compose-preview.ts +0 -78
  537. package/src/ui/overlays/mailbox-detail-overlay.ts +0 -154
  538. package/src/ui/pi-ui-compat.ts +0 -68
  539. package/src/ui/powerbar-publisher.ts +0 -459
  540. package/src/ui/render-coalescer.ts +0 -101
  541. package/src/ui/render-diff.ts +0 -160
  542. package/src/ui/render-scheduler.ts +0 -224
  543. package/src/ui/run-action-dispatcher.ts +0 -182
  544. package/src/ui/run-dashboard.ts +0 -913
  545. package/src/ui/run-event-bus.ts +0 -381
  546. package/src/ui/run-snapshot-cache.ts +0 -1057
  547. package/src/ui/settings-overlay.ts +0 -1067
  548. package/src/ui/snapshot-types.ts +0 -90
  549. package/src/ui/spinner.ts +0 -17
  550. package/src/ui/status-colors.ts +0 -143
  551. package/src/ui/syntax-highlight.ts +0 -136
  552. package/src/ui/terminal-status.ts +0 -269
  553. package/src/ui/theme-adapter.ts +0 -271
  554. package/src/ui/theme-discovery.ts +0 -199
  555. package/src/ui/tool-progress-formatter.ts +0 -93
  556. package/src/ui/tool-render.ts +0 -453
  557. package/src/ui/tool-renderers/brief-mode.ts +0 -225
  558. package/src/ui/tool-renderers/index.ts +0 -726
  559. package/src/ui/transcript-cache.ts +0 -128
  560. package/src/ui/transcript-entries.ts +0 -256
  561. package/src/ui/transcript-viewer.ts +0 -424
  562. package/src/ui/widget/index.ts +0 -357
  563. package/src/ui/widget/widget-formatters.ts +0 -182
  564. package/src/ui/widget/widget-model.ts +0 -110
  565. package/src/ui/widget/widget-renderer.ts +0 -182
  566. package/src/ui/widget/widget-types.ts +0 -36
  567. package/src/utils/bm25-search.ts +0 -235
  568. package/src/utils/completion-dedupe.ts +0 -63
  569. package/src/utils/conflict-detect.ts +0 -668
  570. package/src/utils/env-allowlist.ts +0 -30
  571. package/src/utils/env-filter.ts +0 -155
  572. package/src/utils/file-coalescer.ts +0 -98
  573. package/src/utils/fingerprint.ts +0 -180
  574. package/src/utils/frontmatter.ts +0 -70
  575. package/src/utils/fs-watch.ts +0 -37
  576. package/src/utils/gh-protocol.ts +0 -556
  577. package/src/utils/git.ts +0 -260
  578. package/src/utils/guards.ts +0 -107
  579. package/src/utils/ids.ts +0 -24
  580. package/src/utils/incremental-reader.ts +0 -220
  581. package/src/utils/internal-error.ts +0 -10
  582. package/src/utils/names.ts +0 -36
  583. package/src/utils/ndjson.ts +0 -115
  584. package/src/utils/paths.ts +0 -227
  585. package/src/utils/project-detector.ts +0 -160
  586. package/src/utils/redaction.ts +0 -347
  587. package/src/utils/resolve-shell.ts +0 -36
  588. package/src/utils/run-watcher-registry.ts +0 -152
  589. package/src/utils/safe-paths.ts +0 -383
  590. package/src/utils/scan-cache.ts +0 -144
  591. package/src/utils/session-utils.ts +0 -110
  592. package/src/utils/sleep.ts +0 -54
  593. package/src/utils/socket-path.ts +0 -127
  594. package/src/utils/sse-parser.ts +0 -129
  595. package/src/utils/task-name-generator.ts +0 -337
  596. package/src/utils/timings.ts +0 -33
  597. package/src/utils/token-counter.ts +0 -200
  598. package/src/utils/visual.ts +0 -207
  599. package/src/workflows/cost-estimator.ts +0 -34
  600. package/src/workflows/discover-workflows.ts +0 -290
  601. package/src/workflows/intermediate-store.ts +0 -166
  602. package/src/workflows/preflight-validator.ts +0 -168
  603. package/src/workflows/topology-analyzer.ts +0 -203
  604. package/src/workflows/validate-workflow.ts +0 -50
  605. package/src/workflows/workflow-config.ts +0 -70
  606. package/src/workflows/workflow-serializer.ts +0 -32
  607. package/src/worktree/branch-freshness.ts +0 -112
  608. package/src/worktree/cleanup.ts +0 -341
  609. package/src/worktree/worktree-manager.ts +0 -1200
@@ -1,1105 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import type { AgentConfig } from "../agents/agent-config.ts";
6
- import { DEFAULT_CHILD_PI } from "../config/defaults.ts";
7
- import { registerChildProcess, unregisterChildProcess } from "../extension/crew-cleanup.ts";
8
- import { atomicWriteFile } from "../state/atomic-write.ts";
9
- import type { WorkerExitStatus } from "../state/types.ts";
10
- import { logInternalError } from "../utils/internal-error.ts";
11
- import { redactSecretString } from "../utils/redaction.ts";
12
- import { getActiveBrokerIssuer } from "./broker-issuer.ts";
13
- import { FINAL_DRAIN_MS, HARD_KILL_MS, POST_EXIT_STDIO_GUARD_MS, RESPONSE_TIMEOUT_MS } from "./child-pi-constants.ts";
14
- import { appendBoundedTail, clearHardKillTimer, killProcessTree, registerActiveChild, unregisterActiveChild } from "./child-pi-kill.ts";
15
- import { buildFinalChildPiSpawnOptions, prepareSpawnContext } from "./child-pi-spawn.ts";
16
- import { ChildPiSteeringController } from "./child-pi-steering.ts";
17
- // Internal helpers for active-child bookkeeping (extracted to child-pi-kill.ts).
18
- import { ChildPiLineObserver } from "./child-pi-streams.ts";
19
-
20
- // ── Re-exports from child-pi-kill.ts (H-7 decomposition step 2) ──
21
- // killProcessTree is internal (not previously exported) — keep that invariant.
22
- export {
23
- killProcessPid,
24
- terminateActiveChildPiProcesses,
25
- } from "./child-pi-kill.ts";
26
- // ── Re-export from child-pi-spawn.ts (H-7 decomposition step 6) ──
27
- // buildChildPiSpawnOptions was previously exported from child-pi.ts. Keep the
28
- // public API surface stable by re-exporting from the new module.
29
- // buildFinalChildPiSpawnOptions (BLOCKER 2 / S5) — composed spawn helper that
30
- // owns the canary + filter + spread sequence; lives in child-pi-spawn.ts.
31
- export { buildChildPiSpawnOptions, buildFinalChildPiSpawnOptions } from "./child-pi-spawn.ts";
32
- // ── Re-export from child-pi-streams.ts (H-7 decomposition step 4) ──
33
- export { ChildPiLineObserver } from "./child-pi-streams.ts";
34
-
35
- import { classifyProcessCrash } from "./crash-classification.ts";
36
- import { checkCrewDepth, cleanupTempDir } from "./pi-args.ts";
37
-
38
- import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
39
-
40
- /** Maximum size (bytes) for the ChildPiLineObserver's line accumulation buffer.
41
- * When exceeded, the buffer is force-flushed to prevent unbounded memory growth
42
- * from chatty child processes that produce output without newlines.
43
- * (Constant moved to child-pi-constants.ts.) */
44
-
45
- // Periodic cleanup of dead child process entries to prevent memory leaks.
46
- /**
47
- * SEC-1: Extract a redacted stderr/stdout excerpt for embedding in lifecycle
48
- * events and error messages. The in-memory stdout/stderr accumulators receive
49
- * RAW worker output (only structurally compacted via compactChildPiEvent —
50
- * NOT secret-redacted), so any slice embedded into a persisted event must be
51
- * redacted here. Otherwise worker-emitted secrets (API keys, tokens returned
52
- * from a tool call) leak through diagnostic logs that bypass artifact-store
53
- * redaction.
54
- *
55
- * Extracted as a single helper (8 call sites were duplicating this) so the
56
- * redaction boundary is unit-testable directly. The real spawn error/timeout
57
- * paths are integration-level and NOT reachable via PI_TEAMS_MOCK_CHILD_PI
58
- * (the mock returns before the lifecycle-event handlers run), so a behavior
59
- * test must target this helper rather than the full runChildPi path.
60
- */
61
- export function redactStderrExcerpt(stderr: string, maxChars: number): string {
62
- return redactSecretString(stderr.slice(-maxChars));
63
- }
64
-
65
- /**
66
- * B6: spawn taskkill and attach an 'error' listener. spawn() emits ENOENT/EACCES
67
- * asynchronously via the 'error' event (not as a throw), so an unlistened spawn
68
- * can crash the parent as an uncaught exception. taskkill is a standard Windows
69
- * binary so this is defensive, but the listener keeps failures bounded.
70
- */
71
-
72
- /** Structured lifecycle event emitted by child-pi for critical transitions. */
73
- export interface ChildPiLifecycleEvent {
74
- /** Event discriminator. */
75
- type: "spawned" | "spawn_error" | "response_timeout" | "final_drain" | "hard_kill" | "exit" | "close";
76
- /** Process ID when available. */
77
- pid?: number;
78
- /** Exit code for exit/close events. */
79
- exitCode?: number | null;
80
- /** Error message for error events. */
81
- error?: string;
82
- /** Stderr captured at timeout moment (for response_timeout events). */
83
- stderr?: string;
84
- /** Last N chars of stderr for error context (exit/error events). */
85
- stderrExcerpt?: string;
86
- /** Timestamp (ISO). */
87
- ts: string;
88
- /** F12: optional cause for `final_drain` events. `"stdout-quiet"` indicates
89
- * the drain was triggered by the quiet-window early-exit rather than the
90
- * default 5 s ceiling. Other drain reasons (default) leave this undefined. */
91
- reason?: "stdout-quiet";
92
- /** Phase-0 diagnostic (HB-003a): the signal that killed the child (when
93
- * available). Was previously discarded after building the error string. */
94
- signal?: string;
95
- /** Phase-0 diagnostic (HB-003a): final-drain race timing, present only on
96
- * exit events where a drain timer was armed. Surfaces the exit-null race. */
97
- diagnostic?: {
98
- finalDrainArmed: boolean;
99
- forcedFinalDrain: boolean;
100
- finalDrainFiredMonotonicMs?: number;
101
- finalAssistantEventMonotonicMs?: number;
102
- exitMonotonicMs: number;
103
- };
104
- }
105
-
106
- export interface ChildPiRunInput {
107
- cwd: string;
108
- task: string;
109
- agent: AgentConfig;
110
- model?: string;
111
- skillPaths?: string[];
112
- signal?: AbortSignal;
113
- transcriptPath?: string;
114
- onStdoutLine?: (line: string) => void;
115
- onJsonEvent?: (event: unknown) => void;
116
- onSpawn?: (pid: number) => void;
117
- /** Structured lifecycle events for durable logging (spawn, crash, timeout, kill, exit). */
118
- onLifecycleEvent?: (event: ChildPiLifecycleEvent) => void;
119
- maxDepth?: number;
120
- finalDrainMs?: number;
121
- /** F12: early-exit the drain when stdout has been silent for this many ms
122
- * after the final assistant event. Set to ≥ finalDrainMs to disable. */
123
- finalDrainQuietMs?: number;
124
- hardKillMs?: number;
125
- responseTimeoutMs?: number;
126
- /** Soft limit on assistant turns — inject steer at this count. */
127
- maxTurns?: number;
128
- /** Extra turns after soft limit before hard abort. Default: 5. */
129
- graceTurns?: number;
130
- /** Parent conversation context to inherit when inheritContext is true. */
131
- parentContext?: string;
132
- /** When true, prepend parentContext to the task prompt. */
133
- inheritContext?: boolean;
134
- /** Pass to pi to mark certain commands as context-excluded. Default: false */
135
- excludeContextBash?: boolean;
136
- /** pi session ID for session naming (aligns with pi-crew run ID) */
137
- sessionId?: string;
138
- /** Path to steering JSONL file for real-time steer injection. */
139
- steeringFile?: string;
140
- /** Run ID for cleanup tracking */
141
- runId?: string;
142
- /** Agent ID for cleanup tracking */
143
- agentId?: string;
144
- /** Role for tool restrictions (from role-tools.ts) */
145
- role?: string;
146
- /** Root directory for artifacts (used to validate transcriptPath). */
147
- artifactsRoot?: string;
148
- /**
149
- * Optional broker spawn context (Phase 0 inter-pi broker). When present,
150
- * `prepareSpawnContext` injects `PI_CREW_BROKER_SOCKET` and
151
- * `PI_CREW_BROKER_TOKEN` into the child env (control-namespace keys,
152
- * accepted by `assertOnlyControlEnvKeys`). The token is a 128-bit random
153
- * value issued by the parent's CrewBroker at spawn time; it lives only
154
- * in the parent's in-memory `Map<runId, token>` and in the child's env.
155
- * NEVER persisted to manifest, events, mailbox, or any run-dir file.
156
- */
157
- brokerSpawn?: { socketPath: string; token: string };
158
- /**
159
- * Optional Phase 0 broker credentials issuer. Called only when
160
- * `brokerSpawn` is not already set on the input. The default no-op
161
- * issuer returns undefined, leaving the child to run without broker
162
- * credentials (today's behavior — file paths remain authoritative).
163
- * The production wiring passes a closure that delegates to the
164
- * session's `CrewBrokerLifecycleController.issueForChild`.
165
- */
166
- brokerIssuer?: (runId: string) => Promise<{ socketPath: string; token: string } | undefined>;
167
- }
168
-
169
- export interface ChildPiRunResult {
170
- exitCode: number | null;
171
- stdout: string;
172
- stderr: string;
173
- error?: string;
174
- /** RAW (uncapped) final assistant text, captured at stream-parse time BEFORE
175
- * the 16K transcript compaction. This is the AUTHORITATIVE worker output —
176
- * it becomes results/<id>.txt so downstream dependencies are not bounded by
177
- * the transcript's telemetry cap. Undefined when no assistant text was seen
178
- * (mock paths, error paths) — callers MUST fall back to transcript-derived
179
- * finalText. See research-findings/output-handling-deep-dive.md §A. */
180
- rawFinalText?: string;
181
- exitStatus?: WorkerExitStatus;
182
- /** True if the agent was hard-aborted (max_turns + grace exceeded). */
183
- aborted?: boolean;
184
- /** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
185
- steered?: boolean;
186
- /** #7 hardening: bounded digest of intermediate findings (last N tool results or
187
- * assistant text lines) from the run. Populated by ChildPiLineObserver so that
188
- * workers that exhaust their budget on tool calls (never emit final assistant
189
- * text) still produce a non-empty result. Consumers should prefer rawFinalText
190
- * first — this is a last-resort fallback. */
191
- intermediateFindings?: string;
192
- }
193
-
194
- // Base allowlist of non-provider env vars always passed to child workers.
195
- // ── Transcript batching + compaction (H-7 decomposition step 1) ────────
196
- // Extracted to ./child-pi-transcript.ts. Re-exported here to preserve the
197
- // existing public API surface.
198
- export {
199
- appendTranscript,
200
- compactString,
201
- compactValue,
202
- flushPendingTranscriptWrites,
203
- resetTranscriptBatchState,
204
- } from "./child-pi-transcript.ts";
205
-
206
- /** Mock-only path — real code path reuses a single observer.
207
- * OPT-06 follow-up: returns a Promise so callers can await the transcript
208
- * drain before resolving runChildPi. Without this, mock-mode callers that
209
- * read the transcript file post-run see ENOENT (the async file handle had
210
- * not yet been opened). */
211
- async function observeStdoutChunk(input: ChildPiRunInput, text: string): Promise<void> {
212
- const observer = new ChildPiLineObserver(input);
213
- observer.observe(text);
214
- await observer.flush();
215
- }
216
-
217
- function asRecord(value: unknown): Record<string, unknown> | undefined {
218
- return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
219
- }
220
-
221
- function isFinalAssistantEvent(event: unknown): boolean {
222
- const obj = asRecord(event);
223
- if (obj?.type !== "message_end") return false;
224
- const message = asRecord(obj.message);
225
- const role = message?.role;
226
- if (role !== undefined && role !== "assistant") return false;
227
- const stopReason =
228
- typeof message?.stopReason === "string" ? message.stopReason : typeof obj.stopReason === "string" ? obj.stopReason : undefined;
229
- if (stopReason !== undefined && stopReason !== "stop") return false;
230
- const content = Array.isArray(message?.content) ? message.content : [];
231
- return !content.some((part) => asRecord(part)?.type === "toolCall");
232
- }
233
-
234
- export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResult> {
235
- // Phase 1 (live-session parity): prepend parent context when inheritContext is true.
236
- // This mirrors the effectivePrompt logic in live-session-runtime.ts so that
237
- // child-process workers receive the same inherited-context treatment.
238
- const effectiveTask =
239
- input.inheritContext === true && input.parentContext
240
- ? `${input.parentContext}\n\n---\n# Child Worker Task\n${input.task}`
241
- : input.task;
242
- const depth = checkCrewDepth(input.maxDepth);
243
- if (depth.blocked)
244
- return {
245
- exitCode: 1,
246
- stdout: "",
247
- stderr: `pi-crew depth guard blocked child worker: depth ${depth.depth} >= max ${depth.maxDepth}`,
248
- };
249
- const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
250
- if (mock) {
251
- // SECURITY (Issue #2): Mock mode security model is intentionally asymmetric.
252
- // PI_TEAMS_MOCK_CHILD_PI is in the allowlist (passed to children) but
253
- // PI_CREW_ALLOW_MOCK is NOT in the allowlist — it is only checked in the
254
- // parent process scope. This means:
255
- // (1) If an attacker sets PI_CREW_ALLOW_MOCK in the parent's environment,
256
- // it will NOT be passed to child processes (safe).
257
- // (2) Mock mode activation in the child always fails the PI_CREW_ALLOW_MOCK
258
- // check, so mock mode can only be triggered from the parent process.
259
- // This asymmetry is intentional: PI_CREW_ALLOW_MOCK must be set in the Pi root
260
- // process (the entry point that spawns children), not inherited from a parent.
261
- // Setup hooks cannot inject PI_CREW_ALLOW_MOCK into the parent's env.
262
- const allowMock = process.env.PI_CREW_ALLOW_MOCK === "1" || process.env.PI_CREW_ALLOW_MOCK === "true";
263
- if (!allowMock) {
264
- return {
265
- exitCode: 1,
266
- stdout: "",
267
- stderr: "Mock mode requires PI_CREW_ALLOW_MOCK=1",
268
- };
269
- }
270
- // SECURITY: Log mock mode activation prominently for audit trail
271
- logInternalError("child-pi.mock", new Error(`Mock mode active: ${mock}`), "NOT running real agents");
272
- if (mock === "success") {
273
- const stdout = `[MOCK] Success for ${input.agent.name}\n`;
274
- await observeStdoutChunk(input, stdout);
275
- return { exitCode: 0, stdout, stderr: "" };
276
- }
277
- if (mock === "json-success" || mock === "adaptive-plan") {
278
- const text =
279
- mock === "adaptive-plan" && effectiveTask.includes("ADAPTIVE_PLAN_JSON_START")
280
- ? `[MOCK] Adaptive plan\nADAPTIVE_PLAN_JSON_START\n${JSON.stringify({
281
- phases: [
282
- {
283
- name: "research",
284
- tasks: [
285
- {
286
- role: "explorer",
287
- task: "Explore adaptive target",
288
- },
289
- {
290
- role: "analyst",
291
- task: "Analyze adaptive target",
292
- },
293
- {
294
- role: "planner",
295
- task: "Plan adaptive target",
296
- },
297
- ],
298
- },
299
- {
300
- name: "build",
301
- tasks: [
302
- {
303
- role: "executor",
304
- task: "Implement adaptive target",
305
- },
306
- ],
307
- },
308
- {
309
- name: "check",
310
- tasks: [
311
- {
312
- role: "reviewer",
313
- task: "Review adaptive target",
314
- },
315
- {
316
- role: "test-engineer",
317
- task: "Test adaptive target",
318
- },
319
- {
320
- role: "writer",
321
- task: "Summarize adaptive target",
322
- },
323
- ],
324
- },
325
- ],
326
- })}\nADAPTIVE_PLAN_JSON_END`
327
- : `[MOCK] JSON success for ${input.agent.name}`;
328
- const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
329
- await observeStdoutChunk(input, stdout);
330
- return { exitCode: 0, stdout, stderr: "" };
331
- }
332
- if (mock === "retryable-failure")
333
- return {
334
- exitCode: 1,
335
- stdout: "",
336
- stderr: "[MOCK] rate limit: mock failure",
337
- };
338
- // E2E fallback-chain fixture: invocation #1 returns a SILENT retryable
339
- // failure (exit code 0, no real assistant text, message_end carries a
340
- // retryable-pattern errorMessage). Invocation #2+ delegates to the
341
- // standard json-success shape. Counter lives in os.tmpdir() keyed by
342
- // process.pid + mock name so concurrent test processes don't collide.
343
- // The test cleans up the file in its finally block.
344
- if (mock === "retryable-failure-then-success") {
345
- const counterFile = path.join(os.tmpdir(), `pi-crew-mock-counter-${process.pid}-retryable-failure-then-success`);
346
- let count = 0;
347
- try {
348
- const raw = fs.readFileSync(counterFile, "utf-8");
349
- const parsed = Number.parseInt(raw.trim(), 10);
350
- if (Number.isFinite(parsed) && parsed >= 0) count = parsed;
351
- } catch {
352
- // file missing or unreadable — first invocation in this process
353
- }
354
- count += 1;
355
- try {
356
- atomicWriteFile(counterFile, String(count));
357
- } catch (error) {
358
- logInternalError("child-pi.mock-counter-write", error as Error, `file=${counterFile}`);
359
- }
360
- if (count === 1) {
361
- // Silent retryable failure: exit 0, no real text, message_end
362
- // carries errorMessage matching `/provider[_ ]?error/i` so that
363
- // `detectRetryableModelFailureFromOutput` surfaces it as an error
364
- // and `isRetryableModelFailure` routes the next attempt to the
365
- // next candidate model. `stopReason:"error"` (NOT "stop") so
366
- // `isFinalAssistantEvent` does NOT prematurely terminate the run.
367
- const failureEvent = {
368
- type: "message_end",
369
- message: {
370
- role: "assistant",
371
- content: [],
372
- errorMessage: "Provider error: api_error",
373
- stopReason: "error",
374
- },
375
- };
376
- const stdout = `${JSON.stringify(failureEvent)}\n`;
377
- await observeStdoutChunk(input, stdout);
378
- return { exitCode: 0, stdout, stderr: "" };
379
- }
380
- // Subsequent invocations: delegate to json-success shape so the
381
- // fallback chain's second attempt succeeds and the run completes.
382
- const text = `[MOCK] JSON success for ${input.agent.name}`;
383
- const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
384
- await observeStdoutChunk(input, stdout);
385
- return { exitCode: 0, stdout, stderr: "" };
386
- }
387
- return { exitCode: 1, stdout: "", stderr: `[MOCK] failure: ${mock}` };
388
- }
389
- // H-7 step 6: spawn/env/args preparation extracted to child-pi-spawn.ts.
390
- // prepareSpawnContext builds the worker args, attaches the steering file env,
391
- // and handles the pre-spawn abort check (returns an immediate-abort result
392
- // if the parent signal has already fired).
393
- //
394
- // Phase 0 broker: if the caller did not pre-fill `brokerSpawn`, ask the
395
- // optional issuer for one. The issuer is gated by the lifecycle controller
396
- // (root-session + flag); a no-op issuer yields undefined (no credentials).
397
- let brokerSpawn = input.brokerSpawn;
398
- const brokerIssuer = input.brokerIssuer ?? getActiveBrokerIssuer();
399
- if (!brokerSpawn && brokerIssuer && input.runId) {
400
- try {
401
- brokerSpawn = await brokerIssuer(input.runId);
402
- } catch {
403
- brokerSpawn = undefined;
404
- }
405
- }
406
- const spawnPrep = prepareSpawnContext(brokerSpawn ? { ...input, brokerSpawn } : input, effectiveTask);
407
- if (spawnPrep.kind === "aborted") return spawnPrep.result;
408
- const { spawnSpec, mergedEnv, tempDir, builtEnv } = spawnPrep.ctx;
409
- try {
410
- return await new Promise<ChildPiRunResult>((resolve) => {
411
- // Compose the final SpawnOptions: canary + filter + spread are now
412
- // owned by buildFinalChildPiSpawnOptions (see child-pi-spawn.ts, BLOCKER 2 / S5).
413
- const spawnOptions = buildFinalChildPiSpawnOptions(input.cwd, mergedEnv, builtEnv, input.model);
414
- const child = spawn(spawnSpec.command, spawnSpec.args, spawnOptions);
415
- if (child.pid) {
416
- registerActiveChild(child.pid, child);
417
- input.onSpawn?.(child.pid);
418
- input.onLifecycleEvent?.({
419
- type: "spawned",
420
- pid: child.pid,
421
- ts: new Date().toISOString(),
422
- });
423
- // Register with cleanup handler for graceful shutdown
424
- if (input.runId && input.agentId) {
425
- registerChildProcess(child.pid, input.runId, input.agentId);
426
- }
427
- } else {
428
- input.onLifecycleEvent?.({
429
- type: "spawn_error",
430
- error: "spawn returned no pid",
431
- ts: new Date().toISOString(),
432
- });
433
- }
434
- let stdout = "";
435
- let stderr = "";
436
- let settled = false;
437
- let childExited = false;
438
- let postExitGuardCleanup: (() => void) | undefined;
439
- let finalDrainTimer: NodeJS.Timeout | undefined;
440
- let hardKillTimer: NodeJS.Timeout | undefined;
441
- let noResponseTimer: NodeJS.Timeout | undefined;
442
- const finalDrainMs = input.finalDrainMs ?? FINAL_DRAIN_MS;
443
- const hardKillMs = input.hardKillMs ?? HARD_KILL_MS;
444
- // Phase-0 diagnostic (HB-003a): track the final-drain race that produces
445
- // `exit null` for ctx.agent({disableTools:true}). These vars are READ-ONLY
446
- // instrumentation — no behavior change. finalDrainArmed lets the close
447
- // handler know a drain timer existed even after clearFinalDrainTimers() ran;
448
- // spawnMonotonicMs gives us relative timing to distinguish a race from a crash.
449
- let finalDrainArmed = false;
450
- // F12: monotonic timestamp of the last stdout JSON event (any event —
451
- // we want to know when stdout *stopped*, not when the final assistant
452
- // event arrived). Updated on every onJsonEvent dispatch.
453
- let lastStdoutActivityMonotonicMs = performance.now();
454
- let finalDrainFiredMonotonicMs: number | undefined;
455
- const spawnMonotonicMs = performance.now();
456
- let finalAssistantEventMonotonicMs: number | undefined;
457
- // FIX (Round 14): Bound the env-controlled response timeout to
458
- // [1_000ms, 3_600_000ms] (1s–1h) so a hostile or accidental value
459
- // (e.g. 1, or 999_999_999) cannot disable the timeout or cause
460
- // instant kills. Out-of-range values fall back to the input or
461
- // built-in default.
462
- const RESPONSE_TIMEOUT_MIN_MS = 1_000;
463
- const RESPONSE_TIMEOUT_MAX_MS = 3_600_000;
464
- const responseTimeoutEnv = Number.parseInt(process.env.PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS ?? "", 10);
465
- const envInRange =
466
- Number.isFinite(responseTimeoutEnv) &&
467
- responseTimeoutEnv >= RESPONSE_TIMEOUT_MIN_MS &&
468
- responseTimeoutEnv <= RESPONSE_TIMEOUT_MAX_MS;
469
- const responseTimeoutMs = envInRange ? responseTimeoutEnv : (input.responseTimeoutMs ?? RESPONSE_TIMEOUT_MS);
470
- let responseTimeoutHit = false;
471
- let forcedFinalDrain = false;
472
- let abortRequested = input.signal?.aborted === true;
473
- let hardKilled = false;
474
- const cleanupErrors: string[] = [];
475
- const steeringController = new ChildPiSteeringController(input.maxTurns, input.graceTurns);
476
- // Track in-flight operations for proper rejection on unexpected exit
477
- interface PendingOperation {
478
- id: string;
479
- type: "prompt" | "steer" | "json_event";
480
- startedAt: number;
481
- }
482
- const pendingOperations = new Map<string, PendingOperation>();
483
- let operationIdCounter = 0;
484
-
485
- const startOperation = (type: PendingOperation["type"]): string => {
486
- const id = `op-${++operationIdCounter}`;
487
- pendingOperations.set(id, { id, type, startedAt: Date.now() });
488
- return id;
489
- };
490
-
491
- const completeOperation = (id: string): void => {
492
- pendingOperations.delete(id);
493
- };
494
-
495
- const rejectPendingOperations = (error: Error): void => {
496
- pendingOperations.forEach((op, id) => {
497
- logInternalError(
498
- "child-pi.pending-operation-rejected",
499
- error,
500
- `opId=${id} type=${op.type} elapsed=${Date.now() - op.startedAt}ms`,
501
- );
502
- });
503
- pendingOperations.clear();
504
- };
505
-
506
- const steerInjectionFailed = false;
507
- let abortDueToParentSignal = false;
508
- // CP-1: track whether the turn-limit hard-abort has been initiated. Once
509
- // true, we must NOT restart the no-response timer — the child is already
510
- // being killed via killProcessTree (SIGTERM → SIGKILL after 3s), and
511
- // restarting the timer would delay detection of a SIGTERM-ignoring child.
512
- // Round 27 (BUG 4): extract to a named handler so settle() can remove it.
513
- // The previous anonymous listener was never removed → on runs with >10
514
- // tasks sharing one AbortSignal (background-runner), Node emitted
515
- // MaxListenersExceededWarning and each leaked listener pinned the task's
516
- // stack frame (abortDueToParentSignal closure) in memory. { once: true }
517
- // only auto-removes AFTER the signal fires; on normal completion it leaks.
518
- const onParentAbort = (): void => {
519
- abortDueToParentSignal = true;
520
- };
521
- input.signal?.addEventListener("abort", onParentAbort, {
522
- once: true,
523
- });
524
- const restartNoResponseTimer = (): void => {
525
- if (responseTimeoutMs <= 0) return;
526
- if (noResponseTimer) clearTimeout(noResponseTimer);
527
- noResponseTimer = setTimeout(() => {
528
- responseTimeoutHit = true;
529
- // Capture stderr at timeout moment for debugging
530
- // SEC-1: redact secrets before embedding in lifecycle event so
531
- // worker-emitted secrets (API keys etc.) don't bypass redaction.
532
- const timeoutStderr = redactStderrExcerpt(stderr, 1024); // Last 1KB of stderr (redacted, SEC-1)
533
- input.onLifecycleEvent?.({
534
- type: "response_timeout",
535
- pid: child.pid,
536
- error: `No output for ${responseTimeoutMs}ms`,
537
- ts: new Date().toISOString(),
538
- stderr: timeoutStderr || undefined,
539
- });
540
- killProcessTree(child.pid, child);
541
- try {
542
- child.kill(process.platform === "win32" ? undefined : "SIGTERM");
543
- } catch (error) {
544
- logInternalError("child-pi.response-timeout-term", error, `pid=${child.pid}`);
545
- }
546
- // #3 hardening: if the child never exits (zombie) and neither the
547
- // 'exit' nor 'close' event ever fires, the promise would hang forever.
548
- // SIGKILL fires ~3s after SIGTERM via hardKillTimer in killProcessPid,
549
- // but on platforms where SIGKILL also fails (e.g. permission issues),
550
- // add a bounded safety settle so the promise always resolves. Using
551
- // hardKillMs + 2s as the safety window: enough for SIGKILL to work
552
- // normally, but forces settle if the process is truly immortal.
553
- // NOTE: we do NOT clear hardKillTimer here (that would defeat its purpose);
554
- // we intentionally add a parallel safety path.
555
- const SAFETY_SETTLE_MS = HARD_KILL_MS + 2000;
556
- const safetyTimer = setTimeout(() => {
557
- if (settled || childExited) return;
558
- logInternalError(
559
- "child-pi.settle-safety-fired",
560
- new Error(`Child did not exit within ${SAFETY_SETTLE_MS}ms of kill; forcing settle`),
561
- `pid=${child.pid}, responseTimeoutMs=${responseTimeoutMs}`,
562
- );
563
- // Verify the child is still alive before forcing settle.
564
- // If it somehow exited between childExited=false and here, the
565
- // settled/childExited guard prevents double-settle (harmless but noisy).
566
- try {
567
- process.kill(child.pid!, 0);
568
- // Child still alive — force settle with timeout error.
569
- const timeoutErr = `Child Pi produced no new output for ${responseTimeoutMs}ms; killed but did not exit within ${SAFETY_SETTLE_MS}ms (possible zombie).`;
570
- void settle({
571
- exitCode: null,
572
- stdout,
573
- stderr,
574
- error: timeoutErr,
575
- exitStatus: {
576
- exitCode: null,
577
- cancelled: abortRequested,
578
- timedOut: true,
579
- killed: hardKilled,
580
- cleanupErrors,
581
- finalDrainMs,
582
- crashClass: "timeout",
583
- },
584
- });
585
- } catch {
586
- // ESRCH / EPERM — child is already gone. The 'exit'/'close' handler
587
- // will fire shortly (or already fired in a race). Let it settle normally.
588
- }
589
- }, SAFETY_SETTLE_MS);
590
- safetyTimer.unref();
591
- }, responseTimeoutMs);
592
- noResponseTimer.unref();
593
- };
594
- const clearNoResponseTimer = (): void => {
595
- if (noResponseTimer) clearTimeout(noResponseTimer);
596
- noResponseTimer = undefined;
597
- };
598
- restartNoResponseTimer();
599
- const lineObserver = new ChildPiLineObserver({
600
- ...input,
601
- onStdoutLine: (line) => {
602
- if (!steeringController.isHardAbortInitiated()) restartNoResponseTimer();
603
- stdout = appendBoundedTail(stdout, `${line}\n`);
604
- input.onStdoutLine?.(line);
605
- },
606
- onJsonEvent: (event) => {
607
- if (!steeringController.isHardAbortInitiated()) restartNoResponseTimer();
608
- const eventOpId = startOperation("json_event");
609
- try {
610
- // Turn-count-based steering: soft limit steer + hard abort after graceTurns
611
- if (event && typeof event === "object" && !Array.isArray(event)) {
612
- const obj = event as Record<string, unknown>;
613
- if (obj.type === "turn_end") {
614
- // H-7 step 5: steering state machine extracted to ChildPiSteeringController.
615
- const action = steeringController.onTurnEnd(child.pid, child, input.steeringFile);
616
- if (action.kind === "hardAbort") killProcessTree(action.pid, action.child);
617
- }
618
- }
619
- completeOperation(eventOpId);
620
- } catch (err) {
621
- completeOperation(eventOpId);
622
- throw err;
623
- }
624
- // F12: capture monotonic timestamp BEFORE dispatching — any stdout
625
- // JSON event counts as activity. This lets the quiet-window
626
- // detection measure "time since last byte of stdout" accurately
627
- // regardless of what onJsonEvent does.
628
- lastStdoutActivityMonotonicMs = performance.now();
629
- input.onJsonEvent?.(event);
630
- if (!isFinalAssistantEvent(event) || childExited || settled || finalDrainTimer) return;
631
- finalAssistantEventMonotonicMs = performance.now();
632
- finalDrainArmed = true; // Phase-0 diagnostic: track that a drain timer was created.
633
- // F12: alongside the 5 s ceiling timer, start a polling watcher
634
- // that fires the drain early if stdout goes quiet for `quietMs`
635
- // after the final assistant event. Heavy children that emit a
636
- // stopReason=stop message_end and then sit idle will exit in
637
- // ~quietMs (default 800 ms) instead of up to up to 5 s. unref() so
638
- // the poller never holds the event loop on shutdown.
639
- // NOTE: The polling watcher is NOT explicitly cleared on process exit.
640
- // This is safe because: (1) it's unref()'d, so it won't prevent exit;
641
- // (2) the `settled || childExited` guard at the top prevents firing
642
- // after the child has exited; (3) sending SIGTERM to an already-
643
- // exiting process is harmless. The `finalDrainQuietMs` config allows
644
- // disabling this behavior (set >= finalDrainMs, e.g., 10000).
645
- const quietMs = input.finalDrainQuietMs ?? DEFAULT_CHILD_PI.finalDrainQuietMs;
646
- if (quietMs < (input.finalDrainMs ?? DEFAULT_CHILD_PI.finalDrainMs)) {
647
- const pollHandle = setInterval(() => {
648
- if (settled || childExited) {
649
- clearInterval(pollHandle);
650
- pollHandle.unref();
651
- return;
652
- }
653
- const sinceLast = performance.now() - lastStdoutActivityMonotonicMs;
654
- if (sinceLast >= quietMs) {
655
- clearInterval(pollHandle);
656
- pollHandle.unref();
657
- // Trigger the same drain path as the 5 s timer:
658
- // mark forced, fire final_drain lifecycle, SIGTERM.
659
- forcedFinalDrain = true;
660
- finalDrainFiredMonotonicMs = performance.now();
661
- input.onLifecycleEvent?.({
662
- type: "final_drain",
663
- pid: child.pid,
664
- ts: new Date().toISOString(),
665
- reason: "stdout-quiet",
666
- });
667
- try {
668
- child.kill(process.platform === "win32" ? undefined : "SIGTERM");
669
- } catch (error) {
670
- logInternalError("child-pi.quiet-drain-term", error, `pid=${child.pid}`);
671
- }
672
- // Mark for hard kill fallback so the existing timer is
673
- // still reaped if it ever fires later.
674
- hardKillTimer = setTimeout(() => {
675
- if (settled || childExited) return;
676
- try {
677
- hardKilled = true;
678
- input.onLifecycleEvent?.({
679
- type: "hard_kill",
680
- pid: child.pid,
681
- ts: new Date().toISOString(),
682
- });
683
- child.kill(process.platform === "win32" ? undefined : "SIGKILL");
684
- } catch (error) {
685
- logInternalError("child-pi.quiet-drain-hard-kill", error, `pid=${child.pid}`);
686
- }
687
- }, hardKillMs);
688
- hardKillTimer.unref();
689
- // Cancel the 5 s ceiling so we don't double-fire.
690
- if (finalDrainTimer) {
691
- clearTimeout(finalDrainTimer);
692
- finalDrainTimer = undefined;
693
- }
694
- }
695
- }, 200);
696
- pollHandle.unref();
697
- }
698
- finalDrainTimer = setTimeout(() => {
699
- if (settled || childExited) return;
700
- forcedFinalDrain = true;
701
- finalDrainFiredMonotonicMs = performance.now(); // Phase-0 diagnostic: race timing.
702
- input.onLifecycleEvent?.({
703
- type: "final_drain",
704
- pid: child.pid,
705
- ts: new Date().toISOString(),
706
- });
707
- try {
708
- child.kill(process.platform === "win32" ? undefined : "SIGTERM");
709
- } catch (error) {
710
- logInternalError("child-pi.final-drain-term", error, `pid=${child.pid}`);
711
- }
712
- hardKillTimer = setTimeout(() => {
713
- if (settled || childExited) return;
714
- try {
715
- hardKilled = true;
716
- input.onLifecycleEvent?.({
717
- type: "hard_kill",
718
- pid: child.pid,
719
- ts: new Date().toISOString(),
720
- });
721
- child.kill(process.platform === "win32" ? undefined : "SIGKILL");
722
- } catch (error) {
723
- logInternalError("child-pi.final-drain-kill", error, `pid=${child.pid}`);
724
- }
725
- }, hardKillMs);
726
- hardKillTimer.unref();
727
- }, finalDrainMs);
728
- finalDrainTimer.unref();
729
- },
730
- });
731
-
732
- const clearFinalDrainTimers = (): void => {
733
- if (finalDrainTimer) clearTimeout(finalDrainTimer);
734
- if (hardKillTimer) clearTimeout(hardKillTimer);
735
- finalDrainTimer = undefined;
736
- hardKillTimer = undefined;
737
- };
738
- const clearPostExitGuard = (): void => {
739
- if (postExitGuardCleanup) {
740
- postExitGuardCleanup();
741
- postExitGuardCleanup = undefined;
742
- }
743
- };
744
- const clearChildPiTimeouts = (): void => {
745
- clearNoResponseTimer();
746
- clearFinalDrainTimers();
747
- clearPostExitGuard();
748
- };
749
-
750
- const settle = (result: ChildPiRunResult): Promise<void> => {
751
- if (settled) return Promise.resolve();
752
- settled = true;
753
- clearChildPiTimeouts();
754
- // OPT-06 follow-up: lineObserver.flush() is now async (returns
755
- // Promise<void>) and drains the module-scoped transcript batch buffer
756
- // before resolving. We must await it before calling `resolve()`
757
- // below so callers that read the transcript file post-`runChildPi`
758
- // see all written lines. Caller invocations of `settle` from
759
- // sync event handlers (`child.on('close'|'exit'|'error')`,
760
- // safety timer) use `void settle(...)` — errors are caught and
761
- // logged inside, and `resolve()` only fires after the drain
762
- // completes, so runChildPi's outer Promise resolves with a
763
- // durable transcript on disk.
764
- return lineObserver
765
- .flush()
766
- .then(() => {
767
- input.signal?.removeEventListener("abort", abort);
768
- input.signal?.removeEventListener("abort", onParentAbort);
769
- try {
770
- cleanupTempDir(tempDir);
771
- } catch (error) {
772
- cleanupErrors.push(error instanceof Error ? error.message : String(error));
773
- }
774
- // Catch all errors from settle to prevent unhandled rejection from propagating
775
- try {
776
- resolve({
777
- ...result,
778
- rawFinalText: lineObserver.getRawFinalText(),
779
- intermediateFindings: lineObserver.getIntermediateFindings(),
780
- exitStatus: result.exitStatus ?? {
781
- exitCode: result.exitCode,
782
- cancelled: abortRequested,
783
- timedOut: responseTimeoutHit,
784
- killed: hardKilled,
785
- // Phase-0 diagnostic (HB-003a): surface the final-drain race state.
786
- // finalDrainArmed lets Phase 1 decide whether a signal-death (exitCode=null)
787
- // should be treated as a forced final drain. READ-ONLY for now.
788
- ...(finalDrainArmed || forcedFinalDrain
789
- ? {
790
- finalDrainArmed,
791
- forcedFinalDrain,
792
- finalDrainFiredMonotonicMs,
793
- }
794
- : {}),
795
- cleanupErrors,
796
- finalDrainMs,
797
- },
798
- });
799
- } catch (resolveError) {
800
- logInternalError(
801
- "child-pi.settle-resolve",
802
- resolveError,
803
- `result=${JSON.stringify({ exitCode: result.exitCode })}`,
804
- );
805
- }
806
- })
807
- .catch((flushError) => {
808
- // Drain failed — log and still resolve so runChildPi doesn't hang.
809
- logInternalError(
810
- "child-pi.settle-flush-failed",
811
- flushError,
812
- `result=${JSON.stringify({ exitCode: result.exitCode })}`,
813
- );
814
- input.signal?.removeEventListener("abort", abort);
815
- input.signal?.removeEventListener("abort", onParentAbort);
816
- try {
817
- cleanupTempDir(tempDir);
818
- } catch (error) {
819
- cleanupErrors.push(error instanceof Error ? error.message : String(error));
820
- }
821
- try {
822
- resolve({
823
- ...result,
824
- rawFinalText: lineObserver.getRawFinalText(),
825
- intermediateFindings: lineObserver.getIntermediateFindings(),
826
- exitStatus: result.exitStatus ?? {
827
- exitCode: result.exitCode,
828
- cancelled: abortRequested,
829
- timedOut: responseTimeoutHit,
830
- killed: hardKilled,
831
- ...(finalDrainArmed || forcedFinalDrain
832
- ? {
833
- finalDrainArmed,
834
- forcedFinalDrain,
835
- finalDrainFiredMonotonicMs,
836
- }
837
- : {}),
838
- cleanupErrors,
839
- finalDrainMs,
840
- },
841
- });
842
- } catch (resolveError) {
843
- logInternalError(
844
- "child-pi.settle-resolve",
845
- resolveError,
846
- `result=${JSON.stringify({ exitCode: result.exitCode })}`,
847
- );
848
- }
849
- });
850
- };
851
-
852
- const abort = (): void => {
853
- abortRequested = true;
854
- clearNoResponseTimer();
855
- killProcessTree(child.pid, child);
856
- if (process.platform !== "win32") {
857
- trySignalChild(child, "SIGTERM");
858
- }
859
- try {
860
- child.kill(process.platform === "win32" ? undefined : "SIGTERM");
861
- } catch {
862
- // Ignore kill races.
863
- }
864
- // 3.5 — fast-escalate to SIGKILL within 200ms on explicit cancel
865
- // so /team-cancel completes round-trip well under the operator
866
- // expectation. The standard finalDrainMs / HARD_KILL_MS paths
867
- // are for graceful drain, not user-initiated cancel.
868
- const cancelHardKill = setTimeout(() => {
869
- if (settled || childExited) return;
870
- try {
871
- hardKilled = true;
872
- child.kill(process.platform === "win32" ? undefined : "SIGKILL");
873
- } catch (error) {
874
- logInternalError("child-pi.cancel-fast-kill", error, `pid=${child.pid}`);
875
- }
876
- }, 200);
877
- cancelHardKill.unref();
878
- };
879
-
880
- input.signal?.addEventListener("abort", abort, { once: true });
881
- // 3.1 — soft watermark backpressure. When inbound stdout exceeds
882
- // 256KB before the next macrotask, pause for 50ms so the line
883
- // observer + ancillary handlers get to drain. Prevents the runaway
884
- // case where a chatty child saturates the parent event loop.
885
- const BACKPRESSURE_HIGH = 256 * 1024;
886
- let backpressureBytes = 0;
887
- const releaseBackpressure = (): void => {
888
- backpressureBytes = 0;
889
- try {
890
- child.stdout?.resume();
891
- } catch {
892
- /* ignore */
893
- }
894
- };
895
- child.stdout?.on("data", (chunk: Buffer) => {
896
- if (!steeringController.isHardAbortInitiated()) restartNoResponseTimer();
897
- const text = chunk.toString("utf-8");
898
- backpressureBytes += text.length;
899
- try {
900
- lineObserver.observe(text);
901
- } catch (err) {
902
- logInternalError("child-pi.line-observer-observe", err, `text=${text.slice(0, 100)}`);
903
- }
904
- if (backpressureBytes > BACKPRESSURE_HIGH && child.stdout && !child.stdout.isPaused()) {
905
- try {
906
- child.stdout.pause();
907
- } catch {
908
- /* ignore */
909
- }
910
- const timer = setTimeout(releaseBackpressure, 50);
911
- timer.unref();
912
- }
913
- });
914
- child.stderr?.on("data", (chunk: Buffer) => {
915
- if (!steeringController.isHardAbortInitiated()) restartNoResponseTimer();
916
- stderr = appendBoundedTail(stderr, chunk.toString("utf-8"));
917
- });
918
- child.on("error", (error) => {
919
- // Reject pending operations with process error context
920
- // SEC-1: redact stderr secrets embedded in the error message + excerpt.
921
- const processError = new Error(
922
- `Child Pi process error: ${error.message}. Stderr: ${redactStderrExcerpt(stderr, 500) || "(none)"}`,
923
- );
924
- rejectPendingOperations(processError);
925
- try {
926
- input.onLifecycleEvent?.({
927
- type: "spawn_error",
928
- pid: child.pid,
929
- error: processError.message,
930
- ts: new Date().toISOString(),
931
- stderrExcerpt: redactStderrExcerpt(stderr, 500) || undefined,
932
- });
933
- } catch (err) {
934
- logInternalError("child-pi.on-lifecycle-event", err, `event=error, pid=${child.pid}`);
935
- }
936
- void settle({
937
- exitCode: null,
938
- stdout,
939
- stderr,
940
- error: processError.message,
941
- exitStatus: {
942
- exitCode: null,
943
- cancelled: abortRequested,
944
- timedOut: responseTimeoutHit,
945
- killed: false,
946
- cleanupErrors,
947
- finalDrainMs,
948
- crashClass: classifyProcessCrash({
949
- exitCode: null,
950
- cancelled: abortRequested,
951
- timedOut: responseTimeoutHit,
952
- spawnError: error,
953
- stderrSnippet: stderr ? redactStderrExcerpt(stderr, 1000) : undefined,
954
- }).crashClass,
955
- },
956
- });
957
- });
958
- child.on("exit", (code, signal) => {
959
- if (child.pid) {
960
- unregisterActiveChild(child.pid);
961
- clearHardKillTimer(child.pid);
962
- // Unregister from cleanup handler
963
- unregisterChildProcess(child.pid);
964
- }
965
- // Build comprehensive exit error for unexpected exits
966
- // Round-10 test fix: also require non-zero exit code OR a known abnormal condition.
967
- // Previously fired "exited unexpectedly" on every clean exit (code=0) because the
968
- // OS-level 'exit' event fires BEFORE pi's 'agent_end' JSON event reaches the line
969
- // observer (race). Worker actually succeeded but onLifecycleEvent reported an error.
970
- const abnormalExit = code !== 0 && code !== null;
971
- const isUnexpectedExit = !childExited && !settled && !responseTimeoutHit && !abortRequested && abnormalExit;
972
- const exitError = isUnexpectedExit
973
- ? new Error(
974
- `Child Pi process exited unexpectedly (code=${code ?? "null"} signal=${signal ?? "null"}). ` +
975
- `Stderr: ${redactStderrExcerpt(stderr, 1000) || "(none)"}`,
976
- )
977
- : null;
978
- if (exitError) {
979
- rejectPendingOperations(exitError);
980
- }
981
- try {
982
- // Phase-0 diagnostic (HB-003a): capture signal + drain timing in the
983
- // exit lifecycle event so the exit-null race is diagnosable instead of
984
- // opaque. `signal` was previously discarded after building the error msg.
985
- input.onLifecycleEvent?.({
986
- type: "exit",
987
- pid: child.pid,
988
- exitCode: code,
989
- ts: new Date().toISOString(),
990
- error: exitError?.message,
991
- stderrExcerpt: isUnexpectedExit ? redactStderrExcerpt(stderr, 1000) || undefined : undefined,
992
- // Phase-0 diagnostic fields (kept optional — no type change required).
993
- ...(signal ? { signal } : {}),
994
- ...(finalDrainArmed || forcedFinalDrain
995
- ? {
996
- diagnostic: {
997
- finalDrainArmed,
998
- forcedFinalDrain,
999
- finalDrainFiredMonotonicMs,
1000
- finalAssistantEventMonotonicMs,
1001
- exitMonotonicMs: performance.now() - spawnMonotonicMs,
1002
- },
1003
- }
1004
- : {}),
1005
- });
1006
- } catch (err) {
1007
- logInternalError("child-pi.on-lifecycle-event", err, `event=exit, pid=${child.pid}`);
1008
- }
1009
- childExited = true;
1010
- clearNoResponseTimer();
1011
- clearFinalDrainTimers();
1012
- if (!postExitGuardCleanup) {
1013
- postExitGuardCleanup = attachPostExitStdioGuard(child, {
1014
- idleMs: POST_EXIT_STDIO_GUARD_MS,
1015
- hardMs: HARD_KILL_MS,
1016
- });
1017
- }
1018
- });
1019
- child.on("close", (exitCode) => {
1020
- if (child.pid) {
1021
- unregisterActiveChild(child.pid);
1022
- clearHardKillTimer(child.pid);
1023
- // Unregister from cleanup handler
1024
- unregisterChildProcess(child.pid);
1025
- }
1026
- try {
1027
- input.onLifecycleEvent?.({
1028
- type: "close",
1029
- pid: child.pid,
1030
- exitCode,
1031
- ts: new Date().toISOString(),
1032
- });
1033
- } catch (err) {
1034
- logInternalError("child-pi.on-lifecycle-event", err, `event=close, pid=${child.pid}`);
1035
- }
1036
- const timeoutError =
1037
- responseTimeoutHit && !stderr.trim()
1038
- ? {
1039
- error: `Child Pi produced no new output for ${responseTimeoutMs}ms; process was terminated as unresponsive.`,
1040
- }
1041
- : responseTimeoutHit && stderr.trim()
1042
- ? {
1043
- error: `Child Pi timed out after ${responseTimeoutMs}ms with stderr: ${redactStderrExcerpt(stderr, 500)}`,
1044
- }
1045
- : undefined;
1046
- // M6 fix: log when forced final drain converts non-zero exit to 0.
1047
- // This is expected in normal operation (child finished cleanly but linger was killed),
1048
- // but the telemetry helps detect regressions where crashes are hidden.
1049
- if (forcedFinalDrain && !timeoutError && exitCode !== 0) {
1050
- logInternalError(
1051
- "child-pi.final-drain-zero-exit",
1052
- new Error(`Child exit code overridden to 0 after forced final drain (original=${exitCode})`),
1053
- `pid=${child.pid}, finalDrainMs=${finalDrainMs}`,
1054
- );
1055
- }
1056
- const finalExitCode = forcedFinalDrain && !timeoutError ? 0 : exitCode;
1057
- const wasGraceAborted =
1058
- steeringController.isSoftLimitReached() &&
1059
- steeringController.getTurnCount() >=
1060
- (steeringController.getMaxTurns() ?? 0) + (steeringController.getGraceTurns() ?? 5);
1061
- const wasParentAborted = abortDueToParentSignal && !wasGraceAborted;
1062
- // steerInjectionFailed is now always false (Phase-1 fix: steer backpressure
1063
- // is logged, not fatal). The steerError branch is retained for safety in
1064
- // case a future change reintroduces a fatal steer path.
1065
- const steerError = steerInjectionFailed ? "Steer injection failed due to stdin backpressure; process killed" : undefined;
1066
- // P0 crash taxonomy: classify the exit so callers/dashboards can bucket
1067
- // failure modes (timeout vs cancel vs native panic vs signal …).
1068
- // The classifier is a pure function; this is the single integration point.
1069
- const crashClassification = classifyProcessCrash({
1070
- exitCode: finalExitCode,
1071
- signal: child.signalCode ?? undefined,
1072
- cancelled: abortRequested,
1073
- timedOut: responseTimeoutHit,
1074
- killed: hardKilled,
1075
- spawnError: undefined,
1076
- stderrSnippet: stderr ? redactStderrExcerpt(stderr, 1000) : undefined,
1077
- });
1078
- void settle({
1079
- exitCode: finalExitCode,
1080
- stdout,
1081
- stderr,
1082
- ...(timeoutError ? { error: timeoutError.error } : {}),
1083
- ...(steerError ? { error: steerError } : {}),
1084
- aborted: wasGraceAborted || wasParentAborted,
1085
- steered: steeringController.isSoftLimitReached() && !wasGraceAborted,
1086
- exitStatus: {
1087
- exitCode: finalExitCode,
1088
- cancelled: abortRequested,
1089
- timedOut: responseTimeoutHit,
1090
- killed: hardKilled,
1091
- cleanupErrors,
1092
- finalDrainMs,
1093
- crashClass: crashClassification.crashClass,
1094
- },
1095
- });
1096
- });
1097
- });
1098
- } finally {
1099
- // cleanupTempDir is already called inside settle(), but guard against
1100
- // the case where settle() was never reached (spawn throws synchronously).
1101
- if (tempDir && fs.existsSync(tempDir)) {
1102
- cleanupTempDir(tempDir);
1103
- }
1104
- }
1105
- }