pi-crew 0.9.56 → 0.9.57

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 (307) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +53 -5
  4. package/dist/index.mjs +5350 -4350
  5. package/docs/README.md +54 -0
  6. package/package.json +12 -12
  7. package/skills/real-test-pi-crew/SKILL.md +93 -3
  8. package/src/agents/discover-agents.ts +68 -10
  9. package/src/benchmark/feedback-loop.ts +1 -1
  10. package/src/config/config.ts +2 -1
  11. package/src/config/types.ts +12 -0
  12. package/src/extension/action-suggestions.ts +16 -63
  13. package/src/extension/async-notifier.ts +3 -3
  14. package/src/extension/command-completions.ts +2 -2
  15. package/src/extension/crew-cleanup.ts +3 -3
  16. package/src/extension/crew-vibes/footer.ts +4 -2
  17. package/src/extension/cross-extension-rpc.ts +10 -5
  18. package/src/extension/knowledge-injection.ts +57 -10
  19. package/src/extension/register.ts +2 -2
  20. package/src/extension/registration/artifact-cleanup.ts +41 -3
  21. package/src/extension/registration/commands.ts +4 -4
  22. package/src/extension/registration/context-builder.ts +10 -10
  23. package/src/extension/registration/crash-recovery-cache.ts +3 -3
  24. package/src/extension/registration/foreground-run-controller.ts +1 -1
  25. package/src/extension/registration/lazy-configurers.ts +2 -2
  26. package/src/extension/registration/lifecycle-handlers.ts +12 -12
  27. package/src/extension/registration/lifecycle.ts +2 -2
  28. package/src/extension/registration/observability.ts +2 -2
  29. package/src/extension/registration/registration-types.ts +3 -3
  30. package/src/extension/registration/runtime-cleanup.ts +1 -1
  31. package/src/extension/registration/subagent-helpers.ts +2 -2
  32. package/src/extension/registration/subagent-manager-setup.ts +2 -2
  33. package/src/extension/registration/subagent-tools.ts +21 -16
  34. package/src/extension/registration/team-tool.ts +108 -17
  35. package/src/extension/registration/viewers.ts +3 -4
  36. package/src/extension/registration/wire-cross-extension.ts +7 -7
  37. package/src/extension/rpc-hmac.ts +3 -2
  38. package/src/extension/run-bundle-schema.ts +13 -4
  39. package/src/extension/run-export.ts +2 -2
  40. package/src/extension/run-index.ts +2 -3
  41. package/src/extension/run-maintenance.ts +57 -1
  42. package/src/extension/team-manager-command.ts +3 -1
  43. package/src/extension/team-tool/anchor.ts +8 -2
  44. package/src/extension/team-tool/api.ts +117 -39
  45. package/src/extension/team-tool/cancel.ts +23 -8
  46. package/src/extension/team-tool/chain-executor.ts +1 -1
  47. package/src/extension/team-tool/dispatch/automate.ts +17 -2
  48. package/src/extension/team-tool/dispatch/control.ts +17 -2
  49. package/src/extension/team-tool/dispatch/manage.ts +34 -2
  50. package/src/extension/team-tool/dispatch/run.ts +17 -2
  51. package/src/extension/team-tool/dispatch/status.ts +37 -5
  52. package/src/extension/team-tool/doctor.ts +2 -2
  53. package/src/extension/team-tool/explain.ts +1 -1
  54. package/src/extension/team-tool/goal-wrap.ts +5 -5
  55. package/src/extension/team-tool/goal.ts +30 -9
  56. package/src/extension/team-tool/handle-schedule.ts +37 -19
  57. package/src/extension/team-tool/inspect.ts +21 -5
  58. package/src/extension/team-tool/lifecycle-actions.ts +53 -10
  59. package/src/extension/team-tool/parallel-dispatch.ts +14 -5
  60. package/src/extension/team-tool/param-error.ts +24 -0
  61. package/src/extension/team-tool/respond.ts +20 -6
  62. package/src/extension/team-tool/run.ts +27 -17
  63. package/src/extension/team-tool/status.ts +11 -5
  64. package/src/extension/team-tool.ts +26 -19
  65. package/src/hooks/registry.ts +1 -1
  66. package/src/i18n.ts +22 -0
  67. package/src/observability/event-bus.ts +1 -1
  68. package/src/prompt/prompt-runtime.ts +2 -2
  69. package/src/runtime/README.md +35 -0
  70. package/src/runtime/agent-control.ts +1 -1
  71. package/src/runtime/anchor-manager.ts +0 -2
  72. package/src/runtime/async-runner.ts +1 -1
  73. package/src/runtime/attention-events.ts +1 -1
  74. package/src/runtime/auto-summarize.ts +0 -2
  75. package/src/runtime/background-runner.ts +103 -30
  76. package/src/runtime/{crew-broker-child.ts → broker/crew-broker-child.ts} +2 -2
  77. package/src/runtime/{crew-broker-client.ts → broker/crew-broker-client.ts} +4 -7
  78. package/src/runtime/{crew-broker.ts → broker/crew-broker.ts} +13 -21
  79. package/src/runtime/chain-runner.ts +50 -5
  80. package/src/runtime/{child-pi-constants.ts → child-pi/child-pi-constants.ts} +1 -1
  81. package/src/runtime/{child-pi-kill.ts → child-pi/child-pi-kill.ts} +3 -3
  82. package/src/runtime/{child-pi-spawn.ts → child-pi/child-pi-spawn.ts} +28 -12
  83. package/src/runtime/{child-pi-steering.ts → child-pi/child-pi-steering.ts} +1 -1
  84. package/src/runtime/{child-pi-streams.ts → child-pi/child-pi-streams.ts} +11 -4
  85. package/src/runtime/{child-pi-transcript.ts → child-pi/child-pi-transcript.ts} +6 -6
  86. package/src/runtime/{child-pi.ts → child-pi/child-pi.ts} +28 -72
  87. package/src/runtime/{compact-stages → compaction/compact-stages}/bounded-tail.ts +1 -1
  88. package/src/runtime/{compact-stages → compaction/compact-stages}/index.ts +2 -2
  89. package/src/runtime/{compaction-summary.ts → compaction/compaction-summary.ts} +3 -3
  90. package/src/runtime/{tool-output-pruner.ts → compaction/tool-output-pruner.ts} +1 -1
  91. package/src/runtime/custom-tools/irc-tool.ts +2 -2
  92. package/src/runtime/delivery-coordinator.ts +0 -3
  93. package/src/runtime/diagnostic-export.ts +3 -3
  94. package/src/runtime/foreground-control.ts +1 -1
  95. package/src/runtime/foreground-watchdog.ts +1 -1
  96. package/src/runtime/{adaptive-plan.ts → goal-workflow/adaptive-plan.ts} +8 -8
  97. package/src/runtime/{dynamic-workflow-context.ts → goal-workflow/dynamic-workflow-context.ts} +16 -16
  98. package/src/runtime/{dynamic-workflow-runner.ts → goal-workflow/dynamic-workflow-runner.ts} +10 -10
  99. package/src/runtime/{goal-achievement.ts → goal-workflow/goal-achievement.ts} +3 -3
  100. package/src/runtime/{goal-evaluator.ts → goal-workflow/goal-evaluator.ts} +8 -8
  101. package/src/runtime/{goal-loop-runner.ts → goal-workflow/goal-loop-runner.ts} +27 -25
  102. package/src/runtime/{goal-state-store.ts → goal-workflow/goal-state-store.ts} +8 -8
  103. package/src/runtime/{plan-templates.ts → goal-workflow/plan-templates.ts} +1 -1
  104. package/src/runtime/group-join.ts +3 -3
  105. package/src/runtime/handoff-manager.ts +0 -2
  106. package/src/runtime/{heartbeat-watcher.ts → heartbeat/heartbeat-watcher.ts} +7 -7
  107. package/src/runtime/iteration-hooks.ts +1 -1
  108. package/src/runtime/{live-agent-control.ts → live-session/live-agent-control.ts} +2 -2
  109. package/src/runtime/{live-agent-manager.ts → live-session/live-agent-manager.ts} +4 -4
  110. package/src/runtime/{live-session-runtime.ts → live-session/live-session-runtime.ts} +39 -41
  111. package/src/runtime/manifest-cache.ts +1 -1
  112. package/src/runtime/{model-fallback.ts → model/model-fallback.ts} +1 -1
  113. package/src/runtime/{pi-args.ts → model/pi-args.ts} +20 -5
  114. package/src/runtime/model/provider-extensions.ts +116 -0
  115. package/src/runtime/{runtime-policy.ts → model/runtime-policy.ts} +2 -2
  116. package/src/runtime/{runtime-resolver.ts → model/runtime-resolver.ts} +3 -3
  117. package/src/runtime/{runtime-warmup.ts → model/runtime-warmup.ts} +8 -8
  118. package/src/runtime/orphan-worker-registry.ts +1 -1
  119. package/src/runtime/{progress-tracker.ts → output/progress-tracker.ts} +1 -1
  120. package/src/runtime/{result-extractor.ts → output/result-extractor.ts} +4 -4
  121. package/src/runtime/{sidechain-output.ts → output/sidechain-output.ts} +2 -2
  122. package/src/runtime/{stream-preview.ts → output/stream-preview.ts} +1 -1
  123. package/src/runtime/{streaming-output.ts → output/streaming-output.ts} +2 -2
  124. package/src/runtime/{tool-progress.ts → output/tool-progress.ts} +1 -1
  125. package/src/runtime/parent-guard.ts +30 -10
  126. package/src/runtime/pipeline-runner.ts +2 -2
  127. package/src/runtime/policy-engine.ts +2 -2
  128. package/src/runtime/{cancellation.ts → process/cancellation.ts} +1 -1
  129. package/src/runtime/{post-exit-stdio-guard.ts → process/post-exit-stdio-guard.ts} +1 -1
  130. package/src/runtime/{process-lifecycle.ts → process/process-lifecycle.ts} +3 -3
  131. package/src/runtime/{checkpoint.ts → recovery/checkpoint.ts} +4 -4
  132. package/src/runtime/{crash-recovery.ts → recovery/crash-recovery.ts} +93 -21
  133. package/src/runtime/{overflow-recovery.ts → recovery/overflow-recovery.ts} +1 -1
  134. package/src/runtime/{recovery-recipes.ts → recovery/recovery-recipes.ts} +1 -1
  135. package/src/runtime/{retry-executor.ts → recovery/retry-executor.ts} +2 -2
  136. package/src/runtime/{retry-runner.ts → recovery/retry-runner.ts} +2 -8
  137. package/src/runtime/run-tracker.ts +1 -1
  138. package/src/runtime/run-worker.ts +3 -3
  139. package/src/runtime/{coalesce-tasks.ts → scheduling/coalesce-tasks.ts} +3 -3
  140. package/src/runtime/{concurrency.ts → scheduling/concurrency.ts} +1 -1
  141. package/src/runtime/{parallel-research.ts → scheduling/parallel-research.ts} +4 -2
  142. package/src/runtime/{run-coalesced-task-group.ts → scheduling/run-coalesced-task-group.ts} +95 -36
  143. package/src/runtime/{task-graph-scheduler.ts → scheduling/task-graph-scheduler.ts} +1 -1
  144. package/src/runtime/session-resources.ts +1 -1
  145. package/src/runtime/settings-store.ts +1 -1
  146. package/src/runtime/single-agent-compose.ts +1 -0
  147. package/src/runtime/stale-reconciler.ts +1 -1
  148. package/src/runtime/subagent-manager.ts +1 -1
  149. package/src/runtime/supervisor-contact.ts +1 -1
  150. package/src/runtime/task-output-context.ts +7 -5
  151. package/src/runtime/task-runner/child-executor.ts +62 -22
  152. package/src/runtime/task-runner/live-executor.ts +8 -8
  153. package/src/runtime/task-runner/post-execution.ts +12 -12
  154. package/src/runtime/task-runner/pre-execution.ts +6 -6
  155. package/src/runtime/task-runner/progress.ts +1 -1
  156. package/src/runtime/task-runner/run-projection.ts +1 -1
  157. package/src/runtime/task-runner/scaffold-executor.ts +1 -1
  158. package/src/runtime/task-runner/state-helpers.ts +2 -2
  159. package/src/runtime/task-runner.ts +4 -4
  160. package/src/runtime/team-runner.ts +380 -139
  161. package/src/runtime/{completion-guard.ts → verification/completion-guard.ts} +1 -1
  162. package/src/runtime/{green-contract.ts → verification/green-contract.ts} +1 -1
  163. package/src/runtime/{post-checks.ts → verification/post-checks.ts} +4 -4
  164. package/src/runtime/{verification-gates.ts → verification/verification-gates.ts} +5 -5
  165. package/src/runtime/{verification-integrity.ts → verification/verification-integrity.ts} +1 -1
  166. package/src/schema/config-schema.ts +1 -0
  167. package/src/schema/team-tool-schema.ts +133 -177
  168. package/src/state/README.md +44 -0
  169. package/src/state/atomic-write.ts +24 -3
  170. package/src/state/{locks.ts → coordination/locks.ts} +118 -38
  171. package/src/state/{mailbox.ts → coordination/mailbox.ts} +14 -8
  172. package/src/state/{schedule.ts → coordination/schedule.ts} +3 -3
  173. package/src/state/{task-claims.ts → coordination/task-claims.ts} +1 -1
  174. package/src/state/decision-ledger.ts +1 -1
  175. package/src/state/{event-log-rotation.ts → event-log/event-log-rotation.ts} +161 -31
  176. package/src/state/{event-log.ts → event-log/event-log.ts} +38 -26
  177. package/src/state/{jsonl-writer.ts → event-log/jsonl-writer.ts} +14 -6
  178. package/src/state/hook-instinct-bridge.ts +2 -2
  179. package/src/state/{active-run-registry.ts → stores/active-run-registry.ts} +7 -7
  180. package/src/state/{artifact-store.ts → stores/artifact-store.ts} +62 -4
  181. package/src/state/{blob-store.ts → stores/blob-store.ts} +3 -3
  182. package/src/state/{health-store.ts → stores/health-store.ts} +3 -3
  183. package/src/state/{instinct-store.ts → stores/instinct-store.ts} +3 -3
  184. package/src/state/{observation-store.ts → stores/observation-store.ts} +2 -2
  185. package/src/state/{run-cache.ts → stores/run-cache.ts} +4 -4
  186. package/src/state/{run-graph.ts → stores/run-graph.ts} +4 -4
  187. package/src/state/{run-metrics.ts → stores/run-metrics.ts} +3 -3
  188. package/src/state/{state-store.ts → stores/state-store.ts} +236 -17
  189. package/src/state/types.ts +27 -3
  190. package/src/ui/component.ts +55 -0
  191. package/src/ui/crew-select-list.ts +3 -1
  192. package/src/ui/dashboard-panes/agents-pane.ts +1 -1
  193. package/src/ui/dwf-phase-display.ts +1 -1
  194. package/src/ui/heartbeat-aggregator.ts +1 -1
  195. package/src/ui/keybinding-map.ts +179 -6
  196. package/src/ui/layout-primitives.ts +9 -5
  197. package/src/ui/live-conversation-overlay.ts +1 -1
  198. package/src/ui/live-run-sidebar.ts +1 -1
  199. package/src/ui/overlay-stack.ts +148 -0
  200. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  201. package/src/ui/overlays/mailbox-compose-overlay.ts +1 -1
  202. package/src/ui/overlays/mailbox-detail-overlay.ts +2 -2
  203. package/src/ui/powerbar-publisher.ts +1 -1
  204. package/src/ui/render-scheduler.ts +1 -1
  205. package/src/ui/run-action-dispatcher.ts +3 -3
  206. package/src/ui/run-dashboard.ts +5 -33
  207. package/src/ui/run-event-bus.ts +3 -3
  208. package/src/ui/run-snapshot-cache.ts +37 -18
  209. package/src/ui/settings-overlay.ts +13 -21
  210. package/src/ui/snapshot-types.ts +1 -1
  211. package/src/ui/status-colors.ts +0 -2
  212. package/src/ui/terminal-status.ts +1 -1
  213. package/src/ui/theme-adapter.ts +33 -13
  214. package/src/ui/tool-renderers/index.ts +8 -0
  215. package/src/ui/transcript-viewer.ts +37 -13
  216. package/src/ui/widget/index.ts +32 -9
  217. package/src/ui/widget/widget-formatters.ts +62 -4
  218. package/src/ui/widget/widget-model.ts +2 -2
  219. package/src/ui/widget/widget-renderer.ts +1 -1
  220. package/src/utils/conflict-detect.ts +58 -5
  221. package/src/utils/ndjson.ts +2 -2
  222. package/src/utils/socket-path.ts +1 -1
  223. package/src/workflows/workflow-config.ts +1 -1
  224. package/src/worktree/cleanup.ts +1 -1
  225. package/src/worktree/worktree-manager.ts +88 -39
  226. package/workflows/chain.workflow.md +2 -2
  227. package/index.bundle.ts +0 -25
  228. package/scripts/bench-check.mjs +0 -96
  229. package/scripts/bench-cold-start.mjs +0 -216
  230. package/scripts/build-bundle.mjs +0 -92
  231. package/scripts/check-all-skills.ts +0 -294
  232. package/scripts/check-bundle-size.mjs +0 -38
  233. package/scripts/check-bundle-staleness.mjs +0 -97
  234. package/scripts/check-conflict-markers.mjs +0 -91
  235. package/scripts/check-lazy-imports.mjs +0 -28
  236. package/scripts/dev-runner.mjs +0 -106
  237. package/scripts/profile-startup.mjs +0 -125
  238. package/scripts/release-smoke.mjs +0 -74
  239. package/scripts/run-bench.mjs +0 -47
  240. package/scripts/run-real-chain.ts +0 -41
  241. package/scripts/smoke-team-perf.ts +0 -43
  242. package/scripts/test-issue-29-crash.ts +0 -111
  243. package/scripts/test-issue-29-e2e.ts +0 -183
  244. package/scripts/test-issue-29-real-runtime.ts +0 -330
  245. package/scripts/test-issue-29-real-tasks.ts +0 -387
  246. package/scripts/test-issue-29-team-tool.ts +0 -105
  247. package/scripts/test-runner.mjs +0 -84
  248. package/scripts/verify-flicker-fix.ts +0 -109
  249. package/scripts/verify-skill.ts +0 -550
  250. package/scripts/watch-bundle.mjs +0 -259
  251. package/scripts/watchdog-harness.ts +0 -119
  252. package/src/adapters/claude-adapter.ts +0 -23
  253. package/src/adapters/codex-adapter.ts +0 -21
  254. package/src/adapters/cursor-adapter.ts +0 -17
  255. package/src/adapters/export-util.ts +0 -143
  256. package/src/adapters/index.ts +0 -15
  257. package/src/adapters/registry.ts +0 -18
  258. package/src/adapters/types.ts +0 -23
  259. package/src/runtime/code-summary.ts +0 -292
  260. package/src/runtime/cross-extension-rpc.ts +0 -143
  261. package/src/runtime/hidden-handoff.ts +0 -407
  262. package/src/runtime/prose-compressor.ts +0 -162
  263. package/src/subagents/async-entry.ts +0 -1
  264. package/src/subagents/index.ts +0 -3
  265. package/src/subagents/live/control.ts +0 -1
  266. package/src/subagents/live/manager.ts +0 -1
  267. package/src/subagents/live/realtime.ts +0 -1
  268. package/src/subagents/live/session-runtime.ts +0 -1
  269. package/src/subagents/manager.ts +0 -1
  270. package/src/subagents/spawn.ts +0 -1
  271. package/test-integration-check.ts +0 -118
  272. /package/src/runtime/{broker-issuer.ts → broker/broker-issuer.ts} +0 -0
  273. /package/src/runtime/{crew-broker-tokens.ts → broker/crew-broker-tokens.ts} +0 -0
  274. /package/src/runtime/{child-pi-pool.ts → child-pi/child-pi-pool.ts} +0 -0
  275. /package/src/runtime/{compact-pipeline.ts → compaction/compact-pipeline.ts} +0 -0
  276. /package/src/runtime/{compact-stages → compaction/compact-stages}/ansi-strip-stage.ts +0 -0
  277. /package/src/runtime/{compact-stages → compaction/compact-stages}/blank-collapse-stage.ts +0 -0
  278. /package/src/runtime/{compact-stages → compaction/compact-stages}/deduplicate-stage.ts +0 -0
  279. /package/src/runtime/{compact-stages → compaction/compact-stages}/head-snap-stage.ts +0 -0
  280. /package/src/runtime/{compact-stages → compaction/compact-stages}/tail-capture-stage.ts +0 -0
  281. /package/src/runtime/{compact-stages → compaction/compact-stages}/truncation-stage.ts +0 -0
  282. /package/src/runtime/{important-line-classifier.ts → compaction/important-line-classifier.ts} +0 -0
  283. /package/src/runtime/{heartbeat-gradient.ts → heartbeat/heartbeat-gradient.ts} +0 -0
  284. /package/src/runtime/{worker-heartbeat.ts → heartbeat/worker-heartbeat.ts} +0 -0
  285. /package/src/runtime/{worker-startup.ts → heartbeat/worker-startup.ts} +0 -0
  286. /package/src/runtime/{intercom-bridge.ts → live-session/intercom-bridge.ts} +0 -0
  287. /package/src/runtime/{live-control-realtime.ts → live-session/live-control-realtime.ts} +0 -0
  288. /package/src/runtime/{live-extension-bridge.ts → live-session/live-extension-bridge.ts} +0 -0
  289. /package/src/runtime/{live-irc.ts → live-session/live-irc.ts} +0 -0
  290. /package/src/runtime/{live-session-health.ts → live-session/live-session-health.ts} +0 -0
  291. /package/src/runtime/{model-resolver.ts → model/model-resolver.ts} +0 -0
  292. /package/src/runtime/{model-scope.ts → model/model-scope.ts} +0 -0
  293. /package/src/runtime/{output-validator.ts → output/output-validator.ts} +0 -0
  294. /package/src/runtime/{pi-json-output.ts → output/pi-json-output.ts} +0 -0
  295. /package/src/runtime/{progress-event-coalescer.ts → output/progress-event-coalescer.ts} +0 -0
  296. /package/src/runtime/{cancellation-token.ts → process/cancellation-token.ts} +0 -0
  297. /package/src/runtime/{zombie-scanner.ts → process/zombie-scanner.ts} +0 -0
  298. /package/src/runtime/{crash-classification.ts → recovery/crash-classification.ts} +0 -0
  299. /package/src/runtime/{batch-barrier.ts → scheduling/batch-barrier.ts} +0 -0
  300. /package/src/runtime/{global-worker-cap.ts → scheduling/global-worker-cap.ts} +0 -0
  301. /package/src/runtime/{parallel-utils.ts → scheduling/parallel-utils.ts} +0 -0
  302. /package/src/runtime/{scheduler.ts → scheduling/scheduler.ts} +0 -0
  303. /package/src/runtime/{semaphore.ts → scheduling/semaphore.ts} +0 -0
  304. /package/src/runtime/{task-graph.ts → scheduling/task-graph.ts} +0 -0
  305. /package/src/runtime/{verification-worktree.ts → verification/verification-worktree.ts} +0 -0
  306. /package/src/state/{event-reconstructor.ts → event-log/event-reconstructor.ts} +0 -0
  307. /package/src/state/{worker-atomic-writer.ts → event-log/worker-atomic-writer.ts} +0 -0
@@ -8,47 +8,54 @@ import { childCorrelation, withCorrelation } from "../observability/correlation.
8
8
  import type { MetricRegistry } from "../observability/metric-registry.ts";
9
9
  import { PluginRegistry } from "../plugins/plugin-registry.ts";
10
10
  import { NextJsPlugin, VitePlugin, VitestPlugin } from "../plugins/plugins/index.ts";
11
- import { hashArtifactContent as hashContent, writeArtifact } from "../state/artifact-store.ts";
12
11
  import { atomicWriteFile, flushPendingAtomicWrites } from "../state/atomic-write.ts";
13
- import { appendEvent, appendEventAsync, appendEventBuffered, appendEventFireAndForget, flushEventLogBuffer } from "../state/event-log.ts";
14
- import { HealthStore } from "../state/health-store.ts";
15
- import { withRunLock } from "../state/locks.ts";
16
- import { loadRunManifestById, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/state-store.ts";
12
+ import { canTransitionRunStatus, TEAM_TASK_STATUSES, TEAM_TERMINAL_TASK_STATUSES, type TeamTaskStatus } from "../state/contracts.ts";
13
+ import { withRunLock } from "../state/coordination/locks.ts";
14
+ import {
15
+ appendEvent,
16
+ appendEventAsync,
17
+ appendEventBuffered,
18
+ appendEventFireAndForget,
19
+ flushEventLogBuffer,
20
+ } from "../state/event-log/event-log.ts";
21
+ import { hashArtifactContent as hashContent, writeArtifact } from "../state/stores/artifact-store.ts";
22
+ import { HealthStore } from "../state/stores/health-store.ts";
23
+ import { loadRunManifestById, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/stores/state-store.ts";
17
24
  import type { ArtifactDescriptor, PolicyDecision, TaskAttemptState, TeamRunManifest, TeamTaskState } from "../state/types.ts";
18
25
  import { aggregateUsage, formatTokens, formatUsage } from "../state/usage.ts";
19
26
  import type { TeamConfig } from "../teams/team-config.ts";
20
27
  import { logInternalError } from "../utils/internal-error.ts";
21
28
  import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
22
29
  import { checkBranchFreshness } from "../worktree/branch-freshness.ts";
23
- import { buildSyntheticTerminalEvidence, CrewCancellationError, cancellationReasonFromSignal } from "./cancellation.ts";
24
- import { buildDispatchUnits, type DispatchUnit, planCoalescedGroups } from "./coalesce-tasks.ts";
25
- import { type BatchConcurrencyDecision, resolveBatchConcurrency } from "./concurrency.ts";
26
30
  import { readCrewAgents, saveCrewAgents, saveCrewAgentsCoalesced } from "./crew-agent-records.ts";
27
31
  import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
28
32
  import { crewHooks } from "./crew-hooks.ts";
29
33
  import { appendDeadletter } from "./deadletter.ts";
30
34
  import { effectivenessPolicyDecision, evaluateRunEffectiveness, formatRunEffectivenessLines } from "./effectiveness.ts";
31
- import { applyGoalAchievement, assessGoalAchievement } from "./goal-achievement.ts";
35
+ import { applyGoalAchievement, assessGoalAchievement } from "./goal-workflow/goal-achievement.ts";
32
36
  import { deliverGroupJoin, resolveGroupJoinMode } from "./group-join.ts";
33
- import { terminateLiveAgentsForRun } from "./live-agent-manager.ts";
37
+ import { terminateLiveAgentsForRun } from "./live-session/live-agent-manager.ts";
38
+ import { resolveTaskRuntimeKind } from "./model/runtime-policy.ts";
39
+ import type { CrewRuntimeCapabilities } from "./model/runtime-resolver.ts";
34
40
  import { filterReadyByWriteOverlap } from "./path-overlap.ts";
35
41
  import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
36
- import { buildRecoveryLedger, shouldRerunFailedTask } from "./recovery-recipes.ts";
37
- import { DEFAULT_RETRY_POLICY, executeWithRetry, type RetryPolicy } from "./retry-executor.ts";
42
+ import { buildSyntheticTerminalEvidence, CrewCancellationError, cancellationReasonFromSignal } from "./process/cancellation.ts";
43
+ import { buildRecoveryLedger, shouldRerunFailedTask } from "./recovery/recovery-recipes.ts";
44
+ import { DEFAULT_RETRY_POLICY, executeWithRetry, type RetryPolicy } from "./recovery/retry-executor.ts";
38
45
  import { permissionForRole } from "./role-permission.ts";
39
- import { runCoalescedTaskGroup } from "./run-coalesced-task-group.ts";
40
46
  import { registerRunPromise, rejectRunPromise, resolveRunPromise } from "./run-tracker.ts";
41
- import { resolveTaskRuntimeKind } from "./runtime-policy.ts";
42
- import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
43
- import { recordsForMaterializedTasks } from "./task-display.ts";
44
- import { buildExecutionPlan as buildDagExecutionPlan, getReadyTasks as getDagReadyTasks, type TaskNode } from "./task-graph.ts";
47
+ import { buildDispatchUnits, type DispatchUnit, planCoalescedGroups } from "./scheduling/coalesce-tasks.ts";
48
+ import { type BatchConcurrencyDecision, resolveBatchConcurrency } from "./scheduling/concurrency.ts";
49
+ import { runCoalescedTaskGroup } from "./scheduling/run-coalesced-task-group.ts";
50
+ import { buildExecutionPlan as buildDagExecutionPlan, getReadyTasks as getDagReadyTasks, type TaskNode } from "./scheduling/task-graph.ts";
45
51
  import {
46
52
  buildTaskGraphIndex,
47
53
  refreshTaskGraphQueues,
48
54
  type TaskGraphIndex,
49
55
  type TaskGraphSchedulerSnapshot,
50
56
  taskGraphSnapshot,
51
- } from "./task-graph-scheduler.ts";
57
+ } from "./scheduling/task-graph-scheduler.ts";
58
+ import { recordsForMaterializedTasks } from "./task-display.ts";
52
59
  import { aggregateTaskOutputs } from "./task-output-context.ts";
53
60
  import { clearStablePrefixCache, computeStablePrefixComponents } from "./task-runner/prompt-builder.ts";
54
61
  import { runTeamTask, type SpawnBudget } from "./task-runner.ts";
@@ -246,18 +253,32 @@ function isNonTerminalTaskStatus(status: TeamTaskState["status"]): boolean {
246
253
  * terminalised with the given status.
247
254
  * - With `filter`: the filter is the sole gate — the non-terminal check is
248
255
  * NOT applied automatically, matching per-task/per-id variants.
256
+ * - Optional `transform(task, terminalised)`: lets a caller attach
257
+ * site-specific fields (graph mutation, terminalEvidence) to the
258
+ * terminalised task. `terminalised` already carries status/finishedAt/error;
259
+ * the transform returns it unchanged or a modified copy.
260
+ *
261
+ * RT-14: the two remaining inline cancel sites (cancelPlanTasks,
262
+ * cancelRunFromSignal) route through this helper via `transform` so EVERY
263
+ * cancel site uses the single shared transform. Their extra logic
264
+ * (graph mutation / terminalEvidence) is preserved inside the transform.
249
265
  *
250
266
  * `markBlocked` is intentionally NOT unified here (it sets status "skipped",
251
267
  * not cancelled/failed, and only acts on "queued" tasks).
252
268
  */
253
- function cancelNonTerminalTasks(
269
+ export function cancelNonTerminalTasks(
254
270
  tasks: TeamTaskState[],
255
271
  status: "cancelled" | "failed",
256
272
  reason: string,
257
273
  filter?: (task: TeamTaskState) => boolean,
274
+ transform?: (task: TeamTaskState, terminalised: TeamTaskState) => TeamTaskState,
258
275
  ): TeamTaskState[] {
259
276
  const predicate = filter ?? ((task: TeamTaskState) => isNonTerminalTaskStatus(task.status));
260
- return tasks.map((task) => (predicate(task) ? { ...task, status, finishedAt: new Date().toISOString(), error: reason } : task));
277
+ return tasks.map((task) => {
278
+ if (!predicate(task)) return task;
279
+ const terminalised: TeamTaskState = { ...task, status, finishedAt: new Date().toISOString(), error: reason };
280
+ return transform ? transform(task, terminalised) : terminalised;
281
+ });
261
282
  }
262
283
 
263
284
  /**
@@ -280,34 +301,73 @@ function isMalformedFinishedAtReplacement(currentTime: number, updatedTime: numb
280
301
  return !Number.isFinite(currentTime) && Number.isFinite(updatedTime);
281
302
  }
282
303
 
304
+ /**
305
+ * RT-16: status-level gate for shouldMergeTaskUpdate. Returns the stable
306
+ * "from->to" key used by REJECTED_STATUS_MERGE_TRANSITIONS.
307
+ */
308
+ function statusMergeKey(from: TeamTaskStatus, to: TeamTaskStatus): string {
309
+ return `${from}->${to}`;
310
+ }
311
+
312
+ /**
313
+ * RT-16 — derived merge-gate transition table.
314
+ *
315
+ * The set of old->new status pairs that shouldMergeTaskUpdate must REJECT based
316
+ * solely on the status transition (before any field-level comparison). It
317
+ * replaces the former 13 hand-written status guards that hand-duplicated the
318
+ * lifecycle table. Built once from the single source-of-truth table
319
+ * (TEAM_TASK_STATUSES + TEAM_TERMINAL_TASK_STATUSES — the terminal half of
320
+ * TEAM_TASK_STATUS_TRANSITIONS) plus two merge-specific policies that are
321
+ * STRICTER than the lifecycle table on the parallel-merge path:
322
+ *
323
+ * P1 Terminal preservation — every terminal->non-terminal pair is rejected.
324
+ * The lifecycle table permits retries (e.g. completed->queued), but a stale
325
+ * worker snapshot must never resurrect a settled task.
326
+ * P2 Completed integrity — five terminal->terminal flips that touch the
327
+ * "completed" success terminal are rejected: completed->failed,
328
+ * completed->needs_attention, failed->completed, cancelled->completed,
329
+ * needs_attention->completed. (completed may still move to
330
+ * cancelled/skipped; that is intentionally allowed, so these are NOT simply
331
+ * "every illegal terminal->terminal flip".)
332
+ * P3 waiting->running regression — the single stale-snapshot case.
333
+ *
334
+ * The decision for every old->new pair is byte-for-byte identical to the former
335
+ * 7 status guards (verified exhaustively in
336
+ * test/unit/team-runner-should-merge-table.test.ts).
337
+ */
338
+ const REJECTED_STATUS_MERGE_TRANSITIONS: ReadonlySet<string> = (() => {
339
+ const rejected = new Set<string>();
340
+ // P1 — terminal preservation: reject every terminal->non-terminal pair.
341
+ for (const from of TEAM_TASK_STATUSES) {
342
+ if (!TEAM_TERMINAL_TASK_STATUSES.has(from)) continue;
343
+ for (const to of TEAM_TASK_STATUSES) {
344
+ if (!TEAM_TERMINAL_TASK_STATUSES.has(to)) rejected.add(statusMergeKey(from, to));
345
+ }
346
+ }
347
+ // P3 — waiting->running stale-snapshot regression.
348
+ rejected.add(statusMergeKey("waiting", "running"));
349
+ // P2 — completed integrity flips (bespoke terminal->terminal policy).
350
+ const completedIntegrityFlips: ReadonlyArray<[TeamTaskStatus, TeamTaskStatus]> = [
351
+ ["completed", "failed"],
352
+ ["completed", "needs_attention"],
353
+ ["failed", "completed"],
354
+ ["cancelled", "completed"],
355
+ ["needs_attention", "completed"],
356
+ ];
357
+ for (const [from, to] of completedIntegrityFlips) rejected.add(statusMergeKey(from, to));
358
+ return rejected;
359
+ })();
360
+
283
361
  function shouldMergeTaskUpdate(current: TeamTaskState, updated: TeamTaskState): boolean {
284
- // Parallel workers receive the same input snapshot. A later result may still
285
- // contain stale queued/running copies of tasks that another worker already
286
- // completed. Never let those stale snapshots regress durable task state.
287
- if (current.status === "waiting" && updated.status === "running") return false;
288
- // Block terminal→non-terminal transitions (e.g. completed→running).
289
- // A task that has reached a terminal state must not be resurrected.
290
- const currentIsTerminal = !isNonTerminalTaskStatus(current.status);
291
- const updatedIsNonTerminal = isNonTerminalTaskStatus(updated.status);
292
- if (currentIsTerminal && updatedIsNonTerminal) return false;
293
- // Explicitly block completed↔needs_attention terminal-to-terminal transitions.
294
- // Both are success terminal states used interchangeably; stale worker updates must
295
- // not cause a completed task to appear as needs_attention or vice versa.
296
- if (current.status === "completed" && updated.status === "needs_attention") return false;
297
- if (current.status === "needs_attention" && updated.status === "completed") return false;
298
- // Explicitly block failed→completed resurrection. Both statuses are terminal,
299
- // but completed is the success terminal state and should not be reachable from
300
- // failed via a stale merge. The check above only guards non-terminal→terminal.
301
- if (current.status === "failed" && updated.status === "completed") return false;
302
- // Mirror that guard for the other dangerous terminal→terminal flips (reverse
303
- // audit CANCEL-3 + F3): a cancelled task must not be resurrected to completed,
304
- // and a completed task must not be demoted to failed, by a stale worker merge.
305
- // The task transition table (contracts.ts) only permits terminal→queued
306
- // (retry); these flips are always stale results. A worker that completed after
307
- // the task was cancelled, or a stale failed result arriving after completion,
308
- // must not flip a settled terminal status.
309
- if (current.status === "cancelled" && updated.status === "completed") return false;
310
- if (current.status === "completed" && updated.status === "failed") return false;
362
+ // RT-16: status-level gate — reject stale/dangerous transitions via the
363
+ // derived transition table (REJECTED_STATUS_MERGE_TRANSITIONS) instead of
364
+ // hand-written guards. Parallel workers receive the same input snapshot; a
365
+ // later result may still carry stale copies. The table encodes three
366
+ // merge-specific policies stricter than the lifecycle table: terminal
367
+ // preservation (no terminal->non-terminal resurrection), completed integrity
368
+ // (no flipping the "completed" success terminal to/from failed or
369
+ // needs_attention), and the waiting->running stale-snapshot regression.
370
+ if (REJECTED_STATUS_MERGE_TRANSITIONS.has(statusMergeKey(current.status, updated.status))) return false;
311
371
  // Guard: when current is "running" but has resultArtifact (another worker already
312
372
  // completed it), a stale updated with status="running" and no resultArtifact
313
373
  // must not overwrite the actual completed state.
@@ -359,6 +419,8 @@ function shouldMergeTaskUpdate(current: TeamTaskState, updated: TeamTaskState):
359
419
  updated.agentProgress?.lastActivityAt !== current.agentProgress?.lastActivityAt;
360
420
  return hasMeaningfulUpdate;
361
421
  }
422
+ /** Exposed for the exhaustive status-merge table test (RT-16). */
423
+ export const __test__shouldMergeTaskUpdate = shouldMergeTaskUpdate;
362
424
 
363
425
  // H4 fix: rename to descriptive name. Kept __test__ as alias for backward
364
426
  // compat test imports.
@@ -406,14 +468,14 @@ export function mergeTaskUpdatesPreservingTerminal(base: TeamTaskState[], result
406
468
  /** @deprecated Use mergeTaskUpdatesPreservingTerminal. Kept for backward test import compat. */
407
469
  export const __test__mergeTaskUpdates = mergeTaskUpdatesPreservingTerminal;
408
470
 
409
- // 2.8: adaptive-plan parsing/repair/injection moved to src/runtime/adaptive-plan.ts.
471
+ // 2.8: adaptive-plan parsing/repair/injection moved to src/runtime/goal-workflow/adaptive-plan.ts.
410
472
  // Re-export the test-only helpers so existing test imports still resolve.
411
473
  export {
412
474
  __test__parseAdaptivePlan,
413
475
  __test__repairAdaptivePlan,
414
- } from "./adaptive-plan.ts";
476
+ } from "./goal-workflow/adaptive-plan.ts";
415
477
 
416
- import { injectAdaptivePlanIfReady } from "./adaptive-plan.ts";
478
+ import { injectAdaptivePlanIfReady } from "./goal-workflow/adaptive-plan.ts";
417
479
 
418
480
  function formatTaskProgress(task: TeamTaskState): string {
419
481
  return `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.error ? ` - ${task.error}` : ""}`;
@@ -442,7 +504,11 @@ function runEffectivenessLines(
442
504
  // (the previous implementation ran 2 redundant passes on every batch) to
443
505
  // a single-pass Map-based replacement: remove the existing entry by path, then
444
506
  // append the new one. Net complexity: O(N) build + O(1) replace per write.
445
- const lastProgressContentHash = new WeakMap<TeamRunManifest, string>();
507
+ // RT-7: key on manifest.runId (stable string) instead of object identity
508
+ // (WeakMap). Every writeProgress mutator returns a NEW manifest object via
509
+ // spread, so object-identity keying meant the cache NEVER hit. Using runId
510
+ // makes back-to-back calls (same millisecond) actually dedup.
511
+ const lastProgressContentHash = new Map<string, string>();
446
512
 
447
513
  function writeProgress(
448
514
  manifest: TeamRunManifest,
@@ -481,19 +547,33 @@ function writeProgress(
481
547
  // both calls happen within the same millisecond. It's a minor win but
482
548
  // matches the audit recommendation (skip artifact write when nothing
483
549
  // material changed).
484
- const prevHash = lastProgressContentHash.get(manifest);
550
+ // RT-7: compute the content hash ONCE (was hashed twice per call: once
551
+ // for the canSkip comparison and again for the cache .set). Key the cache
552
+ // on manifest.runId (stable) instead of object identity (never hit).
553
+ const contentHash = hashContent(content);
554
+ const prevHash = lastProgressContentHash.get(manifest.runId);
485
555
  // Cheap pre-check: avoid the redaction + atomicWrite + readback roundtrip
486
556
  // when both the timestamp and the input args are identical to last time.
487
- const canSkip = prevHash === hashContent(content);
557
+ const canSkip = prevHash === contentHash;
488
558
 
489
559
  const progress = canSkip
490
560
  ? (() => {
491
- // Reuse the previous artifact descriptor rather than rebuilding one
492
- // via writeArtifact. This skips mkdirSync, resolveRealContainedPath,
561
+ // Reuse the previous artifact rather than rebuilding one via
562
+ // writeArtifact. This skips mkdirSync, resolveRealContainedPath,
493
563
  // redactSecrets, atomicWriteFile, and the post-write readFileSync +
494
564
  // statSync.
495
565
  const existing = manifest.artifacts.find((a) => a.kind === "progress");
496
- if (existing) return existing;
566
+ if (existing) {
567
+ // RT-7a: return a FRESH descriptor with a refreshed createdAt
568
+ // instead of reusing the stale existing reference. The existing
569
+ // descriptor's createdAt reflects the FIRST write time, not this
570
+ // skip-write; refreshing it matches the non-skip path (writeArtifact
571
+ // stamps createdAt with the actual write time) so the manifest
572
+ // always carries a descriptor whose createdAt reflects the current
573
+ // write. Content is identical (that's why we skipped), so path /
574
+ // sizeBytes / contentHash / retention are unchanged.
575
+ return { ...existing, createdAt: new Date().toISOString() };
576
+ }
497
577
  // No prior progress artifact (rare; first call from a stale manifest
498
578
  // view). Fall through to the normal write.
499
579
  return writeArtifact(manifest.artifactsRoot, {
@@ -509,7 +589,7 @@ function writeProgress(
509
589
  producer,
510
590
  content,
511
591
  });
512
- lastProgressContentHash.set(manifest, hashContent(content));
592
+ lastProgressContentHash.set(manifest.runId, contentHash);
513
593
 
514
594
  // P6 dedup: replace by path in a single Map pass instead of
515
595
  // .filter(...) // O(N) to remove the old entry
@@ -531,6 +611,13 @@ function writeProgress(
531
611
  };
532
612
  }
533
613
 
614
+ /** @internal RT-7 test export — verify cache is keyed on runId (stable string). */
615
+ export const __test__lastProgressContentHash = lastProgressContentHash;
616
+ /** @internal RT-7 test export — exercise writeProgress directly. */
617
+ export const __test__writeProgress = writeProgress;
618
+ /** @internal RT-14 test export — verify cancelPlanTasks preserves graph mutation after consolidation. */
619
+ export const __test__cancelPlanTasks = cancelPlanTasks;
620
+
534
621
  function applyPolicy(manifest: TeamRunManifest, tasks: TeamTaskState[], limits?: CrewLimitsConfig): TeamRunManifest {
535
622
  const branchFreshness = checkBranchFreshness(manifest.cwd);
536
623
  const branchArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -678,17 +765,15 @@ async function ensurePlanApprovalRequested(manifest: TeamRunManifest, tasks: Tea
678
765
  }
679
766
 
680
767
  function cancelPlanTasks(tasks: TeamTaskState[], reason: string): TeamTaskState[] {
681
- return tasks.map((task) =>
682
- task.status === "queued" || task.status === "running" || task.status === "waiting"
683
- ? {
684
- ...task,
685
- status: "cancelled",
686
- finishedAt: new Date().toISOString(),
687
- error: reason,
688
- graph: task.graph ? { ...task.graph, queue: "done" } : undefined,
689
- }
690
- : task,
691
- );
768
+ // RT-14: delegate to the shared cancelNonTerminalTasks helper. The
769
+ // non-terminal gate (queued/running/waiting) is identical to the helper's
770
+ // default filter (isNonTerminalTaskStatus). The only site-specific logic is
771
+ // the graph mutation (move the task graph to the "done" queue), passed via
772
+ // the `transform` hook so every cancel site uses the single shared path.
773
+ return cancelNonTerminalTasks(tasks, "cancelled", reason, undefined, (task, terminalised) => ({
774
+ ...terminalised,
775
+ graph: task.graph ? { ...task.graph, queue: "done" } : undefined,
776
+ }));
692
777
  }
693
778
 
694
779
  function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
@@ -734,6 +819,25 @@ function dagReadyTaskIds(tasks: TeamTaskState[], completedIds: Set<string>): str
734
819
  return getDagReadyTasks(plan, completedIds);
735
820
  }
736
821
 
822
+ /** RT-12: result shape from a settled dispatch unit (pre-created wrapper). */
823
+ type SettledUnit = {
824
+ unitKey: string;
825
+ result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
826
+ error: Error | undefined;
827
+ };
828
+
829
+ /**
830
+ * RT-12: in-flight dispatch unit. `wrapped` is a pre-created wrapper promise
831
+ * (try/catch → SettledUnit) so mergeUnitResult can Promise.race without
832
+ * allocating new async closures every loop iteration (O(C) total wrappers
833
+ * instead of O(C×T) churn).
834
+ */
835
+ type PendingUnit = {
836
+ taskIds: string[];
837
+ promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>;
838
+ wrapped: Promise<SettledUnit>;
839
+ };
840
+
737
841
  /**
738
842
  * Drain in-flight dispatch units (pendingUnits) by aborting the run-scoped
739
843
  * controller and awaiting all settled promises before clearing the map.
@@ -744,14 +848,17 @@ function dagReadyTaskIds(tasks: TeamTaskState[], completedIds: Set<string>): str
744
848
  *
745
849
  * Exported so unit tests can exercise it directly.
746
850
  */
747
- export async function drainPendingUnits(
748
- pendingUnits: Map<string, { taskIds: string[]; promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> }>,
749
- controller?: AbortController,
750
- ): Promise<void> {
751
- if (pendingUnits.size === 0) return;
851
+ /** Settled outcome of a single in-flight dispatch unit promise (returned by drainPendingUnits). */
852
+ export type DrainOutcome = PromiseSettledResult<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>;
853
+
854
+ export async function drainPendingUnits<
855
+ T extends { taskIds: string[]; promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> },
856
+ >(pendingUnits: Map<string, T>, controller?: AbortController): Promise<DrainOutcome[]> {
857
+ if (pendingUnits.size === 0) return [];
752
858
  controller?.abort();
753
- await Promise.allSettled([...pendingUnits.values()].map((p) => p.promise));
859
+ const outcomes = await Promise.allSettled([...pendingUnits.values()].map((p) => p.promise));
754
860
  pendingUnits.clear();
861
+ return outcomes;
755
862
  }
756
863
 
757
864
  export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
@@ -965,6 +1072,12 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
965
1072
  // NEW-M1: clear the stable-prefix cache (keyed by runId) so it does not
966
1073
  // grow unbounded across runs in a long-lived session.
967
1074
  clearStablePrefixCache();
1075
+ // RT-7b: clear the progress-content cache entry for this run so the
1076
+ // module-level lastProgressContentHash Map (keyed by runId) does not grow
1077
+ // unbounded across runs in a long-lived session. The finally block is the
1078
+ // single guaranteed exit point (success, failure, and re-throw), so this
1079
+ // runs on every run completion. Mirrors clearStablePrefixCache() above.
1080
+ lastProgressContentHash.delete(manifest.runId);
968
1081
  }
969
1082
  }
970
1083
 
@@ -988,7 +1101,7 @@ interface SchedulerContext {
988
1101
  tasks: TeamTaskState[];
989
1102
  queueIndex: TaskGraphIndex;
990
1103
  wfMachine: WorkflowStateMachine;
991
- pendingUnits: Map<string, { taskIds: string[]; promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> }>;
1104
+ pendingUnits: Map<string, PendingUnit>;
992
1105
  runController: AbortController;
993
1106
  runtimeKind: CrewRuntimeKind;
994
1107
  adaptivePlanInjected: boolean;
@@ -1020,6 +1133,43 @@ type SchedulerDecision =
1020
1133
  coalesceEnabled: boolean;
1021
1134
  };
1022
1135
 
1136
+ /**
1137
+ * RT-13: Safely normalize the manifest status to "running" so a subsequent
1138
+ * terminal transition (e.g. → cancelled) goes through updateRunStatus legally.
1139
+ *
1140
+ * Replaces the former inline status-rewrite hack
1141
+ * `manifest = { ...manifest, status: "running" }` which bypassed
1142
+ * updateRunStatus entirely. Unlike the raw spread, this validates the
1143
+ * transition via canTransitionRunStatus. Does NOT call updateRunStatus —
1144
+ * no event is emitted, no persistence — preserving the exact observable
1145
+ * behavior of the former hack. If the transition is somehow illegal, the
1146
+ * manifest is returned unchanged (safe fallback, no throw introduced).
1147
+ *
1148
+ * All non-terminal statuses and the terminal statuses failed/cancelled/
1149
+ * completed can legally reach "running" per TEAM_RUN_STATUS_TRANSITIONS, so
1150
+ * in practice the validation always passes.
1151
+ */
1152
+ export function setRunStatusRunning(manifest: TeamRunManifest): TeamRunManifest {
1153
+ if (canTransitionRunStatus(manifest.status, "running")) {
1154
+ return { ...manifest, status: "running" };
1155
+ }
1156
+ return manifest;
1157
+ }
1158
+
1159
+ /**
1160
+ * RT-17: compute a bounded batch-summary filename slug from coalesced task IDs.
1161
+ *
1162
+ * The unbounded join of coalesced task IDs (e.g. "01+02+…+20") can exceed
1163
+ * NAME_MAX (255) on ext4 with ~20 members. For short joins (common case: ≤180
1164
+ * chars) use the raw IDs to preserve readability + uniqueness; for long joins,
1165
+ * hash to a fixed-length slug (SHA-256 hex = 64 chars) with a member-count
1166
+ * prefix for human scannability.
1167
+ */
1168
+ export function batchSummarySlug(taskIds: string[]): string {
1169
+ const joined = taskIds.join("+");
1170
+ return joined.length <= 180 ? joined : `coalesced-${taskIds.length}-${hashContent(joined)}`;
1171
+ }
1172
+
1023
1173
  /**
1024
1174
  * CORE-4 extraction 1: handle a pre-aborted signal at the top of the
1025
1175
  * scheduler loop.
@@ -1039,25 +1189,25 @@ async function cancelRunFromSignal(ctx: SchedulerContext): Promise<SchedulerDeci
1039
1189
  const cancelReason = cancellationReasonFromSignal(ctx.input.signal);
1040
1190
  const message = `${cancelReason.message} (${cancelReason.code})`;
1041
1191
  const cancelledTaskIds: string[] = [];
1042
- ctx.tasks = ctx.tasks.map((task) => {
1043
- if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") return task;
1192
+ // RT-14: delegate to the shared cancelNonTerminalTasks helper. The
1193
+ // non-terminal gate (queued/running/waiting) is identical to the helper's
1194
+ // default filter. The site-specific logic lives in the `transform` hook:
1195
+ // (1) collect cancelledTaskIds for the run-status event payload, and
1196
+ // (2) synthesise terminalEvidence for in-flight ("running") workers so the
1197
+ // signal-abort is recorded. Both are preserved exactly — only the
1198
+ // transform frame changed, not the per-task output.
1199
+ ctx.tasks = cancelNonTerminalTasks(ctx.tasks, "cancelled", message, undefined, (task, terminalised) => {
1044
1200
  cancelledTaskIds.push(task.id);
1045
- const base = {
1046
- ...task,
1047
- status: "cancelled" as const,
1048
- finishedAt: new Date().toISOString(),
1049
- error: message,
1050
- };
1051
1201
  if (task.status === "running") {
1052
1202
  return {
1053
- ...base,
1203
+ ...terminalised,
1054
1204
  terminalEvidence: [
1055
1205
  ...(task.terminalEvidence ?? []),
1056
1206
  buildSyntheticTerminalEvidence("worker", cancelReason, task.startedAt),
1057
1207
  ],
1058
1208
  };
1059
1209
  }
1060
- return base;
1210
+ return terminalised;
1061
1211
  });
1062
1212
  await saveRunTasksAsync(ctx.manifest, ctx.tasks);
1063
1213
  await Promise.all(
@@ -1134,6 +1284,67 @@ async function handleFailedTask(ctx: SchedulerContext): Promise<SchedulerDecisio
1134
1284
  });
1135
1285
  return { kind: "continue" }; // loop re-processes the re-queued task
1136
1286
  }
1287
+ // RT-1: drain in-flight units and merge settled results BEFORE terminalising
1288
+ // tasks. The streaming-dispatch path only adds to ctx.pendingUnits — it
1289
+ // never updates ctx.tasks to "running" — so ctx.tasks is stale ("queued")
1290
+ // for in-flight tasks. A direct saveRunTasksAsync would overwrite disk
1291
+ // "running" with the stale "queued" status, then markBlocked maps that to
1292
+ // "skipped". team resume never re-queues skipped tasks → permanent work
1293
+ // loss. Draining first preserves settled results and gives non-settled
1294
+ // in-flight tasks a re-queueable terminal status (cancelled, not skipped).
1295
+ //
1296
+ // #3 refactor: drainPendingUnits now RETURNS the settled outcomes directly,
1297
+ // so we collect inflightTaskIds before draining (it clears the map), then
1298
+ // consume the returned outcomes — no redundant re-await of the same promises.
1299
+ // After drain, ctx.pendingUnits is empty, so the finally-block
1300
+ // drainPendingUnits call (~:2419) is a no-op (idempotent — no double-drain).
1301
+ const inflightTaskIds = new Set<string>();
1302
+ for (const unit of ctx.pendingUnits.values()) {
1303
+ for (const id of unit.taskIds) inflightTaskIds.add(id);
1304
+ }
1305
+ const outcomes = await drainPendingUnits(ctx.pendingUnits, ctx.runController);
1306
+ const validResults: { manifest: TeamRunManifest; tasks: TeamTaskState[] }[] = [];
1307
+ for (const outcome of outcomes) {
1308
+ if (outcome.status === "fulfilled") validResults.push(outcome.value);
1309
+ }
1310
+ if (validResults.length > 0) {
1311
+ // Merge under the run lock — same pattern as mergeUnitResult:
1312
+ // flush pending writes, load disk state, merge artifacts + tasks,
1313
+ // save atomically.
1314
+ const mergeResult = await withRunLock(ctx.manifest, async () => {
1315
+ flushPendingAtomicWrites();
1316
+ const disk = loadRunManifestById(ctx.manifest.cwd, ctx.manifest.runId);
1317
+ const diskManifest = disk?.manifest ?? ctx.manifest;
1318
+ const reconciledArtifacts = mergeArtifacts([
1319
+ ...diskManifest.artifacts,
1320
+ ...validResults.flatMap((item) => item.manifest.artifacts),
1321
+ ]);
1322
+ const resultManifest = updateRunStatus(
1323
+ { ...diskManifest, artifacts: reconciledArtifacts },
1324
+ "running",
1325
+ "Merged in-flight results during failed-task abort.",
1326
+ );
1327
+ const resultTasks = mergeTaskUpdatesPreservingTerminal(disk?.tasks ?? ctx.tasks, validResults);
1328
+ await saveRunManifestAsync(resultManifest);
1329
+ await saveRunTasksAsync(resultManifest, resultTasks);
1330
+ return { resultManifest, resultTasks };
1331
+ });
1332
+ ctx.manifest = mergeResult.resultManifest;
1333
+ ctx.tasks = mergeResult.resultTasks;
1334
+ }
1335
+ // RT-1: cancel in-flight tasks that did NOT settle (e.g. rejected promises)
1336
+ // so team resume CAN re-queue them. markBlocked maps queued→skipped, which
1337
+ // resume never re-queues — work would be lost permanently. Only cancel
1338
+ // tasks that are both in-flight AND still non-terminal (settled tasks with
1339
+ // a terminal status are preserved).
1340
+ ctx.tasks = cancelNonTerminalTasks(
1341
+ ctx.tasks,
1342
+ "cancelled",
1343
+ `Cancelled by failed task '${failed.id}'.`,
1344
+ (task) => inflightTaskIds.has(task.id) && isNonTerminalTaskStatus(task.status),
1345
+ );
1346
+ // RT-1: remaining queued tasks (never dispatched) → skipped (original
1347
+ // behavior preserved for downstream tasks not yet in-flight).
1137
1348
  ctx.tasks = markBlocked(ctx.tasks, `Blocked by failed task '${failed.id}'.`);
1138
1349
  await saveRunTasksAsync(ctx.manifest, ctx.tasks);
1139
1350
  saveCrewAgents(ctx.manifest, recordsForMaterializedTasks(ctx.manifest, ctx.tasks, ctx.runtimeKind));
@@ -1466,6 +1677,8 @@ async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBatchDecis
1466
1677
  runtimeKind,
1467
1678
  workspaceId: input.workspaceId,
1468
1679
  onJsonEvent: input.onJsonEvent,
1680
+ runtimeConfig: input.runtimeConfig,
1681
+ reliability: input.reliability,
1469
1682
  teamRole,
1470
1683
  perTaskRuntime,
1471
1684
  });
@@ -1674,9 +1887,26 @@ async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBatchDecis
1674
1887
  for (const unit of dispatchUnits) {
1675
1888
  const unitKey = unit.kind === "singleton" ? unit.taskId : unit.group.id;
1676
1889
  const unitTaskIds = unit.kind === "singleton" ? [unit.taskId] : unit.group.tasks.map((t) => t.id);
1890
+ // RT-12: create the wrapper promise ONCE at dispatch time so
1891
+ // mergeUnitResult can Promise.race on pre-existing wrappers instead
1892
+ // of allocating new async closures every loop iteration.
1893
+ const rawPromise = dispatchUnit(unit);
1894
+ const wrapped: Promise<SettledUnit> = (async () => {
1895
+ try {
1896
+ const result = await rawPromise;
1897
+ return {
1898
+ unitKey,
1899
+ result: result as { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined,
1900
+ error: undefined as Error | undefined,
1901
+ };
1902
+ } catch (error) {
1903
+ return { unitKey, result: undefined, error: error instanceof Error ? error : new Error(String(error)) };
1904
+ }
1905
+ })();
1677
1906
  ctx.pendingUnits.set(unitKey, {
1678
1907
  taskIds: unitTaskIds,
1679
- promise: dispatchUnit(unit),
1908
+ promise: rawPromise,
1909
+ wrapped,
1680
1910
  });
1681
1911
  }
1682
1912
  }
@@ -1701,23 +1931,11 @@ async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBatchDecis
1701
1931
  * @param ctx The scheduler context.
1702
1932
  */
1703
1933
  async function mergeUnitResult(ctx: SchedulerContext): Promise<SchedulerDecision | null> {
1704
- // Wait for ONE in-flight unit to complete. Promise.race returns as soon
1705
- // as the first wrapper resolves — others remain pending in ctx.pendingUnits
1706
- // and are re-raced on the next iteration.
1707
- const settled = await Promise.race(
1708
- [...ctx.pendingUnits.entries()].map(async ([key, pending]) => {
1709
- try {
1710
- const result = await pending.promise;
1711
- return {
1712
- unitKey: key,
1713
- result: result as { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined,
1714
- error: undefined as Error | undefined,
1715
- };
1716
- } catch (error) {
1717
- return { unitKey: key, result: undefined, error: error instanceof Error ? error : new Error(String(error)) };
1718
- }
1719
- }),
1720
- );
1934
+ // RT-12: race on pre-created wrapper promises (created once at dispatch
1935
+ // time) instead of rebuilding a wrapper-promise array with new async
1936
+ // closures every iteration. This reduces allocation from O(C×T) wrapper
1937
+ // promises to O(C) total (one per unit, created once at dispatch).
1938
+ const settled = await Promise.race([...ctx.pendingUnits.values()].map((u) => u.wrapped));
1721
1939
  const completedUnit = ctx.pendingUnits.get(settled.unitKey)!;
1722
1940
  ctx.pendingUnits.delete(settled.unitKey);
1723
1941
 
@@ -1946,7 +2164,7 @@ async function enforceRunBudget(ctx: SchedulerContext): Promise<SchedulerDecisio
1946
2164
  taskUsage: taskTotal,
1947
2165
  },
1948
2166
  }).then(
1949
- () => {},
2167
+ () => undefined,
1950
2168
  (error) => logInternalError("team-runner.fair-share-event", error, `taskId=${violatorId}`),
1951
2169
  ),
1952
2170
  );
@@ -2042,6 +2260,14 @@ async function finalizeRun(ctx: SchedulerContext): Promise<{ manifest: TeamRunMa
2042
2260
  // runnable). This is NOT a completed run — mark it blocked rather than
2043
2261
  // false-green "completed".
2044
2262
  manifest = updateRunStatus(manifest, "blocked", "Run exited with queued tasks still pending.");
2263
+ } else if (manifest.status === "failed" || manifest.status === "cancelled") {
2264
+ // The run was already marked failed/cancelled mid-run (e.g. handleFailedTask
2265
+ // on a coalesced-group race where the failing task's status was later
2266
+ // mutated by the group-drain, or a cancel). Preserve that terminal status —
2267
+ // do NOT force "completed" here: failed -> completed is not in
2268
+ // TEAM_RUN_STATUS_TRANSITIONS and would throw an invalid-transition error.
2269
+ // (No updateRunStatus call: from===to is a no-op, but the intent here is
2270
+ // explicitly "leave the earlier decision intact".)
2045
2271
  } else {
2046
2272
  manifest = updateRunStatus(
2047
2273
  manifest,
@@ -2192,7 +2418,7 @@ async function executeTeamRunCore(
2192
2418
  // task can be dispatched as soon as a slot frees, without waiting for
2193
2419
  // the entire batch to complete. Each entry maps a unit key (singleton
2194
2420
  // task ID or coalesced-group ID) to the in-flight promise + member IDs. ──
2195
- const pendingUnits = new Map<string, { taskIds: string[]; promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> }>();
2421
+ const pendingUnits = new Map<string, PendingUnit>();
2196
2422
 
2197
2423
  // CORE-1: run-scoped AbortController linked to input.signal. Aborted by
2198
2424
  // drainPendingUnits() so in-flight dispatch promises are settled (and
@@ -2224,11 +2450,17 @@ async function executeTeamRunCore(
2224
2450
  // CORE-1: single drain point — all early returns + normal exit settle pendingUnits via finally block.
2225
2451
  try {
2226
2452
  while (tasks.some((task) => task.status === "queued") || pendingUnits.size > 0) {
2227
- // CORE-4: full sync of mutable closure locals → ctx at the top of every
2228
- // iteration. The inline post-merge block (adaptive plan re-injection,
2229
- // phase advance, queueIndex rebuild) mutates these locals; without this
2230
- // sync the extracted functions (selectDispatchBatch / dispatchBatch /
2231
- // mergeUnitResult) would read stale ctx fields — e.g. ctx.workflow
2453
+ // CORE-4 / RT-15: full sync of mutable closure locals → ctx at the top
2454
+ // of every iteration. This is the SINGLE forward-sync point — all
2455
+ // extracted scheduler functions (cancelRunFromSignal /
2456
+ // handleFailedTask / selectDispatchBatch / dispatchBatch /
2457
+ // mergeUnitResult / advanceWorkflowPhases / enforceRunBudget) read
2458
+ // ctx directly. The per-function redundant forward-syncs were removed
2459
+ // (RT-15) because back-syncs after each call keep locals aligned with
2460
+ // ctx, making intermediate re-syncs no-ops. The inline post-merge
2461
+ // block (adaptive plan re-injection, phase advance, queueIndex
2462
+ // rebuild) mutates these locals; without this top-of-loop sync the
2463
+ // extracted functions would read stale ctx fields — e.g. ctx.workflow
2232
2464
  // missing newly-injected adaptive steps → E006 step-not-found.
2233
2465
  ctx.tasks = tasks;
2234
2466
  ctx.manifest = manifest;
@@ -2242,24 +2474,35 @@ async function executeTeamRunCore(
2242
2474
  const signalDecision = await cancelRunFromSignal(ctx);
2243
2475
  if (signalDecision?.kind === "return") return signalDecision.result;
2244
2476
 
2245
- // CORE-4 extraction 2: failed-task handling. Sync mutable locals into
2246
- // ctx before the call; handleFailedTask mutates ctx in-place and
2247
- // returns a SchedulerDecision.
2248
- ctx.tasks = tasks;
2249
- ctx.manifest = manifest;
2477
+ // CORE-4 extraction 2: failed-task handling. ctx is already synced
2478
+ // from the top-of-loop sync (RT-15); handleFailedTask mutates ctx
2479
+ // in-place and returns a SchedulerDecision.
2250
2480
  const failedDecision = await handleFailedTask(ctx);
2251
2481
  tasks = ctx.tasks;
2252
2482
  manifest = ctx.manifest;
2253
- if (failedDecision?.kind === "return") return failedDecision.result;
2483
+ if (failedDecision?.kind === "return") {
2484
+ // #4 (completeness): route the failed-run short-circuit through
2485
+ // finalizeRun so the summary artifact + health snapshot + policy
2486
+ // decisions are written (previously bypassed — failed runs had no
2487
+ // closeout artifacts). handleFailedTask already set manifest status
2488
+ // to "failed" and saved tasks; finalizeRun re-derives the status
2489
+ // (still "failed" since a failed task exists — the `if (failed)`
2490
+ // branch in finalizeRun takes priority) and performs the closeout
2491
+ // writes. Policy-decision semantics are UNCHANGED — finalizeRun
2492
+ // uses identical logic for both the normal-completion and
2493
+ // failed-short-circuit paths.
2494
+ const failedResult = await finalizeRun(ctx);
2495
+ manifest = ctx.manifest;
2496
+ tasks = ctx.tasks;
2497
+ return failedResult;
2498
+ }
2254
2499
  if (failedDecision?.kind === "continue") continue;
2255
2500
 
2256
- // CORE-4 extraction 3: batch selection. Sync mutable locals into ctx;
2257
- // selectDispatchBatch computes the ready batch and returns a dispatch
2258
- // decision (or return if the run must block/abort). The caller syncs
2259
- // ctx.wfMachine back because the function may advance phases.
2260
- ctx.tasks = tasks;
2261
- ctx.manifest = manifest;
2262
- ctx.wfMachine = wfMachine;
2501
+ // CORE-4 extraction 3: batch selection. ctx is already synced from
2502
+ // the top-of-loop sync (RT-15); selectDispatchBatch computes the
2503
+ // ready batch and returns a dispatch decision (or return if the run
2504
+ // must block/abort). The caller syncs ctx.wfMachine back because
2505
+ // the function may advance phases.
2263
2506
  const dispatchDecision = await selectDispatchBatch(ctx);
2264
2507
  tasks = ctx.tasks;
2265
2508
  manifest = ctx.manifest;
@@ -2271,9 +2514,7 @@ async function executeTeamRunCore(
2271
2514
  // dispatch units, pre-warms the stable-prefix cache, and adds each
2272
2515
  // unit's promise to ctx.pendingUnits (wrapped in executeWithRetry).
2273
2516
  // The function mutates ctx.tasks (hook skips) and ctx.manifest (hook
2274
- // status) in-place. Verbatim lift of the inline dispatch block.
2275
- ctx.tasks = tasks;
2276
- ctx.manifest = manifest;
2517
+ // status) in-place. ctx is already synced from the top-of-loop sync.
2277
2518
  await dispatchBatch(ctx, dispatchDecision);
2278
2519
  tasks = ctx.tasks;
2279
2520
  manifest = ctx.manifest;
@@ -2289,8 +2530,6 @@ async function executeTeamRunCore(
2289
2530
  // ctx.pendingUnits, and records the merge outcome on ctx. Returns null
2290
2531
  // (continue to phase/budget check) — a return decision is reserved for
2291
2532
  // future run-complete/failure detection.
2292
- ctx.tasks = tasks;
2293
- ctx.manifest = manifest;
2294
2533
  const mergeDecision = await mergeUnitResult(ctx);
2295
2534
  tasks = ctx.tasks;
2296
2535
  manifest = ctx.manifest;
@@ -2299,21 +2538,20 @@ async function executeTeamRunCore(
2299
2538
  // (cancel-during-exec check + batch summary artifact).
2300
2539
  const { taskIds: settledTaskIds, result: resultToMerge } = ctx.settledMerge!;
2301
2540
 
2302
- // CORE-4 extraction 6: workflow phase advance. Sync ctx.wfMachine;
2541
+ // CORE-4 extraction 6: workflow phase advance. ctx.wfMachine is
2542
+ // already synced from the top-of-loop sync (RT-15);
2303
2543
  // advanceWorkflowPhases advances phases whose tasks are all terminal,
2304
2544
  // emits phase_* events, and advances currentPhaseIndex. Mutates
2305
2545
  // ctx.wfMachine in-place.
2306
- ctx.wfMachine = wfMachine;
2307
2546
  await advanceWorkflowPhases(ctx);
2308
2547
  wfMachine = ctx.wfMachine;
2309
2548
 
2310
- // CORE-4 extraction 7: budget enforcement. Sync ctx.tasks/manifest;
2311
- // enforceRunBudget checks cumulative usage against warn/abort thresholds
2312
- // and a fair-share heuristic. On abort it marks the run failed and
2313
- // returns { kind: "return" }; otherwise emits warning events and
2314
- // returns null (continue).
2315
- ctx.tasks = tasks;
2316
- ctx.manifest = manifest;
2549
+ // CORE-4 extraction 7: budget enforcement. ctx is already synced
2550
+ // from the top-of-loop sync (RT-15); enforceRunBudget checks
2551
+ // cumulative usage against warn/abort thresholds and a fair-share
2552
+ // heuristic. On abort it marks the run failed and returns
2553
+ // { kind: "return" }; otherwise emits warning events and returns
2554
+ // null (continue).
2317
2555
  const budgetDecision = await enforceRunBudget(ctx);
2318
2556
  tasks = ctx.tasks;
2319
2557
  manifest = ctx.manifest;
@@ -2323,7 +2561,7 @@ async function executeTeamRunCore(
2323
2561
  if (cancelledResult || input.signal?.aborted) {
2324
2562
  const reason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : undefined;
2325
2563
  const message = reason?.message ?? cancelledResult?.manifest.summary ?? "Run cancelled during task execution.";
2326
- manifest = { ...manifest, status: "running" };
2564
+ manifest = setRunStatusRunning(manifest);
2327
2565
  manifest = updateRunStatus(manifest, "cancelled", message);
2328
2566
  // CANCEL-2: re-cancel non-terminal tasks here, mirroring the batch-loop
2329
2567
  // cancel check at team-runner.ts:~925. A manifest cancelled mid-merge
@@ -2384,7 +2622,7 @@ async function executeTeamRunCore(
2384
2622
  const completedBatch = tasks.filter((t) => settledTaskIds.includes(t.id));
2385
2623
  const batchArtifact = writeArtifact(manifest.artifactsRoot, {
2386
2624
  kind: "summary",
2387
- relativePath: `batches/${settledTaskIds.join("+")}.md`,
2625
+ relativePath: `batches/${batchSummarySlug(settledTaskIds)}.md`,
2388
2626
  producer: "team-runner",
2389
2627
  content: aggregateTaskOutputs(completedBatch, manifest),
2390
2628
  });
@@ -2416,6 +2654,9 @@ async function executeTeamRunCore(
2416
2654
  tasks = ctx.tasks;
2417
2655
  return finalResult;
2418
2656
  } finally {
2657
+ // #3: drainPendingUnits returns settled outcomes, but the finally block
2658
+ // only needs the drain side-effect (abort + await + clear); the return
2659
+ // value is intentionally unused here.
2419
2660
  await drainPendingUnits(pendingUnits, runController);
2420
2661
  }
2421
2662
  }