@pennixrv/trellis 0.7.1 → 0.7.2-beta.2

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 (321) hide show
  1. package/README.md +10 -7
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +6 -4
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/ablate.d.ts.map +1 -1
  6. package/dist/commands/ablate.js +2 -0
  7. package/dist/commands/ablate.js.map +1 -1
  8. package/dist/commands/backup.d.ts +4 -0
  9. package/dist/commands/backup.d.ts.map +1 -0
  10. package/dist/commands/backup.js +75 -0
  11. package/dist/commands/backup.js.map +1 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +705 -681
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/mem.d.ts.map +1 -1
  16. package/dist/commands/mem.js +49 -7
  17. package/dist/commands/mem.js.map +1 -1
  18. package/dist/commands/task-progress.d.ts +1 -0
  19. package/dist/commands/task-progress.d.ts.map +1 -1
  20. package/dist/commands/task-progress.js +67 -25
  21. package/dist/commands/task-progress.js.map +1 -1
  22. package/dist/commands/teamwork/adapters/acp.d.ts +20 -0
  23. package/dist/commands/teamwork/adapters/acp.d.ts.map +1 -0
  24. package/dist/commands/teamwork/adapters/acp.js +268 -0
  25. package/dist/commands/teamwork/adapters/acp.js.map +1 -0
  26. package/dist/commands/teamwork/adapters/claude.d.ts +1 -0
  27. package/dist/commands/teamwork/adapters/claude.d.ts.map +1 -1
  28. package/dist/commands/teamwork/adapters/claude.js +5 -0
  29. package/dist/commands/teamwork/adapters/claude.js.map +1 -1
  30. package/dist/commands/teamwork/adapters/codex.d.ts +3 -2
  31. package/dist/commands/teamwork/adapters/codex.d.ts.map +1 -1
  32. package/dist/commands/teamwork/adapters/codex.js +14 -15
  33. package/dist/commands/teamwork/adapters/codex.js.map +1 -1
  34. package/dist/commands/teamwork/adapters/index.d.ts +23 -1
  35. package/dist/commands/teamwork/adapters/index.d.ts.map +1 -1
  36. package/dist/commands/teamwork/adapters/index.js +163 -24
  37. package/dist/commands/teamwork/adapters/index.js.map +1 -1
  38. package/dist/commands/teamwork/agent-loader.d.ts +2 -1
  39. package/dist/commands/teamwork/agent-loader.d.ts.map +1 -1
  40. package/dist/commands/teamwork/agent-loader.js +2 -1
  41. package/dist/commands/teamwork/agent-loader.js.map +1 -1
  42. package/dist/commands/teamwork/artifact.d.ts +42 -14
  43. package/dist/commands/teamwork/artifact.d.ts.map +1 -1
  44. package/dist/commands/teamwork/artifact.js +188 -85
  45. package/dist/commands/teamwork/artifact.js.map +1 -1
  46. package/dist/commands/teamwork/batch-reservation.d.ts +16 -0
  47. package/dist/commands/teamwork/batch-reservation.d.ts.map +1 -0
  48. package/dist/commands/teamwork/batch-reservation.js +43 -0
  49. package/dist/commands/teamwork/batch-reservation.js.map +1 -0
  50. package/dist/commands/teamwork/batch.d.ts +23 -0
  51. package/dist/commands/teamwork/batch.d.ts.map +1 -0
  52. package/dist/commands/teamwork/batch.js +415 -0
  53. package/dist/commands/teamwork/batch.js.map +1 -0
  54. package/dist/commands/teamwork/context-loader.d.ts +1 -1
  55. package/dist/commands/teamwork/context-loader.d.ts.map +1 -1
  56. package/dist/commands/teamwork/context-loader.js +19 -6
  57. package/dist/commands/teamwork/context-loader.js.map +1 -1
  58. package/dist/commands/teamwork/coordination.d.ts +88 -0
  59. package/dist/commands/teamwork/coordination.d.ts.map +1 -0
  60. package/dist/commands/teamwork/coordination.js +153 -0
  61. package/dist/commands/teamwork/coordination.js.map +1 -0
  62. package/dist/commands/teamwork/coordinator.d.ts +33 -6
  63. package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
  64. package/dist/commands/teamwork/coordinator.js +948 -342
  65. package/dist/commands/teamwork/coordinator.js.map +1 -1
  66. package/dist/commands/teamwork/create.d.ts +1 -0
  67. package/dist/commands/teamwork/create.d.ts.map +1 -1
  68. package/dist/commands/teamwork/create.js +2 -1
  69. package/dist/commands/teamwork/create.js.map +1 -1
  70. package/dist/commands/teamwork/dev-parse-trace.d.ts +2 -1
  71. package/dist/commands/teamwork/dev-parse-trace.d.ts.map +1 -1
  72. package/dist/commands/teamwork/dev-parse-trace.js +2 -0
  73. package/dist/commands/teamwork/dev-parse-trace.js.map +1 -1
  74. package/dist/commands/teamwork/foreground-wait.d.ts +6 -0
  75. package/dist/commands/teamwork/foreground-wait.d.ts.map +1 -0
  76. package/dist/commands/teamwork/foreground-wait.js +65 -0
  77. package/dist/commands/teamwork/foreground-wait.js.map +1 -0
  78. package/dist/commands/teamwork/guard.d.ts +2 -2
  79. package/dist/commands/teamwork/guard.d.ts.map +1 -1
  80. package/dist/commands/teamwork/guard.js +25 -7
  81. package/dist/commands/teamwork/guard.js.map +1 -1
  82. package/dist/commands/teamwork/index.d.ts.map +1 -1
  83. package/dist/commands/teamwork/index.js +163 -10
  84. package/dist/commands/teamwork/index.js.map +1 -1
  85. package/dist/commands/teamwork/kill.d.ts +2 -0
  86. package/dist/commands/teamwork/kill.d.ts.map +1 -1
  87. package/dist/commands/teamwork/kill.js +79 -38
  88. package/dist/commands/teamwork/kill.js.map +1 -1
  89. package/dist/commands/teamwork/messages.d.ts.map +1 -1
  90. package/dist/commands/teamwork/messages.js +3 -0
  91. package/dist/commands/teamwork/messages.js.map +1 -1
  92. package/dist/commands/teamwork/plan.d.ts +22 -0
  93. package/dist/commands/teamwork/plan.d.ts.map +1 -0
  94. package/dist/commands/teamwork/plan.js +288 -0
  95. package/dist/commands/teamwork/plan.js.map +1 -0
  96. package/dist/commands/teamwork/process-identity.d.ts +3 -0
  97. package/dist/commands/teamwork/process-identity.d.ts.map +1 -0
  98. package/dist/commands/teamwork/process-identity.js +24 -0
  99. package/dist/commands/teamwork/process-identity.js.map +1 -0
  100. package/dist/commands/teamwork/profiles.d.ts +43 -6
  101. package/dist/commands/teamwork/profiles.d.ts.map +1 -1
  102. package/dist/commands/teamwork/profiles.js +138 -110
  103. package/dist/commands/teamwork/profiles.js.map +1 -1
  104. package/dist/commands/teamwork/prompt.d.ts +4 -0
  105. package/dist/commands/teamwork/prompt.d.ts.map +1 -0
  106. package/dist/commands/teamwork/prompt.js +33 -0
  107. package/dist/commands/teamwork/prompt.js.map +1 -0
  108. package/dist/commands/teamwork/result.d.ts +55 -0
  109. package/dist/commands/teamwork/result.d.ts.map +1 -0
  110. package/dist/commands/teamwork/result.js +94 -0
  111. package/dist/commands/teamwork/result.js.map +1 -0
  112. package/dist/commands/teamwork/rm.d.ts.map +1 -1
  113. package/dist/commands/teamwork/rm.js +64 -64
  114. package/dist/commands/teamwork/rm.js.map +1 -1
  115. package/dist/commands/teamwork/run.d.ts +5 -0
  116. package/dist/commands/teamwork/run.d.ts.map +1 -1
  117. package/dist/commands/teamwork/run.js +120 -24
  118. package/dist/commands/teamwork/run.js.map +1 -1
  119. package/dist/commands/teamwork/runtime.d.ts +9 -0
  120. package/dist/commands/teamwork/runtime.d.ts.map +1 -0
  121. package/dist/commands/teamwork/runtime.js +401 -0
  122. package/dist/commands/teamwork/runtime.js.map +1 -0
  123. package/dist/commands/teamwork/send.d.ts +1 -0
  124. package/dist/commands/teamwork/send.d.ts.map +1 -1
  125. package/dist/commands/teamwork/send.js +2 -1
  126. package/dist/commands/teamwork/send.js.map +1 -1
  127. package/dist/commands/teamwork/spawn.d.ts +10 -3
  128. package/dist/commands/teamwork/spawn.d.ts.map +1 -1
  129. package/dist/commands/teamwork/spawn.js +42 -27
  130. package/dist/commands/teamwork/spawn.js.map +1 -1
  131. package/dist/commands/teamwork/store/lock.js +28 -4
  132. package/dist/commands/teamwork/store/lock.js.map +1 -1
  133. package/dist/commands/teamwork/supervisor/inbox.d.ts +1 -0
  134. package/dist/commands/teamwork/supervisor/inbox.d.ts.map +1 -1
  135. package/dist/commands/teamwork/supervisor/inbox.js +28 -9
  136. package/dist/commands/teamwork/supervisor/inbox.js.map +1 -1
  137. package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -1
  138. package/dist/commands/teamwork/supervisor/stdout.js +2 -3
  139. package/dist/commands/teamwork/supervisor/stdout.js.map +1 -1
  140. package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -1
  141. package/dist/commands/teamwork/supervisor/warning.js +2 -3
  142. package/dist/commands/teamwork/supervisor/warning.js.map +1 -1
  143. package/dist/commands/teamwork/supervisor.d.ts +5 -0
  144. package/dist/commands/teamwork/supervisor.d.ts.map +1 -1
  145. package/dist/commands/teamwork/supervisor.js +21 -12
  146. package/dist/commands/teamwork/supervisor.js.map +1 -1
  147. package/dist/commands/teamwork/teammate-adapters.d.ts +0 -5
  148. package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
  149. package/dist/commands/teamwork/teammate-adapters.js +9 -56
  150. package/dist/commands/teamwork/teammate-adapters.js.map +1 -1
  151. package/dist/commands/teamwork/teammate-config.d.ts +5 -1
  152. package/dist/commands/teamwork/teammate-config.d.ts.map +1 -1
  153. package/dist/commands/teamwork/teammate-config.js +62 -19
  154. package/dist/commands/teamwork/teammate-config.js.map +1 -1
  155. package/dist/commands/teamwork/teammate-launch.d.ts +15 -6
  156. package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
  157. package/dist/commands/teamwork/teammate-launch.js +129 -22
  158. package/dist/commands/teamwork/teammate-launch.js.map +1 -1
  159. package/dist/commands/teamwork/wait.d.ts +2 -0
  160. package/dist/commands/teamwork/wait.d.ts.map +1 -1
  161. package/dist/commands/teamwork/wait.js +44 -3
  162. package/dist/commands/teamwork/wait.js.map +1 -1
  163. package/dist/commands/uninstall.d.ts.map +1 -1
  164. package/dist/commands/uninstall.js +92 -84
  165. package/dist/commands/uninstall.js.map +1 -1
  166. package/dist/commands/update.d.ts.map +1 -1
  167. package/dist/commands/update.js +635 -592
  168. package/dist/commands/update.js.map +1 -1
  169. package/dist/commands/workflow.d.ts.map +1 -1
  170. package/dist/commands/workflow.js +94 -89
  171. package/dist/commands/workflow.js.map +1 -1
  172. package/dist/configurators/shared.js +2 -2
  173. package/dist/configurators/shared.js.map +1 -1
  174. package/dist/configurators/workflow.d.ts.map +1 -1
  175. package/dist/configurators/workflow.js +14 -32
  176. package/dist/configurators/workflow.js.map +1 -1
  177. package/dist/constants/paths.d.ts +3 -10
  178. package/dist/constants/paths.d.ts.map +1 -1
  179. package/dist/constants/paths.js +4 -13
  180. package/dist/constants/paths.js.map +1 -1
  181. package/dist/migrations/manifests/0.7.1-beta.0.json +9 -0
  182. package/dist/migrations/manifests/0.7.1-beta.1.json +186 -0
  183. package/dist/migrations/manifests/0.7.1-beta.10.json +9 -0
  184. package/dist/migrations/manifests/0.7.1-beta.11.json +9 -0
  185. package/dist/migrations/manifests/0.7.1-beta.12.json +9 -0
  186. package/dist/migrations/manifests/0.7.1-beta.13.json +9 -0
  187. package/dist/migrations/manifests/0.7.1-beta.14.json +11 -0
  188. package/dist/migrations/manifests/0.7.1-beta.15.json +9 -0
  189. package/dist/migrations/manifests/0.7.1-beta.16.json +9 -0
  190. package/dist/migrations/manifests/0.7.1-beta.17.json +9 -0
  191. package/dist/migrations/manifests/0.7.1-beta.18.json +9 -0
  192. package/dist/migrations/manifests/0.7.1-beta.19.json +9 -0
  193. package/dist/migrations/manifests/0.7.1-beta.2.json +522 -0
  194. package/dist/migrations/manifests/0.7.1-beta.3.json +9 -0
  195. package/dist/migrations/manifests/0.7.1-beta.4.json +9 -0
  196. package/dist/migrations/manifests/0.7.1-beta.5.json +9 -0
  197. package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
  198. package/dist/migrations/manifests/0.7.1-beta.7.json +8 -0
  199. package/dist/migrations/manifests/0.7.1-beta.8.json +481 -0
  200. package/dist/migrations/manifests/0.7.1-beta.9.json +191 -0
  201. package/dist/migrations/manifests/0.7.2-beta.0.json +11 -0
  202. package/dist/migrations/manifests/0.7.2-beta.1.json +11 -0
  203. package/dist/migrations/manifests/0.7.2-beta.2.json +11 -0
  204. package/dist/templates/claude/hooks/statusline.py +9 -4
  205. package/dist/templates/codex/hooks/session-start.py +14 -11
  206. package/dist/templates/codex/hooks.json +10 -0
  207. package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +2 -0
  208. package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +2 -0
  209. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
  210. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
  211. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
  212. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
  213. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
  214. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
  215. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +14 -2
  216. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
  217. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +2 -1
  218. package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +2 -2
  219. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +8 -0
  220. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +4 -2
  221. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +2 -2
  222. package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +6 -0
  223. package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +18 -3
  224. package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +34 -3
  225. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +37 -26
  226. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +128 -14
  227. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +152 -20
  228. package/dist/templates/common/commands/start.md +2 -2
  229. package/dist/templates/common/skills/before-dev.md +1 -4
  230. package/dist/templates/common/skills/break-loop.md +7 -18
  231. package/dist/templates/common/skills/check.md +7 -11
  232. package/dist/templates/common/skills/{brainstorm.md → request-intake.md} +5 -3
  233. package/dist/templates/common/skills/update-spec.md +5 -7
  234. package/dist/templates/copilot/hooks/session-start.py +14 -11
  235. package/dist/templates/copilot/index.d.ts +1 -6
  236. package/dist/templates/copilot/index.d.ts.map +1 -1
  237. package/dist/templates/copilot/index.js +1 -14
  238. package/dist/templates/copilot/index.js.map +1 -1
  239. package/dist/templates/extract.js +4 -0
  240. package/dist/templates/extract.js.map +1 -1
  241. package/dist/templates/markdown/spec/backend/index.md.txt +4 -35
  242. package/dist/templates/markdown/spec/frontend/index.md.txt +4 -36
  243. package/dist/templates/omp/extensions/trellis/index.ts.txt +2 -2
  244. package/dist/templates/opencode/lib/session-utils.js +301 -209
  245. package/dist/templates/pi/extensions/trellis/index.ts.txt +17 -1
  246. package/dist/templates/shared-hooks/guard-teamwork-wait.py +58 -0
  247. package/dist/templates/shared-hooks/index.d.ts +1 -1
  248. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  249. package/dist/templates/shared-hooks/index.js +1 -0
  250. package/dist/templates/shared-hooks/index.js.map +1 -1
  251. package/dist/templates/shared-hooks/inject-workflow-state.py +8 -3
  252. package/dist/templates/shared-hooks/session-start.py +16 -12
  253. package/dist/templates/snow/SNOW.md +1 -1
  254. package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +9 -0
  255. package/dist/templates/trellis/agents/profiles/architecture.md +9 -0
  256. package/dist/templates/trellis/agents/profiles/diagnosis.md +9 -0
  257. package/dist/templates/trellis/agents/profiles/exploration.md +9 -0
  258. package/dist/templates/trellis/agents/profiles/research.md +9 -0
  259. package/dist/templates/trellis/agents/profiles/review.md +9 -0
  260. package/dist/templates/trellis/agents/profiles/verification.md +9 -0
  261. package/dist/templates/trellis/agents/teammate-profiles.json +56 -4
  262. package/dist/templates/trellis/agents/teammate.md +4 -52
  263. package/dist/templates/trellis/config.yaml +2 -5
  264. package/dist/templates/trellis/index.d.ts +4 -0
  265. package/dist/templates/trellis/index.d.ts.map +1 -1
  266. package/dist/templates/trellis/index.js +22 -4
  267. package/dist/templates/trellis/index.js.map +1 -1
  268. package/dist/templates/trellis/scripts/add_session.py +1 -1
  269. package/dist/templates/trellis/scripts/common/active_task.py +3 -47
  270. package/dist/templates/trellis/scripts/common/config.py +5 -0
  271. package/dist/templates/trellis/scripts/common/paths.py +9 -25
  272. package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
  273. package/dist/templates/trellis/scripts/common/task_context.py +11 -51
  274. package/dist/templates/trellis/scripts/common/task_continuity.py +191 -60
  275. package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
  276. package/dist/templates/trellis/scripts/common/task_move.py +345 -0
  277. package/dist/templates/trellis/scripts/common/task_store.py +51 -57
  278. package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
  279. package/dist/templates/trellis/scripts/common/task_workflow.py +395 -49
  280. package/dist/templates/trellis/scripts/common/tasks.py +6 -5
  281. package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
  282. package/dist/templates/trellis/scripts/task.py +76 -24
  283. package/dist/templates/trellis/teamwork.json +10 -0
  284. package/dist/templates/trellis/workflow.md +11 -2
  285. package/dist/utils/ablation-store.d.ts +1 -1
  286. package/dist/utils/ablation-store.d.ts.map +1 -1
  287. package/dist/utils/ablation-store.js +58 -24
  288. package/dist/utils/ablation-store.js.map +1 -1
  289. package/dist/utils/asset-backup.d.ts +138 -0
  290. package/dist/utils/asset-backup.d.ts.map +1 -0
  291. package/dist/utils/asset-backup.js +525 -0
  292. package/dist/utils/asset-backup.js.map +1 -0
  293. package/dist/utils/atomic-write.d.ts +1 -1
  294. package/dist/utils/atomic-write.d.ts.map +1 -1
  295. package/dist/utils/atomic-write.js +6 -3
  296. package/dist/utils/atomic-write.js.map +1 -1
  297. package/dist/utils/manifest-prune.d.ts +4 -1
  298. package/dist/utils/manifest-prune.d.ts.map +1 -1
  299. package/dist/utils/manifest-prune.js +17 -6
  300. package/dist/utils/manifest-prune.js.map +1 -1
  301. package/dist/utils/template-fetcher.d.ts +4 -19
  302. package/dist/utils/template-fetcher.d.ts.map +1 -1
  303. package/dist/utils/template-fetcher.js +9 -45
  304. package/dist/utils/template-fetcher.js.map +1 -1
  305. package/dist/utils/template-hash.d.ts +5 -4
  306. package/dist/utils/template-hash.d.ts.map +1 -1
  307. package/dist/utils/template-hash.js +24 -17
  308. package/dist/utils/template-hash.js.map +1 -1
  309. package/package.json +3 -2
  310. package/dist/templates/copilot/prompts/before-dev.prompt.md +0 -39
  311. package/dist/templates/copilot/prompts/brainstorm.prompt.md +0 -13
  312. package/dist/templates/copilot/prompts/break-loop.prompt.md +0 -129
  313. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +0 -170
  314. package/dist/templates/copilot/prompts/check.prompt.md +0 -110
  315. package/dist/templates/copilot/prompts/create-command.prompt.md +0 -116
  316. package/dist/templates/copilot/prompts/finish-work.prompt.md +0 -22
  317. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +0 -223
  318. package/dist/templates/copilot/prompts/onboard.prompt.md +0 -362
  319. package/dist/templates/copilot/prompts/record-session.prompt.md +0 -58
  320. package/dist/templates/copilot/prompts/start.prompt.md +0 -65
  321. package/dist/templates/copilot/prompts/update-spec.prompt.md +0 -358
@@ -1,12 +1,16 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
3
5
  import { isTaskPhaseId, parseTaskWorkflowState, } from "@pennixrv/trellis-core/task";
4
6
  import { assertCoordinationTransition, readTeamworkEvents, readCoordinationState, recordCoordinationEvent, } from "@pennixrv/trellis-core/teamwork";
5
- import { cleanupSettledTeamworkArtifacts, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
6
- import { assertTeammateAdapterContract, } from "./teammate-adapters.js";
7
+ import { TeammateReportReadError, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
8
+ import { assertTeammateAdapterContract, assertTeammateHostCapability, } from "./teammate-adapters.js";
7
9
  import { effectiveTeammateCapacity, resolveTeammateRuntimeConfig, } from "./teammate-config.js";
8
- import { resolveTeammateProfile } from "./profiles.js";
9
- import { buildTeammateLaunchSpec, } from "./teammate-launch.js";
10
+ import { readProcessIdentity } from "./process-identity.js";
11
+ import { withLock } from "./store/lock.js";
12
+ export class TeamworkPolicyChangedError extends Error {
13
+ }
10
14
  export function assertTeamworkPlanCurrent(plan, taskRoot) {
11
15
  const taskFile = path.join(taskRoot, "task.json");
12
16
  const stat = fs.lstatSync(taskFile);
@@ -37,6 +41,169 @@ export function assertTeamworkPlanCurrent(plan, taskRoot) {
37
41
  throw new Error("Teamwork plan does not match the current active task phase and revisions");
38
42
  }
39
43
  }
44
+ function terminalReceiptFromProjection(plan, projection) {
45
+ const mismatch = (reason) => {
46
+ throw new Error(`batch '${plan.batchId}' ${reason}`);
47
+ };
48
+ if (projection.issues.length > 0)
49
+ mismatch("has coordination projection issues");
50
+ const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
51
+ if (!batch)
52
+ return mismatch("does not match the frozen plan");
53
+ if (batch.planHash !== plan.planHash)
54
+ return mismatch("does not match the frozen plan");
55
+ if (!["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
56
+ mismatch("is not terminal");
57
+ const effectiveCapacity = batch.effectiveCapacity;
58
+ if (typeof effectiveCapacity !== "number" ||
59
+ !Number.isSafeInteger(effectiveCapacity) ||
60
+ effectiveCapacity < 1)
61
+ return mismatch("has no valid effective capacity");
62
+ if (projection.leases.some((lease) => lease.batchId === plan.batchId))
63
+ mismatch("still owns capacity leases");
64
+ const projectedAssignments = projection.assignments.filter((assignment) => assignment.batchId === plan.batchId);
65
+ if (projectedAssignments.length !== plan.assignments.length ||
66
+ new Set(projectedAssignments.map((assignment) => assignment.assignmentId))
67
+ .size !== plan.assignments.length)
68
+ mismatch("does not contain the exact frozen assignment set");
69
+ const assignmentById = new Map(projectedAssignments.map((assignment) => [
70
+ assignment.assignmentId,
71
+ assignment,
72
+ ]));
73
+ const planAssignmentIds = new Set(plan.assignments.map((assignment) => assignment.assignmentId));
74
+ const attempts = projection.attempts.filter((attempt) => attempt.batchId === plan.batchId);
75
+ const teammates = projection.teammates.filter((teammate) => teammate.batchId === plan.batchId);
76
+ if (attempts.length !== teammates.length)
77
+ mismatch("has unmatched provider attempts and teammates");
78
+ for (const attempt of attempts) {
79
+ if (!attempt.assignmentId ||
80
+ !planAssignmentIds.has(attempt.assignmentId) ||
81
+ !attempt.teammateId ||
82
+ attempt.terminal !== true ||
83
+ !["REPORT_SUBMITTED", "FAILED", "FINAL_FAILED"].includes(attempt.state))
84
+ mismatch("has an unresolved provider attempt");
85
+ const teammate = teammates.find((entry) => entry.attemptId === attempt.attemptId &&
86
+ entry.teammateId === attempt.teammateId);
87
+ if (teammate?.terminal !== true ||
88
+ (teammate?.state !== "REPORT_SUBMITTED" && teammate?.state !== "FAILED"))
89
+ mismatch("has an unresolved teammate terminal fact");
90
+ }
91
+ const assignments = plan.assignments.map((assignment) => {
92
+ const projected = assignmentById.get(assignment.assignmentId);
93
+ if (!projected ||
94
+ !["AWAITING_REVIEW", "ACCEPTED", "FINAL_FAILED"].includes(projected.state))
95
+ return mismatch(`assignment '${assignment.assignmentId}' is not settled`);
96
+ if (projected.state === "ACCEPTED" ||
97
+ projected.state === "AWAITING_REVIEW") {
98
+ const acceptedAttempt = attempts.find((attempt) => attempt.attemptId === projected.attemptId &&
99
+ attempt.assignmentId === assignment.assignmentId);
100
+ if (acceptedAttempt?.state !== "REPORT_SUBMITTED")
101
+ return mismatch(`assignment '${assignment.assignmentId}' has no accepted attempt`);
102
+ }
103
+ return {
104
+ assignmentId: assignment.assignmentId,
105
+ status: projected.state,
106
+ };
107
+ });
108
+ const status = assignments.some((item) => item.status === "AWAITING_REVIEW")
109
+ ? "AWAITING_REVIEW"
110
+ : assignments.every((item) => item.status === "ACCEPTED")
111
+ ? "ALL_ACCEPTED"
112
+ : "SETTLED_WITH_FAILURE";
113
+ if (batch.state !== status)
114
+ mismatch("terminal state disagrees with its assignment dispositions");
115
+ const retryCounts = Object.fromEntries(plan.assignments.flatMap((assignment) => {
116
+ const count = assignmentById.get(assignment.assignmentId)?.retryCount;
117
+ if (count === undefined)
118
+ return mismatch(`assignment '${assignment.assignmentId}' is missing`);
119
+ const started = attempts.some((attempt) => attempt.assignmentId === assignment.assignmentId);
120
+ return started || count > 0 ? [[assignment.assignmentId, count]] : [];
121
+ }));
122
+ const evaluationGroups = [
123
+ ...new Set(plan.assignments
124
+ .map((assignment) => assignment.evaluationGroup?.id)
125
+ .filter((id) => id !== undefined)),
126
+ ].map((id) => ({
127
+ id,
128
+ status: plan.assignments
129
+ .filter((assignment) => assignment.evaluationGroup?.id === id)
130
+ .every((assignment) => assignmentById.get(assignment.assignmentId)?.state === "ACCEPTED")
131
+ ? "COMPLETE"
132
+ : "INCOMPLETE",
133
+ }));
134
+ return {
135
+ schemaVersion: 1,
136
+ batchId: plan.batchId,
137
+ status,
138
+ assignments,
139
+ capacity: effectiveCapacity,
140
+ retryCounts,
141
+ evaluationGroups,
142
+ };
143
+ }
144
+ function reconciliationReceipt(plan, projection, capacity) {
145
+ return {
146
+ schemaVersion: 1,
147
+ batchId: plan.batchId,
148
+ status: "RECONCILIATION_REQUIRED",
149
+ assignments: plan.assignments.map((item) => {
150
+ const current = projection.assignments.find((entry) => entry.batchId === plan.batchId &&
151
+ entry.assignmentId === item.assignmentId);
152
+ return {
153
+ assignmentId: item.assignmentId,
154
+ status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
155
+ ? current.state
156
+ : "UNKNOWN",
157
+ };
158
+ }),
159
+ capacity: projection.batches.find((entry) => entry.batchId === plan.batchId)
160
+ ?.effectiveCapacity ?? capacity,
161
+ retryCounts: Object.fromEntries(projection.assignments
162
+ .filter((entry) => entry.batchId === plan.batchId)
163
+ .map((entry) => [entry.assignmentId, entry.retryCount])),
164
+ };
165
+ }
166
+ async function recordRecoveryReconciliation(input) {
167
+ const { plan, cwd, by, existingState, projection, capacity, reason } = input;
168
+ if (existingState !== "RECONCILIATION_REQUIRED" &&
169
+ existingState !== "AWAITING_REVIEW" &&
170
+ existingState !== "ALL_ACCEPTED" &&
171
+ existingState !== "SETTLED_WITH_FAILURE") {
172
+ await appendTransition({
173
+ sessionId: plan.sessionId,
174
+ cwd,
175
+ by,
176
+ batchId: plan.batchId,
177
+ entity: "batch",
178
+ id: plan.batchId,
179
+ state: "RECONCILIATION_REQUIRED",
180
+ states: new Map([[plan.batchId, existingState]]),
181
+ planHash: plan.planHash,
182
+ payload: { recoveredAfterRestart: true },
183
+ });
184
+ }
185
+ const receipt = reconciliationReceipt(plan, projection, capacity);
186
+ await appendAction({
187
+ sessionId: plan.sessionId,
188
+ cwd,
189
+ by,
190
+ batchId: plan.batchId,
191
+ entity: "batch",
192
+ action: "reconciliation_required",
193
+ reason,
194
+ });
195
+ await appendAction({
196
+ sessionId: plan.sessionId,
197
+ cwd,
198
+ by,
199
+ batchId: plan.batchId,
200
+ entity: "batch",
201
+ action: "receipt_recorded",
202
+ id: `${plan.batchId}:RECONCILIATION_REQUIRED`,
203
+ payload: { ...receipt, reason },
204
+ });
205
+ return receipt;
206
+ }
40
207
  async function recoverExistingBatch(input) {
41
208
  const projection = await readCoordinationState({
42
209
  sessionId: input.plan.sessionId,
@@ -52,64 +219,56 @@ async function recoverExistingBatch(input) {
52
219
  sessionId: input.plan.sessionId,
53
220
  cwd: input.cwd,
54
221
  });
55
- const saved = events.find((event) => event.kind === "coordination" &&
56
- event.batchId === input.plan.batchId &&
57
- event.action === "receipt_recorded" &&
58
- event.payload &&
59
- (event.payload.status === "ALL_ACCEPTED" ||
60
- event.payload.status === "SETTLED_WITH_FAILURE"));
61
- if (saved?.kind === "coordination" && saved.payload) {
62
- return saved.payload;
63
- }
64
- const states = new Map([
65
- [input.plan.batchId, existing.state],
66
- ]);
67
- if (existing.state !== "RECONCILIATION_REQUIRED") {
68
- await appendTransition({
69
- sessionId: input.plan.sessionId,
70
- by: input.by,
71
- batchId: input.plan.batchId,
72
- entity: "batch",
73
- id: input.plan.batchId,
74
- state: "RECONCILIATION_REQUIRED",
75
- states,
76
- planHash: input.plan.planHash,
77
- payload: { recoveredAfterRestart: true },
78
- });
222
+ if (existing.state === "AWAITING_REVIEW" ||
223
+ existing.state === "ALL_ACCEPTED" ||
224
+ existing.state === "SETTLED_WITH_FAILURE") {
225
+ let receipt;
226
+ try {
227
+ receipt = terminalReceiptFromProjection(input.plan, projection);
228
+ }
229
+ catch (error) {
230
+ return recordRecoveryReconciliation({
231
+ ...input,
232
+ existingState: existing.state,
233
+ projection,
234
+ reason: error instanceof Error ? error.message : String(error),
235
+ });
236
+ }
237
+ const receipts = events.filter((event) => event.kind === "coordination" &&
238
+ event.batchId === input.plan.batchId &&
239
+ event.action === "receipt_recorded" &&
240
+ event.payload?.status === receipt.status &&
241
+ event.payload?.status !== "RECONCILIATION_REQUIRED");
242
+ if (receipts.length > 0 &&
243
+ !isDeepStrictEqual(receipts.at(-1)?.payload, receipt)) {
244
+ return recordRecoveryReconciliation({
245
+ ...input,
246
+ existingState: existing.state,
247
+ projection,
248
+ reason: "saved terminal receipt disagrees with durable coordination facts",
249
+ });
250
+ }
251
+ if (receipts.length === 0) {
252
+ await appendAction({
253
+ sessionId: input.plan.sessionId,
254
+ cwd: input.cwd,
255
+ by: input.by,
256
+ batchId: input.plan.batchId,
257
+ entity: "batch",
258
+ action: "receipt_recorded",
259
+ id: `${input.plan.batchId}:${receipt.status}`,
260
+ payload: { ...receipt },
261
+ });
262
+ }
263
+ await assertTeamworkReceiptDurable(input.plan, receipt, input.cwd);
264
+ return receipt;
79
265
  }
80
- const assignments = input.plan.assignments.map((item) => {
81
- const current = projection.assignments.find((entry) => entry.batchId === input.plan.batchId &&
82
- entry.assignmentId === item.assignmentId);
83
- return {
84
- assignmentId: item.assignmentId,
85
- status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
86
- ? current.state
87
- : "UNKNOWN",
88
- };
266
+ return recordRecoveryReconciliation({
267
+ ...input,
268
+ existingState: existing.state,
269
+ projection,
270
+ reason: "recovered nonterminal batch requires reconciliation",
89
271
  });
90
- const retryCounts = Object.fromEntries(projection.assignments
91
- .filter((entry) => entry.batchId === input.plan.batchId)
92
- .map((entry) => [entry.assignmentId, entry.retryCount]));
93
- const receipt = {
94
- schemaVersion: 1,
95
- batchId: input.plan.batchId,
96
- status: "RECONCILIATION_REQUIRED",
97
- assignments,
98
- capacity: existing.effectiveCapacity ?? input.capacity,
99
- retryCounts,
100
- };
101
- await recordCoordinationEvent({
102
- sessionId: input.plan.sessionId,
103
- cwd: input.cwd,
104
- by: input.by,
105
- batchId: input.plan.batchId,
106
- entity: "batch",
107
- action: "receipt_recorded",
108
- receiptId: `${input.plan.batchId}:RECONCILIATION_REQUIRED`,
109
- payload: { ...receipt, reason: "recovered batch requires reconciliation" },
110
- idempotencyKey: `${input.plan.batchId}:RECONCILIATION_REQUIRED:receipt`,
111
- });
112
- return receipt;
113
272
  }
114
273
  function transitionState(states, key, next, entity) {
115
274
  assertCoordinationTransition(entity, states.get(key), next);
@@ -119,6 +278,7 @@ async function appendTransition(input) {
119
278
  transitionState(input.states, input.id, input.state, input.entity);
120
279
  await recordCoordinationEvent({
121
280
  sessionId: input.sessionId,
281
+ ...(input.cwd ? { cwd: input.cwd } : {}),
122
282
  by: input.by,
123
283
  batchId: input.batchId,
124
284
  entity: input.entity,
@@ -141,6 +301,7 @@ async function appendTransition(input) {
141
301
  async function appendAction(input) {
142
302
  await recordCoordinationEvent({
143
303
  sessionId: input.sessionId,
304
+ ...(input.cwd ? { cwd: input.cwd } : {}),
144
305
  by: input.by,
145
306
  batchId: input.batchId,
146
307
  entity: input.entity,
@@ -164,47 +325,60 @@ async function appendAction(input) {
164
325
  idempotencyKey: `${input.batchId}:${input.action}:${input.id ?? input.leaseId ?? input.attemptId ?? "event"}`,
165
326
  });
166
327
  }
167
- function readPlanFile(planPath) {
328
+ export function readPlanFile(planPath) {
329
+ const stat = fs.lstatSync(planPath);
330
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024)
331
+ throw new Error("Teamwork plan must be a regular file under 2097152 bytes");
168
332
  const raw = fs.readFileSync(planPath, "utf8");
169
333
  return JSON.parse(raw);
170
334
  }
171
335
  export function loadTeamworkPlan(planPath, taskRoot) {
172
- const plan = validateTeamworkPlan(readPlanFile(planPath), taskRoot);
336
+ const plan = validateTeamworkPlan(readPlanFile(planPath), taskRoot, undefined, { allowExpired: true });
173
337
  assertTeamworkPlanCurrent(plan, taskRoot);
174
338
  return plan;
175
339
  }
176
340
  export async function runTeamworkCoordinator(input) {
177
- const plan = validateTeamworkPlan(input.plan, input.taskRoot);
341
+ const plan = validateTeamworkPlan(input.plan, input.taskRoot, undefined, {
342
+ allowExpired: true,
343
+ });
178
344
  assertTeamworkPlanCurrent(plan, input.taskRoot);
179
345
  const cwd = input.cwd ?? input.taskRoot;
180
346
  const frozenLaunches = new Map();
181
347
  for (const assignment of plan.assignments) {
182
348
  assertTeammateAdapterContract(assignment.provider);
183
- const profile = resolveTeammateProfile({
184
- cwd,
185
- profile: assignment.profile,
186
- provider: assignment.provider,
187
- model: assignment.model,
188
- });
189
- frozenLaunches.set(assignment.assignmentId, {
190
- profile,
191
- launch: buildTeammateLaunchSpec({
192
- cwd,
193
- profile,
194
- host: input.host,
195
- contextFiles: assignment.contextFiles,
196
- contextManifests: assignment.contextManifests,
197
- protectedTargets: plan.protectedTargets,
198
- }),
199
- });
349
+ assertTeammateHostCapability(assignment.provider, input.host, assignment.execution.profile.options);
350
+ frozenLaunches.set(assignment.assignmentId, assignment.execution);
200
351
  }
201
- const config = input.config ?? resolveTeammateRuntimeConfig(cwd);
352
+ const config = input.config ??
353
+ resolveTeammateRuntimeConfig(cwd, process.env, plan.coordinationMode);
202
354
  const capacity = effectiveTeammateCapacity({
203
355
  requested: plan.requestedCapacity,
356
+ assignmentCount: plan.assignments.length,
204
357
  config,
205
358
  verifiedHostCap: input.host.verifiedHostCap,
206
359
  });
207
360
  const by = input.by ?? "main";
361
+ const existing = await readCoordinationState({
362
+ sessionId: plan.sessionId,
363
+ cwd,
364
+ });
365
+ const settled = existing.batches.find((batch) => batch.batchId === plan.batchId);
366
+ if (settled &&
367
+ ["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(settled.state)) {
368
+ let terminal = false;
369
+ try {
370
+ terminalReceiptFromProjection(plan, existing);
371
+ terminal = true;
372
+ }
373
+ catch {
374
+ // Inconsistent terminal facts still require the protected recovery path.
375
+ }
376
+ if (terminal) {
377
+ const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
378
+ if (recovered)
379
+ return recovered;
380
+ }
381
+ }
208
382
  const batchStates = new Map();
209
383
  const assignmentStates = new Map();
210
384
  const attemptStates = new Map();
@@ -215,19 +389,8 @@ export async function runTeamworkCoordinator(input) {
215
389
  capacity,
216
390
  });
217
391
  let guardHeld = true;
218
- let recovered;
219
- try {
220
- recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
221
- }
222
- catch (error) {
223
- await input.guard.release({
224
- batchId: plan.batchId,
225
- planHash: plan.planHash,
226
- capacity,
227
- });
228
- guardHeld = false;
229
- throw error;
230
- }
392
+ // Failed recovery cannot prove that an older provider process exited.
393
+ const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
231
394
  if (recovered) {
232
395
  if (recovered.status !== "RECONCILIATION_REQUIRED") {
233
396
  await input.guard.release({
@@ -239,6 +402,14 @@ export async function runTeamworkCoordinator(input) {
239
402
  }
240
403
  return recovered;
241
404
  }
405
+ if (Date.parse(plan.deadline) <= Date.now()) {
406
+ await input.guard.release({
407
+ batchId: plan.batchId,
408
+ planHash: plan.planHash,
409
+ capacity,
410
+ });
411
+ throw new Error("deadline elapsed before batch dispatch");
412
+ }
242
413
  try {
243
414
  for (const [queueIndex, assignment] of plan.assignments.entries()) {
244
415
  writeTeammateQueueFact({
@@ -328,8 +499,8 @@ export async function runTeamworkCoordinator(input) {
328
499
  const active = new Map();
329
500
  const waits = new Map();
330
501
  const final = new Map();
331
- const acceptedAttempts = new Map();
332
502
  const retryCounts = {};
503
+ let admissionClosed = false;
333
504
  async function reconcile(reason) {
334
505
  await appendAction({
335
506
  sessionId: plan.sessionId,
@@ -372,9 +543,39 @@ export async function runTeamworkCoordinator(input) {
372
543
  });
373
544
  return receipt;
374
545
  }
546
+ async function failPending(reason) {
547
+ admissionClosed = true;
548
+ while (pending.length > 0) {
549
+ const assignment = pending.shift();
550
+ if (!assignment)
551
+ continue;
552
+ await appendTransition({
553
+ sessionId: plan.sessionId,
554
+ by,
555
+ batchId: plan.batchId,
556
+ entity: "assignment",
557
+ id: assignment.assignmentId,
558
+ state: "FINAL_FAILED",
559
+ states: assignmentStates,
560
+ assignmentId: assignment.assignmentId,
561
+ retryCount: retryCounts[assignment.assignmentId] ?? 0,
562
+ payload: { reason, dispatched: false },
563
+ });
564
+ final.set(assignment.assignmentId, "FINAL_FAILED");
565
+ }
566
+ }
375
567
  async function dispatch(slot, assignment, retryCount) {
376
- const attemptId = `${assignment.assignmentId}.attempt.${retryCount}`;
377
- const teammateId = `${assignment.assignmentId}.teammate.${retryCount}`;
568
+ const boundedId = (kind) => {
569
+ const candidate = `${assignment.assignmentId}.${kind}.${retryCount}`;
570
+ return candidate.length <= 64
571
+ ? candidate
572
+ : crypto
573
+ .createHash("sha256")
574
+ .update(`${assignment.assignmentId}:${kind}:${retryCount}`)
575
+ .digest("hex");
576
+ };
577
+ const attemptId = boundedId("attempt");
578
+ const teammateId = boundedId("teammate");
378
579
  const frozen = frozenLaunches.get(assignment.assignmentId);
379
580
  if (!frozen)
380
581
  throw new Error(`missing frozen launch for '${assignment.assignmentId}'`);
@@ -388,6 +589,7 @@ export async function runTeamworkCoordinator(input) {
388
589
  profile,
389
590
  launch,
390
591
  };
592
+ input.runtime.prepare?.(context);
391
593
  await appendTransition({
392
594
  sessionId: plan.sessionId,
393
595
  by,
@@ -510,6 +712,7 @@ export async function runTeamworkCoordinator(input) {
510
712
  teammateId,
511
713
  slot,
512
714
  retryCount,
715
+ transitionKey: `attempt:${attemptId}`,
513
716
  ...(context.providerSessionId
514
717
  ? { providerSessionId: context.providerSessionId }
515
718
  : {}),
@@ -562,7 +765,63 @@ export async function runTeamworkCoordinator(input) {
562
765
  });
563
766
  guardHeld = false;
564
767
  }
768
+ async function resumeFromWait(current) {
769
+ if (assignmentStates.get(current.assignment.assignmentId) !==
770
+ "WAITING_FOR_MAIN_COMMAND")
771
+ return;
772
+ const transitionKey = `continuation:${current.continuations}`;
773
+ await appendTransition({
774
+ sessionId: plan.sessionId,
775
+ by,
776
+ batchId: plan.batchId,
777
+ entity: "attempt",
778
+ id: current.context.attemptId,
779
+ state: "RUNNING",
780
+ states: attemptStates,
781
+ assignmentId: current.assignment.assignmentId,
782
+ attemptId: current.context.attemptId,
783
+ teammateId: current.context.teammateId,
784
+ retryCount: current.retryCount,
785
+ ...(current.providerSessionId
786
+ ? { providerSessionId: current.providerSessionId }
787
+ : {}),
788
+ transitionKey,
789
+ });
790
+ await appendTransition({
791
+ sessionId: plan.sessionId,
792
+ by,
793
+ batchId: plan.batchId,
794
+ entity: "teammate",
795
+ id: current.context.teammateId,
796
+ state: "RUNNING",
797
+ states: teammateStates,
798
+ attemptId: current.context.attemptId,
799
+ teammateId: current.context.teammateId,
800
+ ...(current.providerSessionId
801
+ ? { providerSessionId: current.providerSessionId }
802
+ : {}),
803
+ transitionKey,
804
+ });
805
+ await appendTransition({
806
+ sessionId: plan.sessionId,
807
+ by,
808
+ batchId: plan.batchId,
809
+ entity: "assignment",
810
+ id: current.assignment.assignmentId,
811
+ state: "EXECUTING",
812
+ states: assignmentStates,
813
+ assignmentId: current.assignment.assignmentId,
814
+ attemptId: current.context.attemptId,
815
+ teammateId: current.context.teammateId,
816
+ retryCount: current.retryCount,
817
+ ...(current.providerSessionId
818
+ ? { providerSessionId: current.providerSessionId }
819
+ : {}),
820
+ transitionKey,
821
+ });
822
+ }
565
823
  async function cancelAndDrain(reason, terminal) {
824
+ await failPending(reason);
566
825
  if (batchStates.get(plan.batchId) === "RUNNING") {
567
826
  await appendTransition({
568
827
  sessionId: plan.sessionId,
@@ -575,52 +834,68 @@ export async function runTeamworkCoordinator(input) {
575
834
  payload: { reason },
576
835
  });
577
836
  }
578
- if (!input.runtime.cancel && active.size > 0 && !terminal) {
579
- return reconcile(`host cannot confirm cancellation: ${reason}`);
580
- }
581
- for (const current of [...active.values()]) {
582
- let result;
583
- if (terminal?.current === current) {
584
- result = terminal.result;
837
+ const cancel = input.runtime.cancel;
838
+ const outcomes = await Promise.all([...active.values()].map(async (current) => {
839
+ if (terminal?.current === current)
840
+ return { current, result: terminal.result };
841
+ if (!cancel)
842
+ return {
843
+ current,
844
+ error: new Error(`host cannot cancel active attempt: ${reason}`),
845
+ };
846
+ try {
847
+ return { current, result: await cancel(current.context) };
585
848
  }
586
- else {
587
- try {
588
- if (!input.runtime.cancel)
589
- return reconcile(`host cannot cancel active attempt: ${reason}`);
590
- result = await input.runtime.cancel(current.context);
591
- }
592
- catch (error) {
593
- return reconcile(error instanceof Error ? error.message : String(error));
594
- }
849
+ catch (error) {
850
+ return { current, error };
595
851
  }
852
+ }));
853
+ let cancellationFailure;
854
+ for (const outcome of outcomes) {
855
+ const { current } = outcome;
856
+ if ("error" in outcome) {
857
+ cancellationFailure ??=
858
+ outcome.error instanceof Error
859
+ ? outcome.error.message
860
+ : String(outcome.error);
861
+ continue;
862
+ }
863
+ const { result } = outcome;
596
864
  if (result.kind === "reconciliation" ||
597
865
  result.kind === "timeout" ||
598
866
  result.terminal !== true) {
599
- return reconcile(`attempt cancellation is not terminal: ${reason}`);
867
+ cancellationFailure ??= `attempt cancellation is not terminal: ${reason}`;
868
+ continue;
600
869
  }
601
870
  if (result.providerSessionId &&
602
871
  current.providerSessionId &&
603
872
  result.providerSessionId !== current.providerSessionId) {
604
- return reconcile("cancellation returned a different host session");
873
+ cancellationFailure ??=
874
+ "cancellation returned a different host session";
875
+ continue;
605
876
  }
606
877
  if (result.providerSessionId) {
607
878
  current.providerSessionId = result.providerSessionId;
608
879
  current.context.providerSessionId = result.providerSessionId;
609
880
  }
610
881
  try {
611
- if (result.kind === "report")
882
+ await resumeFromWait(current);
883
+ if (result.kind === "report") {
612
884
  await complete(current, false);
885
+ }
613
886
  else
614
887
  await fail(current, result.reason ?? "cancelled by coordinator", false);
615
888
  }
616
889
  catch (error) {
617
- return reconcile(error instanceof Error ? error.message : String(error));
890
+ cancellationFailure ??=
891
+ error instanceof Error ? error.message : String(error);
892
+ continue;
618
893
  }
619
894
  active.delete(current.slot);
620
895
  waits.delete(current.slot);
621
896
  }
622
- if (pending.length > 0)
623
- return reconcile(`queued assignment remains after cancellation: ${reason}`);
897
+ if (cancellationFailure)
898
+ return reconcile(cancellationFailure);
624
899
  return settle();
625
900
  }
626
901
  async function fail(activeWork, reason, allowRetry = true) {
@@ -657,7 +932,7 @@ export async function runTeamworkCoordinator(input) {
657
932
  : {}),
658
933
  terminal: true,
659
934
  });
660
- if (allowRetry && retryCount < assignment.maxRetries) {
935
+ if (allowRetry && !admissionClosed && retryCount < assignment.maxRetries) {
661
936
  await release(activeWork);
662
937
  retryCounts[assignment.assignmentId] = retryCount + 1;
663
938
  pending.unshift(assignment);
@@ -706,7 +981,7 @@ export async function runTeamworkCoordinator(input) {
706
981
  await release(activeWork);
707
982
  final.set(assignment.assignmentId, "FINAL_FAILED");
708
983
  }
709
- async function accept(activeWork) {
984
+ async function submit(activeWork) {
710
985
  const { assignment, context, retryCount } = activeWork;
711
986
  try {
712
987
  assertTeamworkPlanCurrent(plan, input.taskRoot);
@@ -715,26 +990,13 @@ export async function runTeamworkCoordinator(input) {
715
990
  await fail(activeWork, `task phase changed before report acceptance: ${error instanceof Error ? error.message : String(error)}`, false);
716
991
  return;
717
992
  }
718
- writeTeammateDisposition({
719
- taskRoot: input.taskRoot,
720
- briefPath: assignment.briefPath,
721
- identity: {
722
- taskId: plan.taskId,
723
- batchId: plan.batchId,
724
- assignmentId: assignment.assignmentId,
725
- attemptId: context.attemptId,
726
- teammateId: context.teammateId,
727
- },
728
- sourceDigest: assignment.sourceDigest,
729
- status: "accepted",
730
- });
731
993
  await appendTransition({
732
994
  sessionId: plan.sessionId,
733
995
  by,
734
996
  batchId: plan.batchId,
735
997
  entity: "assignment",
736
998
  id: assignment.assignmentId,
737
- state: "ACCEPTED",
999
+ state: "AWAITING_REVIEW",
738
1000
  states: assignmentStates,
739
1001
  assignmentId: assignment.assignmentId,
740
1002
  attemptId: context.attemptId,
@@ -742,8 +1004,7 @@ export async function runTeamworkCoordinator(input) {
742
1004
  retryCount,
743
1005
  });
744
1006
  await release(activeWork);
745
- final.set(assignment.assignmentId, "ACCEPTED");
746
- acceptedAttempts.set(assignment.assignmentId, context.attemptId);
1007
+ final.set(assignment.assignmentId, "AWAITING_REVIEW");
747
1008
  }
748
1009
  async function complete(activeWork, allowRetry = true) {
749
1010
  const { assignment, context, retryCount } = activeWork;
@@ -782,12 +1043,13 @@ export async function runTeamworkCoordinator(input) {
782
1043
  });
783
1044
  }
784
1045
  catch (error) {
785
- await fail(activeWork, error instanceof Error ? error.message : String(error), allowRetry);
1046
+ await fail(activeWork, error instanceof Error ? error.message : String(error), error instanceof TeammateReportReadError && error.kind === "missing"
1047
+ ? allowRetry
1048
+ : false);
786
1049
  return;
787
1050
  }
788
- if (input.runtime.validateReport &&
789
- !(await input.runtime.validateReport(context, report))) {
790
- await fail(activeWork, "report rejected by acceptance contract", allowRetry);
1051
+ if (context.launch.reportVersion === 2 && report.schemaVersion !== 2) {
1052
+ await fail(activeWork, "report schema_version must match the frozen result contract (2)", allowRetry);
791
1053
  return;
792
1054
  }
793
1055
  try {
@@ -855,7 +1117,7 @@ export async function runTeamworkCoordinator(input) {
855
1117
  teammateId: context.teammateId,
856
1118
  retryCount,
857
1119
  });
858
- await accept(activeWork);
1120
+ await submit(activeWork);
859
1121
  }
860
1122
  async function settle() {
861
1123
  await appendTransition({
@@ -867,16 +1129,8 @@ export async function runTeamworkCoordinator(input) {
867
1129
  state: "SETTLING",
868
1130
  states: batchStates,
869
1131
  });
870
- const cleanupFailures = cleanupSettledTeamworkArtifacts({
871
- taskRoot: input.taskRoot,
872
- assignments: plan.assignments,
873
- acceptedAttempts,
874
- });
875
- if (cleanupFailures.length > 0) {
876
- return reconcile(`Teamwork batch cleanup failed: ${cleanupFailures.join("; ")}`);
877
- }
878
- const accepted = plan.assignments.every((assignment) => final.get(assignment.assignmentId) === "ACCEPTED");
879
- const status = accepted ? "ALL_ACCEPTED" : "SETTLED_WITH_FAILURE";
1132
+ const pendingReview = plan.assignments.some((assignment) => final.get(assignment.assignmentId) === "AWAITING_REVIEW");
1133
+ const status = pendingReview ? "AWAITING_REVIEW" : "SETTLED_WITH_FAILURE";
880
1134
  await appendTransition({
881
1135
  sessionId: plan.sessionId,
882
1136
  by,
@@ -886,29 +1140,7 @@ export async function runTeamworkCoordinator(input) {
886
1140
  state: status,
887
1141
  states: batchStates,
888
1142
  });
889
- const receipt = {
890
- schemaVersion: 1,
891
- batchId: plan.batchId,
892
- status,
893
- assignments: plan.assignments.map((assignment) => ({
894
- assignmentId: assignment.assignmentId,
895
- status: final.get(assignment.assignmentId) ?? "FINAL_FAILED",
896
- })),
897
- capacity,
898
- retryCounts,
899
- evaluationGroups: [
900
- ...new Set(plan.assignments
901
- .map((assignment) => assignment.evaluationGroup?.id)
902
- .filter((id) => id !== undefined)),
903
- ].map((id) => ({
904
- id,
905
- status: plan.assignments
906
- .filter((assignment) => assignment.evaluationGroup?.id === id)
907
- .every((assignment) => final.get(assignment.assignmentId) === "ACCEPTED")
908
- ? "COMPLETE"
909
- : "INCOMPLETE",
910
- })),
911
- };
1143
+ const receipt = terminalReceiptFromProjection(plan, await readCoordinationState({ sessionId: plan.sessionId, cwd }));
912
1144
  await appendAction({
913
1145
  sessionId: plan.sessionId,
914
1146
  by,
@@ -918,20 +1150,62 @@ export async function runTeamworkCoordinator(input) {
918
1150
  id: `${plan.batchId}:${status}`,
919
1151
  payload: { ...receipt },
920
1152
  });
921
- await assertTeamworkReceiptDurable(plan.sessionId, plan.batchId, plan.planHash, cwd);
1153
+ await assertTeamworkReceiptDurable(plan, receipt, cwd);
922
1154
  await releaseGuard();
923
1155
  return receipt;
924
1156
  }
925
1157
  async function fill() {
1158
+ if (admissionClosed)
1159
+ return;
926
1160
  while (active.size < capacity && pending.length > 0) {
927
- const assignment = pending.shift();
1161
+ if (Date.parse(plan.deadline) <= Date.now()) {
1162
+ await failPending("batch deadline elapsed before dispatch");
1163
+ return;
1164
+ }
1165
+ try {
1166
+ assertTeamworkPlanCurrent(plan, input.taskRoot);
1167
+ }
1168
+ catch (error) {
1169
+ await failPending(`task phase changed before dispatch: ${error instanceof Error ? error.message : String(error)}`);
1170
+ return;
1171
+ }
1172
+ const assignment = pending[0];
928
1173
  if (!assignment)
929
1174
  return;
930
1175
  const retryCount = retryCounts[assignment.assignmentId] ?? 0;
931
1176
  let slot = 0;
932
1177
  while (active.has(slot))
933
1178
  slot += 1;
934
- await dispatch(slot, assignment, retryCount);
1179
+ try {
1180
+ await dispatch(slot, assignment, retryCount);
1181
+ pending.shift();
1182
+ }
1183
+ catch (error) {
1184
+ if (!assignmentStates.has(assignment.assignmentId) ||
1185
+ assignmentStates.get(assignment.assignmentId) === "QUEUED") {
1186
+ const reason = `launch preflight failed before dispatch: ${error instanceof Error ? error.message : String(error)}`;
1187
+ if (error instanceof TeamworkPolicyChangedError) {
1188
+ await failPending(reason);
1189
+ return;
1190
+ }
1191
+ pending.shift();
1192
+ await appendTransition({
1193
+ sessionId: plan.sessionId,
1194
+ by,
1195
+ batchId: plan.batchId,
1196
+ entity: "assignment",
1197
+ id: assignment.assignmentId,
1198
+ assignmentId: assignment.assignmentId,
1199
+ state: "FINAL_FAILED",
1200
+ states: assignmentStates,
1201
+ retryCount,
1202
+ payload: { reason, dispatched: false },
1203
+ });
1204
+ final.set(assignment.assignmentId, "FINAL_FAILED");
1205
+ continue;
1206
+ }
1207
+ throw error;
1208
+ }
935
1209
  }
936
1210
  }
937
1211
  try {
@@ -991,162 +1265,116 @@ export async function runTeamworkCoordinator(input) {
991
1265
  result: next.result,
992
1266
  });
993
1267
  }
1268
+ try {
1269
+ await resumeFromWait(current);
1270
+ }
1271
+ catch (error) {
1272
+ return reconcile(error instanceof Error ? error.message : String(error));
1273
+ }
994
1274
  let releaseSlot = true;
995
1275
  if (next.result.kind === "timeout") {
996
1276
  current.continuations += 1;
997
- if (current.continuations > 8) {
998
- return reconcile("continuation limit exceeded");
1277
+ if (Date.now() >= Date.parse(plan.deadline))
1278
+ return cancelAndDrain("batch deadline elapsed");
1279
+ const transitionKey = `continuation:${current.continuations}`;
1280
+ await appendTransition({
1281
+ sessionId: plan.sessionId,
1282
+ by,
1283
+ batchId: plan.batchId,
1284
+ entity: "attempt",
1285
+ id: current.context.attemptId,
1286
+ state: "TIMEOUT_YIELDED",
1287
+ states: attemptStates,
1288
+ assignmentId: current.assignment.assignmentId,
1289
+ attemptId: current.context.attemptId,
1290
+ teammateId: current.context.teammateId,
1291
+ retryCount: current.retryCount,
1292
+ ...(current.providerSessionId
1293
+ ? { providerSessionId: current.providerSessionId }
1294
+ : {}),
1295
+ transitionKey,
1296
+ });
1297
+ await appendTransition({
1298
+ sessionId: plan.sessionId,
1299
+ by,
1300
+ batchId: plan.batchId,
1301
+ entity: "teammate",
1302
+ id: current.context.teammateId,
1303
+ state: "TIMEOUT_YIELDED",
1304
+ states: teammateStates,
1305
+ attemptId: current.context.attemptId,
1306
+ teammateId: current.context.teammateId,
1307
+ ...(current.providerSessionId
1308
+ ? { providerSessionId: current.providerSessionId }
1309
+ : {}),
1310
+ transitionKey,
1311
+ });
1312
+ await appendTransition({
1313
+ sessionId: plan.sessionId,
1314
+ by,
1315
+ batchId: plan.batchId,
1316
+ entity: "assignment",
1317
+ id: current.assignment.assignmentId,
1318
+ state: "WAITING_FOR_MAIN_COMMAND",
1319
+ states: assignmentStates,
1320
+ assignmentId: current.assignment.assignmentId,
1321
+ attemptId: current.context.attemptId,
1322
+ teammateId: current.context.teammateId,
1323
+ retryCount: current.retryCount,
1324
+ ...(current.providerSessionId
1325
+ ? { providerSessionId: current.providerSessionId }
1326
+ : {}),
1327
+ transitionKey,
1328
+ });
1329
+ await appendTransition({
1330
+ sessionId: plan.sessionId,
1331
+ by,
1332
+ batchId: plan.batchId,
1333
+ entity: "attempt",
1334
+ id: current.context.attemptId,
1335
+ state: "WAITING_FOR_MAIN_COMMAND",
1336
+ states: attemptStates,
1337
+ assignmentId: current.assignment.assignmentId,
1338
+ attemptId: current.context.attemptId,
1339
+ teammateId: current.context.teammateId,
1340
+ retryCount: current.retryCount,
1341
+ ...(current.providerSessionId
1342
+ ? { providerSessionId: current.providerSessionId }
1343
+ : {}),
1344
+ transitionKey,
1345
+ });
1346
+ await appendTransition({
1347
+ sessionId: plan.sessionId,
1348
+ by,
1349
+ batchId: plan.batchId,
1350
+ entity: "teammate",
1351
+ id: current.context.teammateId,
1352
+ state: "WAITING_FOR_MAIN_COMMAND",
1353
+ states: teammateStates,
1354
+ attemptId: current.context.attemptId,
1355
+ teammateId: current.context.teammateId,
1356
+ ...(current.providerSessionId
1357
+ ? { providerSessionId: current.providerSessionId }
1358
+ : {}),
1359
+ transitionKey,
1360
+ });
1361
+ let continued;
1362
+ try {
1363
+ continued = input.runtime.continue(current.context);
999
1364
  }
1000
- else {
1001
- await appendTransition({
1002
- sessionId: plan.sessionId,
1003
- by,
1004
- batchId: plan.batchId,
1005
- entity: "attempt",
1006
- id: current.context.attemptId,
1007
- state: "TIMEOUT_YIELDED",
1008
- states: attemptStates,
1009
- assignmentId: current.assignment.assignmentId,
1010
- attemptId: current.context.attemptId,
1011
- teammateId: current.context.teammateId,
1012
- retryCount: current.retryCount,
1013
- ...(current.providerSessionId
1014
- ? { providerSessionId: current.providerSessionId }
1015
- : {}),
1016
- transitionKey: `continuation:${current.continuations}`,
1017
- });
1018
- await appendTransition({
1019
- sessionId: plan.sessionId,
1020
- by,
1021
- batchId: plan.batchId,
1022
- entity: "teammate",
1023
- id: current.context.teammateId,
1024
- state: "TIMEOUT_YIELDED",
1025
- states: teammateStates,
1026
- attemptId: current.context.attemptId,
1027
- teammateId: current.context.teammateId,
1028
- ...(current.providerSessionId
1029
- ? { providerSessionId: current.providerSessionId }
1030
- : {}),
1031
- transitionKey: `continuation:${current.continuations}`,
1032
- });
1033
- await appendTransition({
1034
- sessionId: plan.sessionId,
1035
- by,
1036
- batchId: plan.batchId,
1037
- entity: "assignment",
1038
- id: current.assignment.assignmentId,
1039
- state: "WAITING_FOR_MAIN_COMMAND",
1040
- states: assignmentStates,
1041
- assignmentId: current.assignment.assignmentId,
1042
- attemptId: current.context.attemptId,
1043
- teammateId: current.context.teammateId,
1044
- retryCount: current.retryCount,
1045
- ...(current.providerSessionId
1046
- ? { providerSessionId: current.providerSessionId }
1047
- : {}),
1048
- transitionKey: `continuation:${current.continuations}`,
1049
- });
1050
- await appendTransition({
1051
- sessionId: plan.sessionId,
1052
- by,
1053
- batchId: plan.batchId,
1054
- entity: "attempt",
1055
- id: current.context.attemptId,
1056
- state: "WAITING_FOR_MAIN_COMMAND",
1057
- states: attemptStates,
1058
- assignmentId: current.assignment.assignmentId,
1059
- attemptId: current.context.attemptId,
1060
- teammateId: current.context.teammateId,
1061
- retryCount: current.retryCount,
1062
- ...(current.providerSessionId
1063
- ? { providerSessionId: current.providerSessionId }
1064
- : {}),
1065
- transitionKey: `continuation:${current.continuations}`,
1066
- });
1067
- await appendTransition({
1068
- sessionId: plan.sessionId,
1069
- by,
1070
- batchId: plan.batchId,
1071
- entity: "teammate",
1072
- id: current.context.teammateId,
1073
- state: "WAITING_FOR_MAIN_COMMAND",
1074
- states: teammateStates,
1075
- attemptId: current.context.attemptId,
1076
- teammateId: current.context.teammateId,
1077
- ...(current.providerSessionId
1078
- ? { providerSessionId: current.providerSessionId }
1079
- : {}),
1080
- transitionKey: `continuation:${current.continuations}`,
1081
- });
1082
- let continued;
1083
- try {
1084
- continued = await input.runtime.continue(current.context);
1085
- }
1086
- catch (error) {
1087
- return reconcile(error instanceof Error ? error.message : String(error));
1088
- }
1089
- if (continued.providerSessionId &&
1090
- current.providerSessionId &&
1091
- continued.providerSessionId !== current.providerSessionId) {
1092
- return reconcile("continuation returned a different host session");
1093
- }
1094
- if (continued.providerSessionId) {
1095
- current.providerSessionId = continued.providerSessionId;
1096
- current.context.providerSessionId = continued.providerSessionId;
1097
- }
1098
- await appendTransition({
1099
- sessionId: plan.sessionId,
1100
- by,
1101
- batchId: plan.batchId,
1102
- entity: "attempt",
1103
- id: current.context.attemptId,
1104
- state: "RUNNING",
1105
- states: attemptStates,
1106
- assignmentId: current.assignment.assignmentId,
1107
- attemptId: current.context.attemptId,
1108
- teammateId: current.context.teammateId,
1109
- retryCount: current.retryCount,
1110
- ...(current.providerSessionId
1111
- ? { providerSessionId: current.providerSessionId }
1112
- : {}),
1113
- transitionKey: `continuation:${current.continuations}`,
1114
- });
1115
- await appendTransition({
1116
- sessionId: plan.sessionId,
1117
- by,
1118
- batchId: plan.batchId,
1119
- entity: "teammate",
1120
- id: current.context.teammateId,
1121
- state: "RUNNING",
1122
- states: teammateStates,
1123
- attemptId: current.context.attemptId,
1124
- teammateId: current.context.teammateId,
1125
- ...(current.providerSessionId
1126
- ? { providerSessionId: current.providerSessionId }
1127
- : {}),
1128
- transitionKey: `continuation:${current.continuations}`,
1129
- });
1130
- await appendTransition({
1131
- sessionId: plan.sessionId,
1132
- by,
1133
- batchId: plan.batchId,
1134
- entity: "assignment",
1135
- id: current.assignment.assignmentId,
1136
- state: "EXECUTING",
1137
- states: assignmentStates,
1138
- assignmentId: current.assignment.assignmentId,
1139
- attemptId: current.context.attemptId,
1140
- teammateId: current.context.teammateId,
1141
- retryCount: current.retryCount,
1142
- ...(current.providerSessionId
1143
- ? { providerSessionId: current.providerSessionId }
1144
- : {}),
1145
- transitionKey: `continuation:${current.continuations}`,
1146
- });
1147
- waits.set(next.slot, Promise.resolve({ slot: next.slot, result: continued }));
1148
- releaseSlot = false;
1365
+ catch (error) {
1366
+ return reconcile(error instanceof Error ? error.message : String(error));
1149
1367
  }
1368
+ waits.set(next.slot, Promise.resolve(continued)
1369
+ .then((result) => ({ slot: next.slot, result }))
1370
+ .catch((error) => ({
1371
+ slot: next.slot,
1372
+ result: {
1373
+ kind: "reconciliation",
1374
+ reason: error instanceof Error ? error.message : String(error),
1375
+ },
1376
+ })));
1377
+ releaseSlot = false;
1150
1378
  }
1151
1379
  else if (next.result.kind === "report") {
1152
1380
  try {
@@ -1177,22 +1405,400 @@ export async function runTeamworkCoordinator(input) {
1177
1405
  }
1178
1406
  return settle();
1179
1407
  }
1180
- export async function assertTeamworkReceiptDurable(sessionId, batchId, planHash, cwd) {
1408
+ export async function assertTeamworkReceiptDurable(plan, receipt, cwd) {
1181
1409
  const [state, events] = await Promise.all([
1182
- readCoordinationState({ sessionId, ...(cwd ? { cwd } : {}) }),
1183
- readTeamworkEvents({ sessionId, ...(cwd ? { cwd } : {}) }),
1410
+ readCoordinationState({
1411
+ sessionId: plan.sessionId,
1412
+ ...(cwd ? { cwd } : {}),
1413
+ }),
1414
+ readTeamworkEvents({
1415
+ sessionId: plan.sessionId,
1416
+ ...(cwd ? { cwd } : {}),
1417
+ }),
1184
1418
  ]);
1185
- const batch = state.batches.find((entry) => entry.batchId === batchId);
1186
- if (batch?.planHash !== planHash ||
1187
- (batch.state !== "ALL_ACCEPTED" && batch.state !== "SETTLED_WITH_FAILURE")) {
1188
- throw new Error(`batch '${batchId}' has no durable terminal receipt`);
1189
- }
1190
- const receiptRecorded = events.some((event) => event.kind === "coordination" &&
1191
- event.batchId === batchId &&
1419
+ const expected = terminalReceiptFromProjection(plan, state);
1420
+ if (!isDeepStrictEqual(receipt, expected))
1421
+ throw new Error(`batch '${plan.batchId}' receipt disagrees with durable facts`);
1422
+ const receipts = events.filter((event) => event.kind === "coordination" &&
1423
+ event.batchId === plan.batchId &&
1192
1424
  event.action === "receipt_recorded" &&
1193
- event.payload?.status === batch.state);
1194
- if (!receiptRecorded) {
1195
- throw new Error(`batch '${batchId}' has no matching durable receipt event`);
1425
+ event.payload?.status === receipt.status &&
1426
+ event.payload?.status !== "RECONCILIATION_REQUIRED");
1427
+ if (receipts.length === 0 ||
1428
+ !isDeepStrictEqual(receipts.at(-1)?.payload, expected))
1429
+ throw new Error(`batch '${plan.batchId}' has no matching durable receipt event`);
1430
+ }
1431
+ /** Retire uncertain work only from associated terminal or process-birth evidence. */
1432
+ export async function reconcileTeamworkBatch(input) {
1433
+ const { plan, cwd } = input;
1434
+ const projection = await readCoordinationState({
1435
+ sessionId: plan.sessionId,
1436
+ cwd,
1437
+ });
1438
+ const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
1439
+ if (batch?.planHash !== plan.planHash)
1440
+ throw new Error("Batch does not match the frozen plan");
1441
+ const events = await readTeamworkEvents({ sessionId: plan.sessionId, cwd });
1442
+ const resolving = events.some((event) => event.kind === "coordination" &&
1443
+ event.batchId === plan.batchId &&
1444
+ event.action === "reconciliation_resolved");
1445
+ if (["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state)) {
1446
+ const receipt = terminalReceiptFromProjection(plan, projection);
1447
+ if (input.execute) {
1448
+ const recovered = await recoverExistingBatch({
1449
+ plan,
1450
+ cwd,
1451
+ by: "main",
1452
+ capacity: receipt.capacity,
1453
+ });
1454
+ if (!recovered)
1455
+ throw new Error("Settled batch disappeared during reconciliation");
1456
+ return recovered;
1457
+ }
1458
+ return receipt;
1459
+ }
1460
+ if (batch.state !== "RECONCILIATION_REQUIRED" &&
1461
+ !(resolving && batch.state === "SETTLING"))
1462
+ throw new Error("Recover the original batch into RECONCILIATION_REQUIRED first");
1463
+ const attempts = projection.attempts.filter((entry) => entry.batchId === plan.batchId);
1464
+ const blockers = projection.issues.filter((issue) => !issue.includes(": reconciliation required"));
1465
+ for (const attempt of attempts) {
1466
+ if (attempt.terminal)
1467
+ continue;
1468
+ const facts = events.filter((event) => event.kind === "coordination" &&
1469
+ event.batchId === plan.batchId &&
1470
+ event.attemptId === attempt.attemptId &&
1471
+ event.planHash === plan.planHash);
1472
+ if (facts.some((event) => event.kind === "coordination" && event.action === "process_exited"))
1473
+ continue;
1474
+ const started = facts
1475
+ .filter((event) => event.kind === "coordination" && event.action === "process_started")
1476
+ .pop();
1477
+ const pid = started?.kind === "coordination" ? started.payload?.pid : undefined;
1478
+ const birth = started?.kind === "coordination"
1479
+ ? started.payload?.processIdentity
1480
+ : undefined;
1481
+ let exited = false;
1482
+ let identityUnknown = true;
1483
+ if (typeof pid === "number" &&
1484
+ Number.isSafeInteger(pid) &&
1485
+ pid > 0 &&
1486
+ typeof birth === "string" &&
1487
+ birth.startsWith(`${pid}:`)) {
1488
+ const current = readProcessIdentity(pid);
1489
+ if (current) {
1490
+ exited = current !== birth;
1491
+ identityUnknown = false;
1492
+ }
1493
+ else {
1494
+ try {
1495
+ process.kill(pid, 0);
1496
+ }
1497
+ catch (error) {
1498
+ const code = error.code;
1499
+ exited = code === "ESRCH";
1500
+ identityUnknown = code !== "ESRCH";
1501
+ }
1502
+ }
1503
+ }
1504
+ if (!exited &&
1505
+ identityUnknown &&
1506
+ input.execute &&
1507
+ input.confirmUnknownProcessExit &&
1508
+ input.reason?.trim())
1509
+ exited = true;
1510
+ if (!exited)
1511
+ blockers.push(`Attempt ${attempt.attemptId} lacks confirmed process exit; preserve its lease`);
1196
1512
  }
1513
+ if (!input.execute)
1514
+ return { status: blockers.length ? "blocked" : "ready", blockers };
1515
+ if (blockers.length)
1516
+ throw new Error(blockers.join("; "));
1517
+ if (!input.reason?.trim())
1518
+ throw new Error("Reconciliation requires an explicit disposition reason");
1519
+ const by = "main";
1520
+ for (const attempt of attempts) {
1521
+ await recordCoordinationEvent({
1522
+ sessionId: plan.sessionId,
1523
+ cwd,
1524
+ by,
1525
+ batchId: plan.batchId,
1526
+ entity: "attempt",
1527
+ action: "terminal_observed",
1528
+ attemptId: attempt.attemptId,
1529
+ teammateId: attempt.teammateId,
1530
+ terminal: true,
1531
+ reason: input.reason,
1532
+ idempotencyKey: `${plan.batchId}:reconciled-terminal:${attempt.attemptId}`,
1533
+ });
1534
+ if (!["FINAL_FAILED", "REPORT_SUBMITTED"].includes(attempt.state))
1535
+ await appendTransition({
1536
+ sessionId: plan.sessionId,
1537
+ cwd,
1538
+ by,
1539
+ batchId: plan.batchId,
1540
+ entity: "attempt",
1541
+ id: attempt.attemptId,
1542
+ attemptId: attempt.attemptId,
1543
+ assignmentId: attempt.assignmentId,
1544
+ teammateId: attempt.teammateId,
1545
+ retryCount: attempt.retryCount,
1546
+ state: "FINAL_FAILED",
1547
+ terminal: true,
1548
+ states: new Map([[attempt.attemptId, attempt.state]]),
1549
+ transitionKey: "reconcile",
1550
+ });
1551
+ const teammate = projection.teammates.find((entry) => entry.batchId === plan.batchId &&
1552
+ entry.teammateId === attempt.teammateId);
1553
+ if (teammate && !["FAILED", "REPORT_SUBMITTED"].includes(teammate.state))
1554
+ await appendTransition({
1555
+ sessionId: plan.sessionId,
1556
+ cwd,
1557
+ by,
1558
+ batchId: plan.batchId,
1559
+ entity: "teammate",
1560
+ id: teammate.teammateId,
1561
+ teammateId: teammate.teammateId,
1562
+ attemptId: attempt.attemptId,
1563
+ state: "FAILED",
1564
+ terminal: true,
1565
+ states: new Map([[teammate.teammateId, teammate.state]]),
1566
+ transitionKey: "reconcile",
1567
+ });
1568
+ }
1569
+ for (const assignment of projection.assignments.filter((entry) => entry.batchId === plan.batchId)) {
1570
+ if (!["ACCEPTED", "FINAL_FAILED"].includes(assignment.state))
1571
+ await appendTransition({
1572
+ sessionId: plan.sessionId,
1573
+ cwd,
1574
+ by,
1575
+ batchId: plan.batchId,
1576
+ entity: "assignment",
1577
+ id: assignment.assignmentId,
1578
+ assignmentId: assignment.assignmentId,
1579
+ attemptId: assignment.attemptId,
1580
+ teammateId: assignment.teammateId,
1581
+ retryCount: assignment.retryCount,
1582
+ state: "FINAL_FAILED",
1583
+ states: new Map([[assignment.assignmentId, assignment.state]]),
1584
+ transitionKey: "reconcile",
1585
+ payload: { reason: input.reason },
1586
+ });
1587
+ }
1588
+ for (const lease of projection.leases.filter((entry) => entry.batchId === plan.batchId))
1589
+ await recordCoordinationEvent({
1590
+ sessionId: plan.sessionId,
1591
+ cwd,
1592
+ by,
1593
+ batchId: plan.batchId,
1594
+ entity: "attempt",
1595
+ action: "lease_released",
1596
+ leaseId: lease.leaseId,
1597
+ protected: false,
1598
+ reason: input.reason,
1599
+ idempotencyKey: `${plan.batchId}:reconciled-release:${lease.leaseId}`,
1600
+ });
1601
+ const accepted = projection.assignments
1602
+ .filter((entry) => entry.batchId === plan.batchId)
1603
+ .every((entry) => entry.state === "ACCEPTED");
1604
+ const states = new Map([
1605
+ [plan.batchId, batch.state],
1606
+ ]);
1607
+ await recordCoordinationEvent({
1608
+ sessionId: plan.sessionId,
1609
+ cwd,
1610
+ by,
1611
+ batchId: plan.batchId,
1612
+ entity: "batch",
1613
+ action: "reconciliation_resolved",
1614
+ reason: input.reason,
1615
+ ...(input.confirmUnknownProcessExit
1616
+ ? { payload: { confirmedUnknownProcessExit: true } }
1617
+ : {}),
1618
+ idempotencyKey: `${plan.batchId}:reconciled:${crypto
1619
+ .createHash("sha256")
1620
+ .update(input.reason ?? "")
1621
+ .digest("hex")}`,
1622
+ });
1623
+ if (batch.state !== "SETTLING")
1624
+ await appendTransition({
1625
+ sessionId: plan.sessionId,
1626
+ cwd,
1627
+ by,
1628
+ batchId: plan.batchId,
1629
+ entity: "batch",
1630
+ id: plan.batchId,
1631
+ state: "SETTLING",
1632
+ states,
1633
+ transitionKey: "reconcile",
1634
+ });
1635
+ await appendTransition({
1636
+ sessionId: plan.sessionId,
1637
+ cwd,
1638
+ by,
1639
+ batchId: plan.batchId,
1640
+ entity: "batch",
1641
+ id: plan.batchId,
1642
+ state: accepted ? "ALL_ACCEPTED" : "SETTLED_WITH_FAILURE",
1643
+ states,
1644
+ transitionKey: "reconcile",
1645
+ });
1646
+ const receipt = terminalReceiptFromProjection(plan, await readCoordinationState({ sessionId: plan.sessionId, cwd }));
1647
+ await appendAction({
1648
+ sessionId: plan.sessionId,
1649
+ cwd,
1650
+ by,
1651
+ batchId: plan.batchId,
1652
+ entity: "batch",
1653
+ action: "receipt_recorded",
1654
+ id: `${plan.batchId}:${receipt.status}`,
1655
+ payload: { ...receipt },
1656
+ });
1657
+ await assertTeamworkReceiptDurable(plan, receipt, cwd);
1658
+ return receipt;
1659
+ }
1660
+ /** Read settled facts without admitting work or consulting current resource policy. */
1661
+ export async function readSettledTeamworkReceipt(plan, cwd) {
1662
+ const state = await readCoordinationState({ sessionId: plan.sessionId, cwd });
1663
+ const batch = state.batches.find((entry) => entry.batchId === plan.batchId);
1664
+ if (!batch ||
1665
+ !["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
1666
+ return undefined;
1667
+ const receipt = terminalReceiptFromProjection(plan, state);
1668
+ await assertTeamworkReceiptDurable(plan, receipt, cwd);
1669
+ return receipt;
1670
+ }
1671
+ /** Main-agent review starts only after the CLI has drained the entire batch. */
1672
+ export async function reviewTeamworkReport(input) {
1673
+ const { plan, taskRoot, cwd } = input;
1674
+ if (!input.reason.trim())
1675
+ throw new Error("Review requires an explicit reason");
1676
+ const assignment = plan.assignments.find((item) => item.assignmentId === input.assignmentId);
1677
+ if (!assignment)
1678
+ throw new Error("Review assignment is not in the frozen plan");
1679
+ return withLock(path.join(taskRoot, "teammates", `${plan.batchId}.review.lock`), async () => {
1680
+ assertTeamworkPlanCurrent(plan, taskRoot);
1681
+ const projection = await readCoordinationState({
1682
+ sessionId: plan.sessionId,
1683
+ cwd,
1684
+ });
1685
+ const batch = projection.batches.find((item) => item.batchId === plan.batchId);
1686
+ if (!batch ||
1687
+ !["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
1688
+ throw new Error("Review requires the entire batch to be collected first");
1689
+ terminalReceiptFromProjection(plan, projection);
1690
+ const work = projection.assignments.find((item) => item.batchId === plan.batchId &&
1691
+ item.assignmentId === assignment.assignmentId);
1692
+ if (!work?.attemptId || !work.teammateId)
1693
+ throw new Error("Review has no matching report attempt");
1694
+ readTeammateReport({
1695
+ taskRoot,
1696
+ briefPath: assignment.briefPath,
1697
+ attemptId: work.attemptId,
1698
+ identity: {
1699
+ taskId: plan.taskId,
1700
+ batchId: plan.batchId,
1701
+ assignmentId: assignment.assignmentId,
1702
+ teammateId: work.teammateId,
1703
+ planHash: plan.planHash,
1704
+ phaseId: plan.phaseId,
1705
+ taskRouteRevision: plan.taskRouteRevision,
1706
+ inputRevision: plan.inputRevision,
1707
+ },
1708
+ sourceDigest: assignment.sourceDigest,
1709
+ });
1710
+ const reportFile = path.join(taskRoot, path.dirname(assignment.briefPath), "attempts", work.attemptId, "report.json");
1711
+ const reportSha256 = crypto
1712
+ .createHash("sha256")
1713
+ .update(fs.readFileSync(reportFile))
1714
+ .digest("hex");
1715
+ if (reportSha256 !== input.reportSha256)
1716
+ throw new Error("Review report digest does not match the inspected report");
1717
+ const state = input.decision === "accepted" ? "ACCEPTED" : "FINAL_FAILED";
1718
+ if (work.state !== "AWAITING_REVIEW" && work.state !== state)
1719
+ throw new Error("Report already has a different final disposition");
1720
+ const by = input.by ?? "main";
1721
+ writeTeammateDisposition({
1722
+ taskRoot,
1723
+ briefPath: assignment.briefPath,
1724
+ identity: {
1725
+ taskId: plan.taskId,
1726
+ batchId: plan.batchId,
1727
+ assignmentId: assignment.assignmentId,
1728
+ attemptId: work.attemptId,
1729
+ teammateId: work.teammateId,
1730
+ },
1731
+ sourceDigest: assignment.sourceDigest,
1732
+ status: input.decision,
1733
+ reason: input.reason,
1734
+ review: {
1735
+ plan_hash: plan.planHash,
1736
+ phase_id: plan.phaseId,
1737
+ task_route_revision: plan.taskRouteRevision,
1738
+ input_revision: plan.inputRevision,
1739
+ report_sha256: reportSha256,
1740
+ acceptance: assignment.acceptance,
1741
+ by,
1742
+ },
1743
+ });
1744
+ if (work.state === "AWAITING_REVIEW")
1745
+ await appendTransition({
1746
+ sessionId: plan.sessionId,
1747
+ cwd,
1748
+ by,
1749
+ batchId: plan.batchId,
1750
+ entity: "assignment",
1751
+ id: assignment.assignmentId,
1752
+ assignmentId: assignment.assignmentId,
1753
+ attemptId: work.attemptId,
1754
+ teammateId: work.teammateId,
1755
+ retryCount: work.retryCount,
1756
+ state,
1757
+ states: new Map([[assignment.assignmentId, work.state]]),
1758
+ payload: {
1759
+ reason: input.reason,
1760
+ reportSha256,
1761
+ planHash: plan.planHash,
1762
+ acceptance: assignment.acceptance,
1763
+ },
1764
+ });
1765
+ let updated = await readCoordinationState({
1766
+ sessionId: plan.sessionId,
1767
+ cwd,
1768
+ });
1769
+ const remaining = updated.assignments.filter((item) => item.batchId === plan.batchId);
1770
+ if (batch.state === "AWAITING_REVIEW" &&
1771
+ !remaining.some((item) => item.state === "AWAITING_REVIEW")) {
1772
+ await appendTransition({
1773
+ sessionId: plan.sessionId,
1774
+ cwd,
1775
+ by,
1776
+ batchId: plan.batchId,
1777
+ entity: "batch",
1778
+ id: plan.batchId,
1779
+ state: remaining.every((item) => item.state === "ACCEPTED")
1780
+ ? "ALL_ACCEPTED"
1781
+ : "SETTLED_WITH_FAILURE",
1782
+ states: new Map([[plan.batchId, batch.state]]),
1783
+ });
1784
+ updated = await readCoordinationState({
1785
+ sessionId: plan.sessionId,
1786
+ cwd,
1787
+ });
1788
+ }
1789
+ const receipt = terminalReceiptFromProjection(plan, updated);
1790
+ await appendAction({
1791
+ sessionId: plan.sessionId,
1792
+ cwd,
1793
+ by,
1794
+ batchId: plan.batchId,
1795
+ entity: "batch",
1796
+ action: "receipt_recorded",
1797
+ id: `${plan.batchId}:review:${crypto.createHash("sha256").update(JSON.stringify(receipt)).digest("hex")}`,
1798
+ payload: { ...receipt },
1799
+ });
1800
+ await assertTeamworkReceiptDurable(plan, receipt, cwd);
1801
+ return receipt;
1802
+ });
1197
1803
  }
1198
1804
  //# sourceMappingURL=coordinator.js.map