pi-crew 0.9.53 → 0.9.55

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 (493) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +6 -3
  3. package/scripts/clean-strip-types.mjs +60 -0
  4. package/src/adapters/claude-adapter.ts +23 -0
  5. package/src/adapters/codex-adapter.ts +21 -0
  6. package/src/adapters/cursor-adapter.ts +17 -0
  7. package/src/adapters/export-util.ts +143 -0
  8. package/src/adapters/index.ts +15 -0
  9. package/src/adapters/registry.ts +18 -0
  10. package/src/adapters/types.ts +23 -0
  11. package/src/agents/agent-config.ts +194 -0
  12. package/src/agents/agent-search.ts +98 -0
  13. package/src/agents/agent-serializer.ts +38 -0
  14. package/src/agents/discover-agents.ts +623 -0
  15. package/src/benchmark/benchmark-runner.ts +313 -0
  16. package/src/benchmark/feedback-loop.ts +73 -0
  17. package/src/config/config.ts +1270 -0
  18. package/src/config/defaults.ts +193 -0
  19. package/src/config/drift-detector.ts +290 -0
  20. package/src/config/markers.ts +330 -0
  21. package/src/config/resilient-parser.ts +117 -0
  22. package/src/config/role-tools.ts +118 -0
  23. package/src/config/suggestions.ts +75 -0
  24. package/src/config/types.ts +280 -0
  25. package/src/errors.ts +194 -0
  26. package/src/extension/action-suggestions.ts +117 -0
  27. package/src/extension/async-notifier.ts +179 -0
  28. package/src/extension/autonomous-policy.ts +209 -0
  29. package/src/extension/command-completions.ts +120 -0
  30. package/src/extension/context-status-injection.ts +185 -0
  31. package/src/extension/crew-autocomplete.ts +131 -0
  32. package/src/extension/crew-cleanup.ts +168 -0
  33. package/src/extension/crew-input-router.ts +93 -0
  34. package/src/extension/crew-shortcuts.ts +87 -0
  35. package/src/extension/crew-vibes/cat-frames.ts +18 -0
  36. package/src/extension/crew-vibes/config.ts +195 -0
  37. package/src/extension/crew-vibes/figures.ts +101 -0
  38. package/src/extension/crew-vibes/font-detect.ts +71 -0
  39. package/src/extension/crew-vibes/footer.ts +292 -0
  40. package/src/extension/crew-vibes/index.ts +432 -0
  41. package/src/extension/crew-vibes/provider-usage.ts +396 -0
  42. package/src/extension/crew-vibes/render.ts +206 -0
  43. package/src/extension/crew-vibes/speed.ts +286 -0
  44. package/src/extension/cross-extension-rpc.ts +350 -0
  45. package/src/extension/help.ts +62 -0
  46. package/src/extension/import-index.ts +74 -0
  47. package/src/extension/knowledge-injection.ts +402 -0
  48. package/src/extension/management.ts +571 -0
  49. package/src/extension/message-renderers.ts +108 -0
  50. package/src/extension/notification-router.ts +152 -0
  51. package/src/extension/notification-sink.ts +54 -0
  52. package/src/extension/pi-api.ts +56 -0
  53. package/src/extension/plan-orchestrate.ts +302 -0
  54. package/src/extension/project-init.ts +170 -0
  55. package/src/extension/register.ts +113 -0
  56. package/src/extension/registration/artifact-cleanup.ts +20 -0
  57. package/src/extension/registration/command-registration.ts +58 -0
  58. package/src/extension/registration/command-utils.ts +58 -0
  59. package/src/extension/registration/commands.ts +1228 -0
  60. package/src/extension/registration/compaction-guard.ts +333 -0
  61. package/src/extension/registration/context-builder.ts +135 -0
  62. package/src/extension/registration/crash-recovery-cache.ts +54 -0
  63. package/src/extension/registration/foreground-run-controller.ts +238 -0
  64. package/src/extension/registration/hook-registration.ts +119 -0
  65. package/src/extension/registration/lazy-configurers.ts +124 -0
  66. package/src/extension/registration/lifecycle-handlers.ts +923 -0
  67. package/src/extension/registration/lifecycle.ts +259 -0
  68. package/src/extension/registration/observability.ts +322 -0
  69. package/src/extension/registration/registration-types.ts +158 -0
  70. package/src/extension/registration/runtime-cleanup.ts +230 -0
  71. package/src/extension/registration/subagent-helpers.ts +125 -0
  72. package/src/extension/registration/subagent-manager-setup.ts +330 -0
  73. package/src/extension/registration/subagent-tools.ts +456 -0
  74. package/src/extension/registration/team-tool.ts +225 -0
  75. package/src/extension/registration/tool-registration.ts +50 -0
  76. package/src/extension/registration/ui.ts +172 -0
  77. package/src/extension/registration/viewers.ts +121 -0
  78. package/src/extension/registration/wire-cross-extension.ts +37 -0
  79. package/src/extension/result-watcher.ts +139 -0
  80. package/src/extension/rpc-hmac.ts +256 -0
  81. package/src/extension/run-bundle-schema.ts +104 -0
  82. package/src/extension/run-export.ts +97 -0
  83. package/src/extension/run-import.ts +168 -0
  84. package/src/extension/run-index.ts +131 -0
  85. package/src/extension/run-maintenance.ts +190 -0
  86. package/src/extension/session-summary.ts +18 -0
  87. package/src/extension/team-manager-command.ts +153 -0
  88. package/src/extension/team-onboard.ts +180 -0
  89. package/src/extension/team-recommendation.ts +288 -0
  90. package/src/extension/team-tool/anchor.ts +172 -0
  91. package/src/extension/team-tool/api.ts +1244 -0
  92. package/src/extension/team-tool/auto-summarize.ts +142 -0
  93. package/src/extension/team-tool/cache-control.ts +19 -0
  94. package/src/extension/team-tool/cancel.ts +337 -0
  95. package/src/extension/team-tool/chain-dispatch.ts +95 -0
  96. package/src/extension/team-tool/chain-executor.ts +379 -0
  97. package/src/extension/team-tool/config-patch.ts +50 -0
  98. package/src/extension/team-tool/context.ts +149 -0
  99. package/src/extension/team-tool/destructive-gate.ts +52 -0
  100. package/src/extension/team-tool/dispatch/automate.ts +80 -0
  101. package/src/extension/team-tool/dispatch/control.ts +42 -0
  102. package/src/extension/team-tool/dispatch/index.ts +95 -0
  103. package/src/extension/team-tool/dispatch/manage.ts +161 -0
  104. package/src/extension/team-tool/dispatch/run.ts +53 -0
  105. package/src/extension/team-tool/dispatch/status.ts +187 -0
  106. package/src/extension/team-tool/doctor.ts +431 -0
  107. package/src/extension/team-tool/explain.ts +282 -0
  108. package/src/extension/team-tool/failure-patterns.ts +118 -0
  109. package/src/extension/team-tool/goal-wrap.ts +330 -0
  110. package/src/extension/team-tool/goal.ts +556 -0
  111. package/src/extension/team-tool/handle-schedule.ts +332 -0
  112. package/src/extension/team-tool/handle-settings.ts +520 -0
  113. package/src/extension/team-tool/health-monitor.ts +481 -0
  114. package/src/extension/team-tool/inspect.ts +88 -0
  115. package/src/extension/team-tool/intent-policy.ts +45 -0
  116. package/src/extension/team-tool/lifecycle-actions.ts +615 -0
  117. package/src/extension/team-tool/orchestrate.ts +98 -0
  118. package/src/extension/team-tool/parallel-dispatch.ts +183 -0
  119. package/src/extension/team-tool/plan.ts +50 -0
  120. package/src/extension/team-tool/respond.ts +150 -0
  121. package/src/extension/team-tool/run-deadline.ts +66 -0
  122. package/src/extension/team-tool/run-not-found.ts +49 -0
  123. package/src/extension/team-tool/run.ts +1051 -0
  124. package/src/extension/team-tool/status.ts +260 -0
  125. package/src/extension/team-tool/workflow-manage.ts +261 -0
  126. package/src/extension/team-tool-types.ts +26 -0
  127. package/src/extension/team-tool.ts +750 -0
  128. package/src/extension/tool-result.ts +16 -0
  129. package/src/extension/validate-resources.ts +108 -0
  130. package/src/hooks/registry.ts +200 -0
  131. package/src/hooks/types.ts +69 -0
  132. package/src/i18n.ts +212 -0
  133. package/src/observability/correlation.ts +50 -0
  134. package/src/observability/event-bus.ts +86 -0
  135. package/src/observability/event-to-metric.ts +170 -0
  136. package/src/observability/exporters/adapter.ts +27 -0
  137. package/src/observability/exporters/otlp-exporter.ts +356 -0
  138. package/src/observability/exporters/prometheus-exporter.ts +54 -0
  139. package/src/observability/metric-registry.ts +100 -0
  140. package/src/observability/metric-retention.ts +64 -0
  141. package/src/observability/metric-sink.ts +99 -0
  142. package/src/observability/metrics-primitives.ts +219 -0
  143. package/src/plugins/plugin-define.ts +6 -0
  144. package/src/plugins/plugin-registry.ts +32 -0
  145. package/src/plugins/plugins/index.ts +3 -0
  146. package/src/plugins/plugins/nextjs.ts +19 -0
  147. package/src/plugins/plugins/vite.ts +10 -0
  148. package/src/plugins/plugins/vitest.ts +9 -0
  149. package/src/prompt/prompt-runtime.ts +380 -0
  150. package/src/runtime/adaptive-plan.ts +563 -0
  151. package/src/runtime/agent-control.ts +215 -0
  152. package/src/runtime/agent-memory.ts +81 -0
  153. package/src/runtime/agent-observability.ts +122 -0
  154. package/src/runtime/anchor-manager.ts +475 -0
  155. package/src/runtime/async-marker.ts +32 -0
  156. package/src/runtime/async-runner.ts +381 -0
  157. package/src/runtime/attention-events.ts +28 -0
  158. package/src/runtime/auto-summarize.ts +344 -0
  159. package/src/runtime/background-runner.ts +840 -0
  160. package/src/runtime/batch-barrier.ts +147 -0
  161. package/src/runtime/broker-issuer.ts +39 -0
  162. package/src/runtime/cancellation-token.ts +99 -0
  163. package/src/runtime/cancellation.ts +99 -0
  164. package/src/runtime/capability-inventory.ts +137 -0
  165. package/src/runtime/chain-parser.ts +237 -0
  166. package/src/runtime/chain-runner.ts +606 -0
  167. package/src/runtime/checkpoint.ts +291 -0
  168. package/src/runtime/child-pi-constants.ts +42 -0
  169. package/src/runtime/child-pi-kill.ts +180 -0
  170. package/src/runtime/child-pi-pool.ts +68 -0
  171. package/src/runtime/child-pi-spawn.ts +287 -0
  172. package/src/runtime/child-pi-steering.ts +128 -0
  173. package/src/runtime/child-pi-streams.ts +296 -0
  174. package/src/runtime/child-pi-transcript.ts +169 -0
  175. package/src/runtime/child-pi.ts +1105 -0
  176. package/src/runtime/coalesce-tasks.ts +268 -0
  177. package/src/runtime/code-summary.ts +292 -0
  178. package/src/runtime/command-trace.ts +105 -0
  179. package/src/runtime/compact-pipeline.ts +56 -0
  180. package/src/runtime/compact-stages/ansi-strip-stage.ts +25 -0
  181. package/src/runtime/compact-stages/blank-collapse-stage.ts +31 -0
  182. package/src/runtime/compact-stages/deduplicate-stage.ts +34 -0
  183. package/src/runtime/compact-stages/head-snap-stage.ts +57 -0
  184. package/src/runtime/compact-stages/index.ts +23 -0
  185. package/src/runtime/compact-stages/tail-capture-stage.ts +77 -0
  186. package/src/runtime/compact-stages/truncation-stage.ts +74 -0
  187. package/src/runtime/compaction-summary.ts +278 -0
  188. package/src/runtime/completion-guard.ts +207 -0
  189. package/src/runtime/concurrency.ts +58 -0
  190. package/src/runtime/crash-classification.ts +229 -0
  191. package/src/runtime/crash-recovery.ts +594 -0
  192. package/src/runtime/crew-agent-records.ts +595 -0
  193. package/src/runtime/crew-agent-runtime.ts +62 -0
  194. package/src/runtime/crew-broker-child.ts +88 -0
  195. package/src/runtime/crew-broker-client.ts +673 -0
  196. package/src/runtime/crew-broker-tokens.ts +159 -0
  197. package/src/runtime/crew-broker.ts +1304 -0
  198. package/src/runtime/crew-hooks.ts +227 -0
  199. package/src/runtime/cross-extension-rpc.ts +143 -0
  200. package/src/runtime/custom-tools/irc-tool.ts +282 -0
  201. package/src/runtime/custom-tools/submit-result-tool.ts +102 -0
  202. package/src/runtime/deadletter.ts +47 -0
  203. package/src/runtime/delivery-coordinator.ts +211 -0
  204. package/src/runtime/delta-conflict.ts +348 -0
  205. package/src/runtime/deterministic-ast.ts +161 -0
  206. package/src/runtime/diagnostic-export.ts +171 -0
  207. package/src/runtime/direct-run.ts +45 -0
  208. package/src/runtime/dwf-state-store.ts +102 -0
  209. package/src/runtime/dynamic-workflow-context.ts +1013 -0
  210. package/src/runtime/dynamic-workflow-runner.ts +325 -0
  211. package/src/runtime/effectiveness.ts +90 -0
  212. package/src/runtime/errors/crew-errors.ts +162 -0
  213. package/src/runtime/event-stream-bridge.ts +98 -0
  214. package/src/runtime/foreground-control.ts +193 -0
  215. package/src/runtime/foreground-watchdog.ts +132 -0
  216. package/src/runtime/global-worker-cap.ts +96 -0
  217. package/src/runtime/goal-achievement.ts +148 -0
  218. package/src/runtime/goal-evaluator.ts +365 -0
  219. package/src/runtime/goal-loop-runner.ts +825 -0
  220. package/src/runtime/goal-state-store.ts +219 -0
  221. package/src/runtime/green-contract.ts +55 -0
  222. package/src/runtime/group-join.ts +267 -0
  223. package/src/runtime/handoff-manager.ts +598 -0
  224. package/src/runtime/heartbeat-gradient.ts +36 -0
  225. package/src/runtime/heartbeat-watcher.ts +239 -0
  226. package/src/runtime/hidden-handoff.ts +407 -0
  227. package/src/runtime/important-line-classifier.ts +140 -0
  228. package/src/runtime/intercom-bridge.ts +187 -0
  229. package/src/runtime/iteration-hooks.ts +305 -0
  230. package/src/runtime/live-agent-control.ts +136 -0
  231. package/src/runtime/live-agent-manager.ts +704 -0
  232. package/src/runtime/live-control-realtime.ts +56 -0
  233. package/src/runtime/live-extension-bridge.ts +148 -0
  234. package/src/runtime/live-irc.ts +97 -0
  235. package/src/runtime/live-session-health.ts +108 -0
  236. package/src/runtime/live-session-runtime.ts +1146 -0
  237. package/src/runtime/loop-gates.ts +128 -0
  238. package/src/runtime/manifest-cache.ts +375 -0
  239. package/src/runtime/mcp-proxy.ts +105 -0
  240. package/src/runtime/metric-parser.ts +36 -0
  241. package/src/runtime/model-fallback.ts +409 -0
  242. package/src/runtime/model-resolver.ts +126 -0
  243. package/src/runtime/model-scope.ts +158 -0
  244. package/src/runtime/orphan-worker-registry.ts +465 -0
  245. package/src/runtime/output-validator.ts +202 -0
  246. package/src/runtime/overflow-recovery.ts +206 -0
  247. package/src/runtime/parallel-research.ts +68 -0
  248. package/src/runtime/parallel-utils.ts +160 -0
  249. package/src/runtime/parent-guard.ts +134 -0
  250. package/src/runtime/path-overlap.ts +150 -0
  251. package/src/runtime/peer-dep.ts +292 -0
  252. package/src/runtime/per-write-validator.ts +181 -0
  253. package/src/runtime/phase-progress.ts +217 -0
  254. package/src/runtime/phase-tracker.ts +385 -0
  255. package/src/runtime/pi-args.ts +655 -0
  256. package/src/runtime/pi-json-output.ts +170 -0
  257. package/src/runtime/pi-spawn.ts +273 -0
  258. package/src/runtime/pipeline-runner.ts +523 -0
  259. package/src/runtime/plan-templates.ts +201 -0
  260. package/src/runtime/policy-engine.ts +115 -0
  261. package/src/runtime/post-checks.ts +142 -0
  262. package/src/runtime/post-exit-stdio-guard.ts +109 -0
  263. package/src/runtime/process-lifecycle.ts +491 -0
  264. package/src/runtime/process-status.ts +154 -0
  265. package/src/runtime/progress-event-coalescer.ts +44 -0
  266. package/src/runtime/progress-tracker.ts +124 -0
  267. package/src/runtime/prose-compressor.ts +162 -0
  268. package/src/runtime/recovery-recipes.ts +203 -0
  269. package/src/runtime/replace.ts +570 -0
  270. package/src/runtime/resilient-edit.ts +153 -0
  271. package/src/runtime/result-extractor.ts +217 -0
  272. package/src/runtime/retry-executor.ts +109 -0
  273. package/src/runtime/retry-runner.ts +336 -0
  274. package/src/runtime/role-permission.ts +59 -0
  275. package/src/runtime/run-coalesced-task-group.ts +308 -0
  276. package/src/runtime/run-drift.ts +219 -0
  277. package/src/runtime/run-tracker.ts +116 -0
  278. package/src/runtime/run-worker.ts +78 -0
  279. package/src/runtime/runtime-policy.ts +29 -0
  280. package/src/runtime/runtime-resolver.ts +172 -0
  281. package/src/runtime/runtime-warmup.ts +160 -0
  282. package/src/runtime/scheduler.ts +353 -0
  283. package/src/runtime/semaphore.ts +141 -0
  284. package/src/runtime/sensitive-paths.ts +93 -0
  285. package/src/runtime/session-resources.ts +25 -0
  286. package/src/runtime/session-snapshot.ts +59 -0
  287. package/src/runtime/session-usage.ts +79 -0
  288. package/src/runtime/settings-store.ts +155 -0
  289. package/src/runtime/sidechain-output.ts +35 -0
  290. package/src/runtime/single-agent-compose.ts +84 -0
  291. package/src/runtime/skill-effectiveness.ts +524 -0
  292. package/src/runtime/skill-instructions.ts +394 -0
  293. package/src/runtime/stale-reconciler.ts +680 -0
  294. package/src/runtime/stream-preview.ts +184 -0
  295. package/src/runtime/streaming-output.ts +50 -0
  296. package/src/runtime/subagent-manager.ts +561 -0
  297. package/src/runtime/subprocess-tool-registry.ts +70 -0
  298. package/src/runtime/supervisor-contact.ts +64 -0
  299. package/src/runtime/task-display.ts +52 -0
  300. package/src/runtime/task-graph-scheduler.ts +227 -0
  301. package/src/runtime/task-graph.ts +290 -0
  302. package/src/runtime/task-health.ts +83 -0
  303. package/src/runtime/task-id.ts +161 -0
  304. package/src/runtime/task-output-context.ts +602 -0
  305. package/src/runtime/task-packet.ts +268 -0
  306. package/src/runtime/task-quality.ts +199 -0
  307. package/src/runtime/task-runner/capabilities.ts +78 -0
  308. package/src/runtime/task-runner/child-executor.ts +790 -0
  309. package/src/runtime/task-runner/context-retrieval.ts +163 -0
  310. package/src/runtime/task-runner/live-executor.ts +227 -0
  311. package/src/runtime/task-runner/output-splitter.ts +152 -0
  312. package/src/runtime/task-runner/post-execution.ts +480 -0
  313. package/src/runtime/task-runner/pre-execution.ts +365 -0
  314. package/src/runtime/task-runner/progress.ts +157 -0
  315. package/src/runtime/task-runner/prompt-builder.ts +278 -0
  316. package/src/runtime/task-runner/prompt-pipeline.ts +88 -0
  317. package/src/runtime/task-runner/result-utils.ts +16 -0
  318. package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
  319. package/src/runtime/task-runner/run-projection.ts +125 -0
  320. package/src/runtime/task-runner/scaffold-executor.ts +25 -0
  321. package/src/runtime/task-runner/state-helpers.ts +176 -0
  322. package/src/runtime/task-runner/tail-read.ts +34 -0
  323. package/src/runtime/task-runner.ts +216 -0
  324. package/src/runtime/team-runner-artifacts.ts +13 -0
  325. package/src/runtime/team-runner.ts +2396 -0
  326. package/src/runtime/tool-output-pruner.ts +333 -0
  327. package/src/runtime/tool-progress.ts +278 -0
  328. package/src/runtime/usage-tracker.ts +73 -0
  329. package/src/runtime/verification-gates.ts +579 -0
  330. package/src/runtime/verification-integrity.ts +108 -0
  331. package/src/runtime/verification-worktree.ts +186 -0
  332. package/src/runtime/worker-heartbeat.ts +25 -0
  333. package/src/runtime/worker-startup.ts +83 -0
  334. package/src/runtime/workflow-state.ts +195 -0
  335. package/src/runtime/workspace-lock.ts +443 -0
  336. package/src/runtime/workspace-tree.ts +312 -0
  337. package/src/runtime/yield-handler.ts +224 -0
  338. package/src/runtime/zombie-scanner.ts +298 -0
  339. package/src/schema/config-schema.ts +327 -0
  340. package/src/schema/team-tool-schema.ts +492 -0
  341. package/src/schema/validation-types.ts +159 -0
  342. package/src/skills/discover-skills.ts +203 -0
  343. package/src/skills/skill-templates.ts +456 -0
  344. package/src/skills/validate.ts +285 -0
  345. package/src/state/active-run-registry.ts +439 -0
  346. package/src/state/artifact-store.ts +176 -0
  347. package/src/state/atomic-write.ts +970 -0
  348. package/src/state/blob-store.ts +308 -0
  349. package/src/state/contracts.ts +160 -0
  350. package/src/state/crew-init.ts +269 -0
  351. package/src/state/decision-ledger.ts +372 -0
  352. package/src/state/event-log-rotation.ts +399 -0
  353. package/src/state/event-log.ts +1465 -0
  354. package/src/state/event-reconstructor.ts +229 -0
  355. package/src/state/gitignore-manager.ts +46 -0
  356. package/src/state/health-store.ts +79 -0
  357. package/src/state/hook-instinct-bridge.ts +94 -0
  358. package/src/state/hook-integrations.ts +51 -0
  359. package/src/state/instinct-store.ts +275 -0
  360. package/src/state/jsonl-writer.ts +107 -0
  361. package/src/state/locks.ts +562 -0
  362. package/src/state/mailbox.ts +916 -0
  363. package/src/state/observation-store.ts +176 -0
  364. package/src/state/run-cache.ts +193 -0
  365. package/src/state/run-graph.ts +183 -0
  366. package/src/state/run-metrics.ts +165 -0
  367. package/src/state/schedule.ts +166 -0
  368. package/src/state/session-state-map.ts +51 -0
  369. package/src/state/state-store.ts +973 -0
  370. package/src/state/task-claims.ts +61 -0
  371. package/src/state/tiered-eval.ts +480 -0
  372. package/src/state/types-eval.ts +58 -0
  373. package/src/state/types.ts +447 -0
  374. package/src/state/usage.ts +138 -0
  375. package/src/state/worker-atomic-writer.ts +198 -0
  376. package/src/subagents/async-entry.ts +1 -0
  377. package/src/subagents/index.ts +3 -0
  378. package/src/subagents/live/control.ts +1 -0
  379. package/src/subagents/live/manager.ts +1 -0
  380. package/src/subagents/live/realtime.ts +1 -0
  381. package/src/subagents/live/session-runtime.ts +1 -0
  382. package/src/subagents/manager.ts +1 -0
  383. package/src/subagents/spawn.ts +1 -0
  384. package/src/teams/discover-teams.ts +187 -0
  385. package/src/teams/team-config.ts +27 -0
  386. package/src/teams/team-serializer.ts +38 -0
  387. package/src/tools/safe-bash-extension.ts +54 -0
  388. package/src/tools/safe-bash.ts +505 -0
  389. package/src/types/diff.d.ts +18 -0
  390. package/src/types/new-api-types.ts +35 -0
  391. package/src/ui/agent-management-overlay.ts +160 -0
  392. package/src/ui/card-colors.ts +139 -0
  393. package/src/ui/crew-footer.ts +102 -0
  394. package/src/ui/crew-select-list.ts +112 -0
  395. package/src/ui/dashboard-panes/agents-pane.ts +164 -0
  396. package/src/ui/dashboard-panes/cancellation-pane.ts +66 -0
  397. package/src/ui/dashboard-panes/capability-pane.ts +77 -0
  398. package/src/ui/dashboard-panes/health-pane.ts +31 -0
  399. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -0
  400. package/src/ui/dashboard-panes/metrics-pane.ts +37 -0
  401. package/src/ui/dashboard-panes/progress-pane.ts +38 -0
  402. package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
  403. package/src/ui/deploy-bundled-themes.ts +71 -0
  404. package/src/ui/dwf-phase-display.ts +151 -0
  405. package/src/ui/dynamic-border.ts +35 -0
  406. package/src/ui/format-helpers.ts +31 -0
  407. package/src/ui/heartbeat-aggregator.ts +74 -0
  408. package/src/ui/key-utils.ts +42 -0
  409. package/src/ui/keybinding-map.ts +248 -0
  410. package/src/ui/layout-primitives.ts +106 -0
  411. package/src/ui/live-conversation-overlay.ts +183 -0
  412. package/src/ui/live-duration.ts +55 -0
  413. package/src/ui/live-run-sidebar.ts +293 -0
  414. package/src/ui/loaders.ts +6 -0
  415. package/src/ui/mascot.ts +449 -0
  416. package/src/ui/overlays/agent-picker-overlay.ts +66 -0
  417. package/src/ui/overlays/confirm-overlay.ts +60 -0
  418. package/src/ui/overlays/help-overlay.ts +177 -0
  419. package/src/ui/overlays/mailbox-compose-overlay.ts +181 -0
  420. package/src/ui/overlays/mailbox-compose-preview.ts +78 -0
  421. package/src/ui/overlays/mailbox-detail-overlay.ts +155 -0
  422. package/src/ui/pi-ui-compat.ts +68 -0
  423. package/src/ui/powerbar-publisher.ts +459 -0
  424. package/src/ui/render-coalescer.ts +101 -0
  425. package/src/ui/render-diff.ts +160 -0
  426. package/src/ui/render-scheduler.ts +278 -0
  427. package/src/ui/run-action-dispatcher.ts +182 -0
  428. package/src/ui/run-dashboard.ts +910 -0
  429. package/src/ui/run-event-bus.ts +381 -0
  430. package/src/ui/run-snapshot-cache.ts +1057 -0
  431. package/src/ui/settings-overlay.ts +1067 -0
  432. package/src/ui/shared-overlay-scheduler.ts +96 -0
  433. package/src/ui/snapshot-types.ts +90 -0
  434. package/src/ui/spinner.ts +17 -0
  435. package/src/ui/status-colors.ts +143 -0
  436. package/src/ui/syntax-highlight.ts +136 -0
  437. package/src/ui/terminal-status.ts +269 -0
  438. package/src/ui/theme-adapter.ts +271 -0
  439. package/src/ui/theme-discovery.ts +199 -0
  440. package/src/ui/tool-progress-formatter.ts +93 -0
  441. package/src/ui/tool-renderers/brief-mode.ts +225 -0
  442. package/src/ui/tool-renderers/index.ts +726 -0
  443. package/src/ui/transcript-cache.ts +128 -0
  444. package/src/ui/transcript-entries.ts +256 -0
  445. package/src/ui/transcript-viewer.ts +424 -0
  446. package/src/ui/widget/index.ts +418 -0
  447. package/src/ui/widget/widget-formatters.ts +182 -0
  448. package/src/ui/widget/widget-model.ts +110 -0
  449. package/src/ui/widget/widget-renderer.ts +182 -0
  450. package/src/ui/widget/widget-types.ts +36 -0
  451. package/src/utils/bm25-search.ts +235 -0
  452. package/src/utils/completion-dedupe.ts +63 -0
  453. package/src/utils/conflict-detect.ts +668 -0
  454. package/src/utils/env-allowlist.ts +30 -0
  455. package/src/utils/env-filter.ts +155 -0
  456. package/src/utils/file-coalescer.ts +98 -0
  457. package/src/utils/fingerprint.ts +180 -0
  458. package/src/utils/frontmatter.ts +70 -0
  459. package/src/utils/fs-watch.ts +37 -0
  460. package/src/utils/gh-protocol.ts +556 -0
  461. package/src/utils/git.ts +260 -0
  462. package/src/utils/guards.ts +107 -0
  463. package/src/utils/ids.ts +24 -0
  464. package/src/utils/incremental-reader.ts +220 -0
  465. package/src/utils/internal-error.ts +10 -0
  466. package/src/utils/names.ts +36 -0
  467. package/src/utils/ndjson.ts +115 -0
  468. package/src/utils/paths.ts +227 -0
  469. package/src/utils/project-detector.ts +160 -0
  470. package/src/utils/redaction.ts +370 -0
  471. package/src/utils/resolve-shell.ts +36 -0
  472. package/src/utils/run-watcher-registry.ts +152 -0
  473. package/src/utils/safe-paths.ts +393 -0
  474. package/src/utils/scan-cache.ts +144 -0
  475. package/src/utils/session-utils.ts +110 -0
  476. package/src/utils/sleep.ts +54 -0
  477. package/src/utils/socket-path.ts +164 -0
  478. package/src/utils/sse-parser.ts +131 -0
  479. package/src/utils/task-name-generator.ts +337 -0
  480. package/src/utils/timings.ts +33 -0
  481. package/src/utils/token-counter.ts +200 -0
  482. package/src/utils/visual.ts +207 -0
  483. package/src/workflows/cost-estimator.ts +34 -0
  484. package/src/workflows/discover-workflows.ts +308 -0
  485. package/src/workflows/intermediate-store.ts +166 -0
  486. package/src/workflows/preflight-validator.ts +168 -0
  487. package/src/workflows/topology-analyzer.ts +203 -0
  488. package/src/workflows/validate-workflow.ts +50 -0
  489. package/src/workflows/workflow-config.ts +75 -0
  490. package/src/workflows/workflow-serializer.ts +32 -0
  491. package/src/worktree/branch-freshness.ts +112 -0
  492. package/src/worktree/cleanup.ts +341 -0
  493. package/src/worktree/worktree-manager.ts +1204 -0
@@ -0,0 +1,491 @@
1
+ /**
2
+ * Owned-process lifecycle abstraction (P0 item #3).
3
+ *
4
+ * Distilled and adapted from gajae-code's `runtime/process-lifecycle.ts`.
5
+ *
6
+ * Two complementary primitives:
7
+ *
8
+ * F1(a) {@link spawnOwnedProcess} / {@link OwnedProcess} — wraps a
9
+ * `child_process.spawn` child with explicit ownership: escalating
10
+ * (SIGTERM → grace → SIGKILL) teardown, idempotent `dispose()`, bounded
11
+ * `awaitExit()`, abort-signal wiring, and postmortem registration so
12
+ * an owned child can never outlive its owner.
13
+ *
14
+ * F1(b) {@link registerResourceOwner} — a generic postmortem registry for
15
+ * NON-process resources (timers, sockets, Workers, VM contexts) with
16
+ * `disposeAllOwners()` / `disposeOwner(name)`.
17
+ *
18
+ * ╔═══════════════════════════════════════════════════════════════════════════╗
19
+ * ║ IMPORTANT — INCREMENTAL, NOT FULL MIGRATION ║
20
+ * ║ pi-crew ALREADY has sophisticated kill logic in child-pi.ts ║
21
+ * ║ (killProcessTree, escalating SIGTERM→grace→SIGKILL, hard-kill timer, ║
22
+ * ║ post-exit stdio guard) and async-runner.ts does detached/setsid spawns. ║
23
+ * ║ Those paths are NOT rewritten here. This module provides a clean ║
24
+ * ║ ownership primitive for NEW code paths that need guaranteed teardown ║
25
+ * ║ without re-implementing the escalation dance each time. ║
26
+ * ╚═══════════════════════════════════════════════════════════════════════════╝
27
+ *
28
+ * Cross-platform: on Windows there is no SIGTERM; teardown uses
29
+ * `taskkill /F /T /PID` escalation directly (force-kill the whole tree).
30
+ * See `.crew/knowledge.md` gotchas: BSD/Windows signal handling differs.
31
+ */
32
+ import { type ChildProcess, type SpawnOptions, spawn } from "node:child_process";
33
+ import { logInternalError } from "../utils/internal-error.ts";
34
+
35
+ // ── tunables ──────────────────────────────────────────────────────────────────
36
+
37
+ const DEFAULT_GRACEFUL_MS = 2_000;
38
+ /** Hard cap on how long dispose() waits after SIGKILL before giving up, so a
39
+ * wedged/unkillable child can never block shutdown forever. */
40
+ const SIGKILL_REAP_CAP_MS = 2_000;
41
+ /** After the root child exits on its own, how long to wait for the process
42
+ * group to drain before deregistering. Clean servers drain immediately. */
43
+ const ROOT_EXIT_DRAIN_MS = 250;
44
+
45
+ const isPosix = process.platform !== "win32";
46
+
47
+ const delay = (ms: number): Promise<void> =>
48
+ new Promise((resolve) => {
49
+ setTimeout(resolve, Math.max(0, ms));
50
+ });
51
+
52
+ /** Poll `predicate` until true or `timeoutMs` elapses. Returns the final value. */
53
+ async function pollUntil(predicate: () => boolean, timeoutMs: number, intervalMs = 20): Promise<boolean> {
54
+ if (predicate()) return true;
55
+ const deadline = Date.now() + Math.max(0, timeoutMs);
56
+ while (Date.now() < deadline) {
57
+ await delay(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
58
+ if (predicate()) return true;
59
+ }
60
+ return predicate();
61
+ }
62
+
63
+ /** Whether a POSIX process group still has any member (zombies count as alive). */
64
+ function groupAlive(pgid: number): boolean {
65
+ try {
66
+ process.kill(-pgid, 0);
67
+ return true;
68
+ } catch (err) {
69
+ // EPERM => the group exists but we cannot signal it; treat as alive.
70
+ return (err as NodeJS.ErrnoException).code === "EPERM";
71
+ }
72
+ }
73
+
74
+ // ── F1(a) OwnedProcess ────────────────────────────────────────────────────────
75
+
76
+ /** Options for {@link spawnOwnedProcess}. */
77
+ export interface SpawnOwnedOptions {
78
+ cwd?: string;
79
+ env?: Record<string, string | undefined>;
80
+ /** stdin mode passed through to the child. Defaults to `"ignore"`. */
81
+ stdin?: "pipe" | "ignore";
82
+ /** When aborted, the owned process tree is disposed (escalating kill). */
83
+ signal?: AbortSignal;
84
+ /** Grace period (ms) between SIGTERM and SIGKILL on dispose. Default 2000. */
85
+ gracefulMs?: number;
86
+ /**
87
+ * Spawn the child as its own process-group leader so the whole descendant
88
+ * tree can be signalled on dispose. Defaults to `true` on POSIX. Has no
89
+ * effect on Windows, where teardown falls back to single-process kill.
90
+ */
91
+ processGroup?: boolean;
92
+ /** Label used in diagnostics. */
93
+ name?: string;
94
+ /** Extra SpawnOptions merged in (e.g. windowsHide). */
95
+ extraOptions?: SpawnOptions;
96
+ }
97
+
98
+ /** Result of a bounded {@link OwnedProcess.awaitExit}. */
99
+ export interface AwaitExitResult {
100
+ /** `true` when the process has exited; `false` when the timeout fired first. */
101
+ exited: boolean;
102
+ /** Exit code if known, else `null`. */
103
+ code: number | null;
104
+ }
105
+
106
+ /** Exit callback signature for {@link OwnedProcess.onExit}. */
107
+ export type OwnedExitCallback = (code: number | null, signal: NodeJS.Signals | null) => void;
108
+
109
+ /**
110
+ * A spawned child process owned by the runtime with guaranteed teardown.
111
+ *
112
+ * Implemented as a class so callers retain a strong handle and so `dispose()`
113
+ * can be idempotent (concurrent/repeated calls return the same in-flight
114
+ * promise). Never throws from `dispose()` / `awaitExit()`.
115
+ */
116
+ export class OwnedProcess {
117
+ readonly child: ChildProcess;
118
+ readonly pid: number | undefined;
119
+ /** Process-group id (POSIX detached only); `undefined` on Windows / opt-out. */
120
+ readonly pgid: number | undefined;
121
+ private readonly gracefulMs: number;
122
+ private readonly name: string | undefined;
123
+ private disposed = false;
124
+ private disposePromise: Promise<void> | undefined;
125
+ private deregistered = false;
126
+ /** Terminal once teardown/reconciliation has confirmed the group is gone. */
127
+ private terminated = false;
128
+ private exitPromise: Promise<{
129
+ code: number | null;
130
+ signal: NodeJS.Signals | null;
131
+ }>;
132
+ private exitCallbacks = new Set<OwnedExitCallback>();
133
+ private onAbort: (() => void) | undefined;
134
+ private readonly abortSignal: AbortSignal | undefined;
135
+
136
+ constructor(child: ChildProcess, opts: SpawnOwnedOptions, registerSelf: (owner: OwnedProcess) => () => void) {
137
+ this.child = child;
138
+ this.pid = child.pid;
139
+ this.gracefulMs = opts.gracefulMs ?? DEFAULT_GRACEFUL_MS;
140
+ this.name = opts.name;
141
+ this.abortSignal = opts.signal;
142
+
143
+ const useGroup = (opts.processGroup ?? true) && isPosix;
144
+ // On POSIX with `detached`, the child is its own process-group leader,
145
+ // so the group id equals its pid.
146
+ this.pgid = useGroup ? child.pid : undefined;
147
+
148
+ this.exitPromise = new Promise((resolve) => {
149
+ child.once("exit", (code, signal) => {
150
+ resolve({ code: code, signal: signal });
151
+ for (const cb of this.exitCallbacks) {
152
+ try {
153
+ cb(code, signal);
154
+ } catch (err) {
155
+ logInternalError("owned-process.onExit-callback", err, this.name ? `name=${this.name}` : undefined);
156
+ }
157
+ }
158
+ });
159
+ });
160
+
161
+ // Register for postmortem cleanup and wire abort.
162
+ const deregister = registerSelf(this);
163
+ this.deregisterFn = deregister;
164
+
165
+ // When the root exits on its own (not via dispose), reconcile ownership
166
+ // by the *group*: after a short drain window, deregister if the group is
167
+ // empty, otherwise reap the owned group (no child outlives its owner).
168
+ void this.exitPromise
169
+ .then(() => {
170
+ if (this.disposed) return; // dispose() owns deregistration
171
+ if (this.pgid === undefined) {
172
+ this.deregisterInternal();
173
+ return;
174
+ }
175
+ void (async () => {
176
+ const drained = await pollUntil(() => !groupAlive(this.pgid!), ROOT_EXIT_DRAIN_MS);
177
+ if (this.disposed) return;
178
+ if (drained) {
179
+ this.deregisterInternal();
180
+ return;
181
+ }
182
+ // Root exited but the owned group still has descendants: reap them.
183
+ await this.dispose();
184
+ })();
185
+ })
186
+ .catch(() => undefined);
187
+
188
+ if (this.abortSignal) {
189
+ if (this.abortSignal.aborted) {
190
+ void this.dispose();
191
+ } else {
192
+ this.onAbort = () => void this.dispose();
193
+ this.abortSignal.addEventListener("abort", this.onAbort, {
194
+ once: true,
195
+ });
196
+ }
197
+ }
198
+ }
199
+
200
+ private deregisterFn: () => void = () => {};
201
+
202
+ /** `true` once `dispose()` has started. */
203
+ get isDisposed(): boolean {
204
+ return this.disposed;
205
+ }
206
+
207
+ /**
208
+ * Register a callback invoked exactly once when the root child exits.
209
+ * If the child has already exited, the callback is invoked synchronously
210
+ * with the cached exit info. Returns an unsubscribe function.
211
+ */
212
+ onExit(callback: OwnedExitCallback): () => void {
213
+ this.exitCallbacks.add(callback);
214
+ // If already exited, the exitPromise is resolved; fire immediately.
215
+ // We race to detect resolution without awaiting.
216
+ let settled = false;
217
+ this.exitPromise.then((info) => {
218
+ if (settled) return; // callback may have been removed
219
+ if (this.exitCallbacks.has(callback)) {
220
+ try {
221
+ callback(info.code, info.signal);
222
+ } catch (err) {
223
+ logInternalError("owned-process.onExit-immediate", err, this.name ? `name=${this.name}` : undefined);
224
+ }
225
+ }
226
+ });
227
+ return () => {
228
+ settled = true;
229
+ this.exitCallbacks.delete(callback);
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Wait for the root child to exit, optionally bounded by `timeoutMs`.
235
+ * With no timeout it resolves only when the child exits. Never rejects.
236
+ */
237
+ async awaitExit(opts?: { timeoutMs?: number }): Promise<AwaitExitResult> {
238
+ const exitResult = this.exitPromise.then((info) => ({
239
+ exited: true as const,
240
+ code: info.code,
241
+ }));
242
+ if (opts?.timeoutMs === undefined) return exitResult;
243
+ let timer: ReturnType<typeof setTimeout> | undefined;
244
+ const timeout = new Promise<AwaitExitResult>((resolve) => {
245
+ timer = setTimeout(() => resolve({ exited: false, code: this.child.exitCode }), Math.max(0, opts.timeoutMs!));
246
+ });
247
+ try {
248
+ return await Promise.race([exitResult, timeout]);
249
+ } finally {
250
+ if (timer) clearTimeout(timer);
251
+ }
252
+ }
253
+
254
+ /** Signal the process tree with `signal` (group-aware on POSIX). */
255
+ private signalTree(signal: NodeJS.Signals): void {
256
+ const pid = this.child.pid;
257
+ if (pid === undefined) return;
258
+ if (this.pgid !== undefined) {
259
+ try {
260
+ process.kill(-this.pgid, signal);
261
+ return;
262
+ } catch {
263
+ /* group already gone */
264
+ }
265
+ return;
266
+ }
267
+ try {
268
+ this.child.kill(signal);
269
+ } catch {
270
+ /* already gone */
271
+ }
272
+ }
273
+
274
+ private deregisterInternal(): void {
275
+ if (this.deregistered) return;
276
+ this.deregistered = true;
277
+ this.terminated = true;
278
+ this.deregisterFn();
279
+ if (this.onAbort && this.abortSignal) {
280
+ this.abortSignal.removeEventListener("abort", this.onAbort);
281
+ this.onAbort = undefined;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Idempotently terminate the owned process *group*: SIGTERM the group, wait
287
+ * `gracefulMs`, then SIGKILL, polling liveness throughout. On Windows,
288
+ * escalate directly to taskkill /F /T /PID. Removes the abort listener and
289
+ * deregisters from the live-owner set only after teardown has completed.
290
+ * Repeated/concurrent calls return the same in-flight promise. Never throws.
291
+ */
292
+ dispose(): Promise<void> {
293
+ // Already terminal: never re-probe a recycled pgid.
294
+ if (this.terminated) {
295
+ this.disposed = true;
296
+ if (!this.disposePromise) this.disposePromise = Promise.resolve();
297
+ return this.disposePromise;
298
+ }
299
+ if (this.disposePromise) return this.disposePromise;
300
+ this.disposed = true;
301
+ if (this.onAbort && this.abortSignal) {
302
+ this.abortSignal.removeEventListener("abort", this.onAbort);
303
+ this.onAbort = undefined;
304
+ }
305
+ this.disposePromise = (async () => {
306
+ try {
307
+ if (!isPosix) {
308
+ await this.disposeWindows();
309
+ return;
310
+ }
311
+ if (this.pgid !== undefined) {
312
+ // Group ownership: reap until the whole group is gone, even if
313
+ // the root has already exited (it may have backgrounded children).
314
+ if (!groupAlive(this.pgid)) return;
315
+ this.signalTree("SIGTERM");
316
+ if (await pollUntil(() => !groupAlive(this.pgid!), this.gracefulMs)) return;
317
+ this.signalTree("SIGKILL");
318
+ if (!(await pollUntil(() => !groupAlive(this.pgid!), SIGKILL_REAP_CAP_MS))) {
319
+ console.warn(
320
+ `[pi-crew] owned process group still alive after SIGKILL (name=${this.name ?? "?"}, pgid=${this.pgid})`,
321
+ );
322
+ }
323
+ return;
324
+ }
325
+ // Single-process fallback (processGroup:false).
326
+ if (this.child.exitCode !== null) return;
327
+ this.signalTree("SIGTERM");
328
+ if ((await this.awaitExit({ timeoutMs: this.gracefulMs })).exited) return;
329
+ this.signalTree("SIGKILL");
330
+ await this.awaitExit({ timeoutMs: SIGKILL_REAP_CAP_MS });
331
+ } catch (err) {
332
+ logInternalError("owned-process.dispose", err, this.name ? `name=${this.name}` : undefined);
333
+ } finally {
334
+ // Deregister only after teardown completes so a postmortem firing
335
+ // mid-grace still sees the owner.
336
+ this.deregisterInternal();
337
+ }
338
+ })();
339
+ return this.disposePromise;
340
+ }
341
+
342
+ /** Windows teardown: no SIGTERM; escalate to taskkill /F /T /PID. */
343
+ private async disposeWindows(): Promise<void> {
344
+ const pid = this.child.pid;
345
+ if (pid === undefined) return;
346
+ if (this.child.exitCode !== null) return;
347
+ // First try a graceful taskkill (no /F), then escalate to /F /T.
348
+ const tryTaskkill = (force: boolean): Promise<void> =>
349
+ new Promise((resolve) => {
350
+ const args = ["/T", "/PID", String(pid), ...(force ? ["/F"] : [])];
351
+ const tk = spawn("taskkill", args, {
352
+ stdio: "ignore",
353
+ windowsHide: true,
354
+ });
355
+ tk.on("error", () => resolve());
356
+ tk.on("exit", () => resolve());
357
+ });
358
+ await tryTaskkill(false);
359
+ if ((await this.awaitExit({ timeoutMs: this.gracefulMs })).exited) return;
360
+ await tryTaskkill(true);
361
+ await this.awaitExit({ timeoutMs: SIGKILL_REAP_CAP_MS });
362
+ }
363
+ }
364
+
365
+ // ── live-owner set + postmortem ───────────────────────────────────────────────
366
+
367
+ const liveOwners = new Set<OwnedProcess>();
368
+ let ownedPostmortemRegistered = false;
369
+
370
+ function ensureOwnedPostmortem(): void {
371
+ if (ownedPostmortemRegistered) return;
372
+ ownedPostmortemRegistered = true;
373
+ // Register a process-exit handler that disposes every live owned process.
374
+ // We wire both beforeExit (event-loop empty) and exit (synchronous final).
375
+ const drain = async (): Promise<void> => {
376
+ await Promise.all([...liveOwners].map((owner) => owner.dispose().catch(() => undefined)));
377
+ };
378
+ process.once("beforeExit", () => {
379
+ void drain().catch(() => undefined);
380
+ });
381
+ }
382
+
383
+ /**
384
+ * Spawn a child process owned by the runtime. The returned {@link OwnedProcess}
385
+ * is registered for postmortem cleanup and tears down its whole process group
386
+ * on dispose/abort.
387
+ *
388
+ * NOTE: this is for NEW ownership-scoped spawns. Do NOT use it to replace
389
+ * child-pi.ts's runChildPi (which has its own battle-tested escalation logic)
390
+ * or async-runner.ts's intentionally-detached background spawns.
391
+ */
392
+ export function spawnOwnedProcess(command: string, args: readonly string[] = [], opts: SpawnOwnedOptions = {}): OwnedProcess {
393
+ ensureOwnedPostmortem();
394
+ const useGroup = (opts.processGroup ?? true) && isPosix;
395
+ const spawnOpts: SpawnOptions = {
396
+ cwd: opts.cwd,
397
+ env: opts.env as NodeJS.ProcessEnv | undefined,
398
+ stdio: [opts.stdin ?? "ignore", "pipe", "pipe"],
399
+ detached: useGroup,
400
+ windowsHide: true,
401
+ ...opts.extraOptions,
402
+ };
403
+ const child = spawn(command, args as string[], spawnOpts);
404
+ const owner = new OwnedProcess(child, opts, (self) => {
405
+ liveOwners.add(self);
406
+ return () => {
407
+ liveOwners.delete(self);
408
+ };
409
+ });
410
+ return owner;
411
+ }
412
+
413
+ /** Number of currently live owned processes. Exposed for leak assertions/tests. */
414
+ export function liveOwnedProcessCount(): number {
415
+ return liveOwners.size;
416
+ }
417
+
418
+ /** Dispose every live owned process. For owner-scoped teardown and tests. */
419
+ export async function disposeAllOwnedProcesses(): Promise<void> {
420
+ await Promise.all([...liveOwners].map((owner) => owner.dispose().catch(() => undefined)));
421
+ }
422
+
423
+ // ── F1(b) generic resource owners ─────────────────────────────────────────────
424
+
425
+ type ResourceDisposer = () => void | Promise<void>;
426
+
427
+ const resourceOwners = new Map<string, ResourceDisposer>();
428
+ let resourcePostmortemRegistered = false;
429
+
430
+ function ensureResourcePostmortem(): void {
431
+ if (resourcePostmortemRegistered) return;
432
+ resourcePostmortemRegistered = true;
433
+ process.once("beforeExit", () => {
434
+ void disposeAllOwners().catch(() => undefined);
435
+ });
436
+ }
437
+
438
+ /**
439
+ * Register a non-process resource for postmortem/fatal-exit cleanup.
440
+ *
441
+ * Idempotent by `name`: re-registering the same name replaces the prior
442
+ * disposer (last wins). Returns an unregister function that removes the owner
443
+ * only while it is still the active registration for that name.
444
+ */
445
+ export function registerResourceOwner(name: string, disposer: ResourceDisposer): () => void {
446
+ resourceOwners.set(name, disposer);
447
+ ensureResourcePostmortem();
448
+ let unregistered = false;
449
+ return () => {
450
+ if (unregistered) return;
451
+ unregistered = true;
452
+ if (resourceOwners.get(name) === disposer) {
453
+ resourceOwners.delete(name);
454
+ }
455
+ };
456
+ }
457
+
458
+ /** Number of registered resource owners. Exposed for leak assertions/tests. */
459
+ export function resourceOwnerCount(): number {
460
+ return resourceOwners.size;
461
+ }
462
+
463
+ /**
464
+ * Run and clear every registered resource disposer. Attempts all disposers even
465
+ * if some throw, then surfaces the failures as an `AggregateError` so callers
466
+ * can distinguish "all closed" from "a resource may still be alive".
467
+ */
468
+ export async function disposeAllOwners(): Promise<void> {
469
+ const disposers = [...resourceOwners.values()];
470
+ resourceOwners.clear();
471
+ const errors: unknown[] = [];
472
+ for (const disposer of disposers) {
473
+ try {
474
+ await disposer();
475
+ } catch (err) {
476
+ errors.push(err);
477
+ }
478
+ }
479
+ if (errors.length > 0) {
480
+ throw new AggregateError(errors, `${errors.length} resource disposer(s) failed during teardown`);
481
+ }
482
+ }
483
+
484
+ /** Dispose a single named resource owner. Returns true if it was registered. */
485
+ export async function disposeOwner(name: string): Promise<boolean> {
486
+ const disposer = resourceOwners.get(name);
487
+ if (!disposer) return false;
488
+ resourceOwners.delete(name);
489
+ await disposer();
490
+ return true;
491
+ }
@@ -0,0 +1,154 @@
1
+ import type { TeamRunManifest } from "../state/types.ts";
2
+ import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
3
+
4
+ export { hasAsyncStartMarker } from "./async-marker.ts";
5
+
6
+ export interface ProcessLiveness {
7
+ pid?: number;
8
+ alive: boolean;
9
+ detail: string;
10
+ }
11
+
12
+ /**
13
+ * How long (ms) a foreground team run with status "running" but no active agents
14
+ * survives before being flagged as orphaned. Reduced from 10min to 2min to
15
+ * improve UX: stuck foreground runs (e.g. planner with 0 tool calls) no longer
16
+ * linger for 10min before the dashboard shows them as stale.
17
+ */
18
+ const ORPHANED_ACTIVE_RUN_MS = 2 * 60 * 1000;
19
+ /** How long a completed run stays visible in the widget after completion. */
20
+ const COMPLETED_VISIBILITY_GRACE_MS = 8_000;
21
+ /** Errors (failed/cancelled) linger far longer so a failed run leaves a visible
22
+ * trace in the crew widget for ~10 min (F-5). Successful completions vanish
23
+ * quickly to keep the widget quiet. */
24
+ const ERROR_VISIBILITY_GRACE_MS = 10 * 60 * 1000;
25
+ /** Maximum age (ms) for an active run before it's considered stale.
26
+ * After this time, PID-only liveness is unreliable due to PID recycling. */
27
+ const STALE_ACTIVE_RUN_MS = 30 * 60 * 1000;
28
+
29
+ export function checkProcessLiveness(pid: number | undefined): ProcessLiveness {
30
+ if (pid === undefined || !Number.isInteger(pid) || pid <= 0) {
31
+ return { pid, alive: false, detail: "no pid recorded" };
32
+ }
33
+ try {
34
+ process.kill(pid, 0);
35
+ return { pid, alive: true, detail: "process is alive" };
36
+ } catch (error) {
37
+ const nodeError = error as NodeJS.ErrnoException;
38
+ if (nodeError.code === "EPERM")
39
+ return {
40
+ pid,
41
+ alive: true,
42
+ detail: "process exists but permission is denied",
43
+ };
44
+ if (nodeError.code === "ESRCH") return { pid, alive: false, detail: "process does not exist" };
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ return { pid, alive: false, detail: message };
47
+ }
48
+ }
49
+
50
+ export function isActiveRunStatus(status: string): boolean {
51
+ return status === "queued" || status === "planning" || status === "running" || status === "waiting";
52
+ }
53
+
54
+ export function isFinishedRunStatus(status: string): boolean {
55
+ return status === "completed" || status === "failed" || status === "cancelled" || status === "blocked";
56
+ }
57
+
58
+ /**
59
+ * Secondary threshold: runs that have been "running" for more than this without
60
+ * any active agents (all queued, no progress) are also considered orphaned.
61
+ * This catches foreground team runs where the planner got stuck with 0 tool calls.
62
+ */
63
+ const ORPHANED_NO_PROGRESS_MS = 5 * 60 * 1000;
64
+
65
+ export function isLikelyOrphanedActiveRun(
66
+ run: TeamRunManifest,
67
+ agents: CrewAgentRecord[] = [],
68
+ now = Date.now(),
69
+ staleMs = ORPHANED_ACTIVE_RUN_MS,
70
+ ): boolean {
71
+ if (!isActiveRunStatus(run.status)) return false;
72
+ if (run.async?.pid !== undefined) return false;
73
+ const updatedAt = new Date(run.updatedAt).getTime();
74
+ if (!Number.isFinite(updatedAt)) return false;
75
+
76
+ // Primary check: run hasn't been updated in a while
77
+ if (now - updatedAt >= staleMs) {
78
+ if (agents.length === 0) return run.summary === "Creating workflow prompts and placeholder results.";
79
+ return agents.every((agent) => agent.status === "queued" && !agent.completedAt && !agent.progress);
80
+ }
81
+
82
+ // Secondary check: run has been running without any progress for too long
83
+ if (now - updatedAt >= ORPHANED_NO_PROGRESS_MS) {
84
+ // If no agent is "running" or has made progress, the run is likely stuck
85
+ const hasActiveAgent = agents.some((agent) => agent.status === "running" || agent.progress || agent.toolUses);
86
+ if (!hasActiveAgent && agents.length > 0) {
87
+ return true;
88
+ }
89
+ }
90
+
91
+ return false;
92
+ }
93
+
94
+ function hasDurableActiveAgentEvidence(agent: CrewAgentRecord): boolean {
95
+ if (agent.status === "running") {
96
+ // Running agents are actively executing — trust them.
97
+ // Activity evidence is only required for queued agents (zombie prevention).
98
+ return true;
99
+ }
100
+ if (agent.status === "queued") {
101
+ // Queued agents need actual activity evidence to distinguish from zombies:
102
+ // spawned-but-never-executed agents should not appear as active.
103
+ return Boolean(
104
+ (agent.progress && (agent.progress.toolCount > 0 || agent.progress.recentOutput.length > 0)) ||
105
+ (agent.jsonEvents && agent.jsonEvents > 0) ||
106
+ (agent.toolUses && agent.toolUses > 0),
107
+ );
108
+ }
109
+ return false;
110
+ }
111
+
112
+ export function hasStaleAsyncProcess(run: TeamRunManifest, now = Date.now()): boolean {
113
+ if (!isActiveRunStatus(run.status) || !run.async) return false;
114
+ const pidAlive = checkProcessLiveness(run.async.pid).alive;
115
+ if (!pidAlive) return true;
116
+ // PID is alive, but check if the run is suspiciously old.
117
+ // PID recycling means a stale PID could point to an unrelated process.
118
+ const updatedAt = new Date(run.updatedAt).getTime();
119
+ const age = now - updatedAt;
120
+ if (Number.isFinite(updatedAt) && age > STALE_ACTIVE_RUN_MS) {
121
+ // Additional evidence: if no agent has recent activity, treat as stale.
122
+ // The real process would have updated the manifest within 30 minutes.
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+
128
+ export function isDisplayActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now()): boolean {
129
+ if (hasStaleAsyncProcess(run, now) || isLikelyOrphanedActiveRun(run, agents, now)) return false;
130
+ // Hard filter: if an active-status run hasn't been updated in > STALE_ACTIVE_RUN_MS
131
+ // and has no async PID tracking, it's a ghost from a crashed process.
132
+ if (isActiveRunStatus(run.status) && !run.async) {
133
+ const updatedAt = new Date(run.updatedAt).getTime();
134
+ if (Number.isFinite(updatedAt) && now - updatedAt > STALE_ACTIVE_RUN_MS) return false;
135
+ }
136
+ // Grace period: show completed runs for a few seconds so users see the result.
137
+ if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
138
+ const grace = run.status === "completed" ? COMPLETED_VISIBILITY_GRACE_MS : ERROR_VISIBILITY_GRACE_MS;
139
+ const lastAgentActivity = agents.reduce<number>((max, agent) => {
140
+ const ts = agent.completedAt ?? agent.startedAt;
141
+ const parsed = ts ? new Date(ts).getTime() : 0;
142
+ return Number.isFinite(parsed) && parsed > max ? parsed : max;
143
+ }, new Date(run.updatedAt).getTime());
144
+ if (Number.isFinite(lastAgentActivity) && now - lastAgentActivity < grace) return true;
145
+ return false;
146
+ }
147
+ if (!isActiveRunStatus(run.status)) return false;
148
+ // Keep the always-visible widget quiet until a worker actually exists.
149
+ // Empty active manifests can be created briefly at startup, by old fixture/scaffold
150
+ // runs, or from cross-cwd registry history; showing them causes noisy 0/0 rows and
151
+ // needless spinner redraws. The full dashboard can still list historical runs.
152
+ if (agents.length === 0) return false;
153
+ return agents.some(hasDurableActiveAgentEvidence);
154
+ }
@@ -0,0 +1,44 @@
1
+ export interface ProgressEventSummary {
2
+ eventType: string;
3
+ currentTool?: string;
4
+ toolCount?: number;
5
+ tokens?: number;
6
+ turns?: number;
7
+ activityState?: string;
8
+ lastActivityAt?: string;
9
+ }
10
+
11
+ export interface ProgressEventCoalesceDecision {
12
+ shouldAppend: boolean;
13
+ reason: string;
14
+ }
15
+
16
+ export interface ProgressEventCoalesceInput {
17
+ previous?: ProgressEventSummary;
18
+ next: ProgressEventSummary;
19
+ nowMs: number;
20
+ lastAppendMs?: number;
21
+ minIntervalMs: number;
22
+ force?: boolean;
23
+ tokenThreshold?: number;
24
+ }
25
+
26
+ const DEFAULT_TOKEN_THRESHOLD = 256;
27
+
28
+ function numericIncrease(previous: number | undefined, next: number | undefined): number {
29
+ return next !== undefined && previous !== undefined ? next - previous : next !== undefined ? next : 0;
30
+ }
31
+
32
+ export function shouldAppendProgressEventUpdate(input: ProgressEventCoalesceInput): ProgressEventCoalesceDecision {
33
+ if (input.force) return { shouldAppend: true, reason: "force" };
34
+ if (!input.previous) return { shouldAppend: true, reason: "first" };
35
+ if (input.previous.activityState !== input.next.activityState) return { shouldAppend: true, reason: "activity_changed" };
36
+ if (input.previous.currentTool !== input.next.currentTool) return { shouldAppend: true, reason: "tool_changed" };
37
+ if (numericIncrease(input.previous.toolCount, input.next.toolCount) > 0) return { shouldAppend: true, reason: "tool_count_increased" };
38
+ if (numericIncrease(input.previous.turns, input.next.turns) > 0) return { shouldAppend: true, reason: "turns_increased" };
39
+ const tokenIncrease = numericIncrease(input.previous.tokens, input.next.tokens);
40
+ if (tokenIncrease >= (input.tokenThreshold ?? DEFAULT_TOKEN_THRESHOLD)) return { shouldAppend: true, reason: "tokens_increased" };
41
+ if (input.lastAppendMs === undefined || input.nowMs - input.lastAppendMs >= input.minIntervalMs)
42
+ return { shouldAppend: true, reason: "interval" };
43
+ return { shouldAppend: false, reason: "coalesced" };
44
+ }