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,1146 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AgentConfig } from "../agents/agent-config.ts";
4
+ import { resolveToolPolicy } from "../agents/agent-config.ts";
5
+ import type { CrewRuntimeConfig } from "../config/config.ts";
6
+ import { loadConfig } from "../config/config.ts";
7
+ import { DEFAULT_LIVE_SESSION } from "../config/defaults.ts";
8
+ import { appendEvent, appendEventFireAndForget } from "../state/event-log.ts";
9
+ import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
10
+ import { logInternalError } from "../utils/internal-error.ts";
11
+ import { redactSecrets } from "../utils/redaction.ts";
12
+ import type { WorkflowStep } from "../workflows/workflow-config.ts";
13
+ import { createIrcTool } from "./custom-tools/irc-tool.ts";
14
+ import { createSubmitResultTool } from "./custom-tools/submit-result-tool.ts";
15
+ import { applyLiveAgentControlRequest, applyLiveAgentControlRequests, type LiveAgentControlCursor } from "./live-agent-control.ts";
16
+ import {
17
+ disposeLiveAgentSession,
18
+ listLiveAgents,
19
+ markLiveAgentCompleted,
20
+ registerLiveAgent,
21
+ terminateLiveAgent,
22
+ trackLiveAgentResponseText,
23
+ trackLiveAgentToolEnd,
24
+ trackLiveAgentToolStart,
25
+ trackLiveAgentTurnEnd,
26
+ updateLiveAgentStatus,
27
+ } from "./live-agent-manager.ts";
28
+ import { subscribeLiveControlRealtime } from "./live-control-realtime.ts";
29
+ import { buildExtensionBridge } from "./live-extension-bridge.ts";
30
+ import { collectLiveSessionHealth, formatLiveSessionDiagnostics } from "./live-session-health.ts";
31
+ import { buildMcpProxyFromSession } from "./mcp-proxy.ts";
32
+ import { buildConfiguredModelRouting } from "./model-fallback.ts";
33
+ import { readEnabledModelsPatterns } from "./model-scope.ts";
34
+ import { isLiveSessionRuntimeAvailable } from "./runtime-resolver.ts";
35
+ import { awaitRuntimeWarmup } from "./runtime-warmup.ts";
36
+ // prose-compressor imported for custom tool descriptions below;
37
+ // tool description compression for SDK-managed tools awaits SDK support.
38
+ import { buildSensitivePathConstraint } from "./sensitive-paths.ts";
39
+ import { eventToSidechainType, sidechainOutputPath, writeSidechainEntry } from "./sidechain-output.ts";
40
+ // NOTE: buildMemoryBlock is intentionally NOT imported here. The agent memory
41
+ // block is injected via renderTaskPrompt().full (the USER prompt), which is
42
+ // shared by both the child-pi path (no system prompt) and the live-session
43
+ // path. Adding it to liveSystemPrompt() too duplicated the entire memory
44
+ // block (up to 200 lines) in both the user and system prompts. Keep memory
45
+ // in a single place: the shared user prompt. See G3 fix.
46
+ import { createStreamingOutput, type StreamingOutputHandle } from "./streaming-output.ts";
47
+ import { trackTaskUsage } from "./usage-tracker.ts";
48
+ import {
49
+ buildYieldReminder,
50
+ DEFAULT_YIELD_CONFIG,
51
+ extractYieldResult,
52
+ hasYieldInOutput,
53
+ isYieldEvent,
54
+ validateYieldData,
55
+ type YieldResult,
56
+ } from "./yield-handler.ts";
57
+
58
+ /**
59
+ * Module-scoped latch for the optional peer dependency import. When N
60
+ * in-process live-session subagents spawn CONCURRENTLY (e.g. several
61
+ * `Agent({run_in_background:true})` started at once), each used to call
62
+ // LAZY: defer dynamic import of @earendil-works/pi-coding-agent to its call site.
63
+ * `await import("@earendil-works/pi-coding-agent")` independently. Under the
64
+ * tsx loader (registering load/resolve hooks), concurrent first-imports can
65
+ * each enter the loader and race module-record instantiation, yielding
66
+ * `Cannot read properties of undefined (reading 'existsSync')` /
67
+ * `'validateWorkflowForTeam'` as namespace bindings observed mid-evaluation.
68
+ * Sequential retries always succeed → this is a cold-start race, not a logic
69
+ * bug. ESM engines memoize imports, but that memoization is not guaranteed
70
+ * to be observed synchronously across concurrent evaluation under transpiling
71
+ * loaders, so we add an explicit JS-level latch: the first caller wins, every
72
+ * later caller awaits the same in-flight promise. (Observed 2026-06-16 when 4
73
+ * explorer subagents launched together; 3 of 4 crashed.)
74
+ */
75
+ let liveSessionModulePromise: Promise<LiveSessionModule> | undefined;
76
+ function loadLiveSessionModule(): Promise<LiveSessionModule> {
77
+ if (!liveSessionModulePromise) {
78
+ liveSessionModulePromise = import("@earendil-works/pi-coding-agent") as unknown as Promise<LiveSessionModule>;
79
+ }
80
+ return liveSessionModulePromise;
81
+ }
82
+
83
+ export interface LiveSessionSpawnInput {
84
+ manifest: TeamRunManifest;
85
+ task: TeamTaskState;
86
+ step: WorkflowStep;
87
+ agent: AgentConfig;
88
+ prompt: string;
89
+ signal?: AbortSignal;
90
+ transcriptPath?: string;
91
+ onEvent?: (event: unknown) => void;
92
+ onOutput?: (text: string) => void;
93
+ runtimeConfig?: CrewRuntimeConfig;
94
+ parentContext?: string;
95
+ parentModel?: unknown;
96
+ modelRegistry?: unknown;
97
+ modelOverride?: string;
98
+ teamRoleModel?: string;
99
+ isCurrent?: () => boolean;
100
+ /** Workspace where this run was initiated — used for session-scoped live-agent visibility. */
101
+ workspaceId: string;
102
+ /** Phase 2: Output schema for validating yield data. */
103
+ outputSchema?: unknown;
104
+ }
105
+
106
+ export interface LiveSessionRunResult {
107
+ available: true;
108
+ exitCode: number | null;
109
+ stdout: string;
110
+ stderr: string;
111
+ jsonEvents: number;
112
+ usage?: UsageState;
113
+ error?: string;
114
+ /** Phase 1: Extracted yield result from submit_result tool call. */
115
+ yieldResult?: YieldResult;
116
+ }
117
+
118
+ export interface LiveSessionUnavailableResult {
119
+ available: false;
120
+ reason: string;
121
+ }
122
+
123
+ export interface LiveSessionPlannedResult {
124
+ available: true;
125
+ reason: string;
126
+ }
127
+
128
+ type LiveSessionModule = Record<string, unknown> & {
129
+ createAgentSession?: (options?: Record<string, unknown>) => Promise<{ session: LiveSessionLike; modelFallbackMessage?: string }>;
130
+ DefaultResourceLoader?: new (options: Record<string, unknown>) => { reload?: () => Promise<void> };
131
+ SessionManager?: {
132
+ inMemory?: (cwd?: string) => unknown;
133
+ create?: (cwd?: string, sessionDir?: string) => unknown;
134
+ };
135
+ SettingsManager?: { create?: (cwd?: string, agentDir?: string) => unknown };
136
+ getAgentDir?: () => string;
137
+ };
138
+
139
+ type LiveSessionLike = {
140
+ subscribe?: (listener: (event: unknown) => void) => () => void;
141
+ prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>;
142
+ steer?: (text: string) => Promise<void>;
143
+ abort?: () => Promise<void> | void;
144
+ dispose?: () => void;
145
+ getStats?: () => unknown;
146
+ stats?: unknown;
147
+ bindExtensions?: (bindings?: Record<string, unknown>) => Promise<void>;
148
+ getActiveToolNames?: () => string[];
149
+ setActiveToolsByName?: (names: string[]) => void;
150
+ };
151
+
152
+ function appendTranscript(filePath: string | undefined, event: unknown): void {
153
+ if (!filePath) return;
154
+ // Fire-and-forget async write to avoid blocking the event loop.
155
+ // Transcript writes are best-effort telemetry — callers do not need to await.
156
+ void appendTranscriptAsync(filePath, event);
157
+ }
158
+
159
+ /** Async version of appendTranscript — fire-and-forget for non-blocking writes. */
160
+ async function appendTranscriptAsync(filePath: string, event: unknown): Promise<void> {
161
+ try {
162
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
163
+ const content = `${JSON.stringify(redactSecrets(event))}\n`;
164
+ await fs.promises.appendFile(filePath, content, "utf-8");
165
+ } catch (error) {
166
+ logInternalError("live-session.transcript-write-failed", error as Error, `path=${filePath}`);
167
+ }
168
+ }
169
+
170
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
171
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
172
+ }
173
+
174
+ function isString(value: unknown): value is string {
175
+ return typeof value === "string";
176
+ }
177
+
178
+ /**
179
+ * Type-safe extractor for message role from streaming events.
180
+ * Handles the case where message may be a Record with a role field.
181
+ */
182
+ function extractMessageRole(obj: Record<string, unknown> | undefined): string | undefined {
183
+ const message = obj?.message ? asRecord(obj.message) : undefined;
184
+ return isString(message?.role) ? message.role : undefined;
185
+ }
186
+
187
+ /**
188
+ * Type-safe extractor for message usage from streaming events.
189
+ * Handles the case where message may be a Record with usage data.
190
+ */
191
+ function extractMessageUsage(obj: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
192
+ const message = obj?.message ? asRecord(obj.message) : undefined;
193
+ return message?.usage && typeof message.usage === "object" ? (message.usage as Record<string, unknown>) : undefined;
194
+ }
195
+
196
+ /**
197
+ * Type-safe extractor for tool name from streaming events.
198
+ * Handles various event structures: { tool: { name } }, { toolName }, { name }.
199
+ */
200
+ function extractToolName(obj: Record<string, unknown> | undefined): string | undefined {
201
+ if (!obj) return undefined;
202
+ const tool = asRecord(obj.tool);
203
+ if (isString(tool?.name)) return tool.name;
204
+ if (isString(obj.toolName)) return obj.toolName;
205
+ if (isString(obj.name)) return obj.name;
206
+ return undefined;
207
+ }
208
+
209
+ function textFromContent(content: unknown): string[] {
210
+ if (typeof content === "string") return [content];
211
+ if (!Array.isArray(content)) return [];
212
+ return content.flatMap((part) => {
213
+ const obj = asRecord(part);
214
+ if (!obj) return [];
215
+ if (obj.type === "text" && typeof obj.text === "string") return [obj.text];
216
+ if (typeof obj.content === "string") return [obj.content];
217
+ return [];
218
+ });
219
+ }
220
+
221
+ function eventText(event: unknown): string[] {
222
+ const obj = asRecord(event);
223
+ if (!obj) return [];
224
+ const text: string[] = [];
225
+ if (typeof obj.text === "string") text.push(obj.text);
226
+ text.push(...textFromContent(obj.content));
227
+ const message = asRecord(obj.message);
228
+ if (message) text.push(...textFromContent(message.content));
229
+ return text.filter((entry) => entry.trim());
230
+ }
231
+
232
+ function finalAssistantText(event: unknown): string[] {
233
+ const obj = asRecord(event);
234
+ if (obj?.type !== "message_end") return [];
235
+ const message = asRecord(obj.message);
236
+ if (message?.role !== "assistant") return [];
237
+ return textFromContent(message.content);
238
+ }
239
+
240
+ function numberField(obj: Record<string, unknown> | undefined, keys: string[]): number | undefined {
241
+ if (!obj) return undefined;
242
+ for (const key of keys) {
243
+ const value = obj[key];
244
+ if (typeof value === "number" && Number.isFinite(value)) return value;
245
+ }
246
+ return undefined;
247
+ }
248
+
249
+ /**
250
+ * F7: resolve the enabledModels allowlist for the current project, but only
251
+ * if the `runtime.reliability.scopeModels` toggle is ON. Returns an empty
252
+ * array when the toggle is off or no allowlist is configured — the routing
253
+ * gate treats empty patterns as "no enforcement" (no-op). Best-effort:
254
+ * any failure to read the toggle or the allowlist silently disables the gate
255
+ * rather than blocking spawn.
256
+ */
257
+ async function resolveScopeModelsPatterns(cwd: string, agentDir?: string): Promise<string[]> {
258
+ let scopeModels = false;
259
+ try {
260
+ scopeModels = loadConfig(cwd).config.reliability?.scopeModels === true;
261
+ } catch {
262
+ return [];
263
+ }
264
+ if (!scopeModels) return [];
265
+ return readEnabledModelsPatterns(cwd, agentDir);
266
+ }
267
+
268
+ function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined): unknown {
269
+ if (!modelId?.includes("/")) return undefined;
270
+ const registry = asRecord(modelRegistry);
271
+ const find = registry?.find;
272
+ if (typeof find !== "function") return undefined;
273
+ const [provider, ...modelParts] = modelId.split("/");
274
+ const id = modelParts.join("/");
275
+ try {
276
+ return find.call(modelRegistry, provider, id);
277
+ } catch {
278
+ return undefined;
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Round 18: when agent declares `model: false`, the inherited `parentModel`
284
+ * (= `ctx.model` from Pi runtime, set via `team-tool.ts:541/655`) is the
285
+ * session's SAVED model. That saved model can be stale (e.g. a previous
286
+ * session used claude-sonnet-4-5 and saved it as session.model; the new
287
+ * session actually runs on minimax-M3 displayed in the footer). If the
288
+ * saved model has no auth in `modelRegistry`, the worker fails immediately
289
+ * with "No API key found" before reaching any fallback candidate.
290
+ *
291
+ * This helper prefers the saved model when it is in the auth-available
292
+ * registry; otherwise falls back to the first auth-available registry
293
+ * model (e.g. minimax/MiniMax-M3, zai/glm-5.2); otherwise returns the
294
+ * raw `parentModel` unchanged so the caller surfaces E008.
295
+ */
296
+ export function resolveParentModelFromRegistry(modelRegistry: unknown, rawParentModel: unknown): string | undefined {
297
+ const raw = typeof rawParentModel === "string" ? rawParentModel.trim() : undefined;
298
+ if (raw) {
299
+ const candidate = raw.includes("/")
300
+ ? raw
301
+ : (() => {
302
+ const m = modelFromRegistry(modelRegistry, raw);
303
+ if (m && typeof m === "object" && "fullId" in m) {
304
+ return String((m as { fullId?: unknown }).fullId ?? raw);
305
+ }
306
+ return undefined;
307
+ })();
308
+ if (candidate && modelFromRegistry(modelRegistry, candidate)) return candidate;
309
+ }
310
+ const registry = modelRegistry as { getAvailable?: () => unknown[] } | undefined;
311
+ if (registry && typeof registry.getAvailable === "function") {
312
+ try {
313
+ const available = registry.getAvailable();
314
+ if (Array.isArray(available) && available.length > 0) {
315
+ const first = available[0] as { provider?: unknown; id?: unknown } | undefined;
316
+ if (first && typeof first.provider === "string" && typeof first.id === "string") {
317
+ return `${first.provider}/${first.id}`;
318
+ }
319
+ }
320
+ } catch {
321
+ // ignore — fall through to raw
322
+ }
323
+ }
324
+ return raw;
325
+ }
326
+
327
+ /** Communication intensity by role (caveman-inspired token optimization) */
328
+ const ROLE_INTENSITY: Record<string, "lite" | "full" | "ultra"> = {
329
+ explorer: "ultra",
330
+ analyst: "full",
331
+ planner: "full",
332
+ critic: "full",
333
+ executor: "full",
334
+ reviewer: "full",
335
+ "security-reviewer": "full",
336
+ "test-engineer": "full",
337
+ verifier: "full",
338
+ writer: "lite",
339
+ };
340
+
341
+ function buildCommunicationStyle(role: string): string {
342
+ const intensity = ROLE_INTENSITY[role] ?? "full";
343
+ if (intensity === "lite") return "## Communication\nProfessional concise. No filler/hedging. Full sentences OK.";
344
+ if (intensity === "ultra")
345
+ return [
346
+ "## Communication (ultra-compressed)",
347
+ "Drop: articles, filler, hedging, pleasantries. Fragments OK.",
348
+ "Pattern: [thing] [action] [reason].",
349
+ "Code/paths/symbols: exact, never abbreviated. Errors quoted exact.",
350
+ "Abbreviate prose words: DB/auth/config/req/res/fn/impl.",
351
+ "Arrows for causality: X → Y. One word when one word enough.",
352
+ "Security/destructive: write normal English. Resume compressed after.",
353
+ ].join("\n");
354
+ return [
355
+ "## Communication (compressed)",
356
+ "Drop: articles (a/an/the), filler (just/really/basically/actually/simply), hedging, pleasantries.",
357
+ "Short synonyms. Fragments OK. Pattern: [thing] [action] [reason]. [next step].",
358
+ "Code/paths/symbols: exact. Errors quoted exact.",
359
+ "Security/destructive: write normal English. Resume compressed after.",
360
+ ].join("\n");
361
+ }
362
+
363
+ function buildOutputContract(role: string): string {
364
+ if (role === "explorer")
365
+ return [
366
+ "## Output Contract",
367
+ "<path>:<line> — `<symbol>` — <≤6 word note>",
368
+ "Group: Defs: / Refs: / Callers: / Tests: / Sites:",
369
+ 'Zero hits → "No match."',
370
+ "Last line → totals: N defs, M refs.",
371
+ ].join("\n");
372
+ if (role === "executor")
373
+ return [
374
+ "## Output Contract",
375
+ "<path>:<line-range> — <change ≤10 words>.",
376
+ "verified: <re-read OK | mismatch @ path:line>.",
377
+ "Refusal tokens: too-big. / needs-confirm. / ambiguous. / regressed.",
378
+ ].join("\n");
379
+ if (role === "reviewer" || role === "security-reviewer")
380
+ return [
381
+ "## Output Contract",
382
+ "<path>:<line>: <emoji> <severity>: <problem>. <fix>.",
383
+ "Severity: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question.",
384
+ 'Zero findings → "No issues."',
385
+ "Sorted: file order → ascending line numbers.",
386
+ ].join("\n");
387
+ if (role === "verifier")
388
+ return [
389
+ "## Output Contract",
390
+ "PASS: <what verified> — <evidence ≤20 words>.",
391
+ "FAIL: <what failed> — <reason>. <expected vs actual>.",
392
+ "Evidence: file paths, test output, or diffs.",
393
+ ].join("\n");
394
+ if (role === "writer") return "## Output Contract\nWrite clear documentation. Full sentences. No compression.";
395
+ return ""; // planner, critic, analyst, test-engineer: no strict format
396
+ }
397
+
398
+ /**
399
+ * Phase 3 (caveman): Compress tool descriptions in a live session to reduce
400
+ * input token cost per tool call. MCP tools often have verbose descriptions
401
+ * (e.g. "This tool allows you to search for files in the filesystem..." → "Search files in filesystem.").
402
+ * Compresses only description text, never modifies tool names or parameters.
403
+ */
404
+ function compressSessionToolDescriptions(session: LiveSessionLike): void {
405
+ if (typeof session.getActiveToolNames !== "function") return;
406
+ // The Pi SDK doesn't expose a setDescription API, but we can attempt
407
+ // to compress via setActiveToolsByName if the session supports it.
408
+ // For now, this is a no-op that documents the intent for future SDK support.
409
+ // When Pi SDK adds tool description mutation, this function will compress.
410
+ // Side benefit: the import of compressToolDescription ensures the module
411
+ // is loaded and tree-shakeable, so adding the actual logic later is trivial.
412
+ }
413
+
414
+ export function liveSystemPrompt(input: LiveSessionSpawnInput): string {
415
+ // Agent MEMORY is intentionally omitted here — it is already injected via
416
+ // renderTaskPrompt().full (the user prompt, shared with the child-pi path).
417
+ // See the import-block note above and the G3 fix.
418
+ const role = input.task.role;
419
+ const styleBlock = buildCommunicationStyle(role);
420
+ const contractBlock = buildOutputContract(role);
421
+ const sensitiveConstraint = buildSensitivePathConstraint();
422
+ return [
423
+ "# pi-crew Live Subagent",
424
+ `Run ID: ${input.manifest.runId}`,
425
+ `Task ID: ${input.task.id}`,
426
+ `Role: ${role}`,
427
+ `Agent: ${input.agent.name}`,
428
+ `Working directory: ${input.task.cwd}`,
429
+ "",
430
+ styleBlock,
431
+ contractBlock,
432
+ sensitiveConstraint,
433
+ "",
434
+ input.agent.systemPrompt || "Follow the user task exactly and report verification evidence.",
435
+ ]
436
+ .filter(Boolean)
437
+ .join("\n");
438
+ }
439
+
440
+ function filterActiveTools(session: LiveSessionLike, agent: AgentConfig, role?: string): void {
441
+ if (typeof session.getActiveToolNames !== "function" || typeof session.setActiveToolsByName !== "function") return;
442
+ const recursiveTools = new Set(["team", "Team", "Agent", "get_subagent_result", "steer_subagent"]);
443
+ // F1 unify (v0.8.0): use the shared resolveToolPolicy so this path agrees
444
+ // with child-pi (pi-args.ts). Before this, live-session used frontmatter
445
+ // only and ignored role-config entirely — so a builtin explorer on the
446
+ // live-session path wasn't bound by the role's read-only security constraint.
447
+ // Now allowlist precedence is source-aware and the denylist is additive.
448
+ const policy = resolveToolPolicy(agent, role);
449
+ const disallowed = policy.excludeTools?.length ? new Set(policy.excludeTools) : undefined;
450
+ const allowed = policy.tools?.length ? new Set(policy.tools) : undefined;
451
+ const active = session
452
+ .getActiveToolNames()
453
+ .filter((name) => !recursiveTools.has(name) && !disallowed?.has(name) && (!allowed || allowed.has(name)));
454
+ session.setActiveToolsByName(active);
455
+ }
456
+
457
+ function usageFromStats(stats: unknown): UsageState | undefined {
458
+ const obj = asRecord(stats);
459
+ if (!obj) return undefined;
460
+ const input = numberField(obj, ["input", "inputTokens", "input_tokens"]);
461
+ const output = numberField(obj, ["output", "outputTokens", "output_tokens"]);
462
+ const cacheRead = numberField(obj, ["cacheRead", "cache_read"]);
463
+ const cacheWrite = numberField(obj, ["cacheWrite", "cache_write"]);
464
+ const cost = numberField(obj, ["cost"]);
465
+ const turns = numberField(obj, ["turns", "turnCount", "turn_count"]);
466
+ return [input, output, cacheRead, cacheWrite, cost, turns].some((value) => value !== undefined)
467
+ ? { input, output, cacheRead, cacheWrite, cost, turns }
468
+ : undefined;
469
+ }
470
+
471
+ async function promptWithTimeout(session: LiveSessionLike, text: string, timeoutMs: number, label: string): Promise<boolean> {
472
+ // CORE-11: pass an AbortSignal into session.prompt so the underlying prompt is
473
+ // genuinely cancelled on timeout instead of being abandoned by Promise.race.
474
+ const ac = new AbortController();
475
+ const promptPromise = session.prompt?.(text, {
476
+ source: "api",
477
+ expandPromptTemplates: false,
478
+ signal: ac.signal,
479
+ });
480
+ if (!promptPromise) return false;
481
+ let timer: ReturnType<typeof setTimeout> | undefined;
482
+ try {
483
+ await Promise.race([
484
+ promptPromise,
485
+ new Promise<void>((_, reject) => {
486
+ timer = setTimeout(() => {
487
+ ac.abort(); // cancel the underlying prompt
488
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
489
+ }, timeoutMs);
490
+ timer.unref?.();
491
+ }),
492
+ ]);
493
+ return true;
494
+ } finally {
495
+ if (timer) clearTimeout(timer);
496
+ }
497
+ }
498
+
499
+ export async function probeLiveSessionRuntime(): Promise<LiveSessionUnavailableResult | LiveSessionPlannedResult> {
500
+ const availability = await isLiveSessionRuntimeAvailable();
501
+ if (!availability.available)
502
+ return {
503
+ available: false,
504
+ reason: availability.reason ?? "Live-session runtime is unavailable.",
505
+ };
506
+ return {
507
+ available: true,
508
+ reason: "Live-session SDK exports are available. pi-crew can run in-process live agents when runtime.mode=live-session.",
509
+ };
510
+ }
511
+
512
+ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<LiveSessionRunResult> {
513
+ // Cold-start race fix: ensure the hot module graph is warm before touching
514
+ // any module. Under tsx, concurrent first-imports race module-record
515
+ // instantiation; awaiting the registration-time warmup eliminates the window.
516
+ await awaitRuntimeWarmup();
517
+ const isCurrent = input.isCurrent ?? (() => true);
518
+ let streamOut: StreamingOutputHandle | undefined;
519
+
520
+ // G1: Capture yield result from custom tool callback
521
+ let customToolYieldResult: YieldResult | undefined;
522
+ let customToolYieldResolved = false;
523
+ if (process.env.PI_CREW_MOCK_LIVE_SESSION === "success") {
524
+ const agentId = `${input.manifest.runId}:${input.task.id}`;
525
+ const inherited =
526
+ input.runtimeConfig?.inheritContext === true && input.parentContext ? ` with inherited context: ${input.parentContext}` : "";
527
+ const event = {
528
+ type: "message_end",
529
+ message: {
530
+ role: "assistant",
531
+ content: [
532
+ {
533
+ type: "text",
534
+ text: `Mock live-session success for ${input.agent.name}${inherited}`,
535
+ },
536
+ ],
537
+ },
538
+ };
539
+ const mockSession = {
540
+ steer: async () => {},
541
+ prompt: async () => {},
542
+ abort: async () => {},
543
+ };
544
+ registerLiveAgent(
545
+ {
546
+ agentId,
547
+ runId: input.manifest.runId,
548
+ taskId: input.task.id,
549
+ role: input.task.role,
550
+ agent: input.agent?.name ?? "mock",
551
+ description: "mock",
552
+ session: mockSession,
553
+ status: "running",
554
+ workspaceId: input.workspaceId,
555
+ },
556
+ appendEvent,
557
+ input.manifest.eventsPath,
558
+ );
559
+ appendTranscript(input.transcriptPath, event);
560
+ const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
561
+ writeSidechainEntry(sidechainPath, {
562
+ agentId,
563
+ type: "user",
564
+ message: { role: "user", content: input.prompt },
565
+ cwd: input.task.cwd,
566
+ });
567
+ writeSidechainEntry(sidechainPath, {
568
+ agentId,
569
+ type: "message",
570
+ message: event,
571
+ cwd: input.task.cwd,
572
+ });
573
+ if (isCurrent()) input.onEvent?.(event);
574
+ const stdout = `Mock live-session success for ${input.agent.name}${inherited}`;
575
+ if (isCurrent()) input.onOutput?.(stdout);
576
+ updateLiveAgentStatus(agentId, "completed");
577
+ markLiveAgentCompleted(agentId);
578
+ return {
579
+ available: true,
580
+ exitCode: 0,
581
+ stdout,
582
+ stderr: "",
583
+ jsonEvents: 1,
584
+ };
585
+ }
586
+ const availability = await isLiveSessionRuntimeAvailable();
587
+ if (!availability.available)
588
+ return {
589
+ available: true,
590
+ exitCode: 1,
591
+ stdout: "",
592
+ stderr: availability.reason ?? "Live-session runtime unavailable.",
593
+ jsonEvents: 0,
594
+ error: availability.reason,
595
+ };
596
+ // LAZY: optional peer dependency — only loaded when live-session runtime is
597
+ // chosen. Goes through the module-scoped latch (loadLiveSessionModule) so
598
+ // concurrent first-imports share ONE in-flight promise instead of racing
599
+ // module-record instantiation under the tsx loader.
600
+ const mod = await loadLiveSessionModule();
601
+ if (typeof mod.createAgentSession !== "function")
602
+ return {
603
+ available: true,
604
+ exitCode: 1,
605
+ stdout: "",
606
+ stderr: "createAgentSession export is unavailable.",
607
+ jsonEvents: 0,
608
+ error: "createAgentSession export is unavailable.",
609
+ };
610
+ // OPT-03: compute yield-enabled flag early to conditionally allocate
611
+ // collectedJsonEvents — avoids ~1KB array allocation per task when yield
612
+ // detection is disabled. Pattern mirrors task-runner.ts:165. Uses the same
613
+ // `enabled !== false` semantics as the late yield-detection block below so
614
+ // the array is always allocated when (and only when) it will be read.
615
+ const yieldConfig = input.runtimeConfig?.yield ?? { enabled: DEFAULT_YIELD_CONFIG.enabled };
616
+ const yieldEnabled = yieldConfig.enabled !== false;
617
+ let session: LiveSessionLike | undefined;
618
+ let unsubscribe: (() => void) | undefined;
619
+ let unsubscribeControlRealtime: (() => void) | undefined;
620
+ let controlTimer: ReturnType<typeof setInterval> | undefined;
621
+ let stdout = "";
622
+ let jsonEvents = 0;
623
+ const collectedJsonEvents: Record<string, unknown>[] | undefined = yieldEnabled ? [] : undefined;
624
+ const maxCollectedJsonEvents = 1000;
625
+ let yieldResult: YieldResult | undefined;
626
+
627
+ const agentId = `${input.manifest.runId}:${input.task.id}`;
628
+
629
+ // Round 27 (BUG 4): hoisted to function scope so the finally block can remove
630
+ // it. const inside try{} is block-scoped and invisible to finally{}. The
631
+ // handler resolves `session` lazily at call time (it may be assigned later
632
+ // inside the try), so declaring it here is safe.
633
+ let onSignalAbort: (() => void) | undefined;
634
+
635
+ try {
636
+ const agentDir = typeof mod.getAgentDir === "function" ? mod.getAgentDir() : undefined;
637
+ let resourceLoader: unknown;
638
+ // F1 (v0.7.9) NOTE: `agent.excludeExtensions` is applied on the
639
+ // child-pi path (see `pi-args.ts`). The live-session path loads
640
+ // extensions via pi's `DefaultResourceLoader`, which has no explicit
641
+ // per-extension allow/deny API at the point we hand off. For
642
+ // v0.7.9, the denylist is honored on the default async path only;
643
+ // the live-session path (opt-in via `runtime.preferLiveSession`)
644
+ // ignores it. This is a documented limitation, not a silent bug.
645
+ if (mod.DefaultResourceLoader && agentDir) {
646
+ resourceLoader = new mod.DefaultResourceLoader({
647
+ cwd: input.task.cwd,
648
+ agentDir,
649
+ noPromptTemplates: true,
650
+ noThemes: true,
651
+ noContextFiles: input.runtimeConfig?.inheritContext !== true,
652
+ systemPromptOverride: () => liveSystemPrompt(input),
653
+ appendSystemPromptOverride: () => [],
654
+ });
655
+ await (resourceLoader as { reload?: () => Promise<void> }).reload?.();
656
+ }
657
+ const effectiveParentModel = resolveParentModelFromRegistry(input.modelRegistry, input.parentModel);
658
+ const modelRouting = buildConfiguredModelRouting({
659
+ overrideModel: input.modelOverride,
660
+ stepModel: input.step.model,
661
+ teamRoleModel: input.teamRoleModel,
662
+ agentModel: input.agent.model,
663
+ fallbackModels: input.agent.fallbackModels,
664
+ parentModel: effectiveParentModel,
665
+ modelRegistry: input.modelRegistry,
666
+ cwd: input.manifest.cwd,
667
+ scopeModelsPatterns: await resolveScopeModelsPatterns(input.manifest.cwd),
668
+ });
669
+ const resolvedModel =
670
+ modelFromRegistry(input.modelRegistry, modelRouting.candidates[0] ?? modelRouting.requested) ?? input.parentModel;
671
+ // Phase 4: MCP proxy — will be determined after session creation
672
+ // (we check parent's MCP tools and share connections when available)
673
+ const mcpProxy = buildMcpProxyFromSession([], { shareMcp: true });
674
+
675
+ // G1: Build custom tools (submit_result + irc)
676
+ const submitResultTool = createSubmitResultTool((result) => {
677
+ customToolYieldResult = result;
678
+ customToolYieldResolved = true;
679
+ });
680
+ const ircTool = createIrcTool(agentId);
681
+ const customTools = [submitResultTool, ircTool];
682
+
683
+ const sessionCreateStart = Date.now();
684
+ const created = await mod.createAgentSession({
685
+ cwd: input.task.cwd,
686
+ ...(agentDir ? { agentDir } : {}),
687
+ ...(resourceLoader ? { resourceLoader } : {}),
688
+ ...(mod.SessionManager?.inMemory
689
+ ? {
690
+ sessionManager: mod.SessionManager.inMemory(input.task.cwd),
691
+ }
692
+ : {}),
693
+ ...(mod.SettingsManager?.create && agentDir
694
+ ? {
695
+ settingsManager: mod.SettingsManager.create(input.task.cwd, agentDir),
696
+ }
697
+ : {}),
698
+ ...(input.modelRegistry ? { modelRegistry: input.modelRegistry } : {}),
699
+ ...(resolvedModel ? { model: resolvedModel } : {}),
700
+ ...(input.agent.thinking ? { thinkingLevel: input.agent.thinking } : {}),
701
+ ...(mcpProxy.enableMcp ? {} : { enableMCP: false }),
702
+ customTools,
703
+ });
704
+ session = created.session;
705
+ // P7 (perf): fire-and-forget — return value not needed, blocks the
706
+ // event loop less than the sync appendEvent under file-lock contention.
707
+ appendEventFireAndForget(input.manifest.eventsPath, {
708
+ type: "live-session.session_created",
709
+ runId: input.manifest.runId,
710
+ taskId: input.task.id,
711
+ data: {
712
+ elapsedMs: Date.now() - sessionCreateStart,
713
+ modelFallbackMessage: created.modelFallbackMessage,
714
+ },
715
+ });
716
+ filterActiveTools(session, input.agent, input.task.role);
717
+
718
+ // Diagnostic: log before bindExtensions so we can identify extension-loading hangs
719
+ const bindExtensionsStart = Date.now();
720
+ try {
721
+ await Promise.race([
722
+ session.bindExtensions?.({}) ?? Promise.resolve(),
723
+ new Promise<void>((_, reject) => setTimeout(() => reject(new Error("bindExtensions timed out after 30s")), 30_000)),
724
+ ]);
725
+ } catch (bindError) {
726
+ const msg = bindError instanceof Error ? bindError.message : String(bindError);
727
+ // P7: fire-and-forget — return value not needed.
728
+ appendEventFireAndForget(input.manifest.eventsPath, {
729
+ type: "live-session.bind_extensions_error",
730
+ runId: input.manifest.runId,
731
+ taskId: input.task.id,
732
+ data: {
733
+ elapsedMs: Date.now() - bindExtensionsStart,
734
+ error: msg,
735
+ },
736
+ });
737
+ // Continue without extensions — they should not block the session
738
+ }
739
+
740
+ // Phase 3 (caveman): Compress tool descriptions to reduce input token cost
741
+ compressSessionToolDescriptions(session);
742
+
743
+ // Phase 5: Initialize extension runner bridge if available
744
+ // The bridge provides extension-like APIs (sendMessage, setActiveTools, etc.)
745
+ // to the extension runner if the session exposes one.
746
+ const extensionBridge = buildExtensionBridge(session as never);
747
+ if (extensionBridge) {
748
+ const extRunner = (session as Record<string, unknown>).extensionRunner;
749
+ if (extRunner && typeof (extRunner as Record<string, unknown>).initialize === "function") {
750
+ try {
751
+ (
752
+ extRunner as {
753
+ initialize: (apis: unknown, host: unknown) => void;
754
+ }
755
+ ).initialize(extensionBridge.apis, extensionBridge.host);
756
+ if (typeof (extRunner as Record<string, unknown>).emit === "function") {
757
+ await (
758
+ extRunner as {
759
+ emit: (event: unknown) => Promise<void>;
760
+ }
761
+ ).emit({ type: "session_start" });
762
+ }
763
+ } catch {
764
+ // Extension runner initialization failure should not block the session
765
+ }
766
+ }
767
+ }
768
+
769
+ registerLiveAgent(
770
+ {
771
+ agentId,
772
+ runId: input.manifest.runId,
773
+ taskId: input.task.id,
774
+ role: input.task.role,
775
+ agent: input.agent?.name ?? "unknown",
776
+ description: input.task.adaptive?.task ?? input.step?.task ?? "",
777
+ modelName: (resolvedModel as { name?: string })?.name,
778
+ session,
779
+ status: "running",
780
+ workspaceId: input.workspaceId,
781
+ },
782
+ appendEvent,
783
+ input.manifest.eventsPath,
784
+ );
785
+ streamOut = createStreamingOutput(input.manifest, input.task.id);
786
+ let controlCursor: LiveAgentControlCursor = { offset: 0 };
787
+ const seenControlRequestIds = new Set<string>();
788
+ let controlBusy = false;
789
+ const pollControl = async () => {
790
+ if (!isCurrent() || controlBusy || !session) return;
791
+ controlBusy = true;
792
+ try {
793
+ controlCursor = await applyLiveAgentControlRequests({
794
+ manifest: input.manifest,
795
+ taskId: input.task.id,
796
+ agentId,
797
+ session,
798
+ cursor: controlCursor,
799
+ seenRequestIds: seenControlRequestIds,
800
+ });
801
+ } finally {
802
+ controlBusy = false;
803
+ }
804
+ };
805
+ unsubscribeControlRealtime = subscribeLiveControlRealtime((request) => {
806
+ if (!isCurrent() || request.runId !== input.manifest.runId || request.taskId !== input.task.id || !session) return;
807
+ void applyLiveAgentControlRequest({
808
+ request,
809
+ taskId: input.task.id,
810
+ agentId,
811
+ session,
812
+ seenRequestIds: seenControlRequestIds,
813
+ });
814
+ });
815
+ await pollControl();
816
+ controlTimer = setInterval(() => {
817
+ if (isCurrent()) void pollControl();
818
+ }, 500);
819
+ let turnCount = 0;
820
+ let softLimitReached = false;
821
+ const maxTurns = input.runtimeConfig?.maxTurns;
822
+ const graceTurns = input.runtimeConfig?.graceTurns ?? 5;
823
+ const sidechainPath = sidechainOutputPath(input.manifest.stateRoot, input.task.id);
824
+ writeSidechainEntry(sidechainPath, {
825
+ agentId,
826
+ type: "user",
827
+ message: { role: "user", content: input.prompt },
828
+ cwd: input.task.cwd,
829
+ });
830
+ if (typeof session.subscribe === "function") {
831
+ unsubscribe = session.subscribe((event) => {
832
+ // Wrap callback body in try/catch to prevent unguarded exceptions from
833
+ // propagating into the session internals and crashing the live session.
834
+ try {
835
+ if (!isCurrent()) return;
836
+ jsonEvents += 1;
837
+ appendTranscript(input.transcriptPath, event);
838
+ const sidechainType = eventToSidechainType(event);
839
+ if (sidechainType)
840
+ writeSidechainEntry(sidechainPath, {
841
+ agentId,
842
+ type: sidechainType,
843
+ message: event,
844
+ cwd: input.task.cwd,
845
+ });
846
+ const obj = asRecord(event);
847
+ if (obj?.type === "turn_end") {
848
+ turnCount += 1;
849
+ trackLiveAgentTurnEnd(agentId);
850
+ if (maxTurns !== undefined && !softLimitReached && turnCount >= maxTurns) {
851
+ softLimitReached = true;
852
+ void session?.steer?.("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
853
+ } else if (maxTurns !== undefined && softLimitReached && turnCount >= maxTurns + graceTurns) {
854
+ void session?.abort?.();
855
+ }
856
+ }
857
+ // Accumulate lifetime usage that survives compaction
858
+ if (obj?.type === "message_end" && extractMessageRole(obj) === "assistant") {
859
+ const u = extractMessageUsage(obj);
860
+ if (u) {
861
+ trackTaskUsage(input.task.id, {
862
+ input: typeof u.input === "number" ? u.input : 0,
863
+ output: typeof u.output === "number" ? u.output : 0,
864
+ cacheWrite: typeof u.cacheWrite === "number" ? u.cacheWrite : 0,
865
+ });
866
+ }
867
+ }
868
+ input.onEvent?.(event);
869
+ const text = [...eventText(event), ...finalAssistantText(event)].join("\n");
870
+ if (text.trim()) {
871
+ stdout += `${text}\n`;
872
+ streamOut?.write(text + "\n");
873
+ trackLiveAgentResponseText(agentId, text);
874
+ input.onOutput?.(text);
875
+ }
876
+ // G2: Track tool start/end for activity display
877
+ if (obj?.type === "tool_use" || obj?.type === "tool_execution_start") {
878
+ const toolName = extractToolName(obj) ?? "unknown";
879
+ trackLiveAgentToolStart(agentId, toolName);
880
+ }
881
+ if (obj?.type === "tool_result" || obj?.type === "tool_execution_end") {
882
+ const toolName = extractToolName(obj) ?? "unknown";
883
+ trackLiveAgentToolEnd(agentId, toolName);
884
+ }
885
+ // Phase 1: collect events for yield detection
886
+ if (collectedJsonEvents && event && typeof event === "object" && !Array.isArray(event)) {
887
+ collectedJsonEvents.push(event as Record<string, unknown>);
888
+ if (collectedJsonEvents.length > maxCollectedJsonEvents)
889
+ collectedJsonEvents.splice(0, collectedJsonEvents.length - maxCollectedJsonEvents);
890
+ }
891
+ } catch (error) {
892
+ // Prevent unguarded exceptions in the subscribe callback from crashing
893
+ // the live session or propagating into the session internals.
894
+ logInternalError("live-session.subscribe-callback", error, `agentId=${agentId}`);
895
+ }
896
+ });
897
+ }
898
+ // Round 27 (BUG 4): named abort handler (removed in finally below).
899
+ onSignalAbort = (): void => {
900
+ void session?.abort?.();
901
+ };
902
+ if (input.signal) {
903
+ if (input.signal.aborted) await session.abort?.();
904
+ // Round 27 (BUG 4): named handler so the finally block can remove it.
905
+ // The previous anonymous listener leaked on normal completion (only
906
+ // auto-removed by { once: true } AFTER the signal fires).
907
+ else
908
+ input.signal.addEventListener("abort", onSignalAbort, {
909
+ once: true,
910
+ });
911
+ }
912
+ const effectivePrompt =
913
+ input.runtimeConfig?.inheritContext === true && input.parentContext
914
+ ? `${input.parentContext}\n\n---\n# Live Subagent Task\n${input.prompt}`
915
+ : input.prompt;
916
+
917
+ // Diagnostic: log prompt size and timing
918
+ const promptStart = Date.now();
919
+ // P7: fire-and-forget — return value not needed.
920
+ appendEventFireAndForget(input.manifest.eventsPath, {
921
+ type: "live-session.prompt_start",
922
+ runId: input.manifest.runId,
923
+ taskId: input.task.id,
924
+ data: {
925
+ promptLength: effectivePrompt.length,
926
+ agent: input.agent.name,
927
+ role: input.task.role,
928
+ },
929
+ });
930
+
931
+ // Phase 3: Wrap session.prompt with timeout for graceful cancellation
932
+ const sessionTimeoutMs = DEFAULT_LIVE_SESSION.responseTimeoutMs;
933
+ try {
934
+ await promptWithTimeout(session, effectivePrompt, sessionTimeoutMs, "Live-session");
935
+ } catch (promptError) {
936
+ const msg = promptError instanceof Error ? promptError.message : String(promptError);
937
+ // P7: fire-and-forget — return value not needed.
938
+ appendEventFireAndForget(input.manifest.eventsPath, {
939
+ type: "live-session.prompt_error",
940
+ runId: input.manifest.runId,
941
+ taskId: input.task.id,
942
+ data: { elapsedMs: Date.now() - promptStart, error: msg },
943
+ });
944
+ if (msg.includes("timed out")) {
945
+ await session.abort?.();
946
+ updateLiveAgentStatus(agentId, "failed");
947
+ return {
948
+ available: true,
949
+ exitCode: 1,
950
+ stdout: stdout.trim(),
951
+ stderr: msg,
952
+ jsonEvents,
953
+ error: msg,
954
+ };
955
+ }
956
+ throw promptError;
957
+ }
958
+ // P7: fire-and-forget — return value not needed.
959
+ appendEventFireAndForget(input.manifest.eventsPath, {
960
+ type: "live-session.prompt_done",
961
+ runId: input.manifest.runId,
962
+ taskId: input.task.id,
963
+ data: {
964
+ elapsedMs: Date.now() - promptStart,
965
+ jsonEvents,
966
+ outputLength: stdout.length,
967
+ },
968
+ });
969
+
970
+ // --- Phase 1: Yield enforcement loop ---
971
+ // After the initial prompt completes, check if the worker called submit_result.
972
+ // Priority: 1) custom tool callback (G1), 2) JSON event detection (legacy).
973
+ if (yieldEnabled && session) {
974
+ // Check custom tool callback first (G1)
975
+ if (customToolYieldResolved && customToolYieldResult) {
976
+ yieldResult = customToolYieldResult;
977
+ } else if (collectedJsonEvents) {
978
+ // Legacy: detect from JSON events (only when collection is enabled)
979
+ const alreadyYielded = hasYieldInOutput(collectedJsonEvents);
980
+ if (alreadyYielded) {
981
+ const yieldEvent = collectedJsonEvents.find((e) => isYieldEvent(e));
982
+ if (yieldEvent) yieldResult = extractYieldResult(yieldEvent);
983
+ }
984
+ }
985
+ // Phase 2: Validate yield data against output schema if provided
986
+ let schemaFailures = 0;
987
+ const maxSchemaFailures = 2;
988
+ if (yieldResult && input.outputSchema) {
989
+ const validation = await validateYieldData(yieldResult.structuredData, input.outputSchema);
990
+ if (!validation.valid) {
991
+ schemaFailures++;
992
+ yieldResult = undefined;
993
+ customToolYieldResolved = false;
994
+ const schemaReminder = `Your submit_result data did not match the required schema: ${validation.error}. Please fix and call submit_result again with valid data.`;
995
+ try {
996
+ await promptWithTimeout(
997
+ session,
998
+ schemaReminder,
999
+ Math.min(sessionTimeoutMs, DEFAULT_LIVE_SESSION.idleWaitTimeoutMs),
1000
+ "Live-session schema reminder",
1001
+ );
1002
+ } catch {
1003
+ /* ignore */
1004
+ }
1005
+ await new Promise((resolve) => setTimeout(resolve, DEFAULT_LIVE_SESSION.yieldPollIntervalMs));
1006
+ // Check again after schema reminder
1007
+ if (customToolYieldResolved && customToolYieldResult) {
1008
+ yieldResult = customToolYieldResult;
1009
+ } else if (collectedJsonEvents) {
1010
+ const newEvents = collectedJsonEvents.slice(-10);
1011
+ if (hasYieldInOutput(newEvents)) {
1012
+ const yieldEvent = newEvents.find((e) => isYieldEvent(e));
1013
+ if (yieldEvent) {
1014
+ const candidate = extractYieldResult(yieldEvent);
1015
+ if (candidate && input.outputSchema) {
1016
+ const revalidation = await validateYieldData(candidate.structuredData, input.outputSchema);
1017
+ if (revalidation.valid || schemaFailures >= maxSchemaFailures) {
1018
+ yieldResult = candidate;
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+ // Reminder loop — only if yield not yet received
1027
+ const maxReminders = yieldConfig.maxReminders ?? DEFAULT_LIVE_SESSION.maxYieldRetries;
1028
+ let retryCount = 0;
1029
+ while (!customToolYieldResolved && !yieldResult && retryCount < maxReminders && !input.signal?.aborted) {
1030
+ retryCount++;
1031
+ const reminder = buildYieldReminder(retryCount, maxReminders, yieldConfig.reminderPrompt);
1032
+ const prevTools = typeof session.getActiveToolNames === "function" ? session.getActiveToolNames() : [];
1033
+ try {
1034
+ // G6: Constrain tool set to submit_result before sending reminder
1035
+ if (typeof session.setActiveToolsByName === "function" && prevTools.length > 0) {
1036
+ session.setActiveToolsByName(["submit_result"]);
1037
+ }
1038
+ await promptWithTimeout(
1039
+ session,
1040
+ reminder,
1041
+ Math.min(sessionTimeoutMs, DEFAULT_LIVE_SESSION.idleWaitTimeoutMs),
1042
+ "Live-session yield reminder",
1043
+ );
1044
+ } catch {
1045
+ break;
1046
+ } finally {
1047
+ // Restore previous tools even if reminder prompt times out/throws.
1048
+ if (typeof session.setActiveToolsByName === "function" && prevTools.length > 0) {
1049
+ session.setActiveToolsByName(prevTools);
1050
+ }
1051
+ }
1052
+ const pollInterval = DEFAULT_LIVE_SESSION.yieldPollIntervalMs;
1053
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
1054
+ // Check custom tool callback
1055
+ if (customToolYieldResolved && customToolYieldResult) {
1056
+ yieldResult = customToolYieldResult;
1057
+ break;
1058
+ }
1059
+ // Legacy: check JSON events
1060
+ if (collectedJsonEvents && hasYieldInOutput(collectedJsonEvents.slice(-10))) {
1061
+ const yieldEvent = collectedJsonEvents.slice(-10).find((e) => isYieldEvent(e));
1062
+ if (yieldEvent) yieldResult = extractYieldResult(yieldEvent);
1063
+ break;
1064
+ }
1065
+ }
1066
+ if (!customToolYieldResolved && !yieldResult && !input.signal?.aborted && retryCount >= maxReminders) {
1067
+ input.onEvent?.({
1068
+ type: "task.attention",
1069
+ runId: input.manifest.runId,
1070
+ taskId: input.task.id,
1071
+ message: "Live-session worker completed without calling submit_result tool.",
1072
+ data: {
1073
+ activityState: "needs_attention",
1074
+ reason: "no_yield",
1075
+ attempts: retryCount,
1076
+ },
1077
+ });
1078
+ }
1079
+ }
1080
+
1081
+ const usage = usageFromStats(typeof session.getStats === "function" ? session.getStats() : session.stats);
1082
+ updateLiveAgentStatus(agentId, "completed");
1083
+ markLiveAgentCompleted(agentId);
1084
+ return {
1085
+ available: true,
1086
+ exitCode: 0,
1087
+ stdout: stdout.trim(),
1088
+ stderr: created.modelFallbackMessage ?? "",
1089
+ jsonEvents,
1090
+ usage,
1091
+ yieldResult,
1092
+ };
1093
+ } catch (error) {
1094
+ const message = error instanceof Error ? error.message : String(error);
1095
+
1096
+ // Phase 8: Log diagnostics on failure
1097
+ try {
1098
+ const agents = listLiveAgents();
1099
+ const health = collectLiveSessionHealth(agents, () => undefined);
1100
+ const diagnostics = formatLiveSessionDiagnostics(health);
1101
+ input.onEvent?.({
1102
+ type: "live-session.diagnostics",
1103
+ data: diagnostics,
1104
+ });
1105
+ } catch (diagError) {
1106
+ logInternalError("live-session.diagnostics", diagError);
1107
+ }
1108
+
1109
+ updateLiveAgentStatus(`${input.manifest.runId}:${input.task.id}`, "failed");
1110
+ return {
1111
+ available: true,
1112
+ exitCode: 1,
1113
+ stdout: stdout.trim(),
1114
+ stderr: message,
1115
+ jsonEvents,
1116
+ error: message,
1117
+ };
1118
+ } finally {
1119
+ // H6: Unsubscribe listeners FIRST before clearing timer to prevent race
1120
+ unsubscribe?.();
1121
+ unsubscribeControlRealtime?.();
1122
+ // Round 27 (BUG 4): remove the named abort listener to avoid leaking it
1123
+ // on the shared AbortSignal across many live-session tasks.
1124
+ if (onSignalAbort) input.signal?.removeEventListener("abort", onSignalAbort);
1125
+ if (controlTimer) clearInterval(controlTimer);
1126
+ streamOut?.close();
1127
+ if (input.signal?.aborted) {
1128
+ await terminateLiveAgent(agentId, "cancelled", appendEvent, input.manifest.eventsPath);
1129
+ } else {
1130
+ // Dispose the session to free resources, but keep the handle in the registry
1131
+ // for resume/follow-up. Removing the handle entirely breaks steer/followUp/resume.
1132
+ disposeLiveAgentSession(agentId);
1133
+ }
1134
+
1135
+ // Phase 8: Emit final health snapshot
1136
+ try {
1137
+ const agents = listLiveAgents();
1138
+ if (agents.length > 0) {
1139
+ const health = collectLiveSessionHealth(agents, () => undefined);
1140
+ input.onEvent?.({ type: "live-session.health", data: health });
1141
+ }
1142
+ } catch (healthError) {
1143
+ logInternalError("live-session.health-snapshot", healthError);
1144
+ }
1145
+ }
1146
+ }