pi-crew 0.9.16 → 0.9.17

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 (432) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +19 -0
  3. package/dist/build-meta.json +21790 -0
  4. package/dist/index.mjs +83070 -0
  5. package/dist/index.mjs.map +7 -0
  6. package/docs/A +358 -0
  7. package/docs/M-A +357 -0
  8. package/docs/REVIEW-FINDINGS-2026-06 +357 -0
  9. package/docs/Y +357 -0
  10. package/docs/a +357 -0
  11. package/docs/aA +358 -0
  12. package/docs/archive/README.md +91 -0
  13. package/docs/patterns/command-agent-skill.md +1 -1
  14. package/docs/skills/REFERENCE.md +0 -17
  15. package/docs//303/242mmaAAA/303/242 +357 -0
  16. package/index.bundle.ts +25 -0
  17. package/index.ts +95 -4
  18. package/package.json +15 -4
  19. package/skills/iterative-audit/SKILL.md +0 -1
  20. package/skills/widget-rendering/SKILL.md +17 -0
  21. package/src/adapters/claude-adapter.ts +1 -3
  22. package/src/adapters/export-util.ts +16 -16
  23. package/src/adapters/index.ts +5 -5
  24. package/src/agents/agent-config.ts +20 -14
  25. package/src/agents/agent-serializer.ts +1 -1
  26. package/src/agents/discover-agents.ts +83 -39
  27. package/src/benchmark/benchmark-runner.ts +51 -51
  28. package/src/benchmark/feedback-loop.ts +1 -1
  29. package/src/config/config.ts +166 -590
  30. package/src/config/defaults.ts +28 -2
  31. package/src/config/drift-detector.ts +11 -14
  32. package/src/config/markers.ts +201 -203
  33. package/src/config/resilient-parser.ts +14 -6
  34. package/src/config/role-tools.ts +3 -3
  35. package/src/config/suggestions.ts +5 -12
  36. package/src/config/types.ts +9 -25
  37. package/src/errors.ts +151 -157
  38. package/src/extension/action-suggestions.ts +54 -9
  39. package/src/extension/async-notifier.ts +43 -13
  40. package/src/extension/autonomous-policy.ts +77 -37
  41. package/src/extension/command-completions.ts +9 -8
  42. package/src/extension/context-status-injection.ts +14 -12
  43. package/src/extension/crew-autocomplete.ts +8 -16
  44. package/src/extension/crew-cleanup.ts +2 -1
  45. package/src/extension/crew-shortcuts.ts +9 -6
  46. package/src/extension/cross-extension-rpc.ts +136 -42
  47. package/src/extension/help.ts +1 -1
  48. package/src/extension/import-index.ts +10 -5
  49. package/src/extension/knowledge-injection.ts +88 -15
  50. package/src/extension/management.ts +89 -349
  51. package/src/extension/message-renderers.ts +3 -4
  52. package/src/extension/notification-router.ts +22 -5
  53. package/src/extension/notification-sink.ts +6 -3
  54. package/src/extension/pi-api.ts +17 -8
  55. package/src/extension/plan-orchestrate.ts +8 -27
  56. package/src/extension/project-init.ts +21 -6
  57. package/src/extension/register.ts +329 -975
  58. package/src/extension/registration/artifact-cleanup.ts +9 -4
  59. package/src/extension/registration/command-utils.ts +5 -1
  60. package/src/extension/registration/commands.ts +806 -314
  61. package/src/extension/registration/compaction-guard.ts +15 -15
  62. package/src/extension/registration/lifecycle.ts +259 -0
  63. package/src/extension/registration/observability.ts +293 -0
  64. package/src/extension/registration/subagent-helpers.ts +29 -7
  65. package/src/extension/registration/subagent-tools.ts +253 -44
  66. package/src/extension/registration/team-tool.ts +19 -84
  67. package/src/extension/registration/ui.ts +168 -0
  68. package/src/extension/registration/viewers.ts +64 -24
  69. package/src/extension/result-watcher.ts +18 -7
  70. package/src/extension/run-bundle-schema.ts +21 -6
  71. package/src/extension/run-export.ts +15 -6
  72. package/src/extension/run-import.ts +64 -29
  73. package/src/extension/run-index.ts +28 -9
  74. package/src/extension/run-maintenance.ts +32 -12
  75. package/src/extension/session-summary.ts +1 -1
  76. package/src/extension/team-manager-command.ts +64 -7
  77. package/src/extension/team-onboard.ts +151 -150
  78. package/src/extension/team-recommendation.ts +121 -21
  79. package/src/extension/team-tool/anchor.ts +31 -64
  80. package/src/extension/team-tool/api.ts +929 -141
  81. package/src/extension/team-tool/auto-summarize.ts +14 -26
  82. package/src/extension/team-tool/cache-control.ts +1 -5
  83. package/src/extension/team-tool/cancel.ts +138 -38
  84. package/src/extension/team-tool/chain-dispatch.ts +5 -13
  85. package/src/extension/team-tool/chain-executor.ts +29 -51
  86. package/src/extension/team-tool/config-patch.ts +1 -1
  87. package/src/extension/team-tool/context.ts +40 -20
  88. package/src/extension/team-tool/destructive-gate.ts +10 -5
  89. package/src/extension/team-tool/doctor.ts +170 -51
  90. package/src/extension/team-tool/explain.ts +228 -214
  91. package/src/extension/team-tool/failure-patterns.ts +3 -9
  92. package/src/extension/team-tool/goal-wrap.ts +71 -29
  93. package/src/extension/team-tool/goal.ts +185 -35
  94. package/src/extension/team-tool/handle-schedule.ts +34 -15
  95. package/src/extension/team-tool/handle-settings.ts +94 -56
  96. package/src/extension/team-tool/health-monitor.ts +27 -108
  97. package/src/extension/team-tool/inspect.ts +42 -9
  98. package/src/extension/team-tool/intent-policy.ts +4 -1
  99. package/src/extension/team-tool/lifecycle-actions.ts +244 -48
  100. package/src/extension/team-tool/orchestrate.ts +7 -18
  101. package/src/extension/team-tool/parallel-dispatch.ts +51 -29
  102. package/src/extension/team-tool/plan.ts +29 -5
  103. package/src/extension/team-tool/respond.ts +49 -16
  104. package/src/extension/team-tool/run-not-found.ts +3 -8
  105. package/src/extension/team-tool/run.ts +80 -317
  106. package/src/extension/team-tool/status.ts +135 -43
  107. package/src/extension/team-tool/workflow-manage.ts +92 -25
  108. package/src/extension/team-tool-types.ts +8 -1
  109. package/src/extension/team-tool.ts +142 -551
  110. package/src/extension/validate-resources.ts +39 -8
  111. package/src/hooks/registry.ts +66 -17
  112. package/src/hooks/types.ts +1 -1
  113. package/src/i18n.ts +25 -11
  114. package/src/observability/correlation.ts +17 -3
  115. package/src/observability/event-bus.ts +51 -56
  116. package/src/observability/event-to-metric.ts +117 -15
  117. package/src/observability/exporters/otlp-exporter.ts +56 -17
  118. package/src/observability/exporters/prometheus-exporter.ts +1 -1
  119. package/src/observability/metric-registry.ts +16 -4
  120. package/src/observability/metric-retention.ts +8 -2
  121. package/src/observability/metric-sink.ts +11 -3
  122. package/src/observability/metrics-primitives.ts +42 -9
  123. package/src/plugins/plugin-define.ts +2 -2
  124. package/src/plugins/plugin-registry.ts +25 -25
  125. package/src/plugins/plugins/index.ts +1 -1
  126. package/src/plugins/plugins/nextjs.ts +16 -16
  127. package/src/plugins/plugins/vite.ts +7 -11
  128. package/src/plugins/plugins/vitest.ts +6 -11
  129. package/src/runtime/adaptive-plan.ts +225 -40
  130. package/src/runtime/agent-control.ts +58 -15
  131. package/src/runtime/agent-memory.ts +15 -9
  132. package/src/runtime/agent-observability.ts +10 -2
  133. package/src/runtime/anchor-manager.ts +27 -25
  134. package/src/runtime/async-marker.ts +7 -1
  135. package/src/runtime/async-runner.ts +27 -23
  136. package/src/runtime/auto-summarize.ts +7 -13
  137. package/src/runtime/background-runner.ts +189 -251
  138. package/src/runtime/batch-barrier.ts +18 -16
  139. package/src/runtime/cancellation-token.ts +16 -6
  140. package/src/runtime/cancellation.ts +46 -8
  141. package/src/runtime/capability-inventory.ts +6 -4
  142. package/src/runtime/chain-parser.ts +44 -11
  143. package/src/runtime/chain-runner.ts +63 -69
  144. package/src/runtime/checkpoint.ts +12 -56
  145. package/src/runtime/child-pi.ts +343 -78
  146. package/src/runtime/coalesce-tasks.ts +268 -0
  147. package/src/runtime/code-summary.ts +71 -26
  148. package/src/runtime/compact-stages/index.ts +15 -5
  149. package/src/runtime/compact-stages/tail-capture-stage.ts +7 -2
  150. package/src/runtime/compact-stages/truncation-stage.ts +4 -1
  151. package/src/runtime/compaction-summary.ts +17 -10
  152. package/src/runtime/completion-guard.ts +33 -16
  153. package/src/runtime/crash-classification.ts +28 -7
  154. package/src/runtime/crash-recovery.ts +177 -37
  155. package/src/runtime/crew-agent-records.ts +145 -47
  156. package/src/runtime/crew-hooks.ts +9 -24
  157. package/src/runtime/cross-extension-rpc.ts +25 -23
  158. package/src/runtime/custom-tools/irc-tool.ts +43 -15
  159. package/src/runtime/custom-tools/submit-result-tool.ts +16 -5
  160. package/src/runtime/delivery-coordinator.ts +34 -7
  161. package/src/runtime/delta-conflict.ts +9 -21
  162. package/src/runtime/deterministic-ast.ts +1 -1
  163. package/src/runtime/diagnostic-export.ts +53 -20
  164. package/src/runtime/direct-run.ts +12 -2
  165. package/src/runtime/dwf-state-store.ts +1 -1
  166. package/src/runtime/dynamic-workflow-context.ts +186 -58
  167. package/src/runtime/dynamic-workflow-runner.ts +42 -20
  168. package/src/runtime/effectiveness.ts +16 -8
  169. package/src/runtime/errors/crew-errors.ts +7 -11
  170. package/src/runtime/event-stream-bridge.ts +9 -3
  171. package/src/runtime/foreground-control.ts +54 -14
  172. package/src/runtime/foreground-watchdog.ts +9 -6
  173. package/src/runtime/goal-achievement.ts +27 -10
  174. package/src/runtime/goal-evaluator.ts +54 -19
  175. package/src/runtime/goal-loop-runner.ts +280 -99
  176. package/src/runtime/goal-state-store.ts +27 -17
  177. package/src/runtime/green-contract.ts +11 -2
  178. package/src/runtime/group-join.ts +64 -31
  179. package/src/runtime/handoff-manager.ts +37 -42
  180. package/src/runtime/heartbeat-gradient.ts +10 -2
  181. package/src/runtime/heartbeat-watcher.ts +32 -6
  182. package/src/runtime/hidden-handoff.ts +12 -30
  183. package/src/runtime/important-line-classifier.ts +12 -2
  184. package/src/runtime/iteration-hooks.ts +25 -15
  185. package/src/runtime/live-agent-control.ts +55 -7
  186. package/src/runtime/live-agent-manager.ts +130 -27
  187. package/src/runtime/live-control-realtime.ts +23 -3
  188. package/src/runtime/live-irc.ts +4 -1
  189. package/src/runtime/live-session-health.ts +12 -4
  190. package/src/runtime/live-session-runtime.ts +379 -117
  191. package/src/runtime/manifest-cache.ts +28 -12
  192. package/src/runtime/mcp-proxy.ts +8 -16
  193. package/src/runtime/metric-parser.ts +1 -5
  194. package/src/runtime/model-fallback.ts +55 -18
  195. package/src/runtime/model-resolver.ts +2 -6
  196. package/src/runtime/model-scope.ts +19 -3
  197. package/src/runtime/notebook-helpers.ts +60 -62
  198. package/src/runtime/orphan-worker-registry.ts +37 -26
  199. package/src/runtime/output-validator.ts +18 -3
  200. package/src/runtime/overflow-recovery.ts +5 -4
  201. package/src/runtime/parallel-research.ts +29 -5
  202. package/src/runtime/parallel-utils.ts +9 -11
  203. package/src/runtime/path-overlap.ts +150 -0
  204. package/src/runtime/peer-dep.ts +12 -17
  205. package/src/runtime/per-write-validator.ts +1 -3
  206. package/src/runtime/phase-tracker.ts +342 -330
  207. package/src/runtime/pi-args.ts +26 -8
  208. package/src/runtime/pi-json-output.ts +1 -1
  209. package/src/runtime/pi-spawn.ts +30 -19
  210. package/src/runtime/pipeline-runner.ts +56 -55
  211. package/src/runtime/plan-templates.ts +5 -6
  212. package/src/runtime/policy-engine.ts +43 -7
  213. package/src/runtime/post-checks.ts +12 -4
  214. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  215. package/src/runtime/process-lifecycle.ts +20 -10
  216. package/src/runtime/process-status.ts +16 -5
  217. package/src/runtime/progress-event-coalescer.ts +2 -1
  218. package/src/runtime/progress-tracker.ts +103 -103
  219. package/src/runtime/prose-compressor.ts +9 -11
  220. package/src/runtime/recovery-recipes.ts +118 -24
  221. package/src/runtime/replace.ts +25 -10
  222. package/src/runtime/resilient-edit.ts +10 -25
  223. package/src/runtime/result-extractor.ts +1 -2
  224. package/src/runtime/retry-executor.ts +20 -4
  225. package/src/runtime/retry-runner.ts +26 -50
  226. package/src/runtime/role-permission.ts +6 -1
  227. package/src/runtime/run-coalesced-task-group.ts +256 -0
  228. package/src/runtime/run-drift.ts +14 -15
  229. package/src/runtime/run-tracker.ts +6 -28
  230. package/src/runtime/runtime-policy.ts +1 -6
  231. package/src/runtime/runtime-resolver.ts +80 -15
  232. package/src/runtime/scheduler.ts +47 -18
  233. package/src/runtime/semaphore.ts +5 -7
  234. package/src/runtime/sensitive-paths.ts +2 -1
  235. package/src/runtime/session-usage.ts +1 -1
  236. package/src/runtime/settings-store.ts +16 -12
  237. package/src/runtime/sidechain-output.ts +6 -2
  238. package/src/runtime/single-agent-compose.ts +1 -4
  239. package/src/runtime/skill-effectiveness.ts +29 -97
  240. package/src/runtime/skill-instructions.ts +40 -83
  241. package/src/runtime/stale-reconciler.ts +73 -197
  242. package/src/runtime/stream-preview.ts +1 -1
  243. package/src/runtime/streaming-output.ts +1 -1
  244. package/src/runtime/subagent-manager.ts +44 -169
  245. package/src/runtime/subprocess-tool-registry.ts +4 -1
  246. package/src/runtime/supervisor-contact.ts +10 -5
  247. package/src/runtime/task-display.ts +19 -5
  248. package/src/runtime/task-graph-scheduler.ts +95 -21
  249. package/src/runtime/task-graph.ts +5 -11
  250. package/src/runtime/task-health.ts +54 -47
  251. package/src/runtime/task-id.ts +11 -18
  252. package/src/runtime/task-output-context.ts +265 -81
  253. package/src/runtime/task-packet.ts +12 -20
  254. package/src/runtime/task-quality.ts +6 -14
  255. package/src/runtime/task-runner/context-retrieval.ts +4 -13
  256. package/src/runtime/task-runner/live-executor.ts +114 -36
  257. package/src/runtime/task-runner/output-splitter.ts +152 -0
  258. package/src/runtime/task-runner/progress.ts +50 -12
  259. package/src/runtime/task-runner/prompt-builder.ts +31 -7
  260. package/src/runtime/task-runner/prompt-pipeline.ts +31 -7
  261. package/src/runtime/task-runner/result-utils.ts +3 -1
  262. package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
  263. package/src/runtime/task-runner/run-projection.ts +27 -8
  264. package/src/runtime/task-runner/state-helpers.ts +29 -8
  265. package/src/runtime/task-runner/tail-read.ts +2 -6
  266. package/src/runtime/task-runner.ts +180 -326
  267. package/src/runtime/team-runner-artifacts.ts +13 -0
  268. package/src/runtime/team-runner.ts +456 -974
  269. package/src/runtime/tool-output-pruner.ts +6 -9
  270. package/src/runtime/tool-progress.ts +11 -21
  271. package/src/runtime/verification-gates.ts +33 -24
  272. package/src/runtime/verification-integrity.ts +1 -4
  273. package/src/runtime/verification-worktree.ts +62 -12
  274. package/src/runtime/worker-heartbeat.ts +5 -1
  275. package/src/runtime/worker-startup.ts +29 -5
  276. package/src/runtime/workflow-state.ts +17 -9
  277. package/src/runtime/workspace-lock.ts +30 -35
  278. package/src/runtime/workspace-tree.ts +20 -32
  279. package/src/runtime/yield-handler.ts +43 -9
  280. package/src/runtime/zombie-scanner.ts +5 -4
  281. package/src/schema/config-schema.ts +266 -172
  282. package/src/schema/team-tool-schema.ts +29 -74
  283. package/src/schema/validation-types.ts +25 -17
  284. package/src/skills/discover-skills.ts +35 -10
  285. package/src/skills/skill-templates.ts +109 -27
  286. package/src/skills/validate.ts +26 -8
  287. package/src/state/active-run-registry.ts +87 -28
  288. package/src/state/artifact-store.ts +9 -5
  289. package/src/state/atomic-write-v2.ts +85 -63
  290. package/src/state/atomic-write.ts +105 -26
  291. package/src/state/blob-store.ts +47 -20
  292. package/src/state/contracts.ts +20 -5
  293. package/src/state/crew-init.ts +5 -22
  294. package/src/state/decision-ledger.ts +19 -73
  295. package/src/state/event-log-rotation.ts +41 -15
  296. package/src/state/event-log.ts +168 -53
  297. package/src/state/event-reconstructor.ts +11 -1
  298. package/src/state/gitignore-manager.ts +2 -8
  299. package/src/state/health-store.ts +57 -57
  300. package/src/state/hook-instinct-bridge.ts +1 -1
  301. package/src/state/hook-integrations.ts +1 -1
  302. package/src/state/instinct-store.ts +17 -5
  303. package/src/state/jsonl-writer.ts +1 -1
  304. package/src/state/locks.ts +23 -9
  305. package/src/state/mailbox.ts +151 -28
  306. package/src/state/observation-store.ts +20 -14
  307. package/src/state/run-cache.ts +142 -135
  308. package/src/state/run-graph.ts +6 -15
  309. package/src/state/run-metrics.ts +5 -18
  310. package/src/state/schedule.ts +15 -9
  311. package/src/state/state-store.ts +129 -45
  312. package/src/state/task-claims.ts +12 -2
  313. package/src/state/tiered-eval.ts +52 -43
  314. package/src/state/types-eval.ts +2 -2
  315. package/src/state/types.ts +24 -20
  316. package/src/state/usage.ts +20 -4
  317. package/src/state/worker-atomic-writer.ts +6 -3
  318. package/src/subagents/index.ts +2 -2
  319. package/src/teams/discover-teams.ts +21 -6
  320. package/src/tools/safe-bash-extension.ts +5 -6
  321. package/src/tools/safe-bash.ts +10 -11
  322. package/src/types/new-api-types.ts +6 -10
  323. package/src/ui/agent-management-overlay.ts +52 -40
  324. package/src/ui/card-colors.ts +13 -4
  325. package/src/ui/crew-footer.ts +8 -7
  326. package/src/ui/crew-select-list.ts +1 -1
  327. package/src/ui/dashboard-panes/agents-pane.ts +41 -25
  328. package/src/ui/dashboard-panes/cancellation-pane.ts +1 -1
  329. package/src/ui/dashboard-panes/capability-pane.ts +32 -15
  330. package/src/ui/dashboard-panes/health-pane.ts +2 -1
  331. package/src/ui/dashboard-panes/metrics-pane.ts +4 -1
  332. package/src/ui/dashboard-panes/progress-pane.ts +12 -9
  333. package/src/ui/heartbeat-aggregator.ts +15 -4
  334. package/src/ui/keybinding-map.ts +40 -7
  335. package/src/ui/live-conversation-overlay.ts +36 -20
  336. package/src/ui/live-duration.ts +1 -4
  337. package/src/ui/live-run-sidebar.ts +88 -25
  338. package/src/ui/loaders.ts +2 -8
  339. package/src/ui/mascot.ts +20 -36
  340. package/src/ui/overlays/agent-picker-overlay.ts +11 -3
  341. package/src/ui/overlays/confirm-overlay.ts +4 -2
  342. package/src/ui/overlays/help-overlay.ts +25 -14
  343. package/src/ui/overlays/mailbox-compose-overlay.ts +48 -11
  344. package/src/ui/overlays/mailbox-compose-preview.ts +20 -5
  345. package/src/ui/overlays/mailbox-detail-overlay.ts +27 -6
  346. package/src/ui/pi-ui-compat.ts +7 -7
  347. package/src/ui/powerbar-publisher.ts +120 -42
  348. package/src/ui/render-diff.ts +10 -3
  349. package/src/ui/render-scheduler.ts +12 -4
  350. package/src/ui/run-action-dispatcher.ts +81 -18
  351. package/src/ui/run-dashboard.ts +172 -76
  352. package/src/ui/run-event-bus.ts +59 -37
  353. package/src/ui/run-snapshot-cache.ts +276 -86
  354. package/src/ui/settings-overlay.ts +361 -74
  355. package/src/ui/status-colors.ts +12 -1
  356. package/src/ui/syntax-highlight.ts +1 -1
  357. package/src/ui/theme-adapter.ts +16 -14
  358. package/src/ui/theme-discovery.ts +13 -2
  359. package/src/ui/tool-progress-formatter.ts +7 -7
  360. package/src/ui/tool-render.ts +128 -56
  361. package/src/ui/tool-renderers/brief-mode.ts +45 -27
  362. package/src/ui/tool-renderers/index.ts +109 -43
  363. package/src/ui/transcript-cache.ts +32 -6
  364. package/src/ui/transcript-entries.ts +25 -23
  365. package/src/ui/transcript-viewer.ts +78 -29
  366. package/src/ui/widget/index.ts +99 -40
  367. package/src/ui/widget/widget-formatters.ts +13 -6
  368. package/src/ui/widget/widget-model.ts +19 -8
  369. package/src/ui/widget/widget-renderer.ts +23 -13
  370. package/src/ui/widget/widget-types.ts +1 -1
  371. package/src/utils/bm25-search.ts +199 -199
  372. package/src/utils/conflict-detect.ts +22 -21
  373. package/src/utils/env-filter.ts +14 -7
  374. package/src/utils/file-coalescer.ts +5 -1
  375. package/src/utils/fingerprint.ts +3 -6
  376. package/src/utils/frontmatter.ts +3 -1
  377. package/src/utils/fs-watch.ts +2 -6
  378. package/src/utils/gh-protocol.ts +119 -44
  379. package/src/utils/git.ts +13 -15
  380. package/src/utils/guards.ts +2 -5
  381. package/src/utils/ids.ts +9 -2
  382. package/src/utils/incremental-reader.ts +14 -3
  383. package/src/utils/internal-error.ts +2 -1
  384. package/src/utils/names.ts +12 -3
  385. package/src/utils/paths.ts +49 -5
  386. package/src/utils/project-detector.ts +2 -2
  387. package/src/utils/redaction.ts +29 -19
  388. package/src/utils/resolve-shell.ts +9 -7
  389. package/src/utils/run-watcher-registry.ts +19 -31
  390. package/src/utils/safe-paths.ts +45 -24
  391. package/src/utils/scan-cache.ts +9 -2
  392. package/src/utils/session-utils.ts +2 -4
  393. package/src/utils/sleep.ts +12 -5
  394. package/src/utils/sse-parser.ts +5 -17
  395. package/src/utils/visual.ts +52 -32
  396. package/src/workflows/discover-workflows.ts +41 -109
  397. package/src/workflows/intermediate-store.ts +5 -21
  398. package/src/workflows/preflight-validator.ts +6 -33
  399. package/src/workflows/topology-analyzer.ts +5 -21
  400. package/src/workflows/workflow-config.ts +7 -6
  401. package/src/worktree/branch-freshness.ts +66 -8
  402. package/src/worktree/cleanup.ts +130 -20
  403. package/src/worktree/worktree-manager.ts +212 -69
  404. package/skills/artifact-analysis-loop/SKILL.md +0 -303
  405. package/skills/detection-pipeline-design/SKILL.md +0 -286
  406. package/skills/hunting-investigation-loop/SKILL.md +0 -402
  407. package/skills/incident-playbook-construction/SKILL.md +0 -384
  408. package/skills/security-review/SKILL.md +0 -561
  409. package/skills/threat-hypothesis-framework/SKILL.md +0 -176
  410. package/skills/ui-render-performance/SKILL.md +0 -58
  411. /package/docs/{followup-review-round3-2026-05-12.md → archive/followup-review-round3-2026-05-12.md} +0 -0
  412. /package/docs/{followup-review-round4-2026-05-13.md → archive/followup-review-round4-2026-05-13.md} +0 -0
  413. /package/docs/{pi-crew-bugs.md → archive/pi-crew-bugs.md} +0 -0
  414. /package/docs/{pi-crew-test-final.md → archive/pi-crew-test-final.md} +0 -0
  415. /package/docs/{pi-crew-test-results.md → archive/pi-crew-test-results.md} +0 -0
  416. /package/docs/{pi-crew-test-round2.md → archive/pi-crew-test-round2.md} +0 -0
  417. /package/docs/{pi-crew-test-round4.md → archive/pi-crew-test-round4.md} +0 -0
  418. /package/docs/{pi-crew-test-round5.md → archive/pi-crew-test-round5.md} +0 -0
  419. /package/docs/{pi-crew-test-round6.md → archive/pi-crew-test-round6.md} +0 -0
  420. /package/docs/{pi-crew-v0.5.10-audit-fix-plan.md → archive/pi-crew-v0.5.10-audit-fix-plan.md} +0 -0
  421. /package/docs/{pi-crew-v0.5.11-audit-fix-plan.md → archive/pi-crew-v0.5.11-audit-fix-plan.md} +0 -0
  422. /package/docs/{pi-crew-v0.5.12-audit-fix-plan.md → archive/pi-crew-v0.5.12-audit-fix-plan.md} +0 -0
  423. /package/docs/{pi-crew-v0.5.13-audit-fix-plan.md → archive/pi-crew-v0.5.13-audit-fix-plan.md} +0 -0
  424. /package/docs/{pi-crew-v0.5.14-audit-fix-plan.md → archive/pi-crew-v0.5.14-audit-fix-plan.md} +0 -0
  425. /package/docs/{pi-crew-v0.5.16-audit-fix-plan.md → archive/pi-crew-v0.5.16-audit-fix-plan.md} +0 -0
  426. /package/docs/{pi-crew-v0.5.17-audit-fix-plan.md → archive/pi-crew-v0.5.17-audit-fix-plan.md} +0 -0
  427. /package/docs/{pi-crew-v0.5.5-audit-fix-plan.md → archive/pi-crew-v0.5.5-audit-fix-plan.md} +0 -0
  428. /package/docs/{pi-crew-v0.5.9-audit-fix-plan.md → archive/pi-crew-v0.5.9-audit-fix-plan.md} +0 -0
  429. /package/docs/{pi-mono-opportunities.md → archive/pi-mono-opportunities.md} +0 -0
  430. /package/docs/{pi-mono-review.md → archive/pi-mono-review.md} +0 -0
  431. /package/docs/{pi-subagent4-comparison.md → archive/pi-subagent4-comparison.md} +0 -0
  432. /package/docs/{pi-subagents3-deep-analysis.md → archive/pi-subagents3-deep-analysis.md} +0 -0
@@ -1,80 +1,40 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import type {
5
- ExtensionAPI,
6
- ExtensionContext,
7
- } from "@earendil-works/pi-coding-agent";
4
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
5
  import { asRecord, loadConfig } from "../config/config.ts";
9
- import { applyCrewSettingsToConfig, loadCrewSettings } from "../runtime/settings-store.ts";
10
- // 2.7: Lazy-load LiveRunSidebar — only constructed when the user actually opens
11
- // a live run sidebar overlay. The class pulls in transcript-viewer and other
12
- // heavy UI modules.
13
- import type { LiveRunSidebar as LiveRunSidebarType } from "../ui/live-run-sidebar.ts";
14
- import {
15
- type AsyncNotifierState,
16
- startAsyncRunNotifier,
17
- stopAsyncRunNotifier,
18
- } from "./async-notifier.ts";
19
- import { registerAutonomousPolicy } from "./autonomous-policy.ts";
20
- import { registerKnowledgeInjection } from "./knowledge-injection.ts";
21
- import { registerCleanupHandler } from "./crew-cleanup.ts";
22
- import type { ScheduledJob } from "../runtime/scheduler.ts";
23
- import { clearHooksScoped } from "../hooks/registry.ts";
24
- import { uninstallCrewGlobalRegistry } from "./team-tool.ts";
25
- import { notifyActiveRuns } from "./session-summary.ts";
26
-
27
- let _cachedLiveRunSidebar: typeof LiveRunSidebarType | undefined;
28
- async function importLiveRunSidebar(): Promise<typeof LiveRunSidebarType> {
29
- if (!_cachedLiveRunSidebar) {
30
- // LAZY: defer LiveRunSidebar import until the user opens a sidebar overlay.
31
- const mod = await import("../ui/live-run-sidebar.ts");
32
- _cachedLiveRunSidebar = mod.LiveRunSidebar;
33
- }
34
- return _cachedLiveRunSidebar;
35
- }
36
-
37
6
  import { DEFAULT_NOTIFICATIONS, DEFAULT_UI } from "../config/defaults.ts";
38
- import {
39
- type EventToMetricSubscription,
40
- wireEventToMetrics,
41
- } from "../observability/event-to-metric.ts";
42
- // 2.7: Lazy-load OTLPExporter — only loaded when otlp.enabled=true. The
43
- // exporter pulls in node:http/https and serialization helpers that 99% of
44
- // users never need.
45
- import type { OTLPExporter as OTLPExporterType } from "../observability/exporters/otlp-exporter.ts";
46
- import {
47
- createMetricRegistry,
48
- type MetricRegistry,
49
- } from "../observability/metric-registry.ts";
50
- import {
51
- createMetricFileSink,
52
- type MetricSink,
53
- } from "../observability/metric-sink.ts";
7
+ import { clearHooksScoped } from "../hooks/registry.ts";
8
+ // 2.7: Lazy-load OTLPExporter — moved to registration/observability.ts (H3-L2 split).
9
+ import { BatchBarrier, type BatchMember } from "../runtime/batch-barrier.ts";
10
+ import type {
11
+ cancelOrphanedRuns as CancelOrphanedRunsFn,
12
+ detectInterruptedRuns as DetectInterruptedRunsFn,
13
+ purgeStaleActiveRunIndex as PurgeStaleActiveRunIndexFn,
14
+ } from "../runtime/crash-recovery.ts";
15
+ // 2.7: Lazy-load crash-recovery helpers — only invoked from session_start
16
+ // deferred cleanup and cleanupRuntime. Each function is awaited inside an
17
+ // async context that already runs after registration completes.
18
+ import { reconcileAllStaleRuns } from "../runtime/crash-recovery.ts";
19
+ import { appendDeadletter } from "../runtime/deadletter.ts";
54
20
  import { listLiveAgents } from "../runtime/live-agent-manager.ts";
55
21
  import { createManifestCache } from "../runtime/manifest-cache.ts";
22
+ import { cleanupOrphanWorkers } from "../runtime/orphan-worker-registry.ts";
23
+ import { primePeerDep } from "../runtime/peer-dep.ts";
24
+ import { buildValidationBlocker, extractPathFromInput, validateWrittenFile } from "../runtime/per-write-validator.ts";
25
+ import { cleanupLegacyOrphanTempDirs, cleanupOrphanTempDirs } from "../runtime/pi-args.ts";
26
+ import { startRuntimeWarmup } from "../runtime/runtime-warmup.ts";
27
+ import type { ScheduledJob } from "../runtime/scheduler.ts";
56
28
  import { CrewScheduler } from "../runtime/scheduler.ts";
29
+ import { applyCrewSettingsToConfig, loadCrewSettings } from "../runtime/settings-store.ts";
30
+ import { reconcileOrphanedTempWorkspaces } from "../runtime/stale-reconciler.ts";
57
31
  import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
58
32
  import type { TeamRunManifest } from "../state/types.ts";
59
- import {
60
- SubagentManager,
61
- readPersistedSubagentRecord,
62
- } from "../subagents/manager.ts";
63
- import { BatchBarrier, type BatchMember } from "../runtime/batch-barrier.ts";
33
+ import { readPersistedSubagentRecord, SubagentManager } from "../subagents/manager.ts";
64
34
  import { terminateActiveChildPiProcesses } from "../subagents/spawn.ts";
65
- import {
66
- type CrewWidgetState,
67
- stopCrewWidget,
68
- updateCrewWidget,
69
- } from "../ui/widget/index.ts";
70
- import { summarizeHeartbeats } from "../ui/heartbeat-aggregator.ts";
71
35
  import { deployBundledThemes } from "../ui/deploy-bundled-themes.ts";
72
- import {
73
- requestRender,
74
- setExtensionWidget,
75
- setWorkingIndicator,
76
- showCustom,
77
- } from "../ui/pi-ui-compat.ts";
36
+ import { summarizeHeartbeats } from "../ui/heartbeat-aggregator.ts";
37
+ import { requestRender, setExtensionWidget, setWorkingIndicator } from "../ui/pi-ui-compat.ts";
78
38
  import {
79
39
  clearPiCrewPowerbar,
80
40
  disposePowerbarCoalescer,
@@ -85,76 +45,56 @@ import {
85
45
  } from "../ui/powerbar-publisher.ts";
86
46
  import { RenderScheduler } from "../ui/render-scheduler.ts";
87
47
  import { runEventBus } from "../ui/run-event-bus.ts";
88
- import { createTerminalStatusController, type TerminalStatusController } from "../ui/terminal-status.ts";
89
- import { extractPathFromInput, validateWrittenFile, buildValidationBlocker } from "../runtime/per-write-validator.ts";
90
- import { startRuntimeWarmup } from "../runtime/runtime-warmup.ts";
91
- import { primePeerDep } from "../runtime/peer-dep.ts";
92
48
  import { createRunSnapshotCache } from "../ui/run-snapshot-cache.ts";
49
+ import { createTerminalStatusController, type TerminalStatusController } from "../ui/terminal-status.ts";
50
+ import { type CrewWidgetState, stopCrewWidget, updateCrewWidget } from "../ui/widget/index.ts";
93
51
  import { closeWatcher } from "../utils/fs-watch.ts";
94
- import { RunWatcherRegistry } from "../utils/run-watcher-registry.ts";
95
52
  import { logInternalError } from "../utils/internal-error.ts";
96
- import {
97
- clearProjectRootCache,
98
- projectCrewRoot,
99
- userCrewRoot,
100
- } from "../utils/paths.ts";
53
+ import { clearProjectRootCache, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
54
+ import { RunWatcherRegistry } from "../utils/run-watcher-registry.ts";
101
55
  import { resolveContainedPath } from "../utils/safe-paths.ts";
102
56
  import { extractSessionId } from "../utils/session-utils.ts";
103
57
  import { resetTimings, time } from "../utils/timings.ts";
104
- import {
105
- type PiCrewRpcHandle,
106
- registerPiCrewRpc,
107
- } from "./cross-extension-rpc.ts";
108
- import {
109
- type NotificationDescriptor,
110
- NotificationRouter,
111
- } from "./notification-router.ts";
112
- import { createJsonlSink, type NotificationSink } from "./notification-sink.ts";
58
+ // 2.7: Lazy-load LiveRunSidebar — moved to registration/ui.ts (H3-L2 split).
59
+ // The class pulls in transcript-viewer and other heavy UI modules.
60
+ import { type AsyncNotifierState, startAsyncRunNotifier, stopAsyncRunNotifier } from "./async-notifier.ts";
61
+ import { registerAutonomousPolicy } from "./autonomous-policy.ts";
62
+ import { registerContextStatusInjection } from "./context-status-injection.ts";
63
+ import { registerCrewAutocomplete } from "./crew-autocomplete.ts";
64
+ import { registerCleanupHandler } from "./crew-cleanup.ts";
65
+ import { registerCrewInputRouter } from "./crew-input-router.ts";
66
+ import { registerCrewShortcuts } from "./crew-shortcuts.ts";
67
+ import { type PiCrewRpcHandle, registerPiCrewRpc } from "./cross-extension-rpc.ts";
68
+ import { registerKnowledgeInjection } from "./knowledge-injection.ts";
69
+ import { registerCrewMessageRenderers } from "./message-renderers.ts";
70
+ import { type NotificationDescriptor } from "./notification-router.ts";
113
71
  import { runArtifactCleanup } from "./registration/artifact-cleanup.ts";
114
72
  import { registerTeamCommands } from "./registration/commands.ts";
115
73
  import { registerCompactionGuard } from "./registration/compaction-guard.ts";
74
+ // H3-L2 split: lifecycle installer extracted to registration/lifecycle.ts.
75
+ // This module owns the async-run notifier, notification router, delivery coordinator, and run-lifecycle subscriptions.
116
76
  import {
117
- __test__subagentSpawnParams,
118
- sendAgentWakeUp,
119
- sendFollowUp,
120
- } from "./registration/subagent-helpers.ts";
77
+ configureDeliveryCoordinator as configureDeliveryCoordinatorFromRegistration,
78
+ configureNotifications as configureNotificationsFromRegistration,
79
+ disposeDeliveryCoordinator,
80
+ disposeNotifications,
81
+ type LifecycleState,
82
+ startLifecycleWatchers,
83
+ stopLifecycleWatchers,
84
+ } from "./registration/lifecycle.ts";
85
+ // H3-L2 split: observability installer extracted to registration/observability.ts.
86
+ // This module owns metric registry, OTLP, heartbeat-watcher, and auto-repair timers.
87
+ import { configureObservability, disposeObservability, type ObservabilityState } from "./registration/observability.ts";
88
+ import { __test__subagentSpawnParams, sendAgentWakeUp, sendFollowUp } from "./registration/subagent-helpers.ts";
121
89
  import { registerSubagentTools } from "./registration/subagent-tools.ts";
122
- import { registerCrewMessageRenderers } from "./message-renderers.ts";
123
- import { registerCrewInputRouter } from "./crew-input-router.ts";
124
- import { registerCrewAutocomplete } from "./crew-autocomplete.ts";
125
- import { registerCrewShortcuts } from "./crew-shortcuts.ts";
126
- import { registerContextStatusInjection } from "./context-status-injection.ts";
127
90
  import { registerTeamTool } from "./registration/team-tool.ts";
128
- import { handleTeamTool } from "./team-tool.ts";
129
- import { persistScheduledJobUpdate } from "./team-tool/handle-schedule.ts";
91
+ // H3-L2 split: ui installer extracted to registration/ui.ts.
92
+ // This module owns live-run sidebar + powerbar overlay logic.
93
+ import { clearDashboardPowerbar, installLiveSidebar, pushPowerbarUpdate, type UiState } from "./registration/ui.ts";
94
+ import { notifyActiveRuns } from "./session-summary.ts";
130
95
  import { shouldBlockDestructiveTeamAction } from "./team-tool/destructive-gate.ts";
131
-
132
- let _cachedOTLPExporter: typeof OTLPExporterType | undefined;
133
- async function importOTLPExporter(): Promise<typeof OTLPExporterType> {
134
- if (!_cachedOTLPExporter) {
135
- // LAZY: opt-in OTLP metric export — load only when otlp.enabled=true.
136
- const mod = await import("../observability/exporters/otlp-exporter.ts");
137
- _cachedOTLPExporter = mod.OTLPExporter;
138
- }
139
- return _cachedOTLPExporter;
140
- }
141
-
142
- import type {
143
- cancelOrphanedRuns as CancelOrphanedRunsFn,
144
- detectInterruptedRuns as DetectInterruptedRunsFn,
145
- purgeStaleActiveRunIndex as PurgeStaleActiveRunIndexFn,
146
- } from "../runtime/crash-recovery.ts";
147
- // 2.7: Lazy-load crash-recovery helpers — only invoked from session_start
148
- // deferred cleanup and cleanupRuntime. Each function is awaited inside an
149
- // async context that already runs after registration completes.
150
- import {
151
- reconcileAllStaleRuns,
152
- } from "../runtime/crash-recovery.ts";
153
- import { appendDeadletter } from "../runtime/deadletter.ts";
154
- import { HeartbeatWatcher } from "../runtime/heartbeat-watcher.ts";
155
- import { cleanupOrphanTempDirs, cleanupLegacyOrphanTempDirs } from "../runtime/pi-args.ts";
156
- import { cleanupOrphanWorkers } from "../runtime/orphan-worker-registry.ts";
157
- import { reconcileOrphanedTempWorkspaces } from "../runtime/stale-reconciler.ts";
96
+ import { persistScheduledJobUpdate } from "./team-tool/handle-schedule.ts";
97
+ import { handleTeamTool, uninstallCrewGlobalRegistry } from "./team-tool.ts";
158
98
 
159
99
  let _cachedCrashRecovery:
160
100
  | {
@@ -163,9 +103,7 @@ let _cachedCrashRecovery:
163
103
  purgeStaleActiveRunIndex: typeof PurgeStaleActiveRunIndexFn;
164
104
  }
165
105
  | undefined;
166
- async function importCrashRecovery(): Promise<
167
- NonNullable<typeof _cachedCrashRecovery>
168
- > {
106
+ async function importCrashRecovery(): Promise<NonNullable<typeof _cachedCrashRecovery>> {
169
107
  if (!_cachedCrashRecovery) {
170
108
  // LAZY: defer crash-recovery (~14 KB) until session_start cleanup runs.
171
109
  const mod = await import("../runtime/crash-recovery.ts");
@@ -189,13 +127,9 @@ function purgeStaleActiveRunIndexSyncIfLoaded(): void {
189
127
  }
190
128
  }
191
129
 
192
- import {
193
- pruneFinishedRuns,
194
- pruneUserLevelRuns,
195
- } from "../extension/run-maintenance.ts";
130
+ import { pruneFinishedRuns, pruneUserLevelRuns } from "../extension/run-maintenance.ts";
196
131
  import { initI18n } from "../i18n.ts";
197
- import { DeliveryCoordinator } from "../runtime/delivery-coordinator.ts";
198
- import { OverflowRecoveryTracker } from "../runtime/overflow-recovery.ts";
132
+ // H3-L2 split: DeliveryCoordinator + OverflowRecoveryTracker moved to registration/lifecycle.ts.
199
133
  import { tryRegisterSessionCleanup } from "../runtime/session-resources.ts";
200
134
  import { createSessionSnapshot } from "../runtime/session-snapshot.ts";
201
135
 
@@ -245,9 +179,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
245
179
  let manifestCache = createManifestCache(process.cwd());
246
180
  let runSnapshotCache = createRunSnapshotCache(process.cwd());
247
181
  let cacheCwd = process.cwd();
248
- const getManifestCache = (
249
- cwd: string,
250
- ): ReturnType<typeof createManifestCache> => {
182
+ const getManifestCache = (cwd: string): ReturnType<typeof createManifestCache> => {
251
183
  if (manifestCache && cacheCwd === cwd) return manifestCache;
252
184
  if (manifestCache) manifestCache.dispose();
253
185
  if (runSnapshotCache) runSnapshotCache.dispose?.();
@@ -256,299 +188,76 @@ export function registerPiTeams(pi: ExtensionAPI): void {
256
188
  runSnapshotCache = createRunSnapshotCache(cwd);
257
189
  return manifestCache;
258
190
  };
259
- const getRunSnapshotCache = (
260
- cwd: string,
261
- ): ReturnType<typeof createRunSnapshotCache> => {
191
+ const getRunSnapshotCache = (cwd: string): ReturnType<typeof createRunSnapshotCache> => {
262
192
  if (cacheCwd !== cwd) getManifestCache(cwd);
263
193
  return runSnapshotCache;
264
194
  };
265
- const telemetryEnabled = (): boolean =>
266
- loadConfig(currentCtx?.cwd ?? process.cwd()).config.telemetry
267
- ?.enabled !== false;
195
+ const telemetryEnabled = (): boolean => loadConfig(currentCtx?.cwd ?? process.cwd()).config.telemetry?.enabled !== false;
268
196
  const widgetState: CrewWidgetState = { frame: 0 };
269
- let notificationSink: NotificationSink | undefined;
270
- let notificationRouter: NotificationRouter | undefined;
271
- let metricRegistry: MetricRegistry | undefined;
272
- let eventMetricSub: EventToMetricSubscription | undefined;
273
- let metricSink: MetricSink | undefined;
274
- let heartbeatWatcher: HeartbeatWatcher | undefined;
275
- let autoRepairTimer: ReturnType<typeof setInterval> | undefined;
276
- let tempReconcileTimer: ReturnType<typeof setInterval> | undefined;
277
- let otlpExporter: OTLPExporterType | undefined;
278
- let deliveryCoordinator: DeliveryCoordinator | undefined;
279
- let overflowTracker: OverflowRecoveryTracker | undefined;
197
+ // H3-L2 split: notification sink + router moved to registration/lifecycle.ts state.
198
+ // H3-L2 split: Observability state lives in registration/observability.ts;
199
+ // orchestrator keeps a holder so cleanupRuntime can dispose in place.
200
+ const observabilityState: ObservabilityState = {
201
+ metricRegistry: undefined,
202
+ eventMetricSub: undefined,
203
+ metricSink: undefined,
204
+ heartbeatWatcher: undefined,
205
+ autoRepairTimer: undefined,
206
+ tempReconcileTimer: undefined,
207
+ otlpExporter: undefined,
208
+ };
209
+ // H3-L2 split: UI state lives in registration/ui.ts; orchestrator keeps
210
+ // a holder for the live sidebar runId and dashboard-opened flag.
211
+ const uiState: UiState = { liveSidebarRunId: undefined, dashboardOpened: false };
212
+ // H3-L2 split: Lifecycle state lives in registration/lifecycle.ts.
213
+ const lifecycleState: LifecycleState = {
214
+ notifierStarted: false,
215
+ notificationSink: undefined,
216
+ notificationRouter: undefined,
217
+ deliveryCoordinator: undefined,
218
+ overflowTracker: undefined,
219
+ };
280
220
  const configureNotifications = (ctx: ExtensionContext): void => {
281
- notificationRouter?.dispose();
282
- notificationSink?.dispose();
283
- notificationRouter = undefined;
284
- notificationSink = undefined;
285
- const config = loadConfig(ctx.cwd).config;
286
- if (config.notifications?.enabled === false) return;
287
- if (config.telemetry?.enabled !== false)
288
- notificationSink = createJsonlSink(
289
- projectCrewRoot(ctx.cwd),
290
- config.notifications?.sinkRetentionDays ??
291
- DEFAULT_NOTIFICATIONS.sinkRetentionDays,
292
- );
293
- notificationRouter = new NotificationRouter(
294
- {
295
- dedupWindowMs:
296
- config.notifications?.dedupWindowMs ??
297
- DEFAULT_NOTIFICATIONS.dedupWindowMs,
298
- batchWindowMs:
299
- config.notifications?.batchWindowMs ??
300
- DEFAULT_NOTIFICATIONS.batchWindowMs,
301
- quietHours: config.notifications?.quietHours,
302
- severityFilter: config.notifications?.severityFilter ?? [
303
- ...DEFAULT_NOTIFICATIONS.severityFilter,
304
- ],
305
- sink: (notification) => notificationSink?.write(notification),
306
- },
307
- (notification) => {
308
- widgetState.notificationCount =
309
- (widgetState.notificationCount ?? 0) + 1;
310
- sendFollowUp(
311
- pi,
312
- [
313
- notification.title,
314
- notification.body,
315
- notification.runId
316
- ? `Run: ${notification.runId}`
317
- : undefined,
318
- ]
319
- .filter((line): line is string => Boolean(line))
320
- .join("\n"),
321
- );
322
- if (currentCtx) {
323
- const uiConfig = loadConfig(currentCtx.cwd).config.ui;
324
- updateCrewWidget(
325
- currentCtx,
326
- widgetState,
327
- uiConfig,
328
- getManifestCache(currentCtx.cwd),
329
- getRunSnapshotCache(currentCtx.cwd),
330
- );
331
- requestPowerbarUpdate(
332
- pi.events,
333
- currentCtx.cwd,
334
- uiConfig,
335
- getManifestCache(currentCtx.cwd),
336
- getRunSnapshotCache(currentCtx.cwd),
337
- currentCtx,
338
- widgetState.notificationCount ?? 0,
339
- );
340
- }
341
- },
342
- );
221
+ // H3-L2 split: notifications wiring delegated to registration/lifecycle.ts.
222
+ void configureNotificationsFromRegistration(ctx);
343
223
  };
344
- const configureObservability = (ctx: ExtensionContext): void => {
345
- heartbeatWatcher?.dispose();
346
- if (autoRepairTimer) {
347
- clearInterval(autoRepairTimer);
348
- autoRepairTimer = undefined;
349
- }
350
- if (tempReconcileTimer) {
351
- clearInterval(tempReconcileTimer);
352
- tempReconcileTimer = undefined;
353
- }
354
- metricSink?.dispose();
355
- eventMetricSub?.dispose();
356
- otlpExporter?.dispose();
357
- metricRegistry?.dispose();
358
- heartbeatWatcher = undefined;
359
- metricSink = undefined;
360
- eventMetricSub = undefined;
361
- otlpExporter = undefined;
362
- metricRegistry = undefined;
363
- const config = loadConfig(ctx.cwd).config;
364
- if (config.observability?.enabled === false) return;
365
- metricRegistry = createMetricRegistry();
366
- eventMetricSub = wireEventToMetrics(pi.events, metricRegistry);
367
- if (config.telemetry?.enabled !== false)
368
- metricSink = createMetricFileSink({
369
- crewRoot: projectCrewRoot(ctx.cwd),
370
- registry: metricRegistry,
371
- retentionDays: config.observability?.metricRetentionDays ?? 7,
372
- });
373
- if (config.otlp?.enabled === true && config.otlp.endpoint) {
374
- const otlpEndpoint = config.otlp.endpoint;
375
- const otlpHeaders = config.otlp.headers;
376
- const otlpInterval = config.otlp.intervalMs;
377
- const owningRegistry = metricRegistry;
378
- // LAZY: opt-in OTLP export — load the exporter module on first enable.
379
- void importOTLPExporter()
380
- .then((Ctor) => {
381
- if (
382
- cleanedUp ||
383
- metricRegistry !== owningRegistry ||
384
- !owningRegistry
385
- )
386
- return;
387
- otlpExporter = new Ctor(
388
- {
389
- endpoint: otlpEndpoint,
390
- headers: otlpHeaders,
391
- intervalMs: otlpInterval,
392
- },
393
- owningRegistry,
394
- );
395
- otlpExporter.start();
396
- })
397
- .catch((error: unknown) =>
398
- logInternalError("register.otlp-lazy-import", error),
399
- );
400
- }
401
- heartbeatWatcher = new HeartbeatWatcher({
402
- cwd: ctx.cwd,
403
- pollIntervalMs: config.observability?.pollIntervalMs ?? 5000,
404
- manifestCache: getManifestCache(ctx.cwd),
405
- registry: metricRegistry,
406
- router: {
407
- enqueue: (notification) => {
408
- notifyOperator(notification);
409
- return true;
410
- },
411
- },
412
- deadletterTickThreshold:
413
- config.reliability?.deadletterThreshold ?? 3,
414
- onDeadletterTrigger: (manifest, taskId) => {
415
- appendDeadletter(manifest, {
416
- taskId,
417
- runId: manifest.runId,
418
- reason: "heartbeat-dead",
419
- attempts: 0,
420
- timestamp: new Date().toISOString(),
421
- });
422
- metricRegistry
423
- ?.counter(
424
- "crew.task.deadletter_total",
425
- "Deadletter triggers by reason",
426
- )
427
- .inc({ reason: "heartbeat-dead" });
428
- pi.events?.emit?.("crew.task.deadletter", {
429
- runId: manifest.runId,
430
- taskId,
431
- reason: "heartbeat-dead",
432
- });
433
- },
224
+ async function configureNotificationsFromRegistration(ctx: ExtensionContext): Promise<void> {
225
+ // LAZY: registration/lifecycle is heavy (notification-router + sink)
226
+ const lifecycleModule = await import("./registration/lifecycle.ts");
227
+ await lifecycleModule.configureNotifications(ctx, lifecycleState, {
228
+ pi,
229
+ widgetState,
230
+ getCurrentCtx: () => currentCtx,
231
+ getManifestCache,
232
+ getRunSnapshotCache,
233
+ requestPowerbarUpdate,
434
234
  });
435
- heartbeatWatcher.start();
436
-
437
- // Auto-repair: periodically reconcile stale/zombie runs during runtime.
438
- // This catches tasks whose worker process died without calling submit_result,
439
- // or whose heartbeat went dead while the session is still active.
440
- if (autoRepairTimer) {
441
- clearInterval(autoRepairTimer);
442
- autoRepairTimer = undefined;
443
- }
444
- if (tempReconcileTimer) {
445
- clearInterval(tempReconcileTimer);
446
- tempReconcileTimer = undefined;
447
- }
448
- const autoRepairIntervalMs =
449
- config.reliability?.autoRepairIntervalMs ?? 60_000;
450
- if (autoRepairIntervalMs > 0) {
451
- autoRepairTimer = setInterval(() => {
452
- if (cleanedUp || !currentCtx) return;
453
- try {
454
- const staleResults = reconcileAllStaleRuns(
455
- currentCtx.cwd,
456
- getManifestCache(currentCtx.cwd),
457
- );
458
- if (staleResults.length > 0) {
459
- for (const result of staleResults) {
460
- if (result.repaired) {
461
- notifyOperator({
462
- id: `auto_repair_${result.runId}`,
463
- severity: "info",
464
- source: "auto-repair",
465
- runId: result.runId,
466
- title: `Auto-repaired stale run`,
467
- body: result.detail,
468
- });
469
- }
470
- }
471
- }
472
- } catch (error) {
473
- logInternalError("register.autoRepair", error);
474
- }
475
- }, autoRepairIntervalMs);
476
- autoRepairTimer.unref();
477
- }
478
-
479
- // Auto-repair: also scan /tmp/ for orphaned pi-crew-* workspaces.
480
- // This catches zombie runs from tests or crashed sessions.
481
- if (autoRepairIntervalMs > 0) {
482
- tempReconcileTimer = setInterval(() => {
483
- if (cleanedUp) return;
484
- try {
485
- reconcileOrphanedTempWorkspaces(Date.now(), {
486
- cleanupOrphanedTempDirs:
487
- config.reliability?.cleanupOrphanedTempDirs,
488
- });
489
- // Layer 4: also clean orphan temp dirs under
490
- // ~/.pi/agent/pi-crew/tmp/ that the SIGKILL'd parent
491
- // processes left behind. Catches anything Layers 1-3 missed.
492
- const orphanResult = cleanupOrphanTempDirs();
493
- if (orphanResult.cleaned > 0) {
494
- notifyOperator({
495
- id: `layer4_temp_cleanup_${Date.now()}`,
496
- severity: "info",
497
- source: "temp-cleanup",
498
- title: `Layer 4: cleaned ${orphanResult.cleaned} orphan temp dir(s)`,
499
- body: `~/.pi/agent/pi-crew/tmp/ orphans older than 24h removed (scanned ${orphanResult.scanned}, failed ${orphanResult.failed}).`,
500
- });
501
- }
502
- // Layer 5: clean legacy /tmp/pi-crew-* prompt/task orphans
503
- // from before commit 8ba270d moved temp dirs out of /tmp.
504
- // The existing reconcileOrphanedTempWorkspaces only cleans
505
- // dirs containing .crew/state/runs/ (run-state dirs), so
506
- // prompt/task orphans are never touched by Layer 3.
507
- const legacyResult = cleanupLegacyOrphanTempDirs();
508
- if (legacyResult.cleaned > 0) {
509
- notifyOperator({
510
- id: `layer5_legacy_temp_cleanup_${Date.now()}`,
511
- severity: "info",
512
- source: "temp-cleanup",
513
- title: `Layer 5: cleaned ${legacyResult.cleaned} legacy /tmp/pi-crew-* orphan(s)`,
514
- body: `Pre-fix /tmp/pi-crew-* prompt/task orphans (no .crew/state/runs/, >24h) removed (scanned ${legacyResult.scanned}, failed ${legacyResult.failed}).`,
515
- });
516
- }
517
- } catch (error) {
518
- logInternalError("register.tempAutoRepair", error);
519
- }
520
- }, autoRepairIntervalMs * 5); // Less frequent: every 5 min by default
521
- tempReconcileTimer.unref();
522
- }
523
-
524
- if (config.reliability?.autoRecover === true) {
525
- const cwdSnapshot = ctx.cwd;
526
- const cacheSnapshot = getManifestCache(cwdSnapshot);
527
- void importCrashRecovery()
528
- .then(({ detectInterruptedRuns }) => {
529
- if (cleanedUp) return;
530
- for (const plan of detectInterruptedRuns(
531
- cwdSnapshot,
532
- cacheSnapshot,
533
- )) {
534
- notifyOperator({
535
- id: `recovery_prompt_${plan.runId}`,
536
- severity: "warning",
537
- source: "crash-recovery",
538
- runId: plan.runId,
539
- title: `Run ${plan.runId} was interrupted`,
540
- body: `${plan.resumableTasks.length} tasks pending recovery. Open dashboard to inspect before resuming.`,
541
- });
542
- }
543
- })
544
- .catch((error: unknown) =>
545
- logInternalError(
546
- "register.crash-recovery-lazy-import",
547
- error,
548
- ),
549
- );
550
- }
235
+ }
236
+ const configureObservability = (ctx: ExtensionContext): void => {
237
+ // H3-L2 split: observability installation delegated to registration/observability.ts.
238
+ // This keeps the orchestrator thin while preserving the same lifecycle:
239
+ // dispose prior install fresh gate on config flags.
240
+ void configureObservabilityFromRegistration(ctx);
551
241
  };
242
+ // H3-L2 split: wraps the registration/observability.configureObservability call.
243
+ // Pulled out as a method so we can build the deps bag once at registration time
244
+ // and reuse it across session_start cycles.
245
+ async function configureObservabilityFromRegistration(ctx: ExtensionContext): Promise<void> {
246
+ // LAZY: registration/observability is heavy (HeartbeatWatcher + metric stack)
247
+ const observabilityModule = await import("./registration/observability.ts");
248
+ await observabilityModule.configureObservability(ctx, observabilityState, {
249
+ pi,
250
+ getManifestCache,
251
+ notifyOperator,
252
+ isCleanedUp: () => cleanedUp,
253
+ reconcileStaleRuns: (cwd, cache) => reconcileAllStaleRuns(cwd, cache),
254
+ reconcileOrphanedTempWorkspaces: (now, opts) => reconcileOrphanedTempWorkspaces(now, opts),
255
+ cleanupOrphanTempDirs,
256
+ cleanupLegacyOrphanTempDirs,
257
+ appendDeadletter: (manifest, entry) => appendDeadletter(manifest, entry as Parameters<typeof appendDeadletter>[1]),
258
+ importCrashRecovery,
259
+ });
260
+ }
552
261
  const autoRecoveryLast = new Map<string, { insertedAt: number; lastAccessAt: number }>();
553
262
  // FIX (Round 22, defensive cap): Bound the cooldown-gate Map. Each run
554
263
  // contributes up to 4 keys (one per maybeNotifyHealth kind). Without a cap,
@@ -560,85 +269,28 @@ export function registerPiTeams(pi: ExtensionAPI): void {
560
269
  // being re-accessed.
561
270
  const AUTO_RECOVERY_LAST_MAX_ENTRIES = 1000;
562
271
  const configureDeliveryCoordinator = (): void => {
563
- deliveryCoordinator?.dispose();
564
- deliveryCoordinator = undefined;
565
- overflowTracker?.dispose();
566
- overflowTracker = undefined;
567
- deliveryCoordinator = new DeliveryCoordinator({
568
- emit: (event, data) => {
569
- pi.events?.emit?.(event, data);
570
- },
571
- sendFollowUp: (title, body) => {
572
- sendFollowUp(
573
- pi,
574
- [title, body]
575
- .filter((line): line is string => Boolean(line))
576
- .join("\n"),
577
- );
578
- },
579
- sendWakeUp: (message) => {
580
- sendAgentWakeUp(pi, message);
581
- },
582
- });
583
- overflowTracker = new OverflowRecoveryTracker({
584
- onPhaseChange: (state, previousPhase) => {
585
- if (metricRegistry) {
586
- metricRegistry
587
- .counter(
588
- "crew.task.overflow_recovery_total",
589
- "Overflow recovery phase transitions",
590
- )
591
- .inc({
592
- phase: state.phase,
593
- previous_phase: previousPhase,
594
- });
595
- }
596
- pi.events?.emit?.("crew.task.overflow", {
597
- runId: state.runId,
598
- taskId: state.taskId,
599
- phase: state.phase,
600
- previousPhase,
601
- });
602
- },
603
- onTimeout: (state) => {
604
- notifyOperator({
605
- id: `overflow_timeout_${state.taskId}`,
606
- severity: "warning",
607
- source: "overflow-recovery",
608
- runId: state.runId,
609
- title: `Task ${state.taskId} overflow recovery timed out`,
610
- body: `Phase: ${state.phase}, compaction_count: ${state.compactionCount}, retry_count: ${state.retryCount}. The task may be stuck.`,
611
- });
612
- },
272
+ // H3-L2 split: delivery coordinator + overflow tracker moved to registration/lifecycle.ts.
273
+ void configureDeliveryCoordinatorFromRegistration(lifecycleState, {
274
+ pi,
275
+ observabilityState,
276
+ notifyOperator,
277
+ sendFollowUp,
278
+ sendAgentWakeUp,
613
279
  });
614
280
  };
615
281
  const notifyOperator = (notification: NotificationDescriptor): void => {
616
282
  try {
617
- notificationRouter?.enqueue(notification);
283
+ lifecycleState.notificationRouter?.enqueue(notification);
618
284
  } catch (error) {
619
285
  logInternalError("register.notification", error);
620
- sendFollowUp(
621
- pi,
622
- [notification.title, notification.body]
623
- .filter((line): line is string => Boolean(line))
624
- .join("\n"),
625
- );
286
+ sendFollowUp(pi, [notification.title, notification.body].filter((line): line is string => Boolean(line)).join("\n"));
626
287
  }
627
288
  };
628
289
  const captureSessionGeneration = (): number => sessionGeneration;
629
- const isOwnerSessionCurrent = (
630
- ownerGeneration: number | undefined,
631
- ): boolean =>
632
- !cleanedUp &&
633
- (ownerGeneration === undefined ||
634
- ownerGeneration === sessionGeneration);
635
- const isContextCurrent = (
636
- ctx: ExtensionContext,
637
- ownerGeneration: number,
638
- ): boolean =>
639
- !cleanedUp &&
640
- currentCtx === ctx &&
641
- sessionGeneration === ownerGeneration;
290
+ const isOwnerSessionCurrent = (ownerGeneration: number | undefined): boolean =>
291
+ !cleanedUp && (ownerGeneration === undefined || ownerGeneration === sessionGeneration);
292
+ const isContextCurrent = (ctx: ExtensionContext, ownerGeneration: number): boolean =>
293
+ !cleanedUp && currentCtx === ctx && sessionGeneration === ownerGeneration;
642
294
  const batchBarrier = new BatchBarrier();
643
295
  const subagentManager = new SubagentManager(
644
296
  4,
@@ -685,13 +337,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
685
337
  setTimeout(() => {
686
338
  if (cleanedUp) return;
687
339
  const fresh = subagentManager.getRecord(agentId);
688
- const persisted = currentCtx
689
- ? readPersistedSubagentRecord(currentCtx.cwd, agentId)
690
- : undefined;
340
+ const persisted = currentCtx ? readPersistedSubagentRecord(currentCtx.cwd, agentId) : undefined;
691
341
  // Leader already joined the result -> suppress redundant notify.
692
342
  if (fresh?.resultConsumed || persisted?.resultConsumed) return;
693
- if (!isOwnerSessionCurrent(fresh?.ownerSessionGeneration ?? ownerGen))
694
- return;
343
+ if (!isOwnerSessionCurrent(fresh?.ownerSessionGeneration ?? ownerGen)) return;
695
344
  // Rule 1 (batch coalescing): if this agent belongs to a batch, never
696
345
  // emit an individual notification. Instead record its terminal state
697
346
  // in the barrier; emit ONE consolidated notification only when ALL
@@ -707,10 +356,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
707
356
  if (snap.allDone && !snap.notified) {
708
357
  batchBarrier.markNotified(agentBatchId);
709
358
  const roster = snap.terminal
710
- .map(
711
- (m) =>
712
- `- ${m.id} [${m.status}] (${m.type ?? "agent"}): ${m.description ?? ""}`,
713
- )
359
+ .map((m) => `- ${m.id} [${m.status}] (${m.type ?? "agent"}): ${m.description ?? ""}`)
714
360
  .join("\n");
715
361
  const joinInstruction = [
716
362
  `All ${snap.terminal.length} background subagents in batch "${agentBatchId}" have finished.`,
@@ -765,26 +411,12 @@ export function registerPiTeams(pi: ExtensionAPI): void {
765
411
  },
766
412
  1000,
767
413
  (event, payload) => {
768
- const ownerGeneration =
769
- typeof payload.ownerSessionGeneration === "number"
770
- ? payload.ownerSessionGeneration
771
- : undefined;
772
- if (
773
- ownerGeneration !== undefined &&
774
- !isOwnerSessionCurrent(ownerGeneration)
775
- )
776
- return;
414
+ const ownerGeneration = typeof payload.ownerSessionGeneration === "number" ? payload.ownerSessionGeneration : undefined;
415
+ if (ownerGeneration !== undefined && !isOwnerSessionCurrent(ownerGeneration)) return;
777
416
  if (event === "subagent.stuck-blocked") {
778
- const id =
779
- typeof payload.id === "string" ? payload.id : "unknown";
780
- const runId =
781
- typeof payload.runId === "string"
782
- ? payload.runId
783
- : "unknown";
784
- const durationMs =
785
- typeof payload.durationMs === "number"
786
- ? payload.durationMs
787
- : 0;
417
+ const id = typeof payload.id === "string" ? payload.id : "unknown";
418
+ const runId = typeof payload.runId === "string" ? payload.runId : "unknown";
419
+ const durationMs = typeof payload.durationMs === "number" ? payload.durationMs : 0;
788
420
  notifyOperator({
789
421
  id: `subagent-stuck:${id}:${runId}`,
790
422
  severity: "warning",
@@ -798,7 +430,6 @@ export function registerPiTeams(pi: ExtensionAPI): void {
798
430
  },
799
431
  );
800
432
  const foregroundControllers = new Map<string | symbol, AbortController>();
801
- let liveSidebarRunId: string | undefined;
802
433
  let renderScheduler: RenderScheduler | undefined;
803
434
  const renderSchedulerUnsubscribers: Array<() => void> = [];
804
435
  // T4 (v0.8.3): terminal tab title + Ghostty native progress bar. Lazily
@@ -834,21 +465,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
834
465
  // P0 fix: stopSessionBoundSubagents must NOT abort foreground team runs on session switch.
835
466
  // Foreground team runs run in the same process as the session; they naturally clean up
836
467
  // when the session context is torn down. Only subagents need explicit abort on switch.
837
- const foregroundTeamRunControllers = new Map<
838
- string | symbol,
839
- AbortController
840
- >();
468
+ const foregroundTeamRunControllers = new Map<string | symbol, AbortController>();
841
469
 
842
470
  const stopSessionBoundSubagents = (): void => {
843
471
  // Only abort subagent controllers — NOT foreground team runs.
844
472
  // Foreground team runs are bound to the session lifecycle; they will be aborted
845
473
  // by cleanupRuntime during session_shutdown.
846
- for (const controller of foregroundControllers.values())
847
- controller.abort();
474
+ for (const controller of foregroundControllers.values()) controller.abort();
848
475
  foregroundControllers.clear();
849
- subagentManager.abortAll(
850
- "Session switching — foreground subagents cancelled.",
851
- );
476
+ subagentManager.abortAll("Session switching — foreground subagents cancelled.");
852
477
  terminateActiveChildPiProcesses();
853
478
  disposeRenderSchedulerSubscriptions();
854
479
  renderScheduler?.dispose();
@@ -856,95 +481,28 @@ export function registerPiTeams(pi: ExtensionAPI): void {
856
481
  terminalStatus?.dispose();
857
482
  terminalStatus = undefined;
858
483
  terminalStatusActive = false;
859
- liveSidebarRunId = undefined;
860
- if (currentCtx)
861
- stopCrewWidget(
862
- currentCtx,
863
- widgetState,
864
- loadConfig(currentCtx.cwd).config.ui,
865
- );
484
+ uiState.liveSidebarRunId = undefined;
485
+ if (currentCtx) stopCrewWidget(currentCtx, widgetState, loadConfig(currentCtx.cwd).config.ui);
866
486
  clearPiCrewPowerbar(pi.events);
867
487
  };
868
488
  const openLiveSidebar = (ctx: ExtensionContext, runId: string): void => {
869
- const uiConfig = loadConfig(ctx.cwd).config.ui;
870
- const autoOpen = uiConfig?.autoOpenDashboard === true;
871
- const foregroundAutoOpen =
872
- uiConfig?.autoOpenDashboardForForegroundRuns ??
873
- DEFAULT_UI.autoOpenDashboardForForegroundRuns;
874
- if (
875
- !ctx.hasUI ||
876
- !autoOpen ||
877
- !foregroundAutoOpen ||
878
- (uiConfig?.dashboardPlacement ?? DEFAULT_UI.dashboardPlacement) !==
879
- "right"
880
- )
881
- return;
882
- if (liveSidebarRunId === runId) return;
883
- liveSidebarRunId = runId;
884
- const widgetPlacement =
885
- uiConfig?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
886
- setExtensionWidget(ctx, "pi-crew", undefined, {
887
- placement: widgetPlacement,
888
- });
889
- setExtensionWidget(ctx, "pi-crew-active", undefined, {
890
- placement: widgetPlacement,
891
- });
892
- widgetState.lastVisibility = "hidden";
893
- widgetState.lastPlacement = widgetPlacement;
894
- widgetState.lastKey = "pi-crew-active";
895
- widgetState.model = undefined;
896
- const width = Math.min(
897
- 90,
898
- Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth),
899
- );
900
- void importLiveRunSidebar()
901
- .then((LiveRunSidebar) => {
902
- if (cleanedUp || !currentCtx) return;
903
- void showCustom<undefined>(
904
- ctx,
905
- (_tui, theme, _keybindings, done) =>
906
- new LiveRunSidebar({
907
- cwd: ctx.cwd,
908
- runId,
909
- done,
910
- theme,
911
- config: uiConfig,
912
- snapshotCache: getRunSnapshotCache(ctx.cwd),
913
- }),
914
- {
915
- overlay: true,
916
- overlayOptions: {
917
- width,
918
- minWidth: 40,
919
- maxHeight: "100%",
920
- anchor: "top-right",
921
- offsetX: 0,
922
- offsetY: 0,
923
- margin: { top: 0, right: 0, bottom: 0, left: 0 },
924
- visible: (termWidth: number) => termWidth >= 100,
925
- },
926
- },
927
- ).finally(() => {
928
- if (liveSidebarRunId === runId)
929
- liveSidebarRunId = undefined;
930
- updateCrewWidget(
931
- ctx,
932
- widgetState,
933
- loadConfig(ctx.cwd).config.ui,
934
- getManifestCache(ctx.cwd),
935
- getRunSnapshotCache(ctx.cwd),
936
- );
937
- });
938
- })
939
- .catch((error: unknown) =>
940
- logInternalError("register.live-sidebar-lazy-import", error),
941
- );
489
+ // H3-L2 split: live sidebar delegated to registration/ui.ts.
490
+ void installLiveSidebarFromRegistration(ctx, runId);
942
491
  };
943
- const startForegroundRun = (
944
- ctx: ExtensionContext,
945
- runner: (signal?: AbortSignal) => Promise<void>,
946
- runId?: string,
947
- ): void => {
492
+ // H3-L2 split: wraps registration/ui.ts installLiveSidebar call.
493
+ async function installLiveSidebarFromRegistration(ctx: ExtensionContext, runId: string): Promise<void> {
494
+ // LAZY: registration/ui only imported when a live sidebar is actually opened.
495
+ const uiModule = await import("./registration/ui.ts");
496
+ await uiModule.installLiveSidebar(ctx, runId, uiState, {
497
+ pi,
498
+ widgetState,
499
+ getManifestCache,
500
+ getRunSnapshotCache,
501
+ isCleanedUp: () => cleanedUp,
502
+ getCurrentCtx: () => currentCtx,
503
+ });
504
+ }
505
+ const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string): void => {
948
506
  const ownerGeneration = captureSessionGeneration();
949
507
  const controller = new AbortController();
950
508
  const key = runId ?? Symbol();
@@ -954,11 +512,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
954
512
  frames: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
955
513
  intervalMs: 80,
956
514
  });
957
- ctx.ui.setWorkingMessage(
958
- runId
959
- ? `pi-crew foreground run ${runId}...`
960
- : "pi-crew foreground run...",
961
- );
515
+ ctx.ui.setWorkingMessage(runId ? `pi-crew foreground run ${runId}...` : "pi-crew foreground run...");
962
516
  }
963
517
  // Start watchdog for foreground run — periodic health check that
964
518
  // auto-notifies the assistant if the run appears hung or completes.
@@ -974,8 +528,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
974
528
  setImmediate(() => {
975
529
  void runner(controller.signal)
976
530
  .catch((error) => {
977
- const message =
978
- error instanceof Error ? error.message : String(error);
531
+ const message = error instanceof Error ? error.message : String(error);
979
532
  if (runId) {
980
533
  try {
981
534
  const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency. Post-run status updates tolerate slight staleness.
@@ -986,30 +539,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
986
539
  loaded.manifest.status !== "cancelled" &&
987
540
  loaded.manifest.status !== "blocked"
988
541
  )
989
- updateRunStatus(
990
- loaded.manifest,
991
- "failed",
992
- message,
993
- );
542
+ updateRunStatus(loaded.manifest, "failed", message);
994
543
  } catch (statusError) {
995
- logInternalError(
996
- "register.foreground-run-failure",
997
- statusError,
998
- `runId=${runId}`,
999
- );
544
+ logInternalError("register.foreground-run-failure", statusError, `runId=${runId}`);
1000
545
  }
1001
546
  }
1002
- if (isContextCurrent(ctx, ownerGeneration))
1003
- ctx.ui.notify(
1004
- `pi-crew foreground run failed: ${message}`,
1005
- "error",
1006
- );
1007
- else
1008
- logInternalError(
1009
- "register.foreground-run-failure",
1010
- error,
1011
- `runId=${runId} context disposed`,
1012
- );
547
+ if (isContextCurrent(ctx, ownerGeneration)) ctx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
548
+ else logInternalError("register.foreground-run-failure", error, `runId=${runId} context disposed`);
1013
549
  })
1014
550
  .finally(() => {
1015
551
  foregroundTeamRunControllers.delete(key);
@@ -1034,12 +570,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1034
570
  if (ownerCurrent && runId) {
1035
571
  const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency. Post-run status updates tolerate slight staleness.
1036
572
  const status = loaded?.manifest.status ?? "finished";
1037
- const level =
1038
- status === "failed" || status === "blocked"
1039
- ? "error"
1040
- : status === "cancelled"
1041
- ? "warning"
1042
- : "info";
573
+ const level = status === "failed" || status === "blocked" ? "error" : status === "cancelled" ? "warning" : "info";
1043
574
  ctx.ui.notify(
1044
575
  `pi-crew run ${runId} ${status}. Use /team-summary ${runId} or /team-status ${runId}.`,
1045
576
  level as "info" | "warning" | "error",
@@ -1101,9 +632,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1101
632
  registerKnowledgeInjection(pi);
1102
633
  time("register.knowledge");
1103
634
  time("register.rpc");
1104
- function getPiEvents():
1105
- | Parameters<typeof registerPiCrewRpc>[0]
1106
- | undefined {
635
+ function getPiEvents(): Parameters<typeof registerPiCrewRpc>[0] | undefined {
1107
636
  if (pi && typeof pi === "object" && "events" in pi) {
1108
637
  // pi.events may not be typed in the original pi type, so cast through unknown
1109
638
  const events = (pi as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events;
@@ -1116,105 +645,58 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1116
645
  // Register global RPC registry for cross-extension access (mirrors pi-subagents3's Symbol.for pattern)
1117
646
  // Uses lazy import to avoid pulling team-tool.ts into module load.
1118
647
  // Other extensions access via: const reg = globalThis[Symbol.for("pi-crew:registry")];
1119
- void import("./team-tool.ts").then(
1120
- ({ registerCrewGlobalRegistry, installCrewGlobalRegistry }) => {
1121
- // Phase 3b: installCrewGlobalRegistry creates a v2 registry with agent registration API.
1122
- // We then patch the manifest-backed methods with real implementations below.
1123
- const manifestCacheForRegistry = getManifestCache(
1124
- currentCtx?.cwd ?? process.cwd(),
1125
- );
1126
- installCrewGlobalRegistry();
1127
- const CREW_REGISTRY_KEY = Symbol.for("pi-crew:registry");
1128
- const registry = (globalThis as Record<symbol | string, unknown>)[
1129
- CREW_REGISTRY_KEY
1130
- ] as Record<string, unknown>;
1131
- // Phase 3b (defensive): Validate registry structure before patching methods.
1132
- // If a previous occupant left a non-conforming object, replace it entirely.
1133
- // This prevents runtime failures if getRecord/listRuns/etc. are called on a
1134
- // malformed predecessor value.
1135
- if (
1136
- registry === null ||
1137
- typeof registry !== "object" ||
1138
- Array.isArray(registry)
1139
- ) {
1140
- (globalThis as Record<symbol | string, unknown>)[
1141
- CREW_REGISTRY_KEY
1142
- ] = {};
1143
- }
1144
- const validatedRegistry = (globalThis as Record<symbol | string, unknown>)[
1145
- CREW_REGISTRY_KEY
1146
- ] as Record<string, unknown>;
1147
- validatedRegistry.getRecord = (runId: string) =>
1148
- manifestCacheForRegistry.get(runId);
1149
- validatedRegistry.listRuns = () =>
1150
- manifestCacheForRegistry
1151
- .list(100)
1152
- .map(
1153
- (m: {
1154
- runId: string;
1155
- status: string;
1156
- goal: string;
1157
- }) => ({
1158
- runId: m.runId,
1159
- status: m.status,
1160
- goal: m.goal,
1161
- }),
1162
- );
1163
- validatedRegistry.appendEvent = (
1164
- runId: string,
1165
- event: Record<string, unknown>,
1166
- ) => {
1167
- const manifest = manifestCacheForRegistry.get(runId);
1168
- if (manifest)
1169
- void import("../state/event-log.ts").then(
1170
- ({ appendEventFireAndForget }) =>
1171
- appendEventFireAndForget(
1172
- manifest.eventsPath,
1173
- event as Parameters<
1174
- typeof appendEventFireAndForget
1175
- >[1],
1176
- ),
1177
- );
1178
- };
1179
- validatedRegistry.waitForAll = async (runId: string) => {
1180
- // LAZY: state-store only needed for post-completion polling (waitForAll) and sync hasRunning check; avoid at startup.
1181
- const { loadRunManifestById } = await import(
1182
- "../state/state-store.ts"
1183
- );
1184
- const check = (): boolean => {
1185
- const loaded = loadRunManifestById(
1186
- currentCtx?.cwd ?? process.cwd(),
1187
- runId,
1188
- );
1189
- if (!loaded) return true;
1190
- return !loaded.tasks.some(
1191
- (t: { status: string }) =>
1192
- t.status === "running" || t.status === "queued",
1193
- );
1194
- };
1195
- while (!check())
1196
- await new Promise((resolve) => setTimeout(resolve, 500));
1197
- };
1198
- validatedRegistry.hasRunning = async (runId: string) => {
1199
- const manifest = manifestCacheForRegistry.get(runId);
1200
- if (!manifest) return false;
1201
- // LAZY: state-store only needed in hasRunning; avoid at startup.
1202
- // Use dynamic import to avoid CJS/ESM mixed module issues.
1203
- const { loadRunManifestById: loadRunForHasRunning } =
1204
- // LAZY: defer dynamic import of ../state/state-store.ts to its call site.
1205
- await import("../state/state-store.ts");
1206
- const loaded = loadRunForHasRunning(
1207
- currentCtx?.cwd ?? process.cwd(),
1208
- runId,
1209
- );
1210
- if (!loaded) return false;
1211
- return loaded.tasks.some(
1212
- (t: { status: string }) =>
1213
- t.status === "running" || t.status === "queued",
648
+ void import("./team-tool.ts").then(({ registerCrewGlobalRegistry, installCrewGlobalRegistry }) => {
649
+ // Phase 3b: installCrewGlobalRegistry creates a v2 registry with agent registration API.
650
+ // We then patch the manifest-backed methods with real implementations below.
651
+ const manifestCacheForRegistry = getManifestCache(currentCtx?.cwd ?? process.cwd());
652
+ installCrewGlobalRegistry();
653
+ const CREW_REGISTRY_KEY = Symbol.for("pi-crew:registry");
654
+ const registry = (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] as Record<string, unknown>;
655
+ // Phase 3b (defensive): Validate registry structure before patching methods.
656
+ // If a previous occupant left a non-conforming object, replace it entirely.
657
+ // This prevents runtime failures if getRecord/listRuns/etc. are called on a
658
+ // malformed predecessor value.
659
+ if (registry === null || typeof registry !== "object" || Array.isArray(registry)) {
660
+ (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] = {};
661
+ }
662
+ const validatedRegistry = (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] as Record<string, unknown>;
663
+ validatedRegistry.getRecord = (runId: string) => manifestCacheForRegistry.get(runId);
664
+ validatedRegistry.listRuns = () =>
665
+ manifestCacheForRegistry.list(100).map((m: { runId: string; status: string; goal: string }) => ({
666
+ runId: m.runId,
667
+ status: m.status,
668
+ goal: m.goal,
669
+ }));
670
+ validatedRegistry.appendEvent = (runId: string, event: Record<string, unknown>) => {
671
+ const manifest = manifestCacheForRegistry.get(runId);
672
+ if (manifest)
673
+ void import("../state/event-log.ts").then(({ appendEventFireAndForget }) =>
674
+ appendEventFireAndForget(manifest.eventsPath, event as Parameters<typeof appendEventFireAndForget>[1]),
1214
675
  );
676
+ };
677
+ validatedRegistry.waitForAll = async (runId: string) => {
678
+ // LAZY: state-store only needed for post-completion polling (waitForAll) and sync hasRunning check; avoid at startup.
679
+ const { loadRunManifestById } = await import("../state/state-store.ts");
680
+ const check = (): boolean => {
681
+ const loaded = loadRunManifestById(currentCtx?.cwd ?? process.cwd(), runId);
682
+ if (!loaded) return true;
683
+ return !loaded.tasks.some((t: { status: string }) => t.status === "running" || t.status === "queued");
1215
684
  };
1216
- },
1217
- );
685
+ while (!check()) await new Promise((resolve) => setTimeout(resolve, 500));
686
+ };
687
+ validatedRegistry.hasRunning = async (runId: string) => {
688
+ const manifest = manifestCacheForRegistry.get(runId);
689
+ if (!manifest) return false;
690
+ // LAZY: state-store only needed in hasRunning; avoid at startup.
691
+ // Use dynamic import to avoid CJS/ESM mixed module issues.
692
+ const { loadRunManifestById: loadRunForHasRunning } =
693
+ // LAZY: defer dynamic import of ../state/state-store.ts to its call site.
694
+ await import("../state/state-store.ts");
695
+ const loaded = loadRunForHasRunning(currentCtx?.cwd ?? process.cwd(), runId);
696
+ if (!loaded) return false;
697
+ return loaded.tasks.some((t: { status: string }) => t.status === "running" || t.status === "queued");
698
+ };
699
+ });
1218
700
 
1219
701
  const cleanupRuntime = (): void => {
1220
702
  if (cleanedUp) return;
@@ -1230,8 +712,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1230
712
  stopSessionBoundSubagents();
1231
713
  // P0 fix: also abort foreground team runs on session shutdown (not on session switch).
1232
714
  // This is the only place where foreground team run controllers should be aborted.
1233
- for (const controller of foregroundTeamRunControllers.values())
1234
- controller.abort();
715
+ for (const controller of foregroundTeamRunControllers.values()) controller.abort();
1235
716
  foregroundTeamRunControllers.clear();
1236
717
  crewScheduler?.stop();
1237
718
  stopAsyncRunNotifier(notifierState);
@@ -1260,37 +741,17 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1260
741
  // the next session_start will fire the lazy import + purge.
1261
742
  purgeStaleActiveRunIndexSyncIfLoaded();
1262
743
 
1263
- stopCrewWidget(
1264
- currentCtx,
1265
- widgetState,
1266
- currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined,
1267
- );
744
+ stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
1268
745
  clearPiCrewPowerbar(pi.events);
1269
746
  disposePowerbarCoalescer();
1270
- heartbeatWatcher?.dispose();
1271
- if (autoRepairTimer) {
1272
- clearInterval(autoRepairTimer);
1273
- autoRepairTimer = undefined;
1274
- }
1275
- if (tempReconcileTimer) {
1276
- clearInterval(tempReconcileTimer);
1277
- tempReconcileTimer = undefined;
1278
- }
1279
- metricSink?.dispose();
1280
- eventMetricSub?.dispose();
1281
- otlpExporter?.dispose();
1282
- metricRegistry?.dispose();
1283
- heartbeatWatcher = undefined;
1284
- metricSink = undefined;
1285
- eventMetricSub = undefined;
1286
- otlpExporter = undefined;
1287
- metricRegistry = undefined;
1288
- deliveryCoordinator?.dispose();
747
+ // H3-L2 split: observability disposal delegated to registration/observability.ts.
748
+ disposeObservability(observabilityState, cleanedUp);
749
+ lifecycleState.deliveryCoordinator?.dispose();
1289
750
  clearHooksScoped();
1290
751
  uninstallCrewGlobalRegistry();
1291
- overflowTracker?.dispose();
1292
- deliveryCoordinator = undefined;
1293
- overflowTracker = undefined;
752
+ lifecycleState.overflowTracker?.dispose();
753
+ lifecycleState.deliveryCoordinator = undefined;
754
+ lifecycleState.overflowTracker = undefined;
1294
755
  manifestCache.dispose();
1295
756
  runSnapshotCache.dispose?.();
1296
757
  // 2.10: drop cached findRepoRoot results when the extension reloads.
@@ -1298,17 +759,14 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1298
759
  renderScheduler?.dispose();
1299
760
  renderScheduler = undefined;
1300
761
  autoRecoveryLast.clear();
1301
- notificationRouter?.dispose();
1302
- notificationSink?.dispose();
1303
- notificationRouter = undefined;
1304
- notificationSink = undefined;
762
+ // H3-L2 split: notification disposal delegated to registration/lifecycle.ts.
763
+ disposeNotifications(lifecycleState);
1305
764
  rpcHandle?.unsubscribe();
1306
765
  rpcHandle = undefined;
1307
766
  disposeI18n();
1308
767
  sessionGeneration += 1;
1309
768
  currentCtx = undefined;
1310
- if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime)
1311
- delete globalStore[runtimeCleanupStoreKey];
769
+ if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime) delete globalStore[runtimeCleanupStoreKey];
1312
770
  };
1313
771
  globalStore[runtimeCleanupStoreKey] = cleanupRuntime;
1314
772
 
@@ -1319,11 +777,17 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1319
777
  try {
1320
778
  const entries = ctx.sessionManager?.getEntries?.();
1321
779
  if (entries) {
1322
- import("../ui/tool-renderers/brief-mode.ts").then(({ restoreBriefState }) => {
1323
- restoreBriefState(entries);
1324
- }).catch(() => {/* non-critical */});
780
+ import("../ui/tool-renderers/brief-mode.ts")
781
+ .then(({ restoreBriefState }) => {
782
+ restoreBriefState(entries);
783
+ })
784
+ .catch(() => {
785
+ /* non-critical */
786
+ });
1325
787
  }
1326
- } catch { /* non-critical */ }
788
+ } catch {
789
+ /* non-critical */
790
+ }
1327
791
 
1328
792
  time("register.session-start");
1329
793
  cleanedUp = false;
@@ -1355,32 +819,20 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1355
819
 
1356
820
  // 2.7: load crash-recovery lazily once per session_start cleanup batch.
1357
821
  void (async () => {
1358
- let crashRecovery:
1359
- | Awaited<ReturnType<typeof importCrashRecovery>>
1360
- | undefined;
822
+ let crashRecovery: Awaited<ReturnType<typeof importCrashRecovery>> | undefined;
1361
823
  try {
1362
824
  crashRecovery = await importCrashRecovery();
1363
825
  } catch (error) {
1364
- logInternalError(
1365
- "register.sessionStart.lazyCrashRecovery",
1366
- error,
1367
- );
826
+ logInternalError("register.sessionStart.lazyCrashRecovery", error);
1368
827
  return;
1369
828
  }
1370
829
  if (cleanedUp || sessionGeneration !== ownerGeneration) return;
1371
- const {
1372
- cancelOrphanedRuns: cancelOrphanedRunsFn,
1373
- purgeStaleActiveRunIndex: purgeStaleActiveRunIndexFn,
1374
- } = crashRecovery;
830
+ const { cancelOrphanedRuns: cancelOrphanedRunsFn, purgeStaleActiveRunIndex: purgeStaleActiveRunIndexFn } = crashRecovery;
1375
831
 
1376
832
  // Auto-cancel orphaned runs
1377
833
  if (currentSessionId) {
1378
834
  try {
1379
- const { cancelled } = cancelOrphanedRunsFn(
1380
- ctx.cwd,
1381
- getManifestCache(ctx.cwd),
1382
- currentSessionId,
1383
- );
835
+ const { cancelled } = cancelOrphanedRunsFn(ctx.cwd, getManifestCache(ctx.cwd), currentSessionId);
1384
836
  if (cancelled.length > 0) {
1385
837
  notifyOperator({
1386
838
  id: `orphan_cleanup`,
@@ -1391,10 +843,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1391
843
  });
1392
844
  }
1393
845
  } catch (error) {
1394
- logInternalError(
1395
- "register.sessionStart.orphanCleanup",
1396
- error,
1397
- );
846
+ logInternalError("register.sessionStart.orphanCleanup", error);
1398
847
  }
1399
848
  }
1400
849
 
@@ -1416,10 +865,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1416
865
  });
1417
866
  }
1418
867
  } catch (error) {
1419
- logInternalError(
1420
- "register.sessionStart.startupTempCleanup",
1421
- error,
1422
- );
868
+ logInternalError("register.sessionStart.startupTempCleanup", error);
1423
869
  }
1424
870
 
1425
871
  // Orphan worker cleanup (Fix B): kill stale background-runner
@@ -1440,10 +886,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1440
886
  });
1441
887
  }
1442
888
  } catch (error) {
1443
- logInternalError(
1444
- "register.sessionStart.orphanWorkers",
1445
- error,
1446
- );
889
+ logInternalError("register.sessionStart.orphanWorkers", error);
1447
890
  }
1448
891
 
1449
892
  // Global purge of stale active-run-index entries
@@ -1459,31 +902,21 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1459
902
  });
1460
903
  }
1461
904
  } catch (error) {
1462
- logInternalError(
1463
- "register.sessionStart.globalIndexPurge",
1464
- error,
1465
- );
905
+ logInternalError("register.sessionStart.globalIndexPurge", error);
1466
906
  }
1467
907
  })();
1468
908
 
1469
909
  // Reconcile stale runs found on disk (not in active-run-index)
1470
910
  // These are ghost runs from crashed processes that were never cleaned up.
1471
911
  try {
1472
- const staleResults =
1473
- reconcileAllStaleRuns(ctx.cwd, getManifestCache(ctx.cwd)) ??
1474
- [];
912
+ const staleResults = reconcileAllStaleRuns(ctx.cwd, getManifestCache(ctx.cwd)) ?? [];
1475
913
  if (staleResults.length > 0) {
1476
914
  notifyOperator({
1477
915
  id: "stale_reconcile",
1478
916
  severity: "info",
1479
917
  source: "crash-recovery",
1480
- title:
1481
- "Reconciled " +
1482
- staleResults.length +
1483
- " stale run(s)",
1484
- body:
1485
- "Found and repaired ghost runs from previous sessions: " +
1486
- staleResults.map((r) => r.runId).join(", "),
918
+ title: "Reconciled " + staleResults.length + " stale run(s)",
919
+ body: "Found and repaired ghost runs from previous sessions: " + staleResults.map((r) => r.runId).join(", "),
1487
920
  });
1488
921
  }
1489
922
  } catch (error) {
@@ -1503,10 +936,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1503
936
  });
1504
937
  }
1505
938
  } catch (error) {
1506
- logInternalError(
1507
- "register.sessionStart.autoPruneProject",
1508
- error,
1509
- );
939
+ logInternalError("register.sessionStart.autoPruneProject", error);
1510
940
  }
1511
941
 
1512
942
  // Auto-prune finished user-level run directories (keep 10 most recent)
@@ -1534,9 +964,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1534
964
  // Resolve sessionId before the scheduler executor closure captures it.
1535
965
  const sessionId =
1536
966
  ctx.sessionManager?.getSessionId?.() ??
1537
- (typeof ctx === "object" && ctx !== null && "sessionId" in ctx
1538
- ? (ctx as Record<string, unknown>).sessionId
1539
- : undefined);
967
+ (typeof ctx === "object" && ctx !== null && "sessionId" in ctx ? (ctx as Record<string, unknown>).sessionId : undefined);
1540
968
  crewScheduler = new CrewScheduler();
1541
969
  crewScheduler.start({
1542
970
  emit: (event) => {
@@ -1548,14 +976,23 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1548
976
  try {
1549
977
  runParams = JSON.parse(job.prompt);
1550
978
  } catch {
1551
- runParams = { action: "run", team: "default", goal: job.prompt };
979
+ runParams = {
980
+ action: "run",
981
+ team: "default",
982
+ goal: job.prompt,
983
+ };
1552
984
  }
1553
985
  if (runParams.action !== "run") return `scheduled-${job.id}-${Date.now()}`;
1554
986
  const agentId = `scheduled-${job.id}-${Date.now()}`;
1555
987
  setImmediate(async () => {
1556
988
  try {
1557
989
  const runResult = await handleTeamTool(
1558
- { action: "run", team: runParams.team, goal: runParams.goal, async: true },
990
+ {
991
+ action: "run",
992
+ team: runParams.team,
993
+ goal: runParams.goal,
994
+ async: true,
995
+ },
1559
996
  { cwd: ctx.cwd, sessionId },
1560
997
  );
1561
998
  // Track the runId so remove() can cancel spawned runs
@@ -1567,7 +1004,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1567
1004
  const cwd = ctx.cwd ?? process.cwd();
1568
1005
  const loaded = loadRunManifestById(cwd, runId);
1569
1006
  if (loaded) {
1570
- // LAZY: defer dynamic import of ../state/atomic-write.ts to its call site.
1007
+ // LAZY: defer dynamic import of ../state/atomic-write.ts to its call site.
1571
1008
  const { atomicWriteJson } = await import("../state/atomic-write.ts");
1572
1009
  atomicWriteJson(loaded.manifest.stateRoot + "/manifest.json", {
1573
1010
  ...loaded.manifest,
@@ -1575,13 +1012,17 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1575
1012
  schedulerName: job.name,
1576
1013
  });
1577
1014
  }
1578
- } catch { /* best-effort provenance tracking */ }
1015
+ } catch {
1016
+ /* best-effort provenance tracking */
1017
+ }
1579
1018
  }
1580
1019
  // Persist updated job with spawnedRunIds to settings
1581
1020
  try {
1582
1021
  const updatedJob = crewScheduler?.list().find((j) => j.id === job.id);
1583
1022
  if (updatedJob) persistScheduledJobUpdate(ctx.cwd, updatedJob);
1584
- } catch { /* best-effort */ }
1023
+ } catch {
1024
+ /* best-effort */
1025
+ }
1585
1026
  // Update run count
1586
1027
  crewScheduler?.update(job.id, {
1587
1028
  runCount: job.runCount + 1,
@@ -1598,10 +1039,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1598
1039
  finalizer: () => {},
1599
1040
  runCancelFn: (runId: string) => {
1600
1041
  try {
1601
- handleTeamTool(
1602
- { action: "cancel", runId, confirm: true },
1603
- { cwd: ctx.cwd, sessionId },
1604
- ).catch((err) => logInternalError("scheduler.runCancelFn", err, `runId=${runId}`));
1042
+ handleTeamTool({ action: "cancel", runId, confirm: true }, { cwd: ctx.cwd, sessionId }).catch((err) =>
1043
+ logInternalError("scheduler.runCancelFn", err, `runId=${runId}`),
1044
+ );
1605
1045
  } catch (err) {
1606
1046
  logInternalError("scheduler.runCancelFn.sync", err, `runId=${runId}`);
1607
1047
  }
@@ -1624,34 +1064,18 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1624
1064
  configureNotifications(ctx);
1625
1065
  configureObservability(ctx);
1626
1066
  configureDeliveryCoordinator();
1627
- if (typeof sessionId === "string" && sessionId)
1628
- deliveryCoordinator?.activate(sessionId);
1067
+ if (typeof sessionId === "string" && sessionId) lifecycleState.deliveryCoordinator?.activate(sessionId);
1629
1068
  tryRegisterSessionCleanup(pi, () => {
1630
1069
  terminateActiveChildPiProcesses();
1631
1070
  cleanupRuntime();
1632
1071
  });
1633
1072
  registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
1634
- startAsyncRunNotifier(
1635
- ctx,
1636
- notifierState,
1637
- loadedConfig.config.notifierIntervalMs ??
1638
- DEFAULT_UI.notifierIntervalMs,
1639
- {
1640
- generation: ownerGeneration,
1641
- isCurrent: (generation) =>
1642
- generation === sessionGeneration &&
1643
- currentCtx === ctx &&
1644
- !cleanedUp,
1645
- },
1646
- );
1073
+ startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs, {
1074
+ generation: ownerGeneration,
1075
+ isCurrent: (generation) => generation === sessionGeneration && currentCtx === ctx && !cleanedUp,
1076
+ });
1647
1077
  const cache = getManifestCache(ctx.cwd);
1648
- updateCrewWidget(
1649
- ctx,
1650
- widgetState,
1651
- loadedConfig.config.ui,
1652
- cache,
1653
- getRunSnapshotCache(ctx.cwd),
1654
- );
1078
+ updateCrewWidget(ctx, widgetState, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd));
1655
1079
  updatePiCrewPowerbar(
1656
1080
  pi.events,
1657
1081
  ctx.cwd,
@@ -1672,12 +1096,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1672
1096
 
1673
1097
  let lastPreloadedConfig: ReturnType<typeof loadConfig> | undefined;
1674
1098
  let lastPreloadedManifests: TeamRunManifest[] = [];
1675
- let lastFrameManifestCache:
1676
- | ReturnType<typeof createManifestCache>
1677
- | undefined;
1678
- let lastFrameSnapshotCache:
1679
- | ReturnType<typeof createRunSnapshotCache>
1680
- | undefined;
1099
+ let lastFrameManifestCache: ReturnType<typeof createManifestCache> | undefined;
1100
+ let lastFrameSnapshotCache: ReturnType<typeof createRunSnapshotCache> | undefined;
1681
1101
 
1682
1102
  const buildFrame = async (): Promise<boolean> => {
1683
1103
  if (!currentCtx) return false;
@@ -1724,10 +1144,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1724
1144
  });
1725
1145
  };
1726
1146
 
1727
- const startPreloadLoop = (
1728
- intervalMs: number,
1729
- dynamicMs?: () => number,
1730
- ): void => {
1147
+ const startPreloadLoop = (intervalMs: number, dynamicMs?: () => number): void => {
1731
1148
  if (preloadTimer) clearTimeout(preloadTimer);
1732
1149
  const tick = (): void => {
1733
1150
  backgroundPreload();
@@ -1742,10 +1159,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1742
1159
  const renderTick = (): void => {
1743
1160
  if (!currentCtx) return;
1744
1161
  const config = lastPreloadedConfig?.config.ui;
1745
- const activeCache =
1746
- lastFrameManifestCache ?? getManifestCache(currentCtx.cwd);
1747
- const snapshotCache =
1748
- lastFrameSnapshotCache ?? getRunSnapshotCache(currentCtx.cwd);
1162
+ const activeCache = lastFrameManifestCache ?? getManifestCache(currentCtx.cwd);
1163
+ const snapshotCache = lastFrameSnapshotCache ?? getRunSnapshotCache(currentCtx.cwd);
1749
1164
  // 1.1: keep render path zero-fs-IO. Always read from the preloaded
1750
1165
  // frame; if it is empty (first tick after session_start, or cwd
1751
1166
  // switched), kick off a background preload and render a skeleton
@@ -1753,22 +1168,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1753
1168
  // frame is ready, avoiding statSync(`runs/`) inside the hot path.
1754
1169
  const manifests = lastPreloadedManifests;
1755
1170
  if (!lastPreloadedConfig) backgroundPreload();
1756
- if (liveSidebarRunId) {
1757
- const placement =
1758
- config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
1759
- if (
1760
- widgetState.lastVisibility !== "hidden" ||
1761
- widgetState.lastPlacement !== placement
1762
- ) {
1171
+ if (uiState.liveSidebarRunId) {
1172
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
1173
+ if (widgetState.lastVisibility !== "hidden" || widgetState.lastPlacement !== placement) {
1763
1174
  setExtensionWidget(currentCtx, "pi-crew", undefined, {
1764
1175
  placement,
1765
1176
  });
1766
- setExtensionWidget(
1767
- currentCtx,
1768
- "pi-crew-active",
1769
- undefined,
1770
- { placement },
1771
- );
1177
+ setExtensionWidget(currentCtx, "pi-crew-active", undefined, { placement });
1772
1178
  widgetState.lastVisibility = "hidden";
1773
1179
  widgetState.lastPlacement = placement;
1774
1180
  widgetState.lastKey = "pi-crew-active";
@@ -1776,14 +1182,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1776
1182
  }
1777
1183
  requestRender(currentCtx);
1778
1184
  } else {
1779
- updateCrewWidget(
1780
- currentCtx,
1781
- widgetState,
1782
- config,
1783
- activeCache,
1784
- snapshotCache,
1785
- manifests,
1786
- );
1185
+ updateCrewWidget(currentCtx, widgetState, config, activeCache, snapshotCache, manifests);
1787
1186
  }
1788
1187
  requestPowerbarUpdate(
1789
1188
  pi.events,
@@ -1814,20 +1213,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1814
1213
  // Skip if snapshot shows run already completed/failed (stale cache)
1815
1214
  if (snapshot.manifest.status !== "running") continue;
1816
1215
  const summary = summarizeHeartbeats(snapshot, { now });
1817
- const maybeNotifyHealth = (
1818
- kind: string,
1819
- count: number,
1820
- title: string,
1821
- body: string,
1822
- ): void => {
1216
+ const maybeNotifyHealth = (kind: string, count: number, title: string, body: string): void => {
1823
1217
  if (count <= 0) return;
1824
1218
  const key = `${kind}_${run.runId}`;
1825
1219
  const previous = autoRecoveryLast.get(key);
1826
- if (
1827
- previous !== undefined &&
1828
- now - previous.lastAccessAt < 5 * 60_000
1829
- )
1830
- return;
1220
+ if (previous !== undefined && now - previous.lastAccessAt < 5 * 60_000) return;
1831
1221
  // Defensive cap: evict entry with oldest lastAccessAt before
1832
1222
  // inserting/updating when size exceeds the limit. Uses LRU
1833
1223
  // semantics so entries that are still being actively
@@ -1844,7 +1234,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1844
1234
  if (oldestKey === undefined) break;
1845
1235
  autoRecoveryLast.delete(oldestKey);
1846
1236
  }
1847
- autoRecoveryLast.set(key, { insertedAt: now, lastAccessAt: now });
1237
+ autoRecoveryLast.set(key, {
1238
+ insertedAt: now,
1239
+ lastAccessAt: now,
1240
+ });
1848
1241
  notifyOperator({
1849
1242
  id: key,
1850
1243
  severity: "warning",
@@ -1867,35 +1260,22 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1867
1260
  "Open /team-dashboard → 5 health → inspect health actions.",
1868
1261
  );
1869
1262
  } catch (error) {
1870
- logInternalError(
1871
- "register.health-notification",
1872
- error,
1873
- run.runId,
1874
- );
1263
+ logInternalError("register.health-notification", error, run.runId);
1875
1264
  }
1876
1265
  }
1877
1266
  };
1878
1267
 
1879
- const fallbackMs =
1880
- loadedConfig.config.ui?.dashboardLiveRefreshMs ??
1881
- DEFAULT_UI.refreshMs;
1268
+ const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? DEFAULT_UI.refreshMs;
1882
1269
  // R3: Use faster refresh when live agents OR background runs are running.
1883
1270
  // 160ms is aligned with SUBAGENT_SPINNER_FRAME_MS so the spinner advances
1884
1271
  // one frame per render tick when a run is active. Falls back to the
1885
1272
  // (slower) configured refresh when idle to save CPU.
1886
1273
  const liveRefreshMs = 160;
1887
1274
  const hasActiveWork = (): boolean => {
1888
- if (listLiveAgents().some((a) => a.status === "running"))
1889
- return true;
1890
- return lastPreloadedManifests.some(
1891
- (r) =>
1892
- r.status === "running" ||
1893
- r.status === "queued" ||
1894
- r.status === "planning",
1895
- );
1275
+ if (listLiveAgents().some((a) => a.status === "running")) return true;
1276
+ return lastPreloadedManifests.some((r) => r.status === "running" || r.status === "queued" || r.status === "planning");
1896
1277
  };
1897
- const effectiveRefreshMs = () =>
1898
- hasActiveWork() ? liveRefreshMs : fallbackMs;
1278
+ const effectiveRefreshMs = () => (hasActiveWork() ? liveRefreshMs : fallbackMs);
1899
1279
  renderScheduler = new RenderScheduler(pi.events, renderTick, {
1900
1280
  // Dynamic fallback: same logic as preload loop so the render timer
1901
1281
  // also ticks at spinner frequency while a run is active.
@@ -1980,39 +1360,23 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1980
1360
  });
1981
1361
  pi.on("session_before_switch", () => {
1982
1362
  sessionGeneration++;
1983
- const pendingCount = deliveryCoordinator?.getPendingCount() ?? 0;
1363
+ const pendingCount = lifecycleState.deliveryCoordinator?.getPendingCount() ?? 0;
1984
1364
  try {
1985
1365
  const activeRuns = currentCtx
1986
1366
  ? getManifestCache(currentCtx.cwd)
1987
1367
  .list(50)
1988
- .filter(
1989
- (run) =>
1990
- run.status === "running" ||
1991
- run.status === "queued" ||
1992
- run.status === "blocked",
1993
- )
1368
+ .filter((run) => run.status === "running" || run.status === "queued" || run.status === "blocked")
1994
1369
  : [];
1995
- const snapshot = createSessionSnapshot(
1996
- activeRuns,
1997
- pendingCount,
1998
- sessionGeneration,
1999
- );
1370
+ const snapshot = createSessionSnapshot(activeRuns, pendingCount, sessionGeneration);
2000
1371
  if (pendingCount > 0 || snapshot.activeRunIds.length > 0)
2001
- logInternalError(
2002
- "register.session-before-switch",
2003
- undefined,
2004
- JSON.stringify(snapshot),
2005
- );
1372
+ logInternalError("register.session-before-switch", undefined, JSON.stringify(snapshot));
2006
1373
  } catch (error) {
2007
1374
  logInternalError("register.session-before-switch.snapshot", error);
2008
1375
  }
2009
1376
  if (pendingCount > 0) {
2010
- logInternalError(
2011
- "register.session-before-switch",
2012
- `Switching session with ${pendingCount} pending deliveries`,
2013
- );
1377
+ logInternalError("register.session-before-switch", `Switching session with ${pendingCount} pending deliveries`);
2014
1378
  }
2015
- deliveryCoordinator?.deactivate();
1379
+ lifecycleState.deliveryCoordinator?.deactivate();
2016
1380
  resetPowerbarDedupState();
2017
1381
  stopAsyncRunNotifier(notifierState);
2018
1382
  stopSessionBoundSubagents();
@@ -2024,12 +1388,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
2024
1388
  pi.on("resources_discover", () => {
2025
1389
  const sessionCwd = currentCtx?.cwd ?? process.cwd();
2026
1390
  const skillDir = path.resolve(sessionCwd, "skills");
2027
- const extSkillDir = path.resolve(
2028
- path.dirname(fileURLToPath(import.meta.url)),
2029
- "..",
2030
- "..",
2031
- "skills",
2032
- );
1391
+ const extSkillDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
2033
1392
  const paths: string[] = [];
2034
1393
  if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
2035
1394
  if (skillDir !== extSkillDir && fs.existsSync(skillDir)) {
@@ -2090,7 +1449,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
2090
1449
  if (!filePath) return;
2091
1450
  const result = validateWrittenFile(filePath);
2092
1451
  if (!result || result.ok) return;
2093
- return { content: [...event.content, buildValidationBlocker(filePath, result.error ?? "validation failed")] };
1452
+ return {
1453
+ content: [...event.content, buildValidationBlocker(filePath, result.error ?? "validation failed")],
1454
+ };
2094
1455
  } catch {
2095
1456
  // best-effort: never break a tool result
2096
1457
  }
@@ -2103,26 +1464,23 @@ export function registerPiTeams(pi: ExtensionAPI): void {
2103
1464
  openLiveSidebar,
2104
1465
  getManifestCache,
2105
1466
  getRunSnapshotCache,
2106
- getMetricRegistry: () => metricRegistry,
1467
+ getMetricRegistry: () => observabilityState.metricRegistry,
2107
1468
  widgetState,
2108
1469
  onJsonEvent: (taskId, runId, event) => {
2109
1470
  const record = event as Record<string, unknown>;
2110
- const eventType =
2111
- typeof record.type === "string" ? record.type : undefined;
2112
- if (eventType) overflowTracker?.feedEvent(taskId, runId, eventType);
1471
+ const eventType = typeof record.type === "string" ? record.type : undefined;
1472
+ if (eventType) lifecycleState.overflowTracker?.feedEvent(taskId, runId, eventType);
2113
1473
  },
2114
1474
  });
2115
1475
  registerSubagentTools(pi, subagentManager, {
2116
1476
  ownerSessionGeneration: captureSessionGeneration,
2117
- startForegroundRun: (ctx, runner, runId) =>
2118
- startForegroundRun(ctx as ExtensionContext, runner, runId),
1477
+ startForegroundRun: (ctx, runner, runId) => startForegroundRun(ctx as ExtensionContext, runner, runId),
2119
1478
  batchBarrier,
2120
1479
  });
2121
1480
  time("register.tools");
2122
1481
 
2123
1482
  registerCleanupHandler(pi);
2124
1483
 
2125
-
2126
1484
  // Resilient edit (Phase 2): OPT-IN via CREW_RESILIENT_EDIT=1.
2127
1485
  // Wraps the native edit tool with the cascading replace() fallback so that
2128
1486
  // slightly-wrong oldText (indentation drift, whitespace) still matches.
@@ -2132,7 +1490,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
2132
1490
  .then(({ wrapEditWithResilientReplace }) => {
2133
1491
  wrapEditWithResilientReplace(pi);
2134
1492
  })
2135
- .catch(() => { /* non-critical */ });
1493
+ .catch(() => {
1494
+ /* non-critical */
1495
+ });
2136
1496
  }
2137
1497
 
2138
1498
  registerTeamCommands(pi, {
@@ -2141,18 +1501,12 @@ export function registerPiTeams(pi: ExtensionAPI): void {
2141
1501
  openLiveSidebar,
2142
1502
  getManifestCache,
2143
1503
  getRunSnapshotCache,
2144
- getMetricRegistry: () => metricRegistry,
1504
+ getMetricRegistry: () => observabilityState.metricRegistry,
2145
1505
  dismissNotifications: () => {
2146
1506
  widgetState.notificationCount = 0;
2147
1507
  if (currentCtx) {
2148
1508
  const uiConfig = loadConfig(currentCtx.cwd).config.ui;
2149
- updateCrewWidget(
2150
- currentCtx,
2151
- widgetState,
2152
- uiConfig,
2153
- getManifestCache(currentCtx.cwd),
2154
- getRunSnapshotCache(currentCtx.cwd),
2155
- );
1509
+ updateCrewWidget(currentCtx, widgetState, uiConfig, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd));
2156
1510
  updatePiCrewPowerbar(
2157
1511
  pi.events,
2158
1512
  currentCtx.cwd,