pi-crew 0.9.16 → 0.9.18

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 (435) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +19 -0
  3. package/dist/build-meta.json +21784 -0
  4. package/dist/index.mjs +83169 -0
  5. package/dist/index.mjs.map +7 -0
  6. package/docs/A +358 -0
  7. package/docs/M-A +357 -0
  8. package/docs/REVIEW-FINDINGS-2026-06 +357 -0
  9. package/docs/Y +357 -0
  10. package/docs/a +357 -0
  11. package/docs/aA +358 -0
  12. package/docs/archive/README.md +91 -0
  13. package/docs/migration/atomic-write-v2-migration.md +297 -0
  14. package/docs/patterns/command-agent-skill.md +1 -1
  15. package/docs/perf/performance-review-2026-07.md +287 -0
  16. package/docs/skills/REFERENCE.md +0 -17
  17. package/docs//303/242mmaAAA/303/242 +357 -0
  18. package/index.bundle.ts +25 -0
  19. package/index.ts +95 -4
  20. package/package.json +15 -4
  21. package/skills/iterative-audit/SKILL.md +0 -1
  22. package/skills/widget-rendering/SKILL.md +17 -0
  23. package/src/adapters/claude-adapter.ts +1 -3
  24. package/src/adapters/export-util.ts +16 -16
  25. package/src/adapters/index.ts +5 -5
  26. package/src/agents/agent-config.ts +20 -14
  27. package/src/agents/agent-serializer.ts +1 -1
  28. package/src/agents/discover-agents.ts +83 -39
  29. package/src/benchmark/benchmark-runner.ts +51 -51
  30. package/src/benchmark/feedback-loop.ts +1 -1
  31. package/src/config/config.ts +303 -591
  32. package/src/config/defaults.ts +28 -2
  33. package/src/config/drift-detector.ts +11 -14
  34. package/src/config/markers.ts +201 -203
  35. package/src/config/resilient-parser.ts +14 -6
  36. package/src/config/role-tools.ts +3 -3
  37. package/src/config/suggestions.ts +5 -12
  38. package/src/config/types.ts +9 -25
  39. package/src/errors.ts +151 -157
  40. package/src/extension/action-suggestions.ts +54 -9
  41. package/src/extension/async-notifier.ts +43 -13
  42. package/src/extension/autonomous-policy.ts +77 -37
  43. package/src/extension/command-completions.ts +9 -8
  44. package/src/extension/context-status-injection.ts +14 -12
  45. package/src/extension/crew-autocomplete.ts +8 -16
  46. package/src/extension/crew-cleanup.ts +2 -1
  47. package/src/extension/crew-shortcuts.ts +9 -6
  48. package/src/extension/cross-extension-rpc.ts +136 -42
  49. package/src/extension/help.ts +1 -1
  50. package/src/extension/import-index.ts +10 -5
  51. package/src/extension/knowledge-injection.ts +88 -15
  52. package/src/extension/management.ts +89 -349
  53. package/src/extension/message-renderers.ts +3 -4
  54. package/src/extension/notification-router.ts +22 -5
  55. package/src/extension/notification-sink.ts +6 -3
  56. package/src/extension/pi-api.ts +17 -8
  57. package/src/extension/plan-orchestrate.ts +8 -27
  58. package/src/extension/project-init.ts +21 -6
  59. package/src/extension/register.ts +329 -975
  60. package/src/extension/registration/artifact-cleanup.ts +9 -4
  61. package/src/extension/registration/command-utils.ts +5 -1
  62. package/src/extension/registration/commands.ts +806 -314
  63. package/src/extension/registration/compaction-guard.ts +15 -15
  64. package/src/extension/registration/lifecycle.ts +259 -0
  65. package/src/extension/registration/observability.ts +293 -0
  66. package/src/extension/registration/subagent-helpers.ts +29 -7
  67. package/src/extension/registration/subagent-tools.ts +253 -44
  68. package/src/extension/registration/team-tool.ts +19 -84
  69. package/src/extension/registration/ui.ts +168 -0
  70. package/src/extension/registration/viewers.ts +64 -24
  71. package/src/extension/result-watcher.ts +18 -7
  72. package/src/extension/run-bundle-schema.ts +21 -6
  73. package/src/extension/run-export.ts +15 -6
  74. package/src/extension/run-import.ts +64 -29
  75. package/src/extension/run-index.ts +28 -9
  76. package/src/extension/run-maintenance.ts +32 -12
  77. package/src/extension/session-summary.ts +1 -1
  78. package/src/extension/team-manager-command.ts +64 -7
  79. package/src/extension/team-onboard.ts +149 -150
  80. package/src/extension/team-recommendation.ts +121 -21
  81. package/src/extension/team-tool/anchor.ts +31 -64
  82. package/src/extension/team-tool/api.ts +929 -141
  83. package/src/extension/team-tool/auto-summarize.ts +14 -26
  84. package/src/extension/team-tool/cache-control.ts +1 -5
  85. package/src/extension/team-tool/cancel.ts +138 -38
  86. package/src/extension/team-tool/chain-dispatch.ts +5 -13
  87. package/src/extension/team-tool/chain-executor.ts +29 -51
  88. package/src/extension/team-tool/config-patch.ts +1 -1
  89. package/src/extension/team-tool/context.ts +40 -20
  90. package/src/extension/team-tool/destructive-gate.ts +10 -5
  91. package/src/extension/team-tool/doctor.ts +170 -51
  92. package/src/extension/team-tool/explain.ts +228 -214
  93. package/src/extension/team-tool/failure-patterns.ts +3 -9
  94. package/src/extension/team-tool/goal-wrap.ts +71 -29
  95. package/src/extension/team-tool/goal.ts +185 -35
  96. package/src/extension/team-tool/handle-schedule.ts +34 -15
  97. package/src/extension/team-tool/handle-settings.ts +94 -56
  98. package/src/extension/team-tool/health-monitor.ts +27 -108
  99. package/src/extension/team-tool/inspect.ts +42 -9
  100. package/src/extension/team-tool/intent-policy.ts +4 -1
  101. package/src/extension/team-tool/lifecycle-actions.ts +244 -48
  102. package/src/extension/team-tool/orchestrate.ts +7 -18
  103. package/src/extension/team-tool/parallel-dispatch.ts +51 -29
  104. package/src/extension/team-tool/plan.ts +29 -5
  105. package/src/extension/team-tool/respond.ts +49 -16
  106. package/src/extension/team-tool/run-not-found.ts +3 -8
  107. package/src/extension/team-tool/run.ts +80 -317
  108. package/src/extension/team-tool/status.ts +135 -43
  109. package/src/extension/team-tool/workflow-manage.ts +92 -25
  110. package/src/extension/team-tool-types.ts +8 -1
  111. package/src/extension/team-tool.ts +142 -551
  112. package/src/extension/validate-resources.ts +39 -8
  113. package/src/hooks/registry.ts +66 -17
  114. package/src/hooks/types.ts +1 -1
  115. package/src/i18n.ts +25 -11
  116. package/src/observability/correlation.ts +17 -3
  117. package/src/observability/event-bus.ts +51 -56
  118. package/src/observability/event-to-metric.ts +117 -15
  119. package/src/observability/exporters/otlp-exporter.ts +56 -17
  120. package/src/observability/exporters/prometheus-exporter.ts +1 -1
  121. package/src/observability/metric-registry.ts +16 -4
  122. package/src/observability/metric-retention.ts +8 -2
  123. package/src/observability/metric-sink.ts +11 -3
  124. package/src/observability/metrics-primitives.ts +42 -9
  125. package/src/plugins/plugin-define.ts +2 -2
  126. package/src/plugins/plugin-registry.ts +25 -25
  127. package/src/plugins/plugins/index.ts +1 -1
  128. package/src/plugins/plugins/nextjs.ts +16 -16
  129. package/src/plugins/plugins/vite.ts +7 -11
  130. package/src/plugins/plugins/vitest.ts +6 -11
  131. package/src/runtime/adaptive-plan.ts +225 -40
  132. package/src/runtime/agent-control.ts +58 -15
  133. package/src/runtime/agent-memory.ts +15 -9
  134. package/src/runtime/agent-observability.ts +10 -2
  135. package/src/runtime/anchor-manager.ts +27 -25
  136. package/src/runtime/async-marker.ts +7 -1
  137. package/src/runtime/async-runner.ts +39 -24
  138. package/src/runtime/auto-summarize.ts +7 -13
  139. package/src/runtime/background-runner.ts +189 -251
  140. package/src/runtime/batch-barrier.ts +18 -16
  141. package/src/runtime/cancellation-token.ts +16 -6
  142. package/src/runtime/cancellation.ts +46 -8
  143. package/src/runtime/capability-inventory.ts +6 -4
  144. package/src/runtime/chain-parser.ts +44 -11
  145. package/src/runtime/chain-runner.ts +63 -69
  146. package/src/runtime/checkpoint.ts +12 -56
  147. package/src/runtime/child-pi.ts +344 -79
  148. package/src/runtime/coalesce-tasks.ts +268 -0
  149. package/src/runtime/code-summary.ts +71 -26
  150. package/src/runtime/compact-stages/index.ts +15 -5
  151. package/src/runtime/compact-stages/tail-capture-stage.ts +7 -2
  152. package/src/runtime/compact-stages/truncation-stage.ts +4 -1
  153. package/src/runtime/compaction-summary.ts +17 -10
  154. package/src/runtime/completion-guard.ts +33 -16
  155. package/src/runtime/crash-classification.ts +28 -7
  156. package/src/runtime/crash-recovery.ts +177 -37
  157. package/src/runtime/crew-agent-records.ts +145 -47
  158. package/src/runtime/crew-hooks.ts +9 -24
  159. package/src/runtime/cross-extension-rpc.ts +25 -23
  160. package/src/runtime/custom-tools/irc-tool.ts +43 -15
  161. package/src/runtime/custom-tools/submit-result-tool.ts +16 -5
  162. package/src/runtime/delivery-coordinator.ts +34 -7
  163. package/src/runtime/delta-conflict.ts +9 -21
  164. package/src/runtime/deterministic-ast.ts +1 -1
  165. package/src/runtime/diagnostic-export.ts +53 -20
  166. package/src/runtime/direct-run.ts +12 -2
  167. package/src/runtime/dwf-state-store.ts +1 -1
  168. package/src/runtime/dynamic-workflow-context.ts +187 -59
  169. package/src/runtime/dynamic-workflow-runner.ts +42 -20
  170. package/src/runtime/effectiveness.ts +16 -8
  171. package/src/runtime/errors/crew-errors.ts +7 -11
  172. package/src/runtime/event-stream-bridge.ts +9 -3
  173. package/src/runtime/foreground-control.ts +54 -14
  174. package/src/runtime/foreground-watchdog.ts +9 -6
  175. package/src/runtime/goal-achievement.ts +27 -10
  176. package/src/runtime/goal-evaluator.ts +54 -19
  177. package/src/runtime/goal-loop-runner.ts +280 -99
  178. package/src/runtime/goal-state-store.ts +27 -17
  179. package/src/runtime/green-contract.ts +11 -2
  180. package/src/runtime/group-join.ts +64 -31
  181. package/src/runtime/handoff-manager.ts +37 -42
  182. package/src/runtime/heartbeat-gradient.ts +10 -2
  183. package/src/runtime/heartbeat-watcher.ts +32 -6
  184. package/src/runtime/hidden-handoff.ts +13 -31
  185. package/src/runtime/important-line-classifier.ts +12 -2
  186. package/src/runtime/iteration-hooks.ts +25 -15
  187. package/src/runtime/live-agent-control.ts +55 -7
  188. package/src/runtime/live-agent-manager.ts +130 -27
  189. package/src/runtime/live-control-realtime.ts +23 -3
  190. package/src/runtime/live-irc.ts +4 -1
  191. package/src/runtime/live-session-health.ts +12 -4
  192. package/src/runtime/live-session-runtime.ts +379 -117
  193. package/src/runtime/manifest-cache.ts +28 -12
  194. package/src/runtime/mcp-proxy.ts +10 -18
  195. package/src/runtime/metric-parser.ts +1 -5
  196. package/src/runtime/model-fallback.ts +55 -18
  197. package/src/runtime/model-resolver.ts +2 -6
  198. package/src/runtime/model-scope.ts +19 -3
  199. package/src/runtime/notebook-helpers.ts +60 -62
  200. package/src/runtime/orphan-worker-registry.ts +37 -26
  201. package/src/runtime/output-validator.ts +18 -3
  202. package/src/runtime/overflow-recovery.ts +5 -4
  203. package/src/runtime/parallel-research.ts +29 -5
  204. package/src/runtime/parallel-utils.ts +9 -11
  205. package/src/runtime/path-overlap.ts +150 -0
  206. package/src/runtime/peer-dep.ts +12 -17
  207. package/src/runtime/per-write-validator.ts +1 -3
  208. package/src/runtime/phase-tracker.ts +342 -330
  209. package/src/runtime/pi-args.ts +26 -8
  210. package/src/runtime/pi-json-output.ts +1 -1
  211. package/src/runtime/pi-spawn.ts +30 -19
  212. package/src/runtime/pipeline-runner.ts +56 -55
  213. package/src/runtime/plan-templates.ts +5 -6
  214. package/src/runtime/policy-engine.ts +43 -7
  215. package/src/runtime/post-checks.ts +12 -4
  216. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  217. package/src/runtime/process-lifecycle.ts +20 -10
  218. package/src/runtime/process-status.ts +16 -5
  219. package/src/runtime/progress-event-coalescer.ts +2 -1
  220. package/src/runtime/progress-tracker.ts +103 -103
  221. package/src/runtime/prose-compressor.ts +9 -11
  222. package/src/runtime/recovery-recipes.ts +118 -24
  223. package/src/runtime/replace.ts +25 -10
  224. package/src/runtime/resilient-edit.ts +10 -25
  225. package/src/runtime/result-extractor.ts +2 -6
  226. package/src/runtime/retry-executor.ts +20 -4
  227. package/src/runtime/retry-runner.ts +27 -51
  228. package/src/runtime/role-permission.ts +6 -1
  229. package/src/runtime/run-coalesced-task-group.ts +256 -0
  230. package/src/runtime/run-drift.ts +14 -15
  231. package/src/runtime/run-tracker.ts +6 -28
  232. package/src/runtime/runtime-policy.ts +1 -6
  233. package/src/runtime/runtime-resolver.ts +80 -15
  234. package/src/runtime/scheduler.ts +47 -18
  235. package/src/runtime/semaphore.ts +5 -7
  236. package/src/runtime/sensitive-paths.ts +2 -1
  237. package/src/runtime/session-usage.ts +1 -1
  238. package/src/runtime/settings-store.ts +16 -12
  239. package/src/runtime/sidechain-output.ts +6 -2
  240. package/src/runtime/single-agent-compose.ts +1 -4
  241. package/src/runtime/skill-effectiveness.ts +29 -97
  242. package/src/runtime/skill-instructions.ts +40 -83
  243. package/src/runtime/stale-reconciler.ts +73 -197
  244. package/src/runtime/stream-preview.ts +1 -1
  245. package/src/runtime/streaming-output.ts +1 -1
  246. package/src/runtime/subagent-manager.ts +44 -169
  247. package/src/runtime/subprocess-tool-registry.ts +4 -1
  248. package/src/runtime/supervisor-contact.ts +10 -5
  249. package/src/runtime/task-display.ts +19 -5
  250. package/src/runtime/task-graph-scheduler.ts +95 -21
  251. package/src/runtime/task-graph.ts +5 -11
  252. package/src/runtime/task-health.ts +54 -47
  253. package/src/runtime/task-id.ts +12 -19
  254. package/src/runtime/task-output-context.ts +265 -81
  255. package/src/runtime/task-packet.ts +12 -20
  256. package/src/runtime/task-quality.ts +6 -14
  257. package/src/runtime/task-runner/context-retrieval.ts +4 -13
  258. package/src/runtime/task-runner/live-executor.ts +114 -36
  259. package/src/runtime/task-runner/output-splitter.ts +152 -0
  260. package/src/runtime/task-runner/progress.ts +50 -12
  261. package/src/runtime/task-runner/prompt-builder.ts +31 -7
  262. package/src/runtime/task-runner/prompt-pipeline.ts +31 -7
  263. package/src/runtime/task-runner/result-utils.ts +3 -1
  264. package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
  265. package/src/runtime/task-runner/run-projection.ts +27 -8
  266. package/src/runtime/task-runner/state-helpers.ts +50 -14
  267. package/src/runtime/task-runner/tail-read.ts +2 -6
  268. package/src/runtime/task-runner.ts +180 -326
  269. package/src/runtime/team-runner-artifacts.ts +13 -0
  270. package/src/runtime/team-runner.ts +456 -974
  271. package/src/runtime/tool-output-pruner.ts +6 -9
  272. package/src/runtime/tool-progress.ts +11 -21
  273. package/src/runtime/verification-gates.ts +33 -24
  274. package/src/runtime/verification-integrity.ts +2 -7
  275. package/src/runtime/verification-worktree.ts +62 -12
  276. package/src/runtime/worker-heartbeat.ts +5 -1
  277. package/src/runtime/worker-startup.ts +29 -5
  278. package/src/runtime/workflow-state.ts +17 -9
  279. package/src/runtime/workspace-lock.ts +30 -35
  280. package/src/runtime/workspace-tree.ts +20 -32
  281. package/src/runtime/yield-handler.ts +43 -9
  282. package/src/runtime/zombie-scanner.ts +5 -4
  283. package/src/schema/config-schema.ts +266 -172
  284. package/src/schema/team-tool-schema.ts +29 -74
  285. package/src/schema/validation-types.ts +25 -17
  286. package/src/skills/discover-skills.ts +35 -10
  287. package/src/skills/skill-templates.ts +109 -27
  288. package/src/skills/validate.ts +26 -8
  289. package/src/state/active-run-registry.ts +87 -28
  290. package/src/state/artifact-store.ts +9 -5
  291. package/src/state/atomic-write-v2.ts +85 -63
  292. package/src/state/atomic-write.ts +106 -27
  293. package/src/state/blob-store.ts +47 -20
  294. package/src/state/contracts.ts +20 -5
  295. package/src/state/crew-init.ts +5 -22
  296. package/src/state/decision-ledger.ts +19 -73
  297. package/src/state/event-log-rotation.ts +41 -15
  298. package/src/state/event-log.ts +168 -53
  299. package/src/state/event-reconstructor.ts +11 -1
  300. package/src/state/gitignore-manager.ts +2 -8
  301. package/src/state/health-store.ts +57 -57
  302. package/src/state/hook-instinct-bridge.ts +1 -1
  303. package/src/state/hook-integrations.ts +1 -1
  304. package/src/state/instinct-store.ts +17 -5
  305. package/src/state/jsonl-writer.ts +1 -1
  306. package/src/state/locks.ts +23 -9
  307. package/src/state/mailbox.ts +151 -28
  308. package/src/state/observation-store.ts +20 -14
  309. package/src/state/run-cache.ts +142 -135
  310. package/src/state/run-graph.ts +6 -15
  311. package/src/state/run-metrics.ts +5 -18
  312. package/src/state/schedule.ts +15 -9
  313. package/src/state/state-store.ts +137 -47
  314. package/src/state/task-claims.ts +12 -2
  315. package/src/state/tiered-eval.ts +52 -43
  316. package/src/state/types-eval.ts +2 -2
  317. package/src/state/types.ts +24 -20
  318. package/src/state/usage.ts +20 -4
  319. package/src/state/worker-atomic-writer.ts +6 -3
  320. package/src/subagents/index.ts +2 -2
  321. package/src/teams/discover-teams.ts +21 -6
  322. package/src/tools/safe-bash-extension.ts +5 -6
  323. package/src/tools/safe-bash.ts +10 -11
  324. package/src/types/new-api-types.ts +6 -10
  325. package/src/ui/agent-management-overlay.ts +52 -40
  326. package/src/ui/card-colors.ts +13 -4
  327. package/src/ui/crew-footer.ts +8 -7
  328. package/src/ui/crew-select-list.ts +1 -1
  329. package/src/ui/dashboard-panes/agents-pane.ts +41 -25
  330. package/src/ui/dashboard-panes/cancellation-pane.ts +1 -1
  331. package/src/ui/dashboard-panes/capability-pane.ts +32 -15
  332. package/src/ui/dashboard-panes/health-pane.ts +2 -1
  333. package/src/ui/dashboard-panes/metrics-pane.ts +4 -1
  334. package/src/ui/dashboard-panes/progress-pane.ts +12 -9
  335. package/src/ui/heartbeat-aggregator.ts +15 -4
  336. package/src/ui/keybinding-map.ts +40 -7
  337. package/src/ui/live-conversation-overlay.ts +36 -20
  338. package/src/ui/live-duration.ts +1 -4
  339. package/src/ui/live-run-sidebar.ts +88 -25
  340. package/src/ui/loaders.ts +2 -8
  341. package/src/ui/mascot.ts +20 -36
  342. package/src/ui/overlays/agent-picker-overlay.ts +11 -3
  343. package/src/ui/overlays/confirm-overlay.ts +4 -2
  344. package/src/ui/overlays/help-overlay.ts +25 -14
  345. package/src/ui/overlays/mailbox-compose-overlay.ts +48 -11
  346. package/src/ui/overlays/mailbox-compose-preview.ts +20 -5
  347. package/src/ui/overlays/mailbox-detail-overlay.ts +27 -6
  348. package/src/ui/pi-ui-compat.ts +7 -7
  349. package/src/ui/powerbar-publisher.ts +120 -42
  350. package/src/ui/render-diff.ts +10 -3
  351. package/src/ui/render-scheduler.ts +12 -4
  352. package/src/ui/run-action-dispatcher.ts +81 -18
  353. package/src/ui/run-dashboard.ts +172 -76
  354. package/src/ui/run-event-bus.ts +59 -37
  355. package/src/ui/run-snapshot-cache.ts +276 -86
  356. package/src/ui/settings-overlay.ts +361 -74
  357. package/src/ui/status-colors.ts +12 -1
  358. package/src/ui/syntax-highlight.ts +1 -1
  359. package/src/ui/terminal-status.ts +1 -3
  360. package/src/ui/theme-adapter.ts +16 -14
  361. package/src/ui/theme-discovery.ts +13 -2
  362. package/src/ui/tool-progress-formatter.ts +7 -7
  363. package/src/ui/tool-render.ts +128 -56
  364. package/src/ui/tool-renderers/brief-mode.ts +45 -27
  365. package/src/ui/tool-renderers/index.ts +109 -43
  366. package/src/ui/transcript-cache.ts +32 -6
  367. package/src/ui/transcript-entries.ts +25 -23
  368. package/src/ui/transcript-viewer.ts +78 -29
  369. package/src/ui/widget/index.ts +99 -40
  370. package/src/ui/widget/widget-formatters.ts +13 -6
  371. package/src/ui/widget/widget-model.ts +19 -8
  372. package/src/ui/widget/widget-renderer.ts +23 -13
  373. package/src/ui/widget/widget-types.ts +1 -1
  374. package/src/utils/bm25-search.ts +199 -199
  375. package/src/utils/conflict-detect.ts +22 -21
  376. package/src/utils/env-filter.ts +14 -7
  377. package/src/utils/file-coalescer.ts +5 -1
  378. package/src/utils/fingerprint.ts +3 -6
  379. package/src/utils/frontmatter.ts +3 -1
  380. package/src/utils/fs-watch.ts +2 -6
  381. package/src/utils/gh-protocol.ts +119 -44
  382. package/src/utils/git.ts +13 -15
  383. package/src/utils/guards.ts +2 -5
  384. package/src/utils/ids.ts +9 -2
  385. package/src/utils/incremental-reader.ts +14 -3
  386. package/src/utils/internal-error.ts +2 -1
  387. package/src/utils/names.ts +12 -3
  388. package/src/utils/paths.ts +49 -5
  389. package/src/utils/project-detector.ts +2 -2
  390. package/src/utils/redaction.ts +29 -19
  391. package/src/utils/resolve-shell.ts +9 -7
  392. package/src/utils/run-watcher-registry.ts +19 -31
  393. package/src/utils/safe-paths.ts +46 -29
  394. package/src/utils/scan-cache.ts +9 -2
  395. package/src/utils/session-utils.ts +2 -4
  396. package/src/utils/sleep.ts +12 -5
  397. package/src/utils/sse-parser.ts +5 -17
  398. package/src/utils/visual.ts +52 -32
  399. package/src/workflows/discover-workflows.ts +41 -109
  400. package/src/workflows/intermediate-store.ts +5 -21
  401. package/src/workflows/preflight-validator.ts +6 -33
  402. package/src/workflows/topology-analyzer.ts +5 -21
  403. package/src/workflows/workflow-config.ts +7 -6
  404. package/src/worktree/branch-freshness.ts +66 -8
  405. package/src/worktree/cleanup.ts +130 -20
  406. package/src/worktree/worktree-manager.ts +212 -69
  407. package/skills/artifact-analysis-loop/SKILL.md +0 -303
  408. package/skills/detection-pipeline-design/SKILL.md +0 -286
  409. package/skills/hunting-investigation-loop/SKILL.md +0 -402
  410. package/skills/incident-playbook-construction/SKILL.md +0 -384
  411. package/skills/security-review/SKILL.md +0 -561
  412. package/skills/threat-hypothesis-framework/SKILL.md +0 -176
  413. package/skills/ui-render-performance/SKILL.md +0 -58
  414. /package/docs/{followup-review-round3-2026-05-12.md → archive/followup-review-round3-2026-05-12.md} +0 -0
  415. /package/docs/{followup-review-round4-2026-05-13.md → archive/followup-review-round4-2026-05-13.md} +0 -0
  416. /package/docs/{pi-crew-bugs.md → archive/pi-crew-bugs.md} +0 -0
  417. /package/docs/{pi-crew-test-final.md → archive/pi-crew-test-final.md} +0 -0
  418. /package/docs/{pi-crew-test-results.md → archive/pi-crew-test-results.md} +0 -0
  419. /package/docs/{pi-crew-test-round2.md → archive/pi-crew-test-round2.md} +0 -0
  420. /package/docs/{pi-crew-test-round4.md → archive/pi-crew-test-round4.md} +0 -0
  421. /package/docs/{pi-crew-test-round5.md → archive/pi-crew-test-round5.md} +0 -0
  422. /package/docs/{pi-crew-test-round6.md → archive/pi-crew-test-round6.md} +0 -0
  423. /package/docs/{pi-crew-v0.5.10-audit-fix-plan.md → archive/pi-crew-v0.5.10-audit-fix-plan.md} +0 -0
  424. /package/docs/{pi-crew-v0.5.11-audit-fix-plan.md → archive/pi-crew-v0.5.11-audit-fix-plan.md} +0 -0
  425. /package/docs/{pi-crew-v0.5.12-audit-fix-plan.md → archive/pi-crew-v0.5.12-audit-fix-plan.md} +0 -0
  426. /package/docs/{pi-crew-v0.5.13-audit-fix-plan.md → archive/pi-crew-v0.5.13-audit-fix-plan.md} +0 -0
  427. /package/docs/{pi-crew-v0.5.14-audit-fix-plan.md → archive/pi-crew-v0.5.14-audit-fix-plan.md} +0 -0
  428. /package/docs/{pi-crew-v0.5.16-audit-fix-plan.md → archive/pi-crew-v0.5.16-audit-fix-plan.md} +0 -0
  429. /package/docs/{pi-crew-v0.5.17-audit-fix-plan.md → archive/pi-crew-v0.5.17-audit-fix-plan.md} +0 -0
  430. /package/docs/{pi-crew-v0.5.5-audit-fix-plan.md → archive/pi-crew-v0.5.5-audit-fix-plan.md} +0 -0
  431. /package/docs/{pi-crew-v0.5.9-audit-fix-plan.md → archive/pi-crew-v0.5.9-audit-fix-plan.md} +0 -0
  432. /package/docs/{pi-mono-opportunities.md → archive/pi-mono-opportunities.md} +0 -0
  433. /package/docs/{pi-mono-review.md → archive/pi-mono-review.md} +0 -0
  434. /package/docs/{pi-subagent4-comparison.md → archive/pi-subagent4-comparison.md} +0 -0
  435. /package/docs/{pi-subagents3-deep-analysis.md → archive/pi-subagents3-deep-analysis.md} +0 -0
@@ -1,109 +1,50 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentConfig } from "../agents/agent-config.ts";
4
- import type {
5
- CrewLimitsConfig,
6
- CrewReliabilityConfig,
7
- CrewRuntimeConfig,
8
- } from "../config/config.ts";
4
+ import type { CrewLimitsConfig, CrewReliabilityConfig, CrewRuntimeConfig } from "../config/config.ts";
9
5
  import { appendHookEvent, executeHook } from "../hooks/registry.ts";
10
- import {
11
- childCorrelation,
12
- withCorrelation,
13
- } from "../observability/correlation.ts";
6
+ import { childCorrelation, withCorrelation } from "../observability/correlation.ts";
14
7
  import type { MetricRegistry } from "../observability/metric-registry.ts";
15
8
  import { PluginRegistry } from "../plugins/plugin-registry.ts";
16
- import {
17
- NextJsPlugin,
18
- VitePlugin,
19
- VitestPlugin,
20
- } from "../plugins/plugins/index.ts";
9
+ import { NextJsPlugin, VitePlugin, VitestPlugin } from "../plugins/plugins/index.ts";
21
10
  import { writeArtifact } from "../state/artifact-store.ts";
22
- import {
23
- appendEvent,
24
- appendEventAsync,
25
- appendEventFireAndForget,
26
- } from "../state/event-log.ts";
11
+ import { appendEvent, appendEventAsync, appendEventBuffered, flushEventLogBuffer } from "../state/event-log.ts";
27
12
  import { HealthStore } from "../state/health-store.ts";
28
13
  import { withRunLock } from "../state/locks.ts";
29
- import {
30
- loadRunManifestById,
31
- saveRunManifest,
32
- saveRunManifestAsync,
33
- saveRunTasksAsync,
34
- updateRunStatus,
35
- } from "../state/state-store.ts";
36
- import type {
37
- ArtifactDescriptor,
38
- PolicyDecision,
39
- TaskAttemptState,
40
- TeamRunManifest,
41
- TeamTaskState,
42
- } from "../state/types.ts";
14
+ import { loadRunManifestById, saveRunManifest, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/state-store.ts";
15
+ import type { ArtifactDescriptor, PolicyDecision, TaskAttemptState, TeamRunManifest, TeamTaskState } from "../state/types.ts";
43
16
  import { aggregateUsage, formatUsage } from "../state/usage.ts";
44
17
  import type { TeamConfig } from "../teams/team-config.ts";
45
18
  import { logInternalError } from "../utils/internal-error.ts";
46
- import type {
47
- WorkflowConfig,
48
- WorkflowStep,
49
- } from "../workflows/workflow-config.ts";
19
+ import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
50
20
  import { checkBranchFreshness } from "../worktree/branch-freshness.ts";
51
- import {
52
- buildSyntheticTerminalEvidence,
53
- CrewCancellationError,
54
- cancellationReasonFromSignal,
55
- } from "./cancellation.ts";
21
+ import { buildSyntheticTerminalEvidence, CrewCancellationError, cancellationReasonFromSignal } from "./cancellation.ts";
22
+ import { buildDispatchUnits, planCoalescedGroups } from "./coalesce-tasks.ts";
56
23
  import { resolveBatchConcurrency } from "./concurrency.ts";
57
24
  import { readCrewAgents, saveCrewAgents } from "./crew-agent-records.ts";
58
25
  import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
59
26
  import { crewHooks } from "./crew-hooks.ts";
60
27
  import { appendDeadletter } from "./deadletter.ts";
61
- import {
62
- effectivenessPolicyDecision,
63
- evaluateRunEffectiveness,
64
- formatRunEffectivenessLines,
65
- } from "./effectiveness.ts";
66
- import {
67
- applyGoalAchievement,
68
- assessGoalAchievement,
69
- } from "./goal-achievement.ts";
28
+ import { effectivenessPolicyDecision, evaluateRunEffectiveness, formatRunEffectivenessLines } from "./effectiveness.ts";
29
+ import { applyGoalAchievement, assessGoalAchievement } from "./goal-achievement.ts";
70
30
  import { deliverGroupJoin, resolveGroupJoinMode } from "./group-join.ts";
71
31
  import { terminateLiveAgentsForRun } from "./live-agent-manager.ts";
72
32
  import { mapConcurrent } from "./parallel-utils.ts";
73
- import {
74
- evaluateCrewPolicy,
75
- summarizePolicyDecisions,
76
- } from "./policy-engine.ts";
77
- import {
78
- buildRecoveryLedger,
79
- shouldRerunFailedTask,
80
- } from "./recovery-recipes.ts";
81
- import {
82
- DEFAULT_RETRY_POLICY,
83
- executeWithRetry,
84
- type RetryPolicy,
85
- } from "./retry-executor.ts";
33
+ import { filterReadyByWriteOverlap } from "./path-overlap.ts";
34
+ import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
35
+ import { buildRecoveryLedger, shouldRerunFailedTask } from "./recovery-recipes.ts";
36
+ import { DEFAULT_RETRY_POLICY, executeWithRetry, type RetryPolicy } from "./retry-executor.ts";
86
37
  import { permissionForRole } from "./role-permission.ts";
87
- import {
88
- registerRunPromise,
89
- rejectRunPromise,
90
- resolveRunPromise,
91
- } from "./run-tracker.ts";
38
+ import { runCoalescedTaskGroup } from "./run-coalesced-task-group.ts";
39
+ import { registerRunPromise, rejectRunPromise, resolveRunPromise } from "./run-tracker.ts";
92
40
  import { resolveTaskRuntimeKind } from "./runtime-policy.ts";
93
41
  import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
94
42
  import { recordsForMaterializedTasks } from "./task-display.ts";
95
- import {
96
- buildExecutionPlan as buildDagExecutionPlan,
97
- getReadyTasks as getDagReadyTasks,
98
- type TaskNode,
99
- } from "./task-graph.ts";
100
- import {
101
- buildTaskGraphIndex,
102
- refreshTaskGraphQueues,
103
- taskGraphSnapshot,
104
- } from "./task-graph-scheduler.ts";
43
+ import { buildExecutionPlan as buildDagExecutionPlan, getReadyTasks as getDagReadyTasks, type TaskNode } from "./task-graph.ts";
44
+ import { buildTaskGraphIndex, refreshTaskGraphQueues, taskGraphSnapshot } from "./task-graph-scheduler.ts";
105
45
  import { aggregateTaskOutputs } from "./task-output-context.ts";
106
46
  import { runTeamTask } from "./task-runner.ts";
47
+ import { mergeArtifacts } from "./team-runner-artifacts.ts";
107
48
  import { clearTrackedTaskUsage } from "./usage-tracker.ts";
108
49
  import {
109
50
  createWorkflowStateMachine,
@@ -192,22 +133,14 @@ export interface ExecuteTeamRunInput {
192
133
  }
193
134
 
194
135
  function findStep(workflow: WorkflowConfig, task: TeamTaskState): WorkflowStep {
195
- const step = workflow.steps.find(
196
- (candidate) => candidate.id === task.stepId,
197
- );
198
- if (!step)
199
- throw new Error(
200
- `Workflow step '${task.stepId}' not found for task '${task.id}'.`,
201
- );
136
+ const step = workflow.steps.find((candidate) => candidate.id === task.stepId);
137
+ if (!step) throw new Error(`Workflow step '${task.stepId}' not found for task '${task.id}'.`);
202
138
  return step;
203
139
  }
204
140
 
205
141
  function findAgent(agents: AgentConfig[], task: TeamTaskState): AgentConfig {
206
142
  const agent = agents.find((candidate) => candidate.name === task.agent);
207
- if (!agent)
208
- throw new Error(
209
- `Agent '${task.agent}' not found for task '${task.id}'.`,
210
- );
143
+ if (!agent) throw new Error(`Agent '${task.agent}' not found for task '${task.id}'.`);
211
144
  return agent;
212
145
  }
213
146
 
@@ -219,20 +152,12 @@ function markBlocked(tasks: TeamTaskState[], reason: string): TeamTaskState[] {
219
152
  status: "skipped",
220
153
  error: reason,
221
154
  finishedAt: new Date().toISOString(),
222
- graph: task.graph
223
- ? { ...task.graph, queue: "blocked" }
224
- : undefined,
155
+ graph: task.graph ? { ...task.graph, queue: "blocked" } : undefined,
225
156
  }
226
157
  : task,
227
158
  );
228
159
  }
229
160
 
230
- function mergeArtifacts(items: ArtifactDescriptor[]): ArtifactDescriptor[] {
231
- const byPath = new Map<string, ArtifactDescriptor>();
232
- for (const item of items) byPath.set(item.path, item);
233
- return [...byPath.values()];
234
- }
235
-
236
161
  function isNonTerminalTaskStatus(status: TeamTaskState["status"]): boolean {
237
162
  return status === "queued" || status === "running" || status === "waiting";
238
163
  }
@@ -253,22 +178,15 @@ function safeFinishedAt(task: TeamTaskState): number {
253
178
  * and the updated task has a valid finite finishedAt. Malformed finishedAt
254
179
  * should be replaced rather than persisting corruption.
255
180
  */
256
- function isMalformedFinishedAtReplacement(
257
- currentTime: number,
258
- updatedTime: number,
259
- ): boolean {
181
+ function isMalformedFinishedAtReplacement(currentTime: number, updatedTime: number): boolean {
260
182
  return !Number.isFinite(currentTime) && Number.isFinite(updatedTime);
261
183
  }
262
184
 
263
- function shouldMergeTaskUpdate(
264
- current: TeamTaskState,
265
- updated: TeamTaskState,
266
- ): boolean {
185
+ function shouldMergeTaskUpdate(current: TeamTaskState, updated: TeamTaskState): boolean {
267
186
  // Parallel workers receive the same input snapshot. A later result may still
268
187
  // contain stale queued/running copies of tasks that another worker already
269
188
  // completed. Never let those stale snapshots regress durable task state.
270
- if (current.status === "waiting" && updated.status === "running")
271
- return false;
189
+ if (current.status === "waiting" && updated.status === "running") return false;
272
190
  // Block terminal→non-terminal transitions (e.g. completed→running).
273
191
  // A task that has reached a terminal state must not be resurrected.
274
192
  const currentIsTerminal = !isNonTerminalTaskStatus(current.status);
@@ -277,34 +195,21 @@ function shouldMergeTaskUpdate(
277
195
  // Explicitly block completed↔needs_attention terminal-to-terminal transitions.
278
196
  // Both are success terminal states used interchangeably; stale worker updates must
279
197
  // not cause a completed task to appear as needs_attention or vice versa.
280
- if (current.status === "completed" && updated.status === "needs_attention")
281
- return false;
282
- if (current.status === "needs_attention" && updated.status === "completed")
283
- return false;
198
+ if (current.status === "completed" && updated.status === "needs_attention") return false;
199
+ if (current.status === "needs_attention" && updated.status === "completed") return false;
284
200
  // Explicitly block failed→completed resurrection. Both statuses are terminal,
285
201
  // but completed is the success terminal state and should not be reachable from
286
202
  // failed via a stale merge. The check above only guards non-terminal→terminal.
287
- if (current.status === "failed" && updated.status === "completed")
288
- return false;
203
+ if (current.status === "failed" && updated.status === "completed") return false;
289
204
  // Guard: when current is "running" but has resultArtifact (another worker already
290
205
  // completed it), a stale updated with status="running" and no resultArtifact
291
206
  // must not overwrite the actual completed state.
292
- if (
293
- current.status === updated.status &&
294
- updated.status === "running" &&
295
- current.resultArtifact &&
296
- !updated.resultArtifact
297
- )
207
+ if (current.status === updated.status && updated.status === "running" && current.resultArtifact && !updated.resultArtifact)
298
208
  return false;
299
209
  // Guard: when current is "completed" and has resultArtifact but updated is also
300
210
  // "completed" without resultArtifact, block the stale update from overwriting
301
211
  // a task that successfully produced output.
302
- if (
303
- current.status === updated.status &&
304
- current.status === "completed" &&
305
- current.resultArtifact &&
306
- !updated.resultArtifact
307
- )
212
+ if (current.status === updated.status && current.status === "completed" && current.resultArtifact && !updated.resultArtifact)
308
213
  return false;
309
214
  // Prevent a stale completed task from overwriting a fresher one.
310
215
  // Restructure to handle undefined current.finishedAt as a special case:
@@ -318,9 +223,7 @@ function shouldMergeTaskUpdate(
318
223
  // Malformed finishedAt (NaN) is treated as Infinity — invalid state should be
319
224
  // replaced rather than persisting corruption. Log warning for visibility.
320
225
  if (!Number.isFinite(currentTime)) {
321
- console.warn(
322
- `[team-runner] Task ${current.id} has malformed finishedAt: ${current.finishedAt}`,
323
- );
226
+ console.warn(`[team-runner] Task ${current.id} has malformed finishedAt: ${current.finishedAt}`);
324
227
  }
325
228
  if (isMalformedFinishedAtReplacement(currentTime, updatedTime)) {
326
229
  return true;
@@ -330,8 +233,7 @@ function shouldMergeTaskUpdate(
330
233
  // Block if updated is trying to establish a terminal status without a finishedAt
331
234
  // timestamp. Heartbeat-only updates (status='running', no finishedAt) are
332
235
  // allowed if heartbeat has changed (checked separately in hasMeaningfulUpdate).
333
- if (!updated.finishedAt && !isNonTerminalTaskStatus(updated.status))
334
- return false;
236
+ if (!updated.finishedAt && !isNonTerminalTaskStatus(updated.status)) return false;
335
237
  // Explicitly enumerate all fields that constitute a meaningful update so that
336
238
  // adding a new important field requires updating this list (rather than silently
337
239
  // losing data if a field is forgotten in the boolean OR chain below).
@@ -340,25 +242,20 @@ function shouldMergeTaskUpdate(
340
242
  updated.finishedAt !== current.finishedAt ||
341
243
  updated.startedAt !== current.startedAt ||
342
244
  Boolean(updated.resultArtifact) !== Boolean(current.resultArtifact) ||
343
- (Boolean(updated.resultArtifact) &&
344
- updated.resultArtifact !== current.resultArtifact) ||
245
+ (Boolean(updated.resultArtifact) && updated.resultArtifact !== current.resultArtifact) ||
345
246
  Boolean(updated.error) ||
346
247
  Boolean(updated.modelAttempts?.length) ||
347
248
  Boolean(updated.usage) ||
348
249
  Boolean(updated.attempts?.length) ||
349
250
  updated.heartbeat?.lastSeenAt !== current.heartbeat?.lastSeenAt ||
350
251
  updated.jsonEvents !== current.jsonEvents ||
351
- updated.agentProgress?.lastActivityAt !==
352
- current.agentProgress?.lastActivityAt;
252
+ updated.agentProgress?.lastActivityAt !== current.agentProgress?.lastActivityAt;
353
253
  return hasMeaningfulUpdate;
354
254
  }
355
255
 
356
256
  // H4 fix: rename to descriptive name. Kept __test__ as alias for backward
357
257
  // compat test imports.
358
- export function mergeTaskUpdatesPreservingTerminal(
359
- base: TeamTaskState[],
360
- results: Array<{ tasks: TeamTaskState[] }>,
361
- ): TeamTaskState[] {
258
+ export function mergeTaskUpdatesPreservingTerminal(base: TeamTaskState[], results: Array<{ tasks: TeamTaskState[] }>): TeamTaskState[] {
362
259
  let merged = base;
363
260
  for (const result of results) {
364
261
  for (const updated of result.tasks) {
@@ -368,21 +265,15 @@ export function mergeTaskUpdatesPreservingTerminal(
368
265
  // Log skipped merges for visibility into rejected parallel updates.
369
266
  // In distributed systems with parallel workers, rejected merges may
370
267
  // indicate bugs (wrong status, timestamp corruption) if they accumulate.
371
- console.debug(
372
- "[team-runner] Skipping stale merge for task",
373
- updated.id,
374
- {
375
- currentStatus: current.status,
376
- updatedStatus: updated.status,
377
- currentFinishedAt: current.finishedAt,
378
- updatedFinishedAt: updated.finishedAt,
379
- },
380
- );
268
+ console.debug("[team-runner] Skipping stale merge for task", updated.id, {
269
+ currentStatus: current.status,
270
+ updatedStatus: updated.status,
271
+ currentFinishedAt: current.finishedAt,
272
+ updatedFinishedAt: updated.finishedAt,
273
+ });
381
274
  continue;
382
275
  }
383
- merged = merged.map((task) =>
384
- task.id === updated.id ? updated : task,
385
- );
276
+ merged = merged.map((task) => (task.id === updated.id ? updated : task));
386
277
  }
387
278
  }
388
279
  return refreshTaskGraphQueues(merged);
@@ -427,8 +318,7 @@ function writeProgress(
427
318
  runtimeConfig?: CrewRuntimeConfig,
428
319
  ): TeamRunManifest {
429
320
  const counts = new Map<string, number>();
430
- for (const task of tasks)
431
- counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
321
+ for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
432
322
  const queue = taskGraphSnapshot(tasks);
433
323
  const progress = writeArtifact(manifest.artifactsRoot, {
434
324
  kind: "progress",
@@ -448,12 +338,7 @@ function writeProgress(
448
338
  ...tasks.map(formatTaskProgress),
449
339
  "",
450
340
  "## Effectiveness",
451
- ...runEffectivenessLines(
452
- manifest,
453
- tasks,
454
- executeWorkers,
455
- runtimeConfig,
456
- ),
341
+ ...runEffectivenessLines(manifest, tasks, executeWorkers, runtimeConfig),
457
342
  "",
458
343
  ].join("\n"),
459
344
  });
@@ -461,26 +346,13 @@ function writeProgress(
461
346
  ...manifest,
462
347
  updatedAt: new Date().toISOString(),
463
348
  artifacts: [
464
- ...manifest.artifacts.filter(
465
- (artifact) =>
466
- !(
467
- artifact.kind === "progress" &&
468
- artifact.path === progress.path
469
- ),
470
- ),
349
+ ...manifest.artifacts.filter((artifact) => !(artifact.kind === "progress" && artifact.path === progress.path)),
471
350
  progress,
472
- ].filter(
473
- (artifact, index, self) =>
474
- self.findIndex((a) => a.path === artifact.path) === index,
475
- ),
351
+ ].filter((artifact, index, self) => self.findIndex((a) => a.path === artifact.path) === index),
476
352
  };
477
353
  }
478
354
 
479
- function applyPolicy(
480
- manifest: TeamRunManifest,
481
- tasks: TeamTaskState[],
482
- limits?: CrewLimitsConfig,
483
- ): TeamRunManifest {
355
+ function applyPolicy(manifest: TeamRunManifest, tasks: TeamTaskState[], limits?: CrewLimitsConfig): TeamRunManifest {
484
356
  const branchFreshness = checkBranchFreshness(manifest.cwd);
485
357
  const branchArtifact = writeArtifact(manifest.artifactsRoot, {
486
358
  kind: "metadata",
@@ -493,10 +365,7 @@ function applyPolicy(
493
365
  tasks,
494
366
  limits,
495
367
  });
496
- if (
497
- branchFreshness.status === "stale" ||
498
- branchFreshness.status === "diverged"
499
- ) {
368
+ if (branchFreshness.status === "stale" || branchFreshness.status === "diverged") {
500
369
  const branchDecision: PolicyDecision = {
501
370
  action: "notify",
502
371
  reason: "branch_stale",
@@ -526,10 +395,7 @@ function applyPolicy(
526
395
  });
527
396
  for (const item of decisions)
528
397
  appendEvent(manifest.eventsPath, {
529
- type:
530
- item.action === "escalate"
531
- ? "policy.escalated"
532
- : "policy.action",
398
+ type: item.action === "escalate" ? "policy.escalated" : "policy.action",
533
399
  runId: manifest.runId,
534
400
  taskId: item.taskId,
535
401
  message: item.message,
@@ -537,10 +403,7 @@ function applyPolicy(
537
403
  });
538
404
  for (const item of recoveryLedger.entries)
539
405
  appendEvent(manifest.eventsPath, {
540
- type:
541
- item.state === "escalation_required"
542
- ? "recovery.escalated"
543
- : "recovery.attempted",
406
+ type: item.state === "escalation_required" ? "recovery.escalated" : "recovery.attempted",
544
407
  runId: manifest.runId,
545
408
  taskId: item.taskId,
546
409
  message: item.message,
@@ -572,9 +435,7 @@ function applyPolicy(
572
435
  };
573
436
  }
574
437
 
575
- function retryPolicyFromConfig(
576
- config: CrewReliabilityConfig | undefined,
577
- ): RetryPolicy {
438
+ function retryPolicyFromConfig(config: CrewReliabilityConfig | undefined): RetryPolicy {
578
439
  return { ...DEFAULT_RETRY_POLICY, ...(config?.retryPolicy ?? {}) };
579
440
  }
580
441
 
@@ -584,25 +445,15 @@ function retryPolicyFromConfig(
584
445
  * automatically. Previously opt-in, which left the entire retry+recovery stack dormant.
585
446
  * Exported for unit testing.
586
447
  */
587
- export function shouldUseRetry(
588
- reliability: CrewReliabilityConfig | undefined,
589
- ): boolean {
448
+ export function shouldUseRetry(reliability: CrewReliabilityConfig | undefined): boolean {
590
449
  return reliability?.autoRetry !== false;
591
450
  }
592
451
 
593
- function failedTaskFrom(
594
- result: { tasks: TeamTaskState[] },
595
- taskId: string,
596
- ): TeamTaskState | undefined {
597
- return result.tasks.find(
598
- (item) => item.id === taskId && item.status === "failed",
599
- );
452
+ function failedTaskFrom(result: { tasks: TeamTaskState[] }, taskId: string): TeamTaskState | undefined {
453
+ return result.tasks.find((item) => item.id === taskId && item.status === "failed");
600
454
  }
601
455
 
602
- function requiresPlanApproval(
603
- _workflow: WorkflowConfig,
604
- runtimeConfig: CrewRuntimeConfig | undefined,
605
- ): boolean {
456
+ function requiresPlanApproval(_workflow: WorkflowConfig, runtimeConfig: CrewRuntimeConfig | undefined): boolean {
606
457
  // ROADMAP T1.2: plan-level HITL applies to ANY workflow when
607
458
  // config.runtime.requirePlanApproval === true (not just 'implementation').
608
459
  // The gate fires at the read-only → mutating (plan → execute) boundary.
@@ -610,31 +461,19 @@ function requiresPlanApproval(
610
461
  }
611
462
 
612
463
  function isPlanApprovalPending(manifest: TeamRunManifest): boolean {
613
- return (
614
- manifest.planApproval?.required === true &&
615
- manifest.planApproval.status === "pending"
616
- );
464
+ return manifest.planApproval?.required === true && manifest.planApproval.status === "pending";
617
465
  }
618
466
 
619
467
  function isMutatingTask(task: TeamTaskState): boolean {
620
468
  return permissionForRole(task.role) !== "read_only";
621
469
  }
622
470
 
623
- function ensurePlanApprovalRequested(
624
- manifest: TeamRunManifest,
625
- tasks: TeamTaskState[],
626
- ): TeamRunManifest {
471
+ function ensurePlanApprovalRequested(manifest: TeamRunManifest, tasks: TeamTaskState[]): TeamRunManifest {
627
472
  if (manifest.planApproval) return manifest;
628
- const assessTask = tasks.find(
629
- (task) => task.stepId === "assess" && task.status === "completed",
630
- );
473
+ const assessTask = tasks.find((task) => task.stepId === "assess" && task.status === "completed");
631
474
  // ROADMAP T1.2: for non-adaptive workflows, fall back to the most recent
632
475
  // completed read-only (planning) task as the plan reference.
633
- const planTask =
634
- assessTask ??
635
- [...tasks]
636
- .reverse()
637
- .find((t) => t.status === "completed" && !isMutatingTask(t));
476
+ const planTask = assessTask ?? [...tasks].reverse().find((t) => t.status === "completed" && !isMutatingTask(t));
638
477
  const now = new Date().toISOString();
639
478
  const updated: TeamRunManifest = {
640
479
  ...manifest,
@@ -653,39 +492,28 @@ function ensurePlanApprovalRequested(
653
492
  type: "plan.approval_required",
654
493
  runId: updated.runId,
655
494
  taskId: planTask?.id,
656
- message:
657
- "Plan requires explicit approval before mutating tasks run. Use: team api op=approve-plan runId=...",
495
+ message: "Plan requires explicit approval before mutating tasks run. Use: team api op=approve-plan runId=...",
658
496
  data: { planArtifactPath: planTask?.resultArtifact?.path },
659
497
  });
660
498
  return updated;
661
499
  }
662
500
 
663
- function cancelPlanTasks(
664
- tasks: TeamTaskState[],
665
- reason: string,
666
- ): TeamTaskState[] {
501
+ function cancelPlanTasks(tasks: TeamTaskState[], reason: string): TeamTaskState[] {
667
502
  return tasks.map((task) =>
668
- task.status === "queued" ||
669
- task.status === "running" ||
670
- task.status === "waiting"
503
+ task.status === "queued" || task.status === "running" || task.status === "waiting"
671
504
  ? {
672
505
  ...task,
673
506
  status: "cancelled",
674
507
  finishedAt: new Date().toISOString(),
675
508
  error: reason,
676
- graph: task.graph
677
- ? { ...task.graph, queue: "done" }
678
- : undefined,
509
+ graph: task.graph ? { ...task.graph, queue: "done" } : undefined,
679
510
  }
680
511
  : task,
681
512
  );
682
513
  }
683
514
 
684
515
  function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
685
- return tasks.some(
686
- (task) =>
687
- task.status === "queued" && task.adaptive && isMutatingTask(task),
688
- );
516
+ return tasks.some((task) => task.status === "queued" && task.adaptive && isMutatingTask(task));
689
517
  }
690
518
 
691
519
  /**
@@ -693,15 +521,9 @@ function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
693
521
  * Fires when there are pending mutating tasks whose prerequisites (read-only
694
522
  * tasks) have completed — i.e. the plan→execute boundary.
695
523
  */
696
- export function hasPendingMutatingTaskAtBoundary(
697
- tasks: TeamTaskState[],
698
- ): boolean {
699
- const hasCompletedReadOnly = tasks.some(
700
- (t) => t.status === "completed" && !isMutatingTask(t),
701
- );
702
- const hasPendingMutating = tasks.some(
703
- (t) => t.status === "queued" && isMutatingTask(t),
704
- );
524
+ export function hasPendingMutatingTaskAtBoundary(tasks: TeamTaskState[]): boolean {
525
+ const hasCompletedReadOnly = tasks.some((t) => t.status === "completed" && !isMutatingTask(t));
526
+ const hasPendingMutating = tasks.some((t) => t.status === "queued" && isMutatingTask(t));
705
527
  return hasCompletedReadOnly && hasPendingMutating;
706
528
  }
707
529
 
@@ -710,10 +532,7 @@ export function hasPendingMutatingTaskAtBoundary(
710
532
  * execution planning. If so, build an execution plan and use `getDagReadyTasks`
711
533
  * to augment the ready-set selection.
712
534
  */
713
- function dagReadyTaskIds(
714
- tasks: TeamTaskState[],
715
- completedIds: Set<string>,
716
- ): string[] | null {
535
+ function dagReadyTaskIds(tasks: TeamTaskState[], completedIds: Set<string>): string[] | null {
717
536
  const hasExplicitDeps = tasks.some((t) => t.dependsOn.length > 0);
718
537
  if (!hasExplicitDeps) return null;
719
538
  // FIX (goal-wrap runtime test): task.dependsOn stores STEP IDs (e.g. "execute"), not
@@ -736,9 +555,7 @@ function dagReadyTaskIds(
736
555
  return getDagReadyTasks(plan, completedIds);
737
556
  }
738
557
 
739
- export async function executeTeamRun(
740
- input: ExecuteTeamRunInput,
741
- ): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
558
+ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
742
559
  const workflow = input.workflow;
743
560
 
744
561
  // DEFENSE-IN-DEPTH (advisory-only since v0.9.15): re-validate topology here in
@@ -748,30 +565,17 @@ export async function executeTeamRun(
748
565
  // Skip for synthetic direct-agent workflows (filePath="<generated>").
749
566
  if (workflow.filePath !== "<generated>") {
750
567
  // LAZY: defer preflight-validator import until the defense-in-depth guard actually runs.
751
- const { validateWorkflowUsage } = await import(
752
- "../workflows/preflight-validator.ts"
753
- );
568
+ const { validateWorkflowUsage } = await import("../workflows/preflight-validator.ts");
754
569
  const preflight = validateWorkflowUsage(workflow, {
755
570
  force: input.reliability?.forcePreflight === true,
756
571
  });
757
- if (
758
- preflight.level === "warn" ||
759
- preflight.level === "note" ||
760
- preflight.level === "info"
761
- ) {
762
- const icon =
763
- preflight.level === "warn"
764
- ? "⚠️ "
765
- : preflight.level === "note"
766
- ? "✅ "
767
- : "ℹ️ ";
572
+ if (preflight.level === "warn" || preflight.level === "note" || preflight.level === "info") {
573
+ const icon = preflight.level === "warn" ? "⚠️ " : preflight.level === "note" ? "✅ " : "ℹ️ ";
768
574
  console.warn(
769
575
  `${icon}[team-runner.preflight] ${preflight.level.toUpperCase()}: ${preflight.message} (workflow=${workflow.name})`,
770
576
  );
771
577
  if (preflight.suggestion) {
772
- console.warn(
773
- `[team-runner.preflight] → ${preflight.suggestion}`,
774
- );
578
+ console.warn(`[team-runner.preflight] → ${preflight.suggestion}`);
775
579
  }
776
580
  }
777
581
  }
@@ -779,9 +583,7 @@ export async function executeTeamRun(
779
583
  let manifest = updateRunStatus(
780
584
  input.manifest,
781
585
  "running",
782
- input.executeWorkers
783
- ? "Executing team workflow."
784
- : "Creating workflow prompts and placeholder results.",
586
+ input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.",
785
587
  );
786
588
 
787
589
  void registerRunPromise(manifest.runId);
@@ -791,10 +593,7 @@ export async function executeTeamRun(
791
593
  // (NO_PID_HEARTBEAT_STALE_MS). Previously only sub-task runners wrote
792
594
  // heartbeats; the team-level run had no heartbeat, so any multi-phase
793
595
  // workflow lasting >5min was marked stale and cancelled.
794
- const stopTeamHeartbeat = startTeamRunHeartbeat(
795
- manifest.stateRoot,
796
- manifest.runId,
797
- );
596
+ const stopTeamHeartbeat = startTeamRunHeartbeat(manifest.stateRoot, manifest.runId);
798
597
 
799
598
  const cleanupUsage = (): void => {
800
599
  for (const task of input.tasks) clearTrackedTaskUsage(task.id);
@@ -807,11 +606,7 @@ export async function executeTeamRun(
807
606
  // (and/or had a failed task) is a false-green. We expose goalAchieved on the
808
607
  // manifest + emit an event so the lie is never silent, and downgrade status
809
608
  // to "failed" only when a failed task corroborates it (conservative).
810
- const gaAssessment = assessGoalAchievement(
811
- result.manifest,
812
- result.tasks,
813
- workflow,
814
- );
609
+ const gaAssessment = assessGoalAchievement(result.manifest, result.tasks, workflow);
815
610
  const gaApplied = applyGoalAchievement(result.manifest, gaAssessment);
816
611
  if (gaApplied.manifest !== result.manifest) {
817
612
  result.manifest = gaApplied.manifest;
@@ -820,9 +615,7 @@ export async function executeTeamRun(
820
615
  } catch (persistError) {
821
616
  logInternalError(
822
617
  "team-runner.goalAchievement.persist",
823
- persistError instanceof Error
824
- ? persistError
825
- : new Error(String(persistError)),
618
+ persistError instanceof Error ? persistError : new Error(String(persistError)),
826
619
  `runId=${manifest.runId}`,
827
620
  );
828
621
  }
@@ -841,27 +634,15 @@ export async function executeTeamRun(
841
634
  if (gaApplied.downgraded)
842
635
  logInternalError(
843
636
  "team-runner.goalAchievement.falseGreen",
844
- new Error(
845
- gaApplied.manifest.goalAchievementNote ??
846
- "false-green detected",
847
- ),
637
+ new Error(gaApplied.manifest.goalAchievementNote ?? "false-green detected"),
848
638
  `runId=${manifest.runId}`,
849
639
  );
850
640
  stopTeamHeartbeat();
851
641
  resolveRunPromise(manifest.runId, result);
852
642
  cleanupUsage();
853
643
  // Terminate live agents for this run — agents are done when the run ends.
854
- void terminateLiveAgentsForRun(
855
- manifest.runId,
856
- "completed",
857
- appendEvent,
858
- manifest.eventsPath,
859
- ).catch((error) =>
860
- logInternalError(
861
- "team-runner.completed.terminate",
862
- error,
863
- `runId=${manifest.runId}`,
864
- ),
644
+ void terminateLiveAgentsForRun(manifest.runId, "completed", appendEvent, manifest.eventsPath).catch((error) =>
645
+ logInternalError("team-runner.completed.terminate", error, `runId=${manifest.runId}`),
865
646
  );
866
647
 
867
648
  // Emit run completion hook (100% reliable, fire-and-forget)
@@ -885,13 +666,16 @@ export async function executeTeamRun(
885
666
  if (afterRunReport.outcome === "block") {
886
667
  logInternalError(
887
668
  "team-runner.after_run_complete.blocked",
888
- new Error(
889
- afterRunReport.reason ?? "after_run_complete hook blocked",
890
- ),
669
+ new Error(afterRunReport.reason ?? "after_run_complete hook blocked"),
891
670
  `runId=${manifest.runId}`,
892
671
  );
893
672
  }
894
673
 
674
+ // M7: flush buffered task.progress events so the final state is durable
675
+ // before the run returns. Buffered producer wins on latency (p95≈0µs);
676
+ // this single flush at run-end coalesces any pending progress bursts
677
+ // before manifest updates are observed by readers.
678
+ await flushEventLogBuffer();
895
679
  return result;
896
680
  } catch (error) {
897
681
  // Round 27 (BUG 1): the success path calls stopTeamHeartbeat() but this
@@ -905,23 +689,25 @@ export async function executeTeamRun(
905
689
  stopTeamHeartbeat();
906
690
  // P1: Catch unhandled errors — ensure manifest/tasks/agents are terminal so they don't stay "running" forever.
907
691
  const message = error instanceof Error ? error.message : String(error);
908
- // Reload manifest with lock to avoid stale data overwriting concurrent writes.
909
- // If lock acquisition fails, use in-memory data rather than stale disk data.
910
- let loaded;
911
- try {
912
- loaded = await withRunLock(input.manifest, async () =>
913
- loadRunManifestById(input.manifest.cwd, input.manifest.runId),
914
- );
915
- } catch {
916
- loaded = undefined; // best-effort: use in-memory data if lock fails
917
- }
918
- const freshManifest = loaded?.manifest ?? manifest;
919
- const freshTasks = refreshTaskGraphQueues(loaded?.tasks ?? input.tasks);
692
+ // FIX (2026-07-02): drop withRunLock here entirely.
693
+ // Previously this catch path acquired withRunLock to reload manifest+tasks
694
+ // from disk (best-effort with in-memory fallback). But the closeout path at
695
+ // line ~1596 ALSO holds the same run lock for its final save — when a late
696
+ // failure fires during closeout (e.g. async hook error after run.completed),
697
+ // this catch path can hit `Run 'run.lock' is locked by another operation.`
698
+ // and the error propagates as "Unhandled error in team runner" after the
699
+ // run has actually completed. Lock acquisition here is unnecessary —
700
+ // the closeout writes through to disk before this catch fires (or after —
701
+ // either is fine), and the in-memory state already contains the latest
702
+ // post-completion manifest/tasks. We use in-memory state unconditionally.
703
+ // The stale-data concern is mitigated by the dispatcher having re-read disk
704
+ // state under lock at line ~1364 for each iteration; the manifest+task
705
+ // objects in the calling scope are already post-merge.
706
+ const freshManifest = manifest;
707
+ const freshTasks = refreshTaskGraphQueues(input.tasks);
920
708
  const failedAt = new Date().toISOString();
921
709
  const tasks = freshTasks.map((task) =>
922
- task.status === "running" ||
923
- task.status === "queued" ||
924
- task.status === "waiting"
710
+ task.status === "running" || task.status === "queued" || task.status === "waiting"
925
711
  ? {
926
712
  ...task,
927
713
  status: "failed" as const,
@@ -932,53 +718,28 @@ export async function executeTeamRun(
932
718
  );
933
719
  manifest = freshManifest;
934
720
  try {
935
- await terminateLiveAgentsForRun(
936
- manifest.runId,
937
- "failed",
938
- appendEvent,
939
- manifest.eventsPath,
940
- );
721
+ await terminateLiveAgentsForRun(manifest.runId, "failed", appendEvent, manifest.eventsPath);
941
722
  await saveRunTasksAsync(manifest, tasks);
942
- const existingRuntimeByTask = new Map(
943
- readCrewAgents(manifest).map((agent) => [
944
- agent.taskId,
945
- agent.runtime,
946
- ]),
947
- );
723
+ const existingRuntimeByTask = new Map(readCrewAgents(manifest).map((agent) => [agent.taskId, agent.runtime]));
948
724
  const globalRuntime = input.runtime?.kind ?? "child-process";
949
- const runtimeForAgent = (
950
- agent: ReturnType<typeof recordsForMaterializedTasks>[number],
951
- ): CrewRuntimeKind => {
725
+ const runtimeForAgent = (agent: ReturnType<typeof recordsForMaterializedTasks>[number]): CrewRuntimeKind => {
952
726
  const task = tasks.find((item) => item.id === agent.taskId);
953
727
  return (
954
728
  existingRuntimeByTask.get(agent.taskId) ??
955
- resolveTaskRuntimeKind(
956
- globalRuntime,
957
- task?.role ?? agent.role,
958
- input.runtimeConfig?.isolationPolicy,
959
- )
729
+ resolveTaskRuntimeKind(globalRuntime, task?.role ?? agent.role, input.runtimeConfig?.isolationPolicy)
960
730
  );
961
731
  };
962
732
  saveCrewAgents(
963
733
  manifest,
964
- recordsForMaterializedTasks(manifest, tasks, globalRuntime).map(
965
- (agent) => ({ ...agent, runtime: runtimeForAgent(agent) }),
966
- ),
967
- );
968
- manifest = updateRunStatus(
969
- manifest,
970
- "failed",
971
- `Unhandled error in team runner: ${message}`,
734
+ recordsForMaterializedTasks(manifest, tasks, globalRuntime).map((agent) => ({ ...agent, runtime: runtimeForAgent(agent) })),
972
735
  );
736
+ manifest = updateRunStatus(manifest, "failed", `Unhandled error in team runner: ${message}`);
973
737
  await saveRunManifestAsync(manifest);
974
738
  } catch {
975
739
  // Best-effort — state write may also fail
976
740
  }
977
741
  const result = { manifest, tasks };
978
- rejectRunPromise(
979
- manifest.runId,
980
- error instanceof Error ? error : new Error(message),
981
- );
742
+ rejectRunPromise(manifest.runId, error instanceof Error ? error : new Error(message));
982
743
  crewHooks.emit({
983
744
  type: "run_failed",
984
745
  timestamp: new Date().toISOString(),
@@ -986,6 +747,9 @@ export async function executeTeamRun(
986
747
  data: { status: manifest.status, error: message },
987
748
  });
988
749
  cleanupUsage();
750
+ // M7: flush buffered events before returning on the error path so the
751
+ // final buffered progress events are durable alongside the failure state.
752
+ await flushEventLogBuffer();
989
753
  return result;
990
754
  }
991
755
  }
@@ -1002,11 +766,7 @@ async function executeTeamRunCore(
1002
766
  });
1003
767
  appendHookEvent(manifest, beforeRunReport);
1004
768
  if (beforeRunReport.outcome === "block") {
1005
- manifest = updateRunStatus(
1006
- manifest,
1007
- "blocked",
1008
- beforeRunReport.reason ?? "before_run_start hook blocked the run.",
1009
- );
769
+ manifest = updateRunStatus(manifest, "blocked", beforeRunReport.reason ?? "before_run_start hook blocked the run.");
1010
770
  return { manifest, tasks: input.tasks };
1011
771
  }
1012
772
  let tasks = refreshTaskGraphQueues(input.tasks);
@@ -1015,12 +775,7 @@ async function executeTeamRunCore(
1015
775
  let adaptivePlanInjected = false;
1016
776
  let adaptivePlanMissing = false;
1017
777
  const attemptAdaptivePlan = () => {
1018
- if (
1019
- !canInjectAdaptivePlan ||
1020
- adaptivePlanInjected ||
1021
- adaptivePlanMissing
1022
- )
1023
- return { injected: false, missing: false };
778
+ if (!canInjectAdaptivePlan || adaptivePlanInjected || adaptivePlanMissing) return { injected: false, missing: false };
1024
779
  const adaptivePlan = injectAdaptivePlanIfReady({
1025
780
  manifest,
1026
781
  tasks,
@@ -1038,69 +793,38 @@ async function executeTeamRunCore(
1038
793
  };
1039
794
  const initialAdaptive = attemptAdaptivePlan();
1040
795
  if (initialAdaptive.missing) {
1041
- tasks = markBlocked(
1042
- tasks,
1043
- "Adaptive planner did not produce a valid subagent plan.",
1044
- );
796
+ tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
1045
797
  await saveRunTasksAsync(manifest, tasks);
1046
- manifest = updateRunStatus(
1047
- manifest,
1048
- "blocked",
1049
- "Adaptive planner did not produce a valid subagent plan.",
1050
- );
798
+ manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
1051
799
  return { manifest, tasks };
1052
800
  }
1053
801
  if (initialAdaptive.injected) {
1054
- manifest = requiresPlanApproval(workflow, input.runtimeConfig)
1055
- ? ensurePlanApprovalRequested(manifest, tasks)
1056
- : manifest;
802
+ manifest = requiresPlanApproval(workflow, input.runtimeConfig) ? ensurePlanApprovalRequested(manifest, tasks) : manifest;
1057
803
  queueIndex = buildTaskGraphIndex(tasks);
1058
804
  } else if (
1059
805
  requiresPlanApproval(workflow, input.runtimeConfig) &&
1060
- (hasPendingMutatingAdaptiveTask(tasks) ||
1061
- hasPendingMutatingTaskAtBoundary(tasks))
806
+ (hasPendingMutatingAdaptiveTask(tasks) || hasPendingMutatingTaskAtBoundary(tasks))
1062
807
  ) {
1063
808
  manifest = ensurePlanApprovalRequested(manifest, tasks);
1064
809
  }
1065
810
  if (manifest.planApproval?.status === "cancelled") {
1066
811
  tasks = cancelPlanTasks(tasks, "Plan approval was cancelled.");
1067
812
  await saveRunTasksAsync(manifest, tasks);
1068
- manifest = updateRunStatus(
1069
- manifest,
1070
- "cancelled",
1071
- "Plan approval was cancelled.",
1072
- );
813
+ manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
1073
814
  return { manifest, tasks };
1074
815
  }
1075
- manifest = writeProgress(
1076
- manifest,
1077
- tasks,
1078
- "team-runner",
1079
- input.executeWorkers,
1080
- input.runtimeConfig,
1081
- );
816
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
1082
817
  await saveRunManifestAsync(manifest);
1083
- const runtimeKind =
1084
- input.runtime?.kind ??
1085
- (input.executeWorkers ? "child-process" : "scaffold");
1086
- saveCrewAgents(
1087
- manifest,
1088
- recordsForMaterializedTasks(manifest, tasks, runtimeKind),
1089
- );
818
+ const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
819
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
1090
820
 
1091
821
  // Build a workflow phase state machine from workflow steps for precondition tracking.
1092
822
  const workflowPhases: PhaseState[] = workflow.steps.map(
1093
823
  (step): PhaseState => ({
1094
824
  name: step.id,
1095
825
  status: "pending",
1096
- inputs:
1097
- step.reads === false
1098
- ? []
1099
- : Array.isArray(step.reads)
1100
- ? step.reads
1101
- : [],
1102
- outputs:
1103
- step.output === false ? [] : step.output ? [step.output] : [],
826
+ inputs: step.reads === false ? [] : Array.isArray(step.reads) ? step.reads : [],
827
+ outputs: step.output === false ? [] : step.output ? [step.output] : [],
1104
828
  }),
1105
829
  );
1106
830
  let wfMachine = createWorkflowStateMachine(workflowPhases);
@@ -1111,12 +835,7 @@ async function executeTeamRunCore(
1111
835
  const message = `${cancelReason.message} (${cancelReason.code})`;
1112
836
  const cancelledTaskIds: string[] = [];
1113
837
  tasks = tasks.map((task) => {
1114
- if (
1115
- task.status !== "queued" &&
1116
- task.status !== "running" &&
1117
- task.status !== "waiting"
1118
- )
1119
- return task;
838
+ if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") return task;
1120
839
  cancelledTaskIds.push(task.id);
1121
840
  const base = {
1122
841
  ...task,
@@ -1129,11 +848,7 @@ async function executeTeamRunCore(
1129
848
  ...base,
1130
849
  terminalEvidence: [
1131
850
  ...(task.terminalEvidence ?? []),
1132
- buildSyntheticTerminalEvidence(
1133
- "worker",
1134
- cancelReason,
1135
- task.startedAt,
1136
- ),
851
+ buildSyntheticTerminalEvidence("worker", cancelReason, task.startedAt),
1137
852
  ],
1138
853
  };
1139
854
  }
@@ -1191,20 +906,10 @@ async function executeTeamRunCore(
1191
906
  });
1192
907
  continue; // loop re-processes the re-queued task
1193
908
  }
1194
- tasks = markBlocked(
1195
- tasks,
1196
- `Blocked by failed task '${failed.id}'.`,
1197
- );
909
+ tasks = markBlocked(tasks, `Blocked by failed task '${failed.id}'.`);
1198
910
  await saveRunTasksAsync(manifest, tasks);
1199
- saveCrewAgents(
1200
- manifest,
1201
- recordsForMaterializedTasks(manifest, tasks, runtimeKind),
1202
- );
1203
- manifest = updateRunStatus(
1204
- manifest,
1205
- "failed",
1206
- `Failed at task '${failed.id}'.`,
1207
- );
911
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
912
+ manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
1208
913
  return { manifest, tasks };
1209
914
  }
1210
915
 
@@ -1213,47 +918,27 @@ async function executeTeamRunCore(
1213
918
  // DAG-based execution plan: when tasks have explicit dependsOn, use the
1214
919
  // topological wave planner to determine ready tasks. Fall back to the
1215
920
  // existing task-graph-scheduler when no explicit deps exist (backward compat).
1216
- const completedIds = new Set(
1217
- tasks
1218
- .filter(
1219
- (t) =>
1220
- t.status === "completed" ||
1221
- t.status === "needs_attention",
1222
- )
1223
- .map((t) => t.id),
1224
- );
921
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed" || t.status === "needs_attention").map((t) => t.id));
1225
922
  const dagReady = dagReadyTaskIds(tasks, completedIds);
1226
- const effectiveReady = dagReady ?? snapshot.ready;
923
+ const readyBeforeFilter = dagReady ?? snapshot.ready;
1227
924
 
1228
925
  // Workflow phase precondition check (non-blocking: log warnings only).
1229
926
  if (wfMachine.currentPhaseIndex < wfMachine.phases.length) {
1230
- const completedArtifacts = manifest.artifacts
1231
- .filter((a) => a.kind === "result" || a.kind === "summary")
1232
- .map((a) => a.path);
927
+ const completedArtifacts = manifest.artifacts.filter((a) => a.kind === "result" || a.kind === "summary").map((a) => a.path);
1233
928
  const previousPhaseStatus =
1234
- wfMachine.currentPhaseIndex > 0
1235
- ? (wfMachine.phases[wfMachine.currentPhaseIndex - 1]
1236
- ?.status ?? "pending")
1237
- : "completed";
929
+ wfMachine.currentPhaseIndex > 0 ? (wfMachine.phases[wfMachine.currentPhaseIndex - 1]?.status ?? "pending") : "completed";
1238
930
  const wfContext: PhaseGuardContext = {
1239
931
  completedArtifacts,
1240
932
  previousPhaseStatus,
1241
933
  taskResults: tasks
1242
- .filter(
1243
- (t) =>
1244
- t.status === "completed" ||
1245
- t.status === "needs_attention",
1246
- )
934
+ .filter((t) => t.status === "completed" || t.status === "needs_attention")
1247
935
  .map((t) => ({
1248
936
  taskId: t.id,
1249
937
  status: t.status,
1250
938
  outputPath: t.resultArtifact?.path,
1251
939
  })),
1252
940
  };
1253
- const preconditions = validatePhasePreconditions(
1254
- wfMachine,
1255
- wfContext,
1256
- );
941
+ const preconditions = validatePhasePreconditions(wfMachine, wfContext);
1257
942
  if (!preconditions.ready) {
1258
943
  await appendEventAsync(manifest.eventsPath, {
1259
944
  type: "workflow.preconditions",
@@ -1261,8 +946,7 @@ async function executeTeamRunCore(
1261
946
  message: `Workflow phase '${wfMachine.phases[wfMachine.currentPhaseIndex]?.name}' is missing inputs: ${preconditions.blocking.join(", ")}`,
1262
947
  data: {
1263
948
  phaseIndex: wfMachine.currentPhaseIndex,
1264
- phaseName:
1265
- wfMachine.phases[wfMachine.currentPhaseIndex]?.name,
949
+ phaseName: wfMachine.phases[wfMachine.currentPhaseIndex]?.name,
1266
950
  blocking: preconditions.blocking,
1267
951
  },
1268
952
  });
@@ -1270,8 +954,7 @@ async function executeTeamRunCore(
1270
954
  // Advance the machine past completed phases.
1271
955
  while (
1272
956
  wfMachine.currentPhaseIndex < wfMachine.phases.length &&
1273
- wfMachine.phases[wfMachine.currentPhaseIndex]?.status ===
1274
- "completed"
957
+ wfMachine.phases[wfMachine.currentPhaseIndex]?.status === "completed"
1275
958
  ) {
1276
959
  wfMachine = {
1277
960
  ...wfMachine,
@@ -1281,7 +964,7 @@ async function executeTeamRunCore(
1281
964
  }
1282
965
  }
1283
966
 
1284
- const readyRoles = effectiveReady
967
+ const readyRoles = readyBeforeFilter
1285
968
  .map((taskId) => tasks.find((task) => task.id === taskId)?.role)
1286
969
  .filter((role): role is string => Boolean(role));
1287
970
  const concurrency = resolveBatchConcurrency({
@@ -1290,16 +973,55 @@ async function executeTeamRunCore(
1290
973
  teamMaxConcurrency: input.team.maxConcurrency,
1291
974
  limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers,
1292
975
  allowUnboundedConcurrency: input.limits?.allowUnboundedConcurrency,
1293
- readyCount: effectiveReady.length,
976
+ readyCount: readyBeforeFilter.length,
1294
977
  workspaceMode: manifest.workspaceMode,
1295
978
  readyRoles,
1296
979
  });
980
+
981
+ // Round 25 (M5): serialize on write-path overlap when opted in.
982
+ // Opt-in via limits.serializeOnPathOverlap; default off (= no behavior change).
983
+ // filterReadyByWriteOverlap returns the same array when enabled=false, so
984
+ // production runs pay nothing for the unused code path. When the flag is on,
985
+ // `serializedReady` MAY be a strict subset of `readyBeforeFilter` (conflicting tasks
986
+ // deferred to next cycle).
987
+ const serializedReady = filterReadyByWriteOverlap(
988
+ readyBeforeFilter,
989
+ tasks,
990
+ workflow,
991
+ concurrency.maxConcurrent,
992
+ input.limits?.serializeOnPathOverlap === true,
993
+ );
994
+
995
+ // Round 25 (M6): coalesce micro-tasks when opted in.
996
+ // Default off; when on, groups same-(role,cwd) tasks into coalesced groups
997
+ // (with write-path safety). In v0.9.17 first ship, we ONLY log the
998
+ // coalesced group count to the event stream (informational). Actual
999
+ // dispatching of one-multi-task worker instead of N workers is deferred
1000
+ // to a follow-up — it's a non-trivial prompt-construction change that
1001
+ // deserves its own PR. For now, every coalesced group => one info event.
1002
+ const coalesceEnabled = workflow.coalesceMicroTasks === true;
1003
+ if (coalesceEnabled) {
1004
+ const coalescedGroups = planCoalescedGroups(serializedReady, tasks, workflow, true);
1005
+ for (const group of coalescedGroups) {
1006
+ if (group.tasks.length < 2) continue; // singletons are not interesting
1007
+ await appendEventAsync(manifest.eventsPath, {
1008
+ type: "task.coalesced",
1009
+ runId: manifest.runId,
1010
+ message: `Coalesced ${group.tasks.length} micro-tasks (role=${group.role}, cwd=${group.cwd})`,
1011
+ data: {
1012
+ groupId: group.id,
1013
+ role: group.role,
1014
+ cwd: group.cwd,
1015
+ taskIds: group.tasks.map((task) => task.id),
1016
+ },
1017
+ });
1018
+ }
1019
+ }
1297
1020
  if (concurrency.reason.includes(";unbounded:")) {
1298
1021
  await appendEventAsync(manifest.eventsPath, {
1299
1022
  type: "limits.unbounded",
1300
1023
  runId: manifest.runId,
1301
- message:
1302
- "Unbounded worker concurrency was explicitly enabled for this run.",
1024
+ message: "Unbounded worker concurrency was explicitly enabled for this run.",
1303
1025
  data: {
1304
1026
  concurrencyReason: concurrency.reason,
1305
1027
  maxConcurrent: concurrency.maxConcurrent,
@@ -1307,50 +1029,29 @@ async function executeTeamRunCore(
1307
1029
  });
1308
1030
  }
1309
1031
  const approvalPending = isPlanApprovalPending(manifest);
1310
- const readyIds = approvalPending
1311
- ? effectiveReady
1312
- : effectiveReady.slice(0, concurrency.selectedCount);
1032
+ const readyIds = approvalPending ? serializedReady : serializedReady.slice(0, concurrency.selectedCount);
1313
1033
  const candidateBatch = readyIds
1314
1034
  .map((id) => tasks.find((task) => task.id === id))
1315
1035
  .filter((task): task is TeamTaskState => Boolean(task));
1316
1036
  const readyBatch = approvalPending
1317
- ? candidateBatch
1318
- .filter((task) => !isMutatingTask(task))
1319
- .slice(0, concurrency.selectedCount)
1037
+ ? candidateBatch.filter((task) => !isMutatingTask(task)).slice(0, concurrency.selectedCount)
1320
1038
  : candidateBatch;
1321
1039
  if (readyBatch.length === 0) {
1322
1040
  if (approvalPending && candidateBatch.some(isMutatingTask)) {
1323
1041
  await saveRunTasksAsync(manifest, tasks);
1324
- saveCrewAgents(
1325
- manifest,
1326
- recordsForMaterializedTasks(manifest, tasks, runtimeKind),
1327
- );
1328
- manifest = updateRunStatus(
1329
- manifest,
1330
- "blocked",
1331
- "Plan approval required before mutating implementation tasks run.",
1332
- );
1042
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
1043
+ manifest = updateRunStatus(manifest, "blocked", "Plan approval required before mutating implementation tasks run.");
1333
1044
  return { manifest, tasks };
1334
1045
  }
1335
- tasks = markBlocked(
1336
- tasks,
1337
- "No ready queued task; dependency graph may be invalid.",
1338
- );
1046
+ tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
1339
1047
  await saveRunTasksAsync(manifest, tasks);
1340
- saveCrewAgents(
1341
- manifest,
1342
- recordsForMaterializedTasks(manifest, tasks, runtimeKind),
1343
- );
1344
- manifest = updateRunStatus(
1345
- manifest,
1346
- "blocked",
1347
- "No ready queued task.",
1348
- );
1048
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
1049
+ manifest = updateRunStatus(manifest, "blocked", "No ready queued task.");
1349
1050
  return { manifest, tasks };
1350
1051
  }
1351
1052
 
1352
- // 2.2 caller migration: batch progress is high-frequency informational.
1353
- appendEventFireAndForget(manifest.eventsPath, {
1053
+ // 2.2 caller migration: batch progress is high-frequency informational (M7 wire).
1054
+ void appendEventBuffered(manifest.eventsPath, {
1354
1055
  type: "task.progress",
1355
1056
  runId: manifest.runId,
1356
1057
  message: `Starting ready batch with ${readyBatch.length} task(s).`,
@@ -1363,9 +1064,7 @@ async function executeTeamRunCore(
1363
1064
  selectedCount: readyBatch.length,
1364
1065
  maxConcurrent: concurrency.maxConcurrent,
1365
1066
  defaultConcurrency: concurrency.defaultConcurrency,
1366
- concurrencyReason: approvalPending
1367
- ? `${concurrency.reason};plan-approval-read-only`
1368
- : concurrency.reason,
1067
+ concurrencyReason: approvalPending ? `${concurrency.reason};plan-approval-read-only` : concurrency.reason,
1369
1068
  },
1370
1069
  });
1371
1070
  // Execute before_task_start hooks for the batch
@@ -1382,22 +1081,14 @@ async function executeTeamRunCore(
1382
1081
  ? {
1383
1082
  ...t,
1384
1083
  status: "skipped" as const,
1385
- error:
1386
- taskReport.reason ??
1387
- "before_task_start hook blocked execution.",
1084
+ error: taskReport.reason ?? "before_task_start hook blocked execution.",
1388
1085
  }
1389
1086
  : t,
1390
1087
  );
1391
- manifest = updateRunStatus(
1392
- manifest,
1393
- manifest.status,
1394
- `Task '${task.id}' blocked by hook.`,
1395
- );
1088
+ manifest = updateRunStatus(manifest, manifest.status, `Task '${task.id}' blocked by hook.`);
1396
1089
  }
1397
1090
  }
1398
- const batchTasks = readyBatch.filter((task) =>
1399
- tasks.find((t) => t.id === task.id && t.status !== "skipped"),
1400
- );
1091
+ const batchTasks = readyBatch.filter((task) => tasks.find((t) => t.id === task.id && t.status !== "skipped"));
1401
1092
  if (batchTasks.length > 1) {
1402
1093
  await appendEventAsync(manifest.eventsPath, {
1403
1094
  type: "task.parallel_start",
@@ -1410,327 +1101,253 @@ async function executeTeamRunCore(
1410
1101
  },
1411
1102
  });
1412
1103
  }
1413
- const results = await mapConcurrent(
1414
- batchTasks,
1415
- concurrency.selectedCount,
1416
- async (task) => {
1417
- const step = findStep(workflow, task);
1418
- const agent = findAgent(input.agents, task);
1419
- const teamRole = input.team.roles.find(
1420
- (role) => role.name === task.role,
1421
- );
1422
- const perTaskRuntime = resolveTaskRuntimeKind(
1423
- runtimeKind,
1424
- task.role,
1425
- input.runtimeConfig?.isolationPolicy,
1426
- );
1427
- const baseInput = {
1104
+
1105
+ // M6 real dispatch: when coalesceMicroTasks is enabled, batch the
1106
+ // ready tasks into dispatch units. Multi-task groups are dispatched
1107
+ // as one worker (single cold-start) instead of N. Singletons fall
1108
+ // through to per-task dispatch.
1109
+ const coalescedGroups = planCoalescedGroups(
1110
+ batchTasks.map((t) => t.id),
1111
+ tasks,
1112
+ workflow,
1113
+ coalesceEnabled,
1114
+ );
1115
+ const dispatchUnits = buildDispatchUnits(
1116
+ batchTasks.map((t) => t.id),
1117
+ coalescedGroups,
1118
+ );
1119
+
1120
+ const results = await mapConcurrent(dispatchUnits, concurrency.selectedCount, async (unit) => {
1121
+ // M6 real dispatch path: single worker for N tasks.
1122
+ if (unit.kind === "group") {
1123
+ const groupTasks = unit.group.tasks;
1124
+ const firstTask = groupTasks[0]!;
1125
+ const step = findStep(workflow, firstTask);
1126
+ const agent = findAgent(input.agents, firstTask);
1127
+ const teamRole = input.team.roles.find((role) => role.name === firstTask.role);
1128
+ const perTaskRuntime = resolveTaskRuntimeKind(runtimeKind, firstTask.role, input.runtimeConfig?.isolationPolicy);
1129
+ return runCoalescedTaskGroup({
1428
1130
  manifest,
1429
1131
  tasks,
1430
- task,
1132
+ groupTasks,
1431
1133
  step,
1432
1134
  agent,
1433
1135
  signal: input.signal,
1434
1136
  executeWorkers: input.executeWorkers,
1435
- runtimeKind: runtimeKind,
1436
- taskRuntimeOverride:
1437
- perTaskRuntime !== runtimeKind
1438
- ? perTaskRuntime
1439
- : undefined,
1440
- runtimeConfig: input.runtimeConfig,
1441
- parentContext: input.parentContext,
1442
- parentModel: input.parentModel,
1443
- modelRegistry: input.modelRegistry,
1444
- modelOverride: input.modelOverride,
1445
- teamRoleModel: teamRole?.model,
1446
- teamRoleSkills: teamRole?.skills,
1447
- skillOverride: input.skillOverride,
1448
- limits: input.limits,
1449
- onJsonEvent: input.onJsonEvent,
1137
+ runtimeKind,
1450
1138
  workspaceId: input.workspaceId,
1451
- };
1452
- // #1 (assessment): autoRetry now defaults ON (opt-out via reliability.autoRetry=false).
1453
- // The dominant v0.9.13 failure was ChildTimeout ("worker became unresponsive") with
1454
- // ZERO retries because this gate was opt-in. isRetryable() defaults to true when
1455
- // retryableErrors is empty, so transient hangs now retry up to maxAttempts (3) with
1456
- // exponential backoff. Set reliability.autoRetry=false to restore old single-shot behavior.
1457
- if (!shouldUseRetry(input.reliability))
1458
- return withCorrelation(
1459
- childCorrelation(manifest.runId, task.id),
1460
- () => runTeamTask(baseInput),
1461
- );
1462
- let lastFailed:
1463
- | { manifest: TeamRunManifest; tasks: TeamTaskState[] }
1464
- | undefined;
1465
- let lastAttemptId: string | undefined;
1466
- const attemptsSoFar: TaskAttemptState[] = [
1467
- ...(task.attempts ?? []),
1468
- ];
1469
- const policy = retryPolicyFromConfig(input.reliability);
1470
- try {
1471
- return await executeWithRetry(
1472
- async (attempt, info) => {
1473
- const startedAt = new Date().toISOString();
1474
- const inFlightAttempts: TaskAttemptState[] = [
1475
- ...attemptsSoFar,
1476
- { attemptId: info.attemptId, startedAt },
1477
- ];
1478
- input.metricRegistry
1479
- ?.counter(
1480
- "crew.task.retry_attempt_total",
1481
- "Retry attempts by run and task",
1482
- )
1483
- .inc({
1139
+ onJsonEvent: input.onJsonEvent,
1140
+ teamRole,
1141
+ perTaskRuntime,
1142
+ });
1143
+ }
1144
+ // Singleton path: original per-task dispatch.
1145
+ const task = batchTasks.find((t) => t.id === unit.taskId)!;
1146
+ const step = findStep(workflow, task);
1147
+ const agent = findAgent(input.agents, task);
1148
+ const teamRole = input.team.roles.find((role) => role.name === task.role);
1149
+ const perTaskRuntime = resolveTaskRuntimeKind(runtimeKind, task.role, input.runtimeConfig?.isolationPolicy);
1150
+ const baseInput = {
1151
+ manifest,
1152
+ tasks,
1153
+ task,
1154
+ step,
1155
+ agent,
1156
+ signal: input.signal,
1157
+ executeWorkers: input.executeWorkers,
1158
+ runtimeKind: runtimeKind,
1159
+ taskRuntimeOverride: perTaskRuntime !== runtimeKind ? perTaskRuntime : undefined,
1160
+ runtimeConfig: input.runtimeConfig,
1161
+ parentContext: input.parentContext,
1162
+ parentModel: input.parentModel,
1163
+ modelRegistry: input.modelRegistry,
1164
+ modelOverride: input.modelOverride,
1165
+ teamRoleModel: teamRole?.model,
1166
+ teamRoleSkills: teamRole?.skills,
1167
+ skillOverride: input.skillOverride,
1168
+ limits: input.limits,
1169
+ onJsonEvent: input.onJsonEvent,
1170
+ workspaceId: input.workspaceId,
1171
+ };
1172
+ // #1 (assessment): autoRetry now defaults ON (opt-out via reliability.autoRetry=false).
1173
+ // The dominant v0.9.13 failure was ChildTimeout ("worker became unresponsive") with
1174
+ // ZERO retries because this gate was opt-in. isRetryable() defaults to true when
1175
+ // retryableErrors is empty, so transient hangs now retry up to maxAttempts (3) with
1176
+ // exponential backoff. Set reliability.autoRetry=false to restore old single-shot behavior.
1177
+ if (!shouldUseRetry(input.reliability))
1178
+ return withCorrelation(childCorrelation(manifest.runId, task.id), () => runTeamTask(baseInput));
1179
+ let lastFailed: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
1180
+ let lastAttemptId: string | undefined;
1181
+ const attemptsSoFar: TaskAttemptState[] = [...(task.attempts ?? [])];
1182
+ const policy = retryPolicyFromConfig(input.reliability);
1183
+ try {
1184
+ return await executeWithRetry(
1185
+ async (attempt, info) => {
1186
+ const startedAt = new Date().toISOString();
1187
+ const inFlightAttempts: TaskAttemptState[] = [...attemptsSoFar, { attemptId: info.attemptId, startedAt }];
1188
+ input.metricRegistry?.counter("crew.task.retry_attempt_total", "Retry attempts by run and task").inc({
1189
+ runId: manifest.runId,
1190
+ taskId: task.id,
1191
+ });
1192
+ // NOTE: no withRunLock — best-effort only; concurrent writes may cause inconsistency
1193
+ const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
1194
+ const freshManifest = fresh?.manifest ?? manifest;
1195
+ const freshTasks = fresh?.tasks ?? tasks;
1196
+ const freshTask = freshTasks.find((item) => item.id === task.id) ?? task;
1197
+ if (freshTask.status !== "queued" && freshTask.status !== "running")
1198
+ return {
1199
+ manifest: freshManifest,
1200
+ tasks: freshTasks,
1201
+ };
1202
+ const taskWithAttempt: TeamTaskState = {
1203
+ ...freshTask,
1204
+ attempts: inFlightAttempts,
1205
+ };
1206
+ const result = await withCorrelation(childCorrelation(freshManifest.runId, task.id), () =>
1207
+ runTeamTask({
1208
+ ...baseInput,
1209
+ manifest: freshManifest,
1210
+ tasks: freshTasks,
1211
+ task: taskWithAttempt,
1212
+ }),
1213
+ );
1214
+ const failed = failedTaskFrom(result, task.id);
1215
+ const endedAt = new Date().toISOString();
1216
+ const finishedAttempt: TaskAttemptState = {
1217
+ attemptId: info.attemptId,
1218
+ startedAt,
1219
+ endedAt,
1220
+ ...(failed?.error ? { error: failed.error } : {}),
1221
+ };
1222
+ attemptsSoFar.push(finishedAttempt);
1223
+ const withAttempt = result.tasks.map((item) =>
1224
+ item.id === task.id ? { ...item, attempts: [...attemptsSoFar] } : item,
1225
+ );
1226
+ const enriched = {
1227
+ manifest: result.manifest,
1228
+ tasks: withAttempt,
1229
+ };
1230
+ if (failed) {
1231
+ lastFailed = enriched;
1232
+ throw new Error(failed.error ?? `Task ${task.id} failed.`);
1233
+ }
1234
+ input.metricRegistry?.histogram("crew.task.retry_count", "Retries per task", [0, 1, 2, 3, 5, 10]).observe(
1235
+ {
1236
+ runId: manifest.runId,
1237
+ team: input.team.name,
1238
+ },
1239
+ Math.max(0, attempt - 1),
1240
+ );
1241
+ return enriched;
1242
+ },
1243
+ policy,
1244
+ {
1245
+ signal: input.signal,
1246
+ attemptId: (attempt) => `${manifest.runId}:${task.id}:attempt-${attempt}`,
1247
+ onAttemptFailed: (attempt, error, delayMs, info) => {
1248
+ lastAttemptId = info.attemptId;
1249
+ appendEventAsync(manifest.eventsPath, {
1250
+ type: "crew.task.retry_attempt",
1251
+ runId: manifest.runId,
1252
+ taskId: task.id,
1253
+ message: error.message,
1254
+ data: {
1255
+ attempt,
1256
+ attemptId: info.attemptId,
1257
+ delayMs,
1258
+ },
1259
+ metadata: { attemptId: info.attemptId },
1260
+ }).catch((error) => logInternalError("team-runner.retry-attempt", error, `taskId=${task.id}`));
1261
+ input.metricRegistry?.histogram("crew.task.retry_delay_ms", "Retry backoff delay, milliseconds").observe(
1262
+ {
1484
1263
  runId: manifest.runId,
1485
1264
  taskId: task.id,
1486
- });
1487
- // NOTE: no withRunLock — best-effort only; concurrent writes may cause inconsistency
1488
- const fresh = loadRunManifestById(
1489
- manifest.cwd,
1490
- manifest.runId,
1491
- );
1492
- const freshManifest = fresh?.manifest ?? manifest;
1493
- const freshTasks = fresh?.tasks ?? tasks;
1494
- const freshTask =
1495
- freshTasks.find(
1496
- (item) => item.id === task.id,
1497
- ) ?? task;
1498
- if (
1499
- freshTask.status !== "queued" &&
1500
- freshTask.status !== "running"
1501
- )
1502
- return {
1503
- manifest: freshManifest,
1504
- tasks: freshTasks,
1505
- };
1506
- const taskWithAttempt: TeamTaskState = {
1507
- ...freshTask,
1508
- attempts: inFlightAttempts,
1509
- };
1510
- const result = await withCorrelation(
1511
- childCorrelation(freshManifest.runId, task.id),
1512
- () =>
1513
- runTeamTask({
1514
- ...baseInput,
1515
- manifest: freshManifest,
1516
- tasks: freshTasks,
1517
- task: taskWithAttempt,
1518
- }),
1265
+ },
1266
+ delayMs,
1519
1267
  );
1520
- const failed = failedTaskFrom(result, task.id);
1521
- const endedAt = new Date().toISOString();
1522
- const finishedAttempt: TaskAttemptState = {
1268
+ },
1269
+ onRetryGivenUp: (attempts, error, info) => {
1270
+ lastAttemptId = info.attemptId;
1271
+ appendDeadletter(manifest, {
1272
+ runId: manifest.runId,
1273
+ taskId: task.id,
1274
+ reason: "max-retries",
1275
+ attempts,
1523
1276
  attemptId: info.attemptId,
1524
- startedAt,
1525
- endedAt,
1526
- ...(failed?.error
1527
- ? { error: failed.error }
1528
- : {}),
1529
- };
1530
- attemptsSoFar.push(finishedAttempt);
1531
- const withAttempt = result.tasks.map((item) =>
1532
- item.id === task.id
1533
- ? { ...item, attempts: [...attemptsSoFar] }
1534
- : item,
1535
- );
1536
- const enriched = {
1537
- manifest: result.manifest,
1538
- tasks: withAttempt,
1539
- };
1540
- if (failed) {
1541
- lastFailed = enriched;
1542
- throw new Error(
1543
- failed.error ?? `Task ${task.id} failed.`,
1544
- );
1545
- }
1277
+ lastError: error.message,
1278
+ timestamp: new Date().toISOString(),
1279
+ });
1546
1280
  input.metricRegistry
1547
- ?.histogram(
1548
- "crew.task.retry_count",
1549
- "Retries per task",
1550
- [0, 1, 2, 3, 5, 10],
1551
- )
1552
- .observe(
1553
- {
1554
- runId: manifest.runId,
1555
- team: input.team.name,
1556
- },
1557
- Math.max(0, attempt - 1),
1558
- );
1559
- return enriched;
1560
- },
1561
- policy,
1562
- {
1563
- signal: input.signal,
1564
- attemptId: (attempt) =>
1565
- `${manifest.runId}:${task.id}:attempt-${attempt}`,
1566
- onAttemptFailed: (
1567
- attempt,
1568
- error,
1569
- delayMs,
1570
- info,
1571
- ) => {
1572
- lastAttemptId = info.attemptId;
1573
- appendEventAsync(manifest.eventsPath, {
1574
- type: "crew.task.retry_attempt",
1575
- runId: manifest.runId,
1576
- taskId: task.id,
1577
- message: error.message,
1578
- data: {
1579
- attempt,
1580
- attemptId: info.attemptId,
1581
- delayMs,
1582
- },
1583
- metadata: { attemptId: info.attemptId },
1584
- }).catch((error) =>
1585
- logInternalError(
1586
- "team-runner.retry-attempt",
1587
- error,
1588
- `taskId=${task.id}`,
1589
- ),
1590
- );
1591
- input.metricRegistry
1592
- ?.histogram(
1593
- "crew.task.retry_delay_ms",
1594
- "Retry backoff delay, milliseconds",
1595
- )
1596
- .observe(
1597
- {
1598
- runId: manifest.runId,
1599
- taskId: task.id,
1600
- },
1601
- delayMs,
1602
- );
1603
- },
1604
- onRetryGivenUp: (attempts, error, info) => {
1605
- lastAttemptId = info.attemptId;
1606
- appendDeadletter(manifest, {
1281
+ ?.counter("crew.task.deadletter_total", "Deadletter triggers by reason")
1282
+ .inc({ reason: "max-retries" });
1283
+ input.metricRegistry?.histogram("crew.task.retry_count", "Retries per task", [0, 1, 2, 3, 5, 10]).observe(
1284
+ {
1607
1285
  runId: manifest.runId,
1608
- taskId: task.id,
1609
- reason: "max-retries",
1610
- attempts,
1611
- attemptId: info.attemptId,
1612
- lastError: error.message,
1613
- timestamp: new Date().toISOString(),
1614
- });
1615
- input.metricRegistry
1616
- ?.counter(
1617
- "crew.task.deadletter_total",
1618
- "Deadletter triggers by reason",
1619
- )
1620
- .inc({ reason: "max-retries" });
1621
- input.metricRegistry
1622
- ?.histogram(
1623
- "crew.task.retry_count",
1624
- "Retries per task",
1625
- [0, 1, 2, 3, 5, 10],
1626
- )
1627
- .observe(
1628
- {
1629
- runId: manifest.runId,
1630
- team: input.team.name,
1631
- },
1632
- Math.max(0, attempts - 1),
1633
- );
1634
- },
1286
+ team: input.team.name,
1287
+ },
1288
+ Math.max(0, attempts - 1),
1289
+ );
1635
1290
  },
1636
- );
1637
- } catch (retryError) {
1638
- if (
1639
- retryError instanceof CrewCancellationError ||
1640
- input.signal?.aborted
1641
- ) {
1642
- const reason =
1643
- retryError instanceof CrewCancellationError
1644
- ? retryError.reason
1645
- : cancellationReasonFromSignal(input.signal);
1646
- // NOTE: no withRunLock — best-effort only; concurrent writes may cause inconsistency
1647
- const fresh = loadRunManifestById(
1648
- manifest.cwd,
1649
- manifest.runId,
1650
- );
1651
- const freshManifest = fresh?.manifest ?? manifest;
1652
- const freshTasks = fresh?.tasks ?? tasks;
1653
- const cancelledTasks = freshTasks.map((item) =>
1654
- item.id === task.id &&
1655
- (item.status === "queued" ||
1656
- item.status === "running")
1657
- ? {
1658
- ...item,
1659
- status: "cancelled" as const,
1660
- finishedAt: new Date().toISOString(),
1661
- error: `${reason.message} (${reason.code})`,
1662
- }
1663
- : item,
1664
- );
1665
- appendEventAsync(freshManifest.eventsPath, {
1666
- type: "task.cancelled",
1667
- runId: freshManifest.runId,
1668
- taskId: task.id,
1669
- message: reason.message,
1670
- data: { reason, phase: "retry" },
1671
- metadata: lastAttemptId
1672
- ? { attemptId: lastAttemptId }
1673
- : undefined,
1674
- }).catch((error) =>
1675
- logInternalError(
1676
- "team-runner.cancelled",
1677
- error,
1678
- `taskId=${task.id}`,
1679
- ),
1680
- );
1681
- return {
1682
- manifest: updateRunStatus(
1683
- freshManifest,
1684
- "cancelled",
1685
- reason.message,
1686
- ),
1687
- tasks: cancelledTasks,
1688
- };
1689
- }
1690
- if (lastFailed) return lastFailed;
1291
+ },
1292
+ );
1293
+ } catch (retryError) {
1294
+ if (retryError instanceof CrewCancellationError || input.signal?.aborted) {
1295
+ const reason =
1296
+ retryError instanceof CrewCancellationError ? retryError.reason : cancellationReasonFromSignal(input.signal);
1691
1297
  // NOTE: no withRunLock — best-effort only; concurrent writes may cause inconsistency
1692
- const fresh = loadRunManifestById(
1693
- manifest.cwd,
1694
- manifest.runId,
1695
- );
1298
+ const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
1696
1299
  const freshManifest = fresh?.manifest ?? manifest;
1697
1300
  const freshTasks = fresh?.tasks ?? tasks;
1698
- const freshTask =
1699
- freshTasks.find((item) => item.id === task.id) ?? task;
1700
- if (
1701
- freshTask.status !== "queued" &&
1702
- freshTask.status !== "running"
1703
- )
1704
- return { manifest: freshManifest, tasks: freshTasks };
1705
- return withCorrelation(
1706
- childCorrelation(freshManifest.runId, task.id),
1707
- () =>
1708
- runTeamTask({
1709
- ...baseInput,
1710
- manifest: freshManifest,
1711
- tasks: freshTasks,
1712
- task: freshTask,
1713
- }),
1301
+ const cancelledTasks = freshTasks.map((item) =>
1302
+ item.id === task.id && (item.status === "queued" || item.status === "running")
1303
+ ? {
1304
+ ...item,
1305
+ status: "cancelled" as const,
1306
+ finishedAt: new Date().toISOString(),
1307
+ error: `${reason.message} (${reason.code})`,
1308
+ }
1309
+ : item,
1714
1310
  );
1311
+ appendEventAsync(freshManifest.eventsPath, {
1312
+ type: "task.cancelled",
1313
+ runId: freshManifest.runId,
1314
+ taskId: task.id,
1315
+ message: reason.message,
1316
+ data: { reason, phase: "retry" },
1317
+ metadata: lastAttemptId ? { attemptId: lastAttemptId } : undefined,
1318
+ }).catch((error) => logInternalError("team-runner.cancelled", error, `taskId=${task.id}`));
1319
+ return {
1320
+ manifest: updateRunStatus(freshManifest, "cancelled", reason.message),
1321
+ tasks: cancelledTasks,
1322
+ };
1715
1323
  }
1716
- },
1717
- );
1324
+ if (lastFailed) return lastFailed;
1325
+ // NOTE: no withRunLock — best-effort only; concurrent writes may cause inconsistency
1326
+ const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
1327
+ const freshManifest = fresh?.manifest ?? manifest;
1328
+ const freshTasks = fresh?.tasks ?? tasks;
1329
+ const freshTask = freshTasks.find((item) => item.id === task.id) ?? task;
1330
+ if (freshTask.status !== "queued" && freshTask.status !== "running") return { manifest: freshManifest, tasks: freshTasks };
1331
+ return withCorrelation(childCorrelation(freshManifest.runId, task.id), () =>
1332
+ runTeamTask({
1333
+ ...baseInput,
1334
+ manifest: freshManifest,
1335
+ tasks: freshTasks,
1336
+ task: freshTask,
1337
+ }),
1338
+ );
1339
+ }
1340
+ });
1718
1341
  if (results.length === 0) break;
1719
1342
  // FIX: Filter out undefined entries from partial results when error occurred
1720
1343
  // during parallel execution. Other workers may have written partial results
1721
1344
  // before one threw. Results may be partial - some tasks in-flight at error
1722
1345
  // time will not have entries in the results array.
1723
- const validResults = results.filter(
1724
- (item): item is NonNullable<typeof item> => item !== undefined,
1725
- );
1346
+ const validResults = results.filter((item): item is NonNullable<typeof item> => item !== undefined);
1726
1347
  // Guard: if ALL parallel workers threw before returning, validResults is empty.
1727
1348
  // at(-1)! would crash. Mark the run failed rather than crashing.
1728
1349
  if (validResults.length === 0) {
1729
- manifest = updateRunStatus(
1730
- manifest,
1731
- "failed",
1732
- "All parallel tasks failed catastrophically.",
1733
- );
1350
+ manifest = updateRunStatus(manifest, "failed", "All parallel tasks failed catastrophically.");
1734
1351
  return { manifest, tasks };
1735
1352
  }
1736
1353
  // Reconstruct manifest from the last worker's snapshot. The .artifacts field
@@ -1750,21 +1367,13 @@ async function executeTeamRunCore(
1750
1367
  const disk = loadRunManifestById(manifest.cwd, manifest.runId);
1751
1368
  const diskManifest = disk?.manifest ?? manifest;
1752
1369
  const diskArtifacts = diskManifest.artifacts;
1753
- const reconciledArtifacts = mergeArtifacts(
1754
- [
1755
- ...diskArtifacts,
1756
- ...validResults.map((item) => item.manifest.artifacts),
1757
- ].flat(),
1758
- );
1370
+ const reconciledArtifacts = mergeArtifacts([...diskArtifacts, ...validResults.map((item) => item.manifest.artifacts)].flat());
1759
1371
  const resultManifest = updateRunStatus(
1760
1372
  { ...diskManifest, artifacts: reconciledArtifacts },
1761
1373
  "running",
1762
1374
  "Merged task updates from parallel batch.",
1763
1375
  );
1764
- const resultTasks = mergeTaskUpdatesPreservingTerminal(
1765
- tasks,
1766
- validResults,
1767
- );
1376
+ const resultTasks = mergeTaskUpdatesPreservingTerminal(tasks, validResults);
1768
1377
  await saveRunManifestAsync(resultManifest);
1769
1378
  await saveRunTasksAsync(resultManifest, resultTasks);
1770
1379
  return { resultManifest, resultTasks };
@@ -1773,13 +1382,7 @@ async function executeTeamRunCore(
1773
1382
  tasks = mergeResult.resultTasks;
1774
1383
 
1775
1384
  // Advance workflow phases whose tasks are all in terminal state
1776
- const terminalStatuses = new Set([
1777
- "completed",
1778
- "failed",
1779
- "skipped",
1780
- "cancelled",
1781
- "needs_attention",
1782
- ]);
1385
+ const terminalStatuses = new Set(["completed", "failed", "skipped", "cancelled", "needs_attention"]);
1783
1386
  const phaseTaskMap = new Map<string, string[]>();
1784
1387
  for (const task of tasks) {
1785
1388
  if (!task.stepId) continue;
@@ -1787,11 +1390,7 @@ async function executeTeamRunCore(
1787
1390
  existing.push(task.id);
1788
1391
  phaseTaskMap.set(task.stepId, existing);
1789
1392
  }
1790
- for (
1791
- let pi = wfMachine.currentPhaseIndex;
1792
- pi < wfMachine.phases.length;
1793
- pi++
1794
- ) {
1393
+ for (let pi = wfMachine.currentPhaseIndex; pi < wfMachine.phases.length; pi++) {
1795
1394
  const phase = wfMachine.phases[pi]!;
1796
1395
  const phaseTaskIds = phaseTaskMap.get(phase.name) ?? [];
1797
1396
  if (phaseTaskIds.length === 0) continue;
@@ -1800,27 +1399,14 @@ async function executeTeamRunCore(
1800
1399
  return task ? terminalStatuses.has(task.status) : false;
1801
1400
  });
1802
1401
  if (!allTerminal) break;
1803
- if (
1804
- phase.status !== "completed" &&
1805
- phase.status !== "failed" &&
1806
- phase.status !== "skipped"
1807
- ) {
1808
- const completedArtifacts = manifest.artifacts
1809
- .filter((a) => a.kind === "result" || a.kind === "summary")
1810
- .map((a) => a.path);
1811
- const previousPhaseStatus =
1812
- pi > 0
1813
- ? (wfMachine.phases[pi - 1]?.status ?? "pending")
1814
- : "completed";
1402
+ if (phase.status !== "completed" && phase.status !== "failed" && phase.status !== "skipped") {
1403
+ const completedArtifacts = manifest.artifacts.filter((a) => a.kind === "result" || a.kind === "summary").map((a) => a.path);
1404
+ const previousPhaseStatus = pi > 0 ? (wfMachine.phases[pi - 1]?.status ?? "pending") : "completed";
1815
1405
  const wfContext: PhaseGuardContext = {
1816
1406
  completedArtifacts,
1817
1407
  previousPhaseStatus,
1818
1408
  taskResults: tasks
1819
- .filter(
1820
- (t) =>
1821
- t.status === "completed" ||
1822
- t.status === "needs_attention",
1823
- )
1409
+ .filter((t) => t.status === "completed" || t.status === "needs_attention")
1824
1410
  .map((t) => ({
1825
1411
  taskId: t.id,
1826
1412
  status: t.status,
@@ -1831,18 +1417,9 @@ async function executeTeamRunCore(
1831
1417
  const phaseTasks = phaseTaskIds
1832
1418
  .map((taskId) => tasks.find((t) => t.id === taskId))
1833
1419
  .filter((t): t is NonNullable<typeof t> => t !== undefined);
1834
- const hasFailedOrCancelled = phaseTasks.some(
1835
- (t) => t.status === "failed" || t.status === "cancelled",
1836
- );
1837
- const phaseStatus = hasFailedOrCancelled
1838
- ? "failed"
1839
- : "completed";
1840
- const transition = transitionPhase(
1841
- wfMachine,
1842
- pi,
1843
- phaseStatus,
1844
- wfContext,
1845
- );
1420
+ const hasFailedOrCancelled = phaseTasks.some((t) => t.status === "failed" || t.status === "cancelled");
1421
+ const phaseStatus = hasFailedOrCancelled ? "failed" : "completed";
1422
+ const transition = transitionPhase(wfMachine, pi, phaseStatus, wfContext);
1846
1423
  wfMachine = transition.machine;
1847
1424
  if (transition.guardResult && !transition.guardResult.allowed) {
1848
1425
  await appendEventAsync(manifest.eventsPath, {
@@ -1858,10 +1435,7 @@ async function executeTeamRunCore(
1858
1435
  break;
1859
1436
  }
1860
1437
  await appendEventAsync(manifest.eventsPath, {
1861
- type:
1862
- phaseStatus === "failed"
1863
- ? "workflow.phase_failed"
1864
- : "workflow.phase_completed",
1438
+ type: phaseStatus === "failed" ? "workflow.phase_failed" : "workflow.phase_completed",
1865
1439
  runId: manifest.runId,
1866
1440
  message: `Workflow phase '${phase.name}' ${phaseStatus}.`,
1867
1441
  data: { phaseIndex: pi, phaseStatus },
@@ -1870,24 +1444,14 @@ async function executeTeamRunCore(
1870
1444
  wfMachine = { ...wfMachine, currentPhaseIndex: pi + 1 };
1871
1445
  }
1872
1446
 
1873
- const cancelledResult = results.find(
1874
- (item) => item.manifest.status === "cancelled",
1875
- );
1447
+ const cancelledResult = results.find((item) => item.manifest.status === "cancelled");
1876
1448
  if (cancelledResult || input.signal?.aborted) {
1877
- const reason = input.signal?.aborted
1878
- ? cancellationReasonFromSignal(input.signal)
1879
- : undefined;
1880
- const message =
1881
- reason?.message ??
1882
- cancelledResult?.manifest.summary ??
1883
- "Run cancelled during task execution.";
1449
+ const reason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : undefined;
1450
+ const message = reason?.message ?? cancelledResult?.manifest.summary ?? "Run cancelled during task execution.";
1884
1451
  manifest = { ...manifest, status: "running" };
1885
1452
  manifest = updateRunStatus(manifest, "cancelled", message);
1886
1453
  await saveRunTasksAsync(manifest, tasks);
1887
- saveCrewAgents(
1888
- manifest,
1889
- recordsForMaterializedTasks(manifest, tasks, runtimeKind),
1890
- );
1454
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
1891
1455
  await saveRunManifestAsync(manifest);
1892
1456
  await appendEventAsync(manifest.eventsPath, {
1893
1457
  type: "run.cancelled",
@@ -1904,56 +1468,31 @@ async function executeTeamRunCore(
1904
1468
  queueIndex = buildTaskGraphIndex(tasks);
1905
1469
  const injectedAfterBatch = attemptAdaptivePlan();
1906
1470
  if (injectedAfterBatch.missing) {
1907
- tasks = markBlocked(
1908
- tasks,
1909
- "Adaptive planner did not produce a valid subagent plan.",
1910
- );
1471
+ tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
1911
1472
  await saveRunTasksAsync(manifest, tasks);
1912
- saveCrewAgents(
1913
- manifest,
1914
- recordsForMaterializedTasks(manifest, tasks, runtimeKind),
1915
- );
1916
- manifest = updateRunStatus(
1917
- manifest,
1918
- "blocked",
1919
- "Adaptive planner did not produce a valid subagent plan.",
1920
- );
1473
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
1474
+ manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
1921
1475
  return { manifest, tasks };
1922
1476
  }
1923
1477
  if (injectedAfterBatch.injected) {
1924
- manifest = requiresPlanApproval(workflow, input.runtimeConfig)
1925
- ? ensurePlanApprovalRequested(manifest, tasks)
1926
- : manifest;
1478
+ manifest = requiresPlanApproval(workflow, input.runtimeConfig) ? ensurePlanApprovalRequested(manifest, tasks) : manifest;
1927
1479
  queueIndex = buildTaskGraphIndex(tasks);
1928
1480
  } else if (
1929
1481
  requiresPlanApproval(workflow, input.runtimeConfig) &&
1930
- (hasPendingMutatingAdaptiveTask(tasks) ||
1931
- hasPendingMutatingTaskAtBoundary(tasks))
1482
+ (hasPendingMutatingAdaptiveTask(tasks) || hasPendingMutatingTaskAtBoundary(tasks))
1932
1483
  ) {
1933
1484
  manifest = ensurePlanApprovalRequested(manifest, tasks);
1934
1485
  }
1935
1486
  if (manifest.planApproval?.status === "cancelled") {
1936
1487
  tasks = cancelPlanTasks(tasks, "Plan approval was cancelled.");
1937
1488
  await saveRunTasksAsync(manifest, tasks);
1938
- saveCrewAgents(
1939
- manifest,
1940
- recordsForMaterializedTasks(manifest, tasks, runtimeKind),
1941
- );
1942
- manifest = updateRunStatus(
1943
- manifest,
1944
- "cancelled",
1945
- "Plan approval was cancelled.",
1946
- );
1489
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
1490
+ manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
1947
1491
  return { manifest, tasks };
1948
1492
  }
1949
1493
  await saveRunTasksAsync(manifest, tasks);
1950
- saveCrewAgents(
1951
- manifest,
1952
- recordsForMaterializedTasks(manifest, tasks, runtimeKind),
1953
- );
1954
- const completedBatch = tasks.filter((t) =>
1955
- batchTasks.some((bt) => bt.id === t.id),
1956
- );
1494
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
1495
+ const completedBatch = tasks.filter((t) => batchTasks.some((bt) => bt.id === t.id));
1957
1496
  const batchArtifact = writeArtifact(manifest.artifactsRoot, {
1958
1497
  kind: "summary",
1959
1498
  relativePath: `batches/${batchTasks.map((task) => task.id).join("+")}.md`,
@@ -1968,19 +1507,9 @@ async function executeTeamRunCore(
1968
1507
  });
1969
1508
  manifest = {
1970
1509
  ...manifest,
1971
- artifacts: mergeArtifacts([
1972
- ...manifest.artifacts,
1973
- batchArtifact,
1974
- ...(groupDelivery?.artifact ? [groupDelivery.artifact] : []),
1975
- ]),
1510
+ artifacts: mergeArtifacts([...manifest.artifacts, batchArtifact, ...(groupDelivery?.artifact ? [groupDelivery.artifact] : [])]),
1976
1511
  };
1977
- manifest = writeProgress(
1978
- manifest,
1979
- tasks,
1980
- "team-runner",
1981
- input.executeWorkers,
1982
- input.runtimeConfig,
1983
- );
1512
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
1984
1513
  await saveRunManifestAsync(manifest);
1985
1514
  }
1986
1515
 
@@ -1998,10 +1527,7 @@ async function executeTeamRunCore(
1998
1527
  if (effectivenessDecision) {
1999
1528
  manifest = {
2000
1529
  ...manifest,
2001
- policyDecisions: [
2002
- ...(manifest.policyDecisions ?? []),
2003
- effectivenessDecision,
2004
- ],
1530
+ policyDecisions: [...(manifest.policyDecisions ?? []), effectivenessDecision],
2005
1531
  updatedAt: new Date().toISOString(),
2006
1532
  };
2007
1533
  await appendEventAsync(manifest.eventsPath, {
@@ -2011,62 +1537,27 @@ async function executeTeamRunCore(
2011
1537
  data: { effectiveness, policyDecision: effectivenessDecision },
2012
1538
  });
2013
1539
  }
2014
- const blockingDecision = manifest.policyDecisions?.find(
2015
- (item) => item.action === "block" || item.action === "escalate",
2016
- );
1540
+ const blockingDecision = manifest.policyDecisions?.find((item) => item.action === "block" || item.action === "escalate");
2017
1541
  if (failed) {
2018
- manifest = updateRunStatus(
2019
- manifest,
2020
- "failed",
2021
- `Failed at task '${failed.id}'.`,
2022
- );
1542
+ manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
2023
1543
  } else if (waiting) {
2024
- manifest = updateRunStatus(
2025
- manifest,
2026
- "blocked",
2027
- `Waiting for response to task '${waiting.id}'.`,
2028
- );
1544
+ manifest = updateRunStatus(manifest, "blocked", `Waiting for response to task '${waiting.id}'.`);
2029
1545
  } else if (running) {
2030
- manifest = updateRunStatus(
2031
- manifest,
2032
- "blocked",
2033
- `Task '${running.id}' is still running.`,
2034
- );
1546
+ manifest = updateRunStatus(manifest, "blocked", `Task '${running.id}' is still running.`);
2035
1547
  } else if (effectiveness.severity === "failed") {
2036
- manifest = updateRunStatus(
2037
- manifest,
2038
- "failed",
2039
- effectivenessDecision?.message ?? "Run effectiveness guard failed.",
2040
- );
1548
+ manifest = updateRunStatus(manifest, "failed", effectivenessDecision?.message ?? "Run effectiveness guard failed.");
2041
1549
  } else if (effectiveness.severity === "blocked") {
2042
- manifest = updateRunStatus(
2043
- manifest,
2044
- "blocked",
2045
- effectivenessDecision?.message ??
2046
- "Run effectiveness guard blocked completion.",
2047
- );
1550
+ manifest = updateRunStatus(manifest, "blocked", effectivenessDecision?.message ?? "Run effectiveness guard blocked completion.");
2048
1551
  } else if (blockingDecision) {
2049
- manifest = updateRunStatus(
2050
- manifest,
2051
- "blocked",
2052
- blockingDecision.message,
2053
- );
1552
+ manifest = updateRunStatus(manifest, "blocked", blockingDecision.message);
2054
1553
  } else {
2055
1554
  manifest = updateRunStatus(
2056
1555
  manifest,
2057
1556
  "completed",
2058
- input.executeWorkers
2059
- ? "Team workflow completed."
2060
- : "Team workflow scaffold completed without launching child workers.",
1557
+ input.executeWorkers ? "Team workflow completed." : "Team workflow scaffold completed without launching child workers.",
2061
1558
  );
2062
1559
  }
2063
- manifest = writeProgress(
2064
- manifest,
2065
- tasks,
2066
- "team-runner",
2067
- input.executeWorkers,
2068
- input.runtimeConfig,
2069
- );
1560
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
2070
1561
  await saveRunManifestAsync(manifest);
2071
1562
  const usage = aggregateUsage(tasks);
2072
1563
  const summaryArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -2086,17 +1577,10 @@ async function executeTeamRunCore(
2086
1577
  ...tasks.map(formatTaskProgress),
2087
1578
  "",
2088
1579
  "## Effectiveness",
2089
- ...runEffectivenessLines(
2090
- manifest,
2091
- tasks,
2092
- input.executeWorkers,
2093
- input.runtimeConfig,
2094
- ),
1580
+ ...runEffectivenessLines(manifest, tasks, input.executeWorkers, input.runtimeConfig),
2095
1581
  "",
2096
1582
  "## Policy decisions",
2097
- ...(manifest.policyDecisions?.length
2098
- ? summarizePolicyDecisions(manifest.policyDecisions)
2099
- : ["- (none)"]),
1583
+ ...(manifest.policyDecisions?.length ? summarizePolicyDecisions(manifest.policyDecisions) : ["- (none)"]),
2100
1584
  "",
2101
1585
  ].join("\n"),
2102
1586
  });
@@ -2125,9 +1609,7 @@ async function executeTeamRunCore(
2125
1609
  // health feature) AND created junk dirs that the recursive state watcher then
2126
1610
  // attached extra inotify watches to. Fix: compute the real crew root (3 up)
2127
1611
  // and make HEALTH_DIR relative to it.
2128
- const crewRoot = path.dirname(
2129
- path.dirname(path.dirname(finalManifest.stateRoot)),
2130
- );
1612
+ const crewRoot = path.dirname(path.dirname(path.dirname(finalManifest.stateRoot)));
2131
1613
  const healthStore = new HealthStore(crewRoot);
2132
1614
  healthStore.saveSnapshot({
2133
1615
  runId: finalManifest.runId,