@fitlab-ai/agent-infra 0.9.2 → 0.9.4

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 (428) hide show
  1. package/README.md +12 -9
  2. package/README.zh-CN.md +12 -9
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +134 -108
  6. package/dist/lib/agent-clients/adapter.js +137 -4
  7. package/dist/lib/agent-clients/adapters/antigravity-cli.js +64 -0
  8. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  9. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  10. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  11. package/dist/lib/agent-clients/adapters/claude-code.js +27 -4
  12. package/dist/lib/agent-clients/adapters/codex.js +9 -0
  13. package/dist/lib/agent-clients/adapters/index.js +2 -2
  14. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  15. package/dist/lib/agent-clients/adapters/opencode.js +24 -3
  16. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  17. package/dist/lib/agent-clients/config.js +42 -4
  18. package/dist/lib/agent-clients/next-steps.js +11 -2
  19. package/dist/lib/agent-clients/reconcile.js +2 -1
  20. package/dist/lib/agent-clients/registry.js +16 -2
  21. package/dist/lib/agent-clients/schema.js +18 -0
  22. package/dist/lib/agent-clients/tokens.js +49 -0
  23. package/dist/lib/agent-clients/types.js +1 -1
  24. package/dist/lib/defaults.json +1 -1
  25. package/dist/lib/git/workflow.js +54 -1
  26. package/dist/lib/internal/agent-client.js +61 -4
  27. package/dist/lib/internal/git-workflow.js +6 -1
  28. package/dist/lib/internal/platform-checks.js +3 -3
  29. package/dist/lib/internal/platform-comment.js +29 -2
  30. package/dist/lib/internal/platform-issue.js +7 -0
  31. package/dist/lib/internal/platform-pr-review.js +132 -0
  32. package/dist/lib/internal/platform-pr.js +20 -2
  33. package/dist/lib/internal/platform-release-notes.js +52 -16
  34. package/dist/lib/internal/pr-review-grade.js +140 -0
  35. package/dist/lib/internal/release-workflow.js +13 -6
  36. package/dist/lib/internal/sandbox-control.js +24 -0
  37. package/dist/lib/internal/task-activity.js +105 -0
  38. package/dist/lib/internal/task-event.js +17 -1
  39. package/dist/lib/internal/task-ledger.js +12 -3
  40. package/dist/lib/internal/task-lifecycle.js +26 -1
  41. package/dist/lib/internal/task-orchestration.js +167 -18
  42. package/dist/lib/internal/task-review.js +8 -3
  43. package/dist/lib/platform/adapters.js +8 -1
  44. package/dist/lib/platform/completion-backfill.js +122 -0
  45. package/dist/lib/platform/github-client.js +19 -2
  46. package/dist/lib/platform/issue-comments.js +3 -2
  47. package/dist/lib/platform/merged-pr-equivalence.js +115 -5
  48. package/dist/lib/platform/pr-checks.js +65 -42
  49. package/dist/lib/platform/pr-review.js +166 -0
  50. package/dist/lib/platform/pull-requests.js +371 -4
  51. package/dist/lib/platform/release-notes.js +85 -3
  52. package/dist/lib/platform/releases.js +13 -5
  53. package/dist/lib/pr-review/evidence-grading.js +229 -0
  54. package/dist/lib/process-data/index.js +239 -0
  55. package/dist/lib/process-data/normalize.js +198 -0
  56. package/dist/lib/process-data/privacy.js +33 -0
  57. package/dist/lib/process-data/reconcile.js +95 -0
  58. package/dist/lib/process-data/repair.js +58 -0
  59. package/dist/lib/process-data/sources.js +345 -0
  60. package/dist/lib/process-data/store.js +282 -0
  61. package/dist/lib/process-data/types.js +2 -0
  62. package/dist/lib/run/index.js +4 -3
  63. package/dist/lib/run/tui.js +10 -13
  64. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  65. package/dist/lib/sandbox/capture.js +7 -2
  66. package/dist/lib/sandbox/commands/create.js +109 -248
  67. package/dist/lib/sandbox/commands/enter.js +15 -47
  68. package/dist/lib/sandbox/commands/list-running.js +16 -6
  69. package/dist/lib/sandbox/commands/ls.js +10 -6
  70. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  71. package/dist/lib/sandbox/commands/refresh.js +4 -84
  72. package/dist/lib/sandbox/commands/rm.js +24 -3
  73. package/dist/lib/sandbox/commands/show.js +13 -4
  74. package/dist/lib/sandbox/commands/start.js +7 -4
  75. package/dist/lib/sandbox/config.js +2 -0
  76. package/dist/lib/sandbox/constants.js +6 -0
  77. package/dist/lib/sandbox/control/client.js +44 -0
  78. package/dist/lib/sandbox/control/protocol.js +35 -0
  79. package/dist/lib/sandbox/control/server.js +109 -0
  80. package/dist/lib/sandbox/dockerfile.js +36 -13
  81. package/dist/lib/sandbox/mounts.js +17 -4
  82. package/dist/lib/sandbox/recovery.js +128 -5
  83. package/dist/lib/sandbox/redaction.js +12 -0
  84. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  85. package/dist/lib/sandbox/workspace-identity.js +95 -0
  86. package/dist/lib/sandbox/workspace-view.js +103 -0
  87. package/dist/lib/task/activity-intent.js +307 -0
  88. package/dist/lib/task/artifact-lifecycle.js +2 -1
  89. package/dist/lib/task/commands/log.js +15 -14
  90. package/dist/lib/task/commit-finalization.js +267 -0
  91. package/dist/lib/task/commit-intent.js +165 -0
  92. package/dist/lib/task/decision-intents.js +31 -17
  93. package/dist/lib/task/delegation-receipts.js +25 -4
  94. package/dist/lib/task/events.js +67 -28
  95. package/dist/lib/task/finalization-artifacts.js +35 -0
  96. package/dist/lib/task/implementation-inputs.js +54 -7
  97. package/dist/lib/task/ledger-intents.js +35 -1
  98. package/dist/lib/task/lifecycle-execution.js +46 -0
  99. package/dist/lib/task/orchestration.js +753 -54
  100. package/dist/lib/task/resolve-ref.js +41 -1
  101. package/dist/lib/task/review-finalization.js +29 -9
  102. package/dist/lib/task/task-execution-lock.js +231 -0
  103. package/dist/lib/task/verification-checks.js +1 -1
  104. package/dist/lib/task/verification-engine.js +218 -2
  105. package/dist/lib/task/verification.js +4 -3
  106. package/dist/lib/task/workspace-snapshot.js +17 -5
  107. package/lib/agent-clients/adapter.ts +201 -4
  108. package/lib/agent-clients/adapters/antigravity-cli.ts +65 -0
  109. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  110. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  111. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  112. package/lib/agent-clients/adapters/claude-code.ts +31 -4
  113. package/lib/agent-clients/adapters/codex.ts +9 -0
  114. package/lib/agent-clients/adapters/index.ts +2 -2
  115. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  116. package/lib/agent-clients/adapters/opencode.ts +27 -3
  117. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  118. package/lib/agent-clients/config.ts +49 -4
  119. package/lib/agent-clients/next-steps.ts +15 -2
  120. package/lib/agent-clients/reconcile.ts +2 -1
  121. package/lib/agent-clients/registry.ts +19 -1
  122. package/lib/agent-clients/schema.ts +18 -0
  123. package/lib/agent-clients/tokens.ts +53 -0
  124. package/lib/agent-clients/types.ts +15 -1
  125. package/lib/defaults.json +1 -1
  126. package/lib/git/workflow.ts +66 -2
  127. package/lib/internal/agent-client.ts +59 -4
  128. package/lib/internal/git-workflow.ts +5 -1
  129. package/lib/internal/platform-checks.ts +4 -4
  130. package/lib/internal/platform-comment.ts +17 -2
  131. package/lib/internal/platform-issue.ts +4 -0
  132. package/lib/internal/platform-pr-review.ts +104 -0
  133. package/lib/internal/platform-pr.ts +14 -1
  134. package/lib/internal/platform-release-notes.ts +59 -16
  135. package/lib/internal/pr-review-grade.ts +130 -0
  136. package/lib/internal/release-workflow.ts +13 -6
  137. package/lib/internal/sandbox-control.ts +24 -0
  138. package/lib/internal/task-activity.ts +91 -0
  139. package/lib/internal/task-event.ts +9 -1
  140. package/lib/internal/task-ledger.ts +12 -3
  141. package/lib/internal/task-lifecycle.ts +26 -1
  142. package/lib/internal/task-orchestration.ts +123 -14
  143. package/lib/internal/task-review.ts +8 -3
  144. package/lib/platform/adapters.ts +29 -2
  145. package/lib/platform/completion-backfill.ts +134 -0
  146. package/lib/platform/github-client.ts +20 -2
  147. package/lib/platform/issue-comments.ts +3 -2
  148. package/lib/platform/merged-pr-equivalence.ts +127 -5
  149. package/lib/platform/pr-checks.ts +73 -46
  150. package/lib/platform/pr-review.ts +201 -0
  151. package/lib/platform/pull-requests.ts +444 -3
  152. package/lib/platform/release-notes.ts +100 -4
  153. package/lib/platform/releases.ts +16 -4
  154. package/lib/pr-review/evidence-grading.ts +318 -0
  155. package/lib/process-data/index.ts +224 -0
  156. package/lib/process-data/normalize.ts +202 -0
  157. package/lib/process-data/privacy.ts +41 -0
  158. package/lib/process-data/reconcile.ts +110 -0
  159. package/lib/process-data/repair.ts +64 -0
  160. package/lib/process-data/sources.ts +354 -0
  161. package/lib/process-data/store.ts +315 -0
  162. package/lib/process-data/types.ts +135 -0
  163. package/lib/run/index.ts +5 -3
  164. package/lib/run/tui.ts +14 -12
  165. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  166. package/lib/sandbox/capture.ts +9 -2
  167. package/lib/sandbox/commands/create.ts +126 -280
  168. package/lib/sandbox/commands/enter.ts +13 -59
  169. package/lib/sandbox/commands/list-running.ts +23 -6
  170. package/lib/sandbox/commands/ls.ts +22 -6
  171. package/lib/sandbox/commands/rebuild.ts +4 -3
  172. package/lib/sandbox/commands/refresh.ts +4 -126
  173. package/lib/sandbox/commands/rm.ts +24 -3
  174. package/lib/sandbox/commands/show.ts +30 -4
  175. package/lib/sandbox/commands/start.ts +6 -4
  176. package/lib/sandbox/config.ts +4 -0
  177. package/lib/sandbox/constants.ts +8 -0
  178. package/lib/sandbox/control/client.ts +55 -0
  179. package/lib/sandbox/control/protocol.ts +75 -0
  180. package/lib/sandbox/control/server.ts +117 -0
  181. package/lib/sandbox/dockerfile.ts +66 -13
  182. package/lib/sandbox/mounts.ts +24 -4
  183. package/lib/sandbox/recovery.ts +163 -6
  184. package/lib/sandbox/redaction.ts +14 -0
  185. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  186. package/lib/sandbox/tool-types.ts +16 -0
  187. package/lib/sandbox/workspace-identity.ts +123 -0
  188. package/lib/sandbox/workspace-view.ts +143 -0
  189. package/lib/server/server.schema.json +2 -2
  190. package/lib/task/activity-intent.ts +417 -0
  191. package/lib/task/artifact-lifecycle.ts +2 -1
  192. package/lib/task/commands/log.ts +15 -14
  193. package/lib/task/commit-finalization.ts +356 -0
  194. package/lib/task/commit-intent.ts +230 -0
  195. package/lib/task/decision-intents.ts +32 -12
  196. package/lib/task/delegation-receipts.ts +34 -5
  197. package/lib/task/events.ts +75 -30
  198. package/lib/task/finalization-artifacts.ts +58 -0
  199. package/lib/task/implementation-inputs.ts +62 -8
  200. package/lib/task/ledger-intents.ts +43 -3
  201. package/lib/task/lifecycle-execution.ts +106 -0
  202. package/lib/task/orchestration.ts +1019 -72
  203. package/lib/task/resolve-ref.ts +46 -1
  204. package/lib/task/review-finalization.ts +40 -9
  205. package/lib/task/task-execution-lock.ts +296 -0
  206. package/lib/task/verification-checks.ts +1 -1
  207. package/lib/task/verification-engine.ts +249 -2
  208. package/lib/task/verification.ts +5 -3
  209. package/lib/task/workspace-snapshot.ts +26 -5
  210. package/package.json +3 -3
  211. package/templates/.agents/QUICKSTART.en.md +25 -8
  212. package/templates/.agents/QUICKSTART.zh-CN.md +25 -8
  213. package/templates/.agents/README.en.md +10 -7
  214. package/templates/.agents/README.zh-CN.md +10 -7
  215. package/templates/.agents/hooks/lifecycle-delegation.js +15 -4
  216. package/templates/.agents/rules/README.en.md +1 -1
  217. package/templates/.agents/rules/README.zh-CN.md +1 -1
  218. package/templates/.agents/rules/create-issue.github.en.md +4 -2
  219. package/templates/.agents/rules/create-issue.github.zh-CN.md +4 -2
  220. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  221. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  222. package/templates/.agents/rules/issue-fields.github.en.md +3 -1
  223. package/templates/.agents/rules/issue-fields.github.zh-CN.md +3 -1
  224. package/templates/.agents/rules/issue-pr-commands.en.md +5 -3
  225. package/templates/.agents/rules/issue-pr-commands.github.en.md +5 -3
  226. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +5 -3
  227. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -3
  228. package/templates/.agents/rules/issue-sync.github.en.md +7 -3
  229. package/templates/.agents/rules/issue-sync.github.zh-CN.md +8 -4
  230. package/templates/.agents/rules/lifecycle-orchestration.en.md +5 -2
  231. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +5 -2
  232. package/templates/.agents/rules/milestone-inference.github.en.md +4 -2
  233. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +4 -2
  234. package/templates/.agents/rules/next-step-output.en.md +3 -1
  235. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -1
  236. package/templates/.agents/rules/no-mid-flow-questions.en.md +17 -0
  237. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +17 -0
  238. package/templates/.agents/rules/pr-checks-commands.en.md +3 -3
  239. package/templates/.agents/rules/pr-checks-commands.github.en.md +2 -2
  240. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +3 -3
  241. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +3 -3
  242. package/templates/.agents/rules/pr-sync.en.md +3 -1
  243. package/templates/.agents/rules/pr-sync.github.en.md +11 -1
  244. package/templates/.agents/rules/pr-sync.github.zh-CN.md +11 -1
  245. package/templates/.agents/rules/pr-sync.zh-CN.md +3 -1
  246. package/templates/.agents/rules/release-commands.en.md +1 -1
  247. package/templates/.agents/rules/release-commands.github.en.md +12 -2
  248. package/templates/.agents/rules/release-commands.github.zh-CN.md +12 -2
  249. package/templates/.agents/rules/release-commands.zh-CN.md +1 -1
  250. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  251. package/templates/.agents/rules/task-management.en.md +13 -0
  252. package/templates/.agents/rules/task-management.zh-CN.md +13 -0
  253. package/templates/.agents/skills/analyze-task/SKILL.en.md +10 -7
  254. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -7
  255. package/templates/.agents/skills/block-task/SKILL.en.md +7 -5
  256. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +7 -5
  257. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -4
  258. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -4
  259. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -1
  260. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -1
  261. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -1
  262. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -1
  263. package/templates/.agents/skills/code-task/SKILL.en.md +10 -7
  264. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +12 -9
  265. package/templates/.agents/skills/commit/SKILL.en.md +23 -11
  266. package/templates/.agents/skills/commit/SKILL.zh-CN.md +29 -11
  267. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  268. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  269. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
  270. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
  271. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  272. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  273. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  274. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  275. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  276. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  277. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +6 -4
  278. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +6 -4
  279. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  280. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  281. package/templates/.agents/skills/complete-task/SKILL.en.md +22 -7
  282. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +22 -7
  283. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  284. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  285. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +17 -0
  286. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +17 -0
  287. package/templates/.agents/skills/create-pr/SKILL.en.md +7 -5
  288. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -5
  289. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
  290. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
  291. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -16
  292. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -16
  293. package/templates/.agents/skills/create-task/SKILL.en.md +5 -3
  294. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -3
  295. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  296. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  297. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -3
  298. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -3
  299. package/templates/.agents/skills/plan-task/SKILL.en.md +8 -5
  300. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +8 -5
  301. package/templates/.agents/skills/post-release/SKILL.en.md +2 -0
  302. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +2 -0
  303. package/templates/.agents/skills/release/SKILL.en.md +16 -10
  304. package/templates/.agents/skills/release/SKILL.zh-CN.md +16 -10
  305. package/templates/.agents/skills/restore-task/SKILL.en.md +4 -1
  306. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -1
  307. package/templates/.agents/skills/review-analysis/SKILL.en.md +7 -4
  308. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +7 -4
  309. package/templates/.agents/skills/review-code/SKILL.en.md +11 -8
  310. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -8
  311. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  312. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  313. package/templates/.agents/skills/review-plan/SKILL.en.md +7 -4
  314. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +7 -4
  315. package/templates/.agents/skills/review-pr/SKILL.en.md +186 -0
  316. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +186 -0
  317. package/templates/.agents/skills/review-pr/config/verify.en.json +37 -0
  318. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +37 -0
  319. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +65 -0
  320. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +65 -0
  321. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +18 -0
  322. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +18 -0
  323. package/templates/.agents/skills/review-pr/reference/report-template.en.md +85 -0
  324. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +85 -0
  325. package/templates/.agents/skills/run-task/SKILL.en.md +8 -8
  326. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +8 -8
  327. package/templates/.agents/skills/run-task/config/verify.en.json +2 -1
  328. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +2 -1
  329. package/templates/.agents/skills/run-task/reference/host-validation.en.md +20 -0
  330. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +20 -0
  331. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +98 -107
  332. package/templates/.agents/skills/watch-pr/SKILL.en.md +13 -13
  333. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +13 -13
  334. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +27 -6
  335. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +26 -5
  336. package/templates/.agents/templates/task.en.md +2 -2
  337. package/templates/.agents/templates/task.zh-CN.md +2 -2
  338. package/templates/.agents/workflows/bug-fix.en.yaml +3 -3
  339. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +3 -3
  340. package/templates/.agents/workflows/code-review.en.yaml +1 -1
  341. package/templates/.agents/workflows/code-review.zh-CN.yaml +1 -1
  342. package/templates/.agents/workflows/feature-development.en.yaml +4 -4
  343. package/templates/.agents/workflows/feature-development.zh-CN.yaml +4 -4
  344. package/templates/.agents/workflows/refactoring.en.yaml +3 -3
  345. package/templates/.agents/workflows/refactoring.zh-CN.yaml +3 -3
  346. package/templates/.claude/commands/post-release.en.md +1 -0
  347. package/templates/.claude/commands/post-release.zh-CN.md +1 -0
  348. package/templates/.claude/commands/review-pr.en.md +8 -0
  349. package/templates/.claude/commands/review-pr.zh-CN.md +8 -0
  350. package/templates/.claude/commands/watch-pr.en.md +1 -1
  351. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  352. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  353. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  354. package/templates/.opencode/commands/post-release.en.md +2 -0
  355. package/templates/.opencode/commands/post-release.zh-CN.md +2 -0
  356. package/templates/.opencode/commands/review-pr.en.md +11 -0
  357. package/templates/.opencode/commands/review-pr.zh-CN.md +11 -0
  358. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  359. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  360. package/dist/lib/agent-clients/adapters/gemini-cli.js +0 -64
  361. package/lib/agent-clients/adapters/gemini-cli.ts +0 -65
  362. package/templates/.gemini/commands/_project_/analyze-task.en.toml +0 -10
  363. package/templates/.gemini/commands/_project_/analyze-task.zh-CN.toml +0 -10
  364. package/templates/.gemini/commands/_project_/archive-tasks.en.toml +0 -10
  365. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +0 -10
  366. package/templates/.gemini/commands/_project_/block-task.en.toml +0 -10
  367. package/templates/.gemini/commands/_project_/block-task.zh-CN.toml +0 -10
  368. package/templates/.gemini/commands/_project_/cancel-task.en.toml +0 -10
  369. package/templates/.gemini/commands/_project_/cancel-task.zh-CN.toml +0 -10
  370. package/templates/.gemini/commands/_project_/check-task.en.toml +0 -10
  371. package/templates/.gemini/commands/_project_/check-task.zh-CN.toml +0 -10
  372. package/templates/.gemini/commands/_project_/close-codescan.en.toml +0 -8
  373. package/templates/.gemini/commands/_project_/close-codescan.zh-CN.toml +0 -8
  374. package/templates/.gemini/commands/_project_/close-dependabot.en.toml +0 -8
  375. package/templates/.gemini/commands/_project_/close-dependabot.zh-CN.toml +0 -8
  376. package/templates/.gemini/commands/_project_/code-task.en.toml +0 -10
  377. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +0 -10
  378. package/templates/.gemini/commands/_project_/commit.en.toml +0 -10
  379. package/templates/.gemini/commands/_project_/commit.zh-CN.toml +0 -10
  380. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +0 -10
  381. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +0 -10
  382. package/templates/.gemini/commands/_project_/complete-task.en.toml +0 -10
  383. package/templates/.gemini/commands/_project_/complete-task.zh-CN.toml +0 -10
  384. package/templates/.gemini/commands/_project_/create-pr.en.toml +0 -10
  385. package/templates/.gemini/commands/_project_/create-pr.zh-CN.toml +0 -10
  386. package/templates/.gemini/commands/_project_/create-release-note.en.toml +0 -8
  387. package/templates/.gemini/commands/_project_/create-release-note.zh-CN.toml +0 -8
  388. package/templates/.gemini/commands/_project_/create-task.en.toml +0 -8
  389. package/templates/.gemini/commands/_project_/create-task.zh-CN.toml +0 -8
  390. package/templates/.gemini/commands/_project_/import-codescan.en.toml +0 -8
  391. package/templates/.gemini/commands/_project_/import-codescan.zh-CN.toml +0 -8
  392. package/templates/.gemini/commands/_project_/import-dependabot.en.toml +0 -8
  393. package/templates/.gemini/commands/_project_/import-dependabot.zh-CN.toml +0 -8
  394. package/templates/.gemini/commands/_project_/import-issue.en.toml +0 -8
  395. package/templates/.gemini/commands/_project_/import-issue.zh-CN.toml +0 -8
  396. package/templates/.gemini/commands/_project_/init-labels.en.toml +0 -8
  397. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +0 -8
  398. package/templates/.gemini/commands/_project_/init-milestones.en.toml +0 -10
  399. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +0 -10
  400. package/templates/.gemini/commands/_project_/plan-task.en.toml +0 -10
  401. package/templates/.gemini/commands/_project_/plan-task.zh-CN.toml +0 -10
  402. package/templates/.gemini/commands/_project_/post-release.en.toml +0 -6
  403. package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +0 -6
  404. package/templates/.gemini/commands/_project_/refine-title.en.toml +0 -8
  405. package/templates/.gemini/commands/_project_/refine-title.zh-CN.toml +0 -8
  406. package/templates/.gemini/commands/_project_/release.en.toml +0 -8
  407. package/templates/.gemini/commands/_project_/release.zh-CN.toml +0 -8
  408. package/templates/.gemini/commands/_project_/restore-task.en.toml +0 -8
  409. package/templates/.gemini/commands/_project_/restore-task.zh-CN.toml +0 -8
  410. package/templates/.gemini/commands/_project_/review-analysis.en.toml +0 -10
  411. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +0 -10
  412. package/templates/.gemini/commands/_project_/review-code.en.toml +0 -10
  413. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +0 -10
  414. package/templates/.gemini/commands/_project_/review-plan.en.toml +0 -10
  415. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +0 -10
  416. package/templates/.gemini/commands/_project_/run-task.en.toml +0 -8
  417. package/templates/.gemini/commands/_project_/run-task.zh-CN.toml +0 -8
  418. package/templates/.gemini/commands/_project_/test-integration.en.toml +0 -6
  419. package/templates/.gemini/commands/_project_/test-integration.zh-CN.toml +0 -6
  420. package/templates/.gemini/commands/_project_/test.en.toml +0 -6
  421. package/templates/.gemini/commands/_project_/test.zh-CN.toml +0 -6
  422. package/templates/.gemini/commands/_project_/update-agent-infra.en.toml +0 -6
  423. package/templates/.gemini/commands/_project_/update-agent-infra.zh-CN.toml +0 -6
  424. package/templates/.gemini/commands/_project_/upgrade-dependency.en.toml +0 -8
  425. package/templates/.gemini/commands/_project_/upgrade-dependency.zh-CN.toml +0 -8
  426. package/templates/.gemini/commands/_project_/watch-pr.en.toml +0 -10
  427. package/templates/.gemini/commands/_project_/watch-pr.zh-CN.toml +0 -10
  428. package/templates/.gemini/settings.json +0 -3
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
+ import { execFileSync, spawnSync } from 'node:child_process';
4
5
 
5
6
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
6
7
  import { parseReviewSummary } from './review-artifacts.ts';
@@ -16,24 +17,93 @@ import {
16
17
  sealDelegation
17
18
  } from './delegation-receipts.ts';
18
19
  import type { DelegationReceipt, DelegationRole, DelegationStage } from './delegation-receipts.ts';
19
- import { getAgentClientCapability } from '../agent-clients/registry.ts';
20
- import type { AgentClientId } from '../agent-clients/types.ts';
21
- import { captureWorkspaceSnapshot, diffWorkspaceSnapshots } from './workspace-snapshot.ts';
20
+ import { normalizeAgentClients } from '../agent-clients/config.ts';
21
+ import { inspectPlatformPullRequest } from '../platform/pull-requests.ts';
22
+ import {
23
+ getAgentClientCapability,
24
+ getAgentClientDelegationEvidence,
25
+ getAgentClientModelSelection
26
+ } from '../agent-clients/registry.ts';
27
+ import { isAgentClientId } from '../agent-clients/types.ts';
28
+ import type {
29
+ AgentClientId,
30
+ OrchestrationModelPolicy,
31
+ OrchestrationRolePolicy
32
+ } from '../agent-clients/types.ts';
33
+ import {
34
+ captureRepositorySnapshot,
35
+ captureWorkspaceSnapshot,
36
+ diffWorkspaceSnapshots
37
+ } from './workspace-snapshot.ts';
38
+ import type { RepositorySnapshot } from './workspace-snapshot.ts';
39
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
40
+ import {
41
+ CommitIntentError,
42
+ commitIntentPath,
43
+ createCommitIntent,
44
+ digest,
45
+ readCommitIntent,
46
+ removeCommitIntent,
47
+ removeCommitIntentByDigest,
48
+ serialize,
49
+ updateCommitIntent
50
+ } from './commit-intent.ts';
51
+ import type { CommitIntent, PushEvidence } from './commit-intent.ts';
52
+ import {
53
+ inspectCommitFinalization,
54
+ planCommitTaskFinalization
55
+ } from './commit-finalization.ts';
56
+ import type { CommitFinalizationInspection } from './commit-finalization.ts';
57
+ import { captureTaskWriteMetadata, writeTask } from './write.ts';
22
58
 
23
59
  type OrchestrationStatus = 'running' | 'paused' | 'completed';
60
+ type LegacyOrchestrationModelPolicy = Readonly<{
61
+ executor: string;
62
+ reviewer: string;
63
+ }>;
64
+ type ModelPolicySource = Readonly<{
65
+ kind: 'explicit' | 'project-config';
66
+ client: AgentClientId;
67
+ resolvedAt: string;
68
+ }>;
69
+ type OrchestrationRecovery = Readonly<{
70
+ code: 'MODEL_POLICY_SUPPLEMENTED';
71
+ recoveredAt: string;
72
+ previousSchemaVersion: 1;
73
+ previousStatus: OrchestrationStatus;
74
+ previousPause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
75
+ policySource: ModelPolicySource;
76
+ receiptCount: 0;
77
+ pendingDelegation: false;
78
+ resultingStatus: OrchestrationStatus;
79
+ }>;
80
+ type CleanCompletionEvidence = Readonly<{
81
+ kind: 'reviewed-head-clean';
82
+ observedAt: string;
83
+ head: string;
84
+ headTree: string;
85
+ worktreeTree: string;
86
+ lastReviewedCommit: string;
87
+ prNumber: number;
88
+ prHead: string;
89
+ }>;
24
90
  type OrchestrationRun = Readonly<{
25
- schemaVersion: 1;
91
+ schemaVersion: 1 | 2;
26
92
  taskId: string;
27
93
  runId: string;
28
94
  status: OrchestrationStatus;
29
95
  nextStage: DelegationStage | null;
30
96
  stepCount: number;
31
97
  maxSteps: number;
98
+ modelPolicy?: OrchestrationModelPolicy | LegacyOrchestrationModelPolicy;
99
+ modelPolicySource?: ModelPolicySource;
100
+ recoveryHistory?: readonly OrchestrationRecovery[];
32
101
  baseline: string;
33
102
  pendingDelegation: DelegationReceipt | null;
34
103
  receipts: readonly DelegationReceipt[];
35
104
  pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
36
105
  commitAuthorization: Readonly<{ issuedAt: string | null; consumedAt: string | null }>;
106
+ completionEvidence?: CleanCompletionEvidence | null;
37
107
  createdAt: string;
38
108
  updatedAt: string;
39
109
  }>;
@@ -43,6 +113,8 @@ type OrchestrationNext = Readonly<{
43
113
  stage: DelegationStage;
44
114
  round: number;
45
115
  artifact: string;
116
+ requestedModel: string | null;
117
+ requestedReasoningEffort: string | null;
46
118
  }>;
47
119
  type OrchestrationResult = Readonly<{
48
120
  status: OrchestrationStatus | 'failed';
@@ -50,17 +122,84 @@ type OrchestrationResult = Readonly<{
50
122
  taskId: string | null;
51
123
  run: OrchestrationRun | null;
52
124
  next: OrchestrationNext | null;
53
- error: Readonly<{ code: string; message: string }> | null;
125
+ error: Readonly<{
126
+ code: string;
127
+ message: string;
128
+ client?: AgentClientId;
129
+ missingFields?: readonly string[];
130
+ modelSelectionContext?: ReturnType<typeof getAgentClientModelSelection>;
131
+ }> | null;
132
+ }>;
133
+ type OrchestrationStageIdentity = Readonly<{
134
+ stage: DelegationStage;
135
+ round: number;
136
+ artifact: string;
137
+ role: DelegationRole;
138
+ }>;
139
+ type OrchestrationStageCompletion = Readonly<{
140
+ taskId: string;
141
+ taskDir: string;
142
+ updatedRun: OrchestrationRun;
143
+ }>;
144
+ type OrchestrationCompletionPlanResult = Readonly<{
145
+ result: OrchestrationResult;
146
+ plan: OrchestrationStageCompletion | null;
54
147
  }>;
55
148
  type OrchestrationOptions = {
56
149
  repoRoot?: string;
57
150
  id?: () => string;
58
151
  now?: () => string;
59
152
  maxSteps?: number;
60
- captureWorkspace?: (repoRoot: string) => string;
153
+ client?: AgentClientId;
154
+ modelPolicy?: OrchestrationModelPolicy;
155
+ captureWorkspace?: (repoRoot: string, taskId: string | null) => string;
156
+ captureRepository?: (repoRoot: string) => RepositorySnapshot;
157
+ inspectPullRequest?: (taskId: string, options: { cwd: string }) => Readonly<{
158
+ status: string;
159
+ task?: Readonly<{ prNumber?: number | null }>;
160
+ prNumber?: number | null;
161
+ pullRequest?: Readonly<{ head?: Readonly<{ sha?: string }> }> | null;
162
+ error?: Readonly<{ message?: string }> | null;
163
+ }>;
61
164
  diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
165
+ supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
166
+ token?: () => string;
62
167
  };
63
168
 
169
+ type CommitIntentResult = Readonly<{
170
+ status: 'ready' | 'failed';
171
+ changed: boolean;
172
+ taskId: string | null;
173
+ intent: Readonly<{
174
+ mode: CommitIntent['mode'];
175
+ phase: CommitIntent['phase'];
176
+ baselineHead: string;
177
+ currentHead: string;
178
+ committedHead: string | null;
179
+ pushEvidence: PushEvidence | null;
180
+ runId: string | null;
181
+ receiptId: string | null;
182
+ }> | null;
183
+ finalization?: Readonly<{
184
+ disposition: CommitFinalizationInspection['disposition'];
185
+ code: CommitFinalizationInspection['code'];
186
+ message: string;
187
+ currentHead: string;
188
+ committedHead: string | null;
189
+ needsAnchor: boolean;
190
+ needsLog: boolean;
191
+ }>;
192
+ token?: string;
193
+ error: Readonly<{ code: string; message: string }> | null;
194
+ }>;
195
+
196
+ function supportsLifecycleDelegation(client: AgentClientId): boolean {
197
+ return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
198
+ && getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
199
+ && getAgentClientDelegationEvidence(client).actualModel !== 'unavailable'
200
+ && getAgentClientDelegationEvidence(client).actualReasoningEffort !== 'unavailable';
201
+ }
202
+
64
203
  function orchestrationPath(taskDir: string): string {
65
204
  return path.join(taskDir, 'orchestration.json');
66
205
  }
@@ -68,7 +207,21 @@ function orchestrationPath(taskDir: string): string {
68
207
  function readRun(taskDir: string): OrchestrationRun | null {
69
208
  const file = orchestrationPath(taskDir);
70
209
  if (!fs.existsSync(file)) return null;
71
- return JSON.parse(fs.readFileSync(file, 'utf8')) as OrchestrationRun;
210
+ const run = JSON.parse(fs.readFileSync(file, 'utf8')) as OrchestrationRun;
211
+ if (run.schemaVersion !== 2 || !isV2Policy(run.modelPolicy)) return run;
212
+ return {
213
+ ...run,
214
+ modelPolicy: {
215
+ executor: {
216
+ model: run.modelPolicy.executor.model,
217
+ reasoningEffort: run.modelPolicy.executor.reasoningEffort
218
+ },
219
+ reviewer: {
220
+ model: run.modelPolicy.reviewer.model,
221
+ reasoningEffort: run.modelPolicy.reviewer.reasoningEffort
222
+ }
223
+ }
224
+ };
72
225
  }
73
226
 
74
227
  function atomicWrite(file: string, value: unknown): void {
@@ -81,40 +234,295 @@ function saveRun(taskDir: string, run: OrchestrationRun): void {
81
234
  atomicWrite(orchestrationPath(taskDir), run);
82
235
  }
83
236
 
84
- function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun>): OrchestrationRun {
85
- return Object.freeze({ ...run, ...updates, updatedAt: new Date().toISOString() });
237
+ function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun>, now?: () => string): OrchestrationRun {
238
+ return Object.freeze({ ...run, ...updates, updatedAt: (now ?? (() => new Date().toISOString()))() });
239
+ }
240
+
241
+ function failed(
242
+ code: string,
243
+ message: string,
244
+ taskId: string | null = null,
245
+ details: Omit<NonNullable<OrchestrationResult['error']>, 'code' | 'message'> = {}
246
+ ): OrchestrationResult {
247
+ return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
248
+ }
249
+
250
+ function commitIntentFailed(code: string, message: string, taskId: string | null = null): CommitIntentResult {
251
+ return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
252
+ }
253
+
254
+ function repositoryHead(repoRoot: string): string {
255
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
256
+ cwd: repoRoot,
257
+ encoding: 'utf8',
258
+ stdio: ['ignore', 'pipe', 'pipe']
259
+ }).trim();
260
+ }
261
+
262
+ function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
263
+ return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
264
+ cwd: repoRoot,
265
+ stdio: 'ignore'
266
+ }).status === 0;
267
+ }
268
+
269
+ function commitIntentView(intent: CommitIntent, currentHead: string): NonNullable<CommitIntentResult['intent']> {
270
+ return {
271
+ mode: intent.mode,
272
+ phase: intent.phase,
273
+ baselineHead: intent.baselineHead,
274
+ currentHead,
275
+ committedHead: intent.committedHead,
276
+ pushEvidence: intent.pushEvidence,
277
+ runId: intent.orchestration?.runId ?? null,
278
+ receiptId: intent.orchestration?.receiptId ?? null
279
+ };
280
+ }
281
+
282
+ function commitFinalizationView(inspection: CommitFinalizationInspection): NonNullable<CommitIntentResult['finalization']> {
283
+ return {
284
+ disposition: inspection.disposition,
285
+ code: inspection.code,
286
+ message: inspection.message,
287
+ currentHead: inspection.currentHead,
288
+ committedHead: inspection.committedHead,
289
+ needsAnchor: inspection.needsAnchor,
290
+ needsLog: inspection.needsLog
291
+ };
292
+ }
293
+
294
+ function validCommitRun(run: OrchestrationRun | null, taskId: string): run is OrchestrationRun {
295
+ return run !== null
296
+ && [1, 2].includes(run.schemaVersion)
297
+ && run.taskId === taskId
298
+ && ['running', 'paused', 'completed'].includes(run.status)
299
+ && Array.isArray(run.receipts)
300
+ && Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
301
+ && typeof run.commitAuthorization === 'object'
302
+ && run.commitAuthorization !== null
303
+ && Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
304
+ && Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
305
+ }
306
+
307
+ function mapCommitIntentError(error: unknown, taskId: string | null): CommitIntentResult {
308
+ if (error instanceof CommitIntentError) return commitIntentFailed(error.code, error.message, taskId);
309
+ if (error instanceof TaskExecutionLockError) return commitIntentFailed(error.code, error.message, taskId);
310
+ return commitIntentFailed(
311
+ 'ORCHESTRATION_COMMIT_INTENT_INVALID',
312
+ error instanceof Error ? error.message : String(error),
313
+ taskId
314
+ );
315
+ }
316
+
317
+ function validModel(value: unknown): value is string {
318
+ return typeof value === 'string' && value.length > 0 && value.trim() === value;
319
+ }
320
+
321
+ function validateModelPolicy(policy: OrchestrationModelPolicy | undefined): Readonly<{ code: string; message: string }> | null {
322
+ if (
323
+ !policy
324
+ || !validModel(policy.executor?.model)
325
+ || !validModel(policy.executor?.reasoningEffort)
326
+ || !validModel(policy.reviewer?.model)
327
+ || !validModel(policy.reviewer?.reasoningEffort)
328
+ ) {
329
+ return { code: 'ORCHESTRATION_MODEL_POLICY_REQUIRED', message: 'executor and reviewer model and reasoning effort are required' };
330
+ }
331
+ return null;
332
+ }
333
+
334
+ function sameModelPolicy(left: OrchestrationModelPolicy, right: OrchestrationModelPolicy): boolean {
335
+ return left.executor.model === right.executor.model
336
+ && left.executor.reasoningEffort === right.executor.reasoningEffort
337
+ && left.reviewer.model === right.reviewer.model
338
+ && left.reviewer.reasoningEffort === right.reviewer.reasoningEffort;
339
+ }
340
+
341
+ function isV2Policy(policy: OrchestrationRun['modelPolicy']): policy is OrchestrationModelPolicy {
342
+ return typeof policy?.executor === 'object' && typeof policy?.reviewer === 'object';
343
+ }
344
+
345
+ function rolePolicy(run: OrchestrationRun, role: DelegationRole): OrchestrationRolePolicy | null {
346
+ return isV2Policy(run.modelPolicy) ? run.modelPolicy[role] : null;
86
347
  }
87
348
 
88
- function failed(code: string, message: string, taskId: string | null = null): OrchestrationResult {
89
- return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message } };
349
+ function resolveProjectPolicy(repoRoot: string, client: AgentClientId): OrchestrationModelPolicy | undefined {
350
+ const configPath = path.join(repoRoot, '.agents', '.airc.json');
351
+ if (!fs.existsSync(configPath)) return undefined;
352
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, unknown>;
353
+ return normalizeAgentClients(raw).state[client].orchestration;
90
354
  }
91
355
 
92
356
  function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
93
357
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
94
358
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
95
- const existing = readRun(resolved.taskDir);
359
+ if (!isAgentClientId(options.client)) {
360
+ return failed('ORCHESTRATION_PAYLOAD_INVALID', 'begin-or-resume requires a known client', resolved.taskId);
361
+ }
362
+ let existing: OrchestrationRun | null;
363
+ try {
364
+ existing = readRun(resolved.taskDir);
365
+ } catch (error) {
366
+ return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
367
+ }
96
368
  if (existing) {
97
- if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
98
- const resumed = withUpdatedRun(existing, { status: 'running', pause: null });
99
- saveRun(resolved.taskDir, resumed);
100
- return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
369
+ if (
370
+ ![1, 2].includes(existing.schemaVersion)
371
+ || !Array.isArray(existing.receipts)
372
+ || !Object.prototype.hasOwnProperty.call(existing, 'pendingDelegation')
373
+ ) {
374
+ return failed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
375
+ }
376
+ if (existing.status === 'completed' && existing.schemaVersion === 1) {
377
+ return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
378
+ }
379
+ if (existing.schemaVersion === 2) {
380
+ const policyError = validateModelPolicy(isV2Policy(existing.modelPolicy) ? existing.modelPolicy : undefined);
381
+ if (
382
+ policyError
383
+ || !existing.modelPolicySource
384
+ || !isAgentClientId(existing.modelPolicySource.client)
385
+ || !Array.isArray(existing.recoveryHistory)
386
+ ) {
387
+ return failed('ORCHESTRATION_STATE_INVALID', 'schemaVersion 2 orchestration state is incomplete', resolved.taskId);
388
+ }
389
+ if (existing.status === 'completed') {
390
+ return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
391
+ }
392
+ if (existing.modelPolicySource.client !== options.client) {
393
+ return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
394
+ }
395
+ if (options.modelPolicy) {
396
+ const suppliedError = validateModelPolicy(options.modelPolicy);
397
+ if (suppliedError) return failed(suppliedError.code, suppliedError.message, resolved.taskId);
398
+ if (!sameModelPolicy(existing.modelPolicy as OrchestrationModelPolicy, options.modelPolicy)) {
399
+ return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
400
+ }
401
+ }
402
+ if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
403
+ const resumed = withUpdatedRun(existing, { status: 'running', pause: null });
404
+ saveRun(resolved.taskDir, resumed);
405
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
406
+ }
407
+ return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
408
+ }
409
+
410
+ let policy: OrchestrationModelPolicy | undefined = options.modelPolicy;
411
+ let sourceKind: ModelPolicySource['kind'] = 'explicit';
412
+ if (!policy) {
413
+ try {
414
+ policy = resolveProjectPolicy(resolved.repoRoot, options.client);
415
+ sourceKind = 'project-config';
416
+ } catch (error) {
417
+ return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
418
+ }
419
+ }
420
+ const policyError = validateModelPolicy(policy);
421
+ if (policyError) {
422
+ return failed(policyError.code, policyError.message, resolved.taskId, {
423
+ client: options.client,
424
+ missingFields: ['executor.model', 'executor.reasoningEffort', 'reviewer.model', 'reviewer.reasoningEffort'],
425
+ modelSelectionContext: getAgentClientModelSelection(options.client)
426
+ });
427
+ }
428
+ if (existing.pendingDelegation !== null) {
429
+ const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_DELEGATION_BUSY';
430
+ if (alreadyPaused) return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
431
+ const paused = withUpdatedRun(existing, {
432
+ status: 'paused',
433
+ pause: { code: 'ORCHESTRATION_DELEGATION_BUSY', message: 'legacy run has a pending delegation', recoverable: false }
434
+ });
435
+ saveRun(resolved.taskDir, paused);
436
+ return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
437
+ }
438
+ if (existing.receipts.length > 0) {
439
+ const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED';
440
+ if (alreadyPaused) return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
441
+ const paused = withUpdatedRun(existing, {
442
+ status: 'paused',
443
+ pause: {
444
+ code: 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED',
445
+ message: 'legacy receipts do not contain actual reasoning-effort evidence',
446
+ recoverable: false
447
+ }
448
+ });
449
+ saveRun(resolved.taskDir, paused);
450
+ return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
451
+ }
452
+ if (
453
+ existing.modelPolicy
454
+ && typeof existing.modelPolicy.executor === 'string'
455
+ && (
456
+ existing.modelPolicy.executor !== policy!.executor.model
457
+ || existing.modelPolicy.reviewer !== policy!.reviewer.model
458
+ )
459
+ ) {
460
+ return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
461
+ }
462
+ const now = (options.now ?? (() => new Date().toISOString()))();
463
+ const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
464
+ const clearsModelPause = existing.pause?.code === 'ORCHESTRATION_MODEL_EVIDENCE_MISSING';
465
+ const resultingStatus = clearsModelPause ? 'running' : existing.status;
466
+ const recovery: OrchestrationRecovery = {
467
+ code: 'MODEL_POLICY_SUPPLEMENTED',
468
+ recoveredAt: now,
469
+ previousSchemaVersion: 1,
470
+ previousStatus: existing.status,
471
+ previousPause: existing.pause,
472
+ policySource: source,
473
+ receiptCount: 0,
474
+ pendingDelegation: false,
475
+ resultingStatus
476
+ };
477
+ const recovered: OrchestrationRun = Object.freeze({
478
+ ...existing,
479
+ schemaVersion: 2,
480
+ status: resultingStatus,
481
+ modelPolicy: policy!,
482
+ modelPolicySource: source,
483
+ recoveryHistory: Object.freeze([recovery]),
484
+ pause: clearsModelPause ? null : existing.pause,
485
+ updatedAt: now
486
+ });
487
+ saveRun(resolved.taskDir, recovered);
488
+ return { status: resultingStatus, changed: true, taskId: resolved.taskId, run: recovered, next: null, error: null };
489
+ }
490
+
491
+ let policy = options.modelPolicy;
492
+ let sourceKind: ModelPolicySource['kind'] = 'explicit';
493
+ if (!policy) {
494
+ try {
495
+ policy = resolveProjectPolicy(resolved.repoRoot, options.client);
496
+ sourceKind = 'project-config';
497
+ } catch (error) {
498
+ return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
101
499
  }
102
- return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
103
500
  }
501
+ const policyError = validateModelPolicy(policy);
502
+ if (policyError) return failed(policyError.code, policyError.message, resolved.taskId, {
503
+ client: options.client,
504
+ missingFields: ['executor.model', 'executor.reasoningEffort', 'reviewer.model', 'reviewer.reasoningEffort'],
505
+ modelSelectionContext: getAgentClientModelSelection(options.client)
506
+ });
104
507
  const now = (options.now ?? (() => new Date().toISOString()))();
508
+ const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
105
509
  const run: OrchestrationRun = {
106
- schemaVersion: 1,
510
+ schemaVersion: 2,
107
511
  taskId: resolved.taskId,
108
512
  runId: (options.id ?? randomUUID)(),
109
513
  status: 'running',
110
514
  nextStage: null,
111
515
  stepCount: 0,
112
516
  maxSteps: options.maxSteps ?? 24,
517
+ modelPolicy: policy!,
518
+ modelPolicySource: source,
519
+ recoveryHistory: [],
113
520
  baseline: '',
114
521
  pendingDelegation: null,
115
522
  receipts: [],
116
523
  pause: null,
117
524
  commitAuthorization: { issuedAt: null, consumedAt: null },
525
+ completionEvidence: null,
118
526
  createdAt: now,
119
527
  updatedAt: now
120
528
  };
@@ -150,7 +558,7 @@ function latestReviewApproved(taskDir: string, family: 'review-analysis' | 'revi
150
558
  return parsed.ok && parsed.summary.verdict === 'Approved' && parsed.summary.counts !== null;
151
559
  }
152
560
 
153
- function routeFromFacts(taskDir: string): OrchestrationNext | null {
561
+ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | null {
154
562
  const analysisRound = highestRound(taskDir, 'analysis');
155
563
  const analysisReviewRound = highestRound(taskDir, 'review-analysis');
156
564
  const planRound = highestRound(taskDir, 'plan');
@@ -205,13 +613,21 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
205
613
  } catch (error) {
206
614
  return failed('ORCHESTRATION_TASK_READ_FAILED', String(error), resolved.taskId);
207
615
  }
616
+ let metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
208
617
  try {
209
- parseTypedTaskFrontmatter(content);
618
+ metadata = parseTypedTaskFrontmatter(content);
210
619
  } catch (error) {
211
620
  return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
212
621
  }
213
- const next = routeFromFacts(resolved.taskDir);
214
- if (!next) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
622
+ const routed = routeFromFacts(resolved.taskDir);
623
+ if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
624
+ const run = readRun(resolved.taskDir);
625
+ const policy = run ? rolePolicy(run, routed.role) : null;
626
+ const next: OrchestrationNext = {
627
+ ...routed,
628
+ requestedModel: policy?.model ?? null,
629
+ requestedReasoningEffort: policy?.reasoningEffort ?? null
630
+ };
215
631
  if (next.stage === 'commit') {
216
632
  const reviewRound = highestRound(resolved.taskDir, 'review-code');
217
633
  const review = parseReviewSummary(fs.readFileSync(
@@ -221,17 +637,466 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
221
637
  if (!review.ok || review.summary.manualValidation === null) {
222
638
  return failed('ORCHESTRATION_REVIEW_INVALID', 'latest code review has no numeric manual-validation count', resolved.taskId);
223
639
  }
224
- if (review.summary.manualValidation > 0) {
225
- return failed('ORCHESTRATION_MANUAL_VALIDATION_PENDING', 'manual validation must be completed before commit', resolved.taskId);
226
- }
227
640
  const rows = parseLedger(content);
228
641
  const ledgerError = validateLedgerRows(rows);
229
642
  if (ledgerError) return failed('ORCHESTRATION_LEDGER_INVALID', ledgerError.message, resolved.taskId);
230
643
  if (!summarizeLedgerStage(rows, 'code').canAdvance) {
231
644
  return failed('ORCHESTRATION_LEDGER_BLOCKED', 'code review ledger has unresolved findings or human decisions', resolved.taskId);
232
645
  }
646
+ if (run?.status === 'completed') {
647
+ return { status: 'completed', changed: false, taskId: resolved.taskId, run, next: null, error: null };
648
+ }
649
+
650
+ let config: unknown;
651
+ try {
652
+ config = JSON.parse(fs.readFileSync(path.join(resolved.repoRoot, '.agents', '.airc.json'), 'utf8'));
653
+ } catch (error) {
654
+ return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
655
+ }
656
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
657
+ return failed('ORCHESTRATION_CONFIG_INVALID', 'project configuration must be an object', resolved.taskId);
658
+ }
659
+ const projectConfig = config as Record<string, unknown>;
660
+ if (!Object.hasOwn(projectConfig, 'prFlow') || projectConfig.prFlow === 'disabled') {
661
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
662
+ }
663
+ if (projectConfig.prFlow !== 'required') {
664
+ return failed('ORCHESTRATION_CONFIG_INVALID', "project configuration 'prFlow' must be 'required' or 'disabled'", resolved.taskId);
665
+ }
666
+ const prNumber = Number(metadata.pr_number);
667
+ if (!Number.isInteger(prNumber) || prNumber <= 0) {
668
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
669
+ }
670
+ if (!run) {
671
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
672
+ }
673
+ if (run.status !== 'running' || run.pendingDelegation !== null) {
674
+ return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'clean completion requires an idle running orchestration', resolved.taskId);
675
+ }
676
+
677
+ const capture = options.captureRepository ?? captureRepositorySnapshot;
678
+ let before: RepositorySnapshot;
679
+ try {
680
+ before = capture(resolved.repoRoot);
681
+ } catch (error) {
682
+ return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
683
+ }
684
+ if (before.headTree !== before.worktreeTree) {
685
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
686
+ }
687
+ const lastReviewedCommit = String(metadata.last_reviewed_commit ?? '');
688
+ if (before.head !== lastReviewedCommit) {
689
+ return failed('ORCHESTRATION_REVIEWED_HEAD_MISMATCH', 'local HEAD does not match last_reviewed_commit', resolved.taskId);
690
+ }
691
+
692
+ const inspect: NonNullable<OrchestrationOptions['inspectPullRequest']> =
693
+ options.inspectPullRequest ?? inspectPlatformPullRequest;
694
+ let inspection: ReturnType<NonNullable<OrchestrationOptions['inspectPullRequest']>>;
695
+ try {
696
+ inspection = inspect(resolved.taskId, { cwd: resolved.repoRoot });
697
+ } catch (error) {
698
+ return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
699
+ }
700
+ if (inspection.status === 'blocked') {
701
+ return failed('ORCHESTRATION_PR_INSPECTION_BLOCKED', inspection.error?.message ?? 'pull request inspection is blocked', resolved.taskId);
702
+ }
703
+ if (inspection.status === 'failed') {
704
+ return failed('ORCHESTRATION_PR_INSPECTION_FAILED', inspection.error?.message ?? 'pull request inspection failed', resolved.taskId);
705
+ }
706
+ const inspectedPrNumber = inspection.task?.prNumber ?? inspection.prNumber;
707
+ const prHead = inspection.pullRequest?.head?.sha;
708
+ if (typeof prHead !== 'string') {
709
+ return failed('ORCHESTRATION_PR_INSPECTION_INVALID', 'pull request inspection returned incomplete identity evidence', resolved.taskId);
710
+ }
711
+ if (inspectedPrNumber !== prNumber || prHead !== before.head) {
712
+ return failed('ORCHESTRATION_PR_HEAD_MISMATCH', 'pull request identity does not match the reviewed local HEAD', resolved.taskId);
713
+ }
714
+
715
+ let after: RepositorySnapshot;
716
+ try {
717
+ after = capture(resolved.repoRoot);
718
+ } catch (error) {
719
+ return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
720
+ }
721
+ if (after.headTree !== after.worktreeTree) {
722
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
723
+ }
724
+ if (after.head !== before.head || after.headTree !== before.headTree) {
725
+ return failed('ORCHESTRATION_REPOSITORY_CHANGED', 'repository changed while clean completion evidence was collected', resolved.taskId);
726
+ }
727
+
728
+ const observedAt = (options.now ?? (() => new Date().toISOString()))();
729
+ const completionEvidence: CleanCompletionEvidence = {
730
+ kind: 'reviewed-head-clean',
731
+ observedAt,
732
+ head: after.head,
733
+ headTree: after.headTree,
734
+ worktreeTree: after.worktreeTree,
735
+ lastReviewedCommit,
736
+ prNumber,
737
+ prHead
738
+ };
739
+ const completed = withUpdatedRun(run, {
740
+ status: 'completed',
741
+ nextStage: null,
742
+ pause: null,
743
+ completionEvidence
744
+ });
745
+ saveRun(resolved.taskDir, completed);
746
+ return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
233
747
  }
234
- return { status: 'running', changed: false, taskId: resolved.taskId, run: readRun(resolved.taskDir), next, error: null };
748
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
749
+ }
750
+
751
+ function beginCommitIntent(
752
+ taskRef: string,
753
+ input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string }>,
754
+ options: OrchestrationOptions = {}
755
+ ): CommitIntentResult {
756
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
757
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
758
+ try {
759
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
760
+ const currentHead = repositoryHead(resolved.repoRoot);
761
+ if (currentHead !== input.baselineHead) {
762
+ return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
763
+ }
764
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
765
+ try {
766
+ readCommitIntent(resolved.taskDir, resolved.taskId);
767
+ } catch (error) {
768
+ return mapCommitIntentError(error, resolved.taskId);
769
+ }
770
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
771
+ }
772
+
773
+ const runFile = orchestrationPath(resolved.taskDir);
774
+ let run: OrchestrationRun | null = null;
775
+ let sourceRunBytes: string | null = null;
776
+ if (fs.existsSync(runFile)) {
777
+ sourceRunBytes = fs.readFileSync(runFile, 'utf8');
778
+ try {
779
+ run = readRun(resolved.taskDir);
780
+ } catch (error) {
781
+ return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
782
+ }
783
+ if (!validCommitRun(run, resolved.taskId)) {
784
+ return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
785
+ }
786
+ }
787
+
788
+ const now = (options.now ?? (() => new Date().toISOString()))();
789
+ const failAndPause = (code: string, message: string): CommitIntentResult => {
790
+ if (run?.status === 'running') {
791
+ saveRun(resolved.taskDir, withUpdatedRun(run, {
792
+ status: 'paused', pause: { code, message, recoverable: true }
793
+ }, () => now));
794
+ }
795
+ return commitIntentFailed(code, message, resolved.taskId);
796
+ };
797
+ let orchestration: CommitIntent['orchestration'] = null;
798
+ if (!input.orchestrated) {
799
+ if (run?.pendingDelegation) {
800
+ return commitIntentFailed(
801
+ 'ORCHESTRATION_STANDALONE_BUSY',
802
+ 'standalone commit is blocked by a pending orchestration delegation',
803
+ resolved.taskId
804
+ );
805
+ }
806
+ } else {
807
+ if (!run || run.status !== 'running') {
808
+ return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
809
+ }
810
+ const receipt = run.pendingDelegation;
811
+ if (
812
+ !receipt
813
+ || receipt.status !== 'activated'
814
+ || receipt.stage !== 'commit'
815
+ || receipt.round !== 1
816
+ || receipt.artifact !== 'commit'
817
+ || receipt.role !== 'executor'
818
+ ) {
819
+ return failAndPause(
820
+ 'ORCHESTRATION_PROVENANCE_MISMATCH',
821
+ 'orchestrated commit requires one matching activated commit delegation'
822
+ );
823
+ }
824
+ if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
825
+ return failAndPause(
826
+ 'ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID',
827
+ 'orchestrated commit requires an unconsumed one-use authorization'
828
+ );
829
+ }
830
+ const planned = planOrchestrationStageCompletion(taskRef, {
831
+ stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
832
+ }, { ...options, now: () => now });
833
+ if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
834
+ return failAndPause(
835
+ planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
836
+ planned.result.error?.message ?? 'commit delegation could not be completed'
837
+ );
838
+ }
839
+ const plannedRunBytes = serialize(planned.plan.updatedRun);
840
+ orchestration = {
841
+ runId: run.runId,
842
+ receiptId: receipt.id,
843
+ authorizationIssuedAt: run.commitAuthorization.issuedAt,
844
+ sourceRunDigest: digest(sourceRunBytes!),
845
+ plannedRunDigest: digest(plannedRunBytes),
846
+ completionUpdatedAt: planned.plan.updatedRun.updatedAt,
847
+ plannedReceipt: planned.plan.updatedRun.pendingDelegation
848
+ };
849
+ }
850
+
851
+ const created = createCommitIntent(resolved.taskDir, {
852
+ taskId: resolved.taskId,
853
+ mode: input.orchestrated ? 'orchestrated' : 'standalone',
854
+ phase: 'prepared',
855
+ baselineHead: input.baselineHead,
856
+ committedHead: null,
857
+ pushEvidence: null,
858
+ orchestration,
859
+ createdAt: now,
860
+ updatedAt: now
861
+ }, { token: options.token });
862
+ return {
863
+ status: 'ready', changed: true, taskId: resolved.taskId,
864
+ intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
865
+ };
866
+ });
867
+ } catch (error) {
868
+ return mapCommitIntentError(error, resolved.taskId);
869
+ }
870
+ }
871
+
872
+ function checkpointCommitIntent(
873
+ taskRef: string,
874
+ input: Readonly<{
875
+ token: string;
876
+ kind: 'committed' | 'pushed';
877
+ head: string;
878
+ remote?: string;
879
+ ref?: string;
880
+ }>,
881
+ options: OrchestrationOptions = {}
882
+ ): CommitIntentResult {
883
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
884
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
885
+ try {
886
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
887
+ const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
888
+ const currentHead = repositoryHead(resolved.repoRoot);
889
+ if (currentHead !== input.head) {
890
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
891
+ }
892
+ if (input.kind === 'committed' && !isAncestor(resolved.repoRoot, intent.baselineHead, input.head)) {
893
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
894
+ }
895
+ if (input.kind === 'pushed') {
896
+ const expectedHead = intent.committedHead ?? intent.baselineHead;
897
+ if (input.head !== expectedHead || !input.remote || !input.ref) {
898
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
899
+ }
900
+ }
901
+ const now = (options.now ?? (() => new Date().toISOString()))();
902
+ const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
903
+ ? { phase: 'committed', committedHead: input.head, updatedAt: now }
904
+ : {
905
+ phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
906
+ pushEvidence: { remote: input.remote!, ref: input.ref!, head: input.head }
907
+ });
908
+ return {
909
+ status: 'ready', changed: true, taskId: resolved.taskId,
910
+ intent: commitIntentView(updated, currentHead), error: null
911
+ };
912
+ });
913
+ } catch (error) {
914
+ return mapCommitIntentError(error, resolved.taskId);
915
+ }
916
+ }
917
+
918
+ function completeCommitOrchestration(
919
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
920
+ intent: CommitIntent,
921
+ agent: string
922
+ ): Readonly<{ code: string; message: string }> | null {
923
+ if (intent.orchestration === null) return null;
924
+ if (intent.orchestration.plannedReceipt.agent !== agent) {
925
+ return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
926
+ }
927
+ const runFile = orchestrationPath(resolved.taskDir);
928
+ if (!fs.existsSync(runFile)) {
929
+ return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
930
+ }
931
+ const currentBytes = fs.readFileSync(runFile, 'utf8');
932
+ const currentDigest = digest(currentBytes);
933
+ if (currentDigest === intent.orchestration.sourceRunDigest) {
934
+ const run = readRun(resolved.taskDir);
935
+ if (!run) return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
936
+ const plannedRun: OrchestrationRun = Object.freeze({
937
+ ...run,
938
+ pendingDelegation: intent.orchestration.plannedReceipt,
939
+ updatedAt: intent.orchestration.completionUpdatedAt
940
+ });
941
+ if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
942
+ return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
943
+ }
944
+ atomicWrite(runFile, plannedRun);
945
+ if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
946
+ return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
947
+ }
948
+ } else if (currentDigest !== intent.orchestration.plannedRunDigest) {
949
+ return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
950
+ }
951
+ return null;
952
+ }
953
+
954
+ function finalizeCommitIntentUnlocked(
955
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
956
+ inspection: CommitFinalizationInspection,
957
+ agent: string,
958
+ remove: () => void
959
+ ): CommitIntentResult {
960
+ if (inspection.disposition !== 'recoverable' || !inspection.intent) {
961
+ return commitIntentFailed(
962
+ inspection.code ?? 'COMMIT_FINALIZATION_PENDING',
963
+ inspection.message,
964
+ resolved.taskId
965
+ );
966
+ }
967
+ if (
968
+ inspection.intent.orchestration !== null
969
+ && inspection.intent.orchestration.plannedReceipt.agent !== agent
970
+ ) {
971
+ return commitIntentFailed(
972
+ 'ORCHESTRATION_PROVENANCE_MISMATCH',
973
+ 'completion agent does not match the planned receipt',
974
+ resolved.taskId
975
+ );
976
+ }
977
+ const metadata = captureTaskWriteMetadata();
978
+ const taskPlan = planCommitTaskFinalization(
979
+ resolved.taskDir,
980
+ inspection,
981
+ agent,
982
+ metadata.timestamp
983
+ );
984
+ const written = writeTask({
985
+ taskRef: resolved.taskId,
986
+ expectedState: 'active',
987
+ mutations: taskPlan.mutations
988
+ }, {
989
+ repoRoot: resolved.repoRoot,
990
+ metadataProvider: () => metadata
991
+ });
992
+ if (written.status === 'failed') {
993
+ return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
994
+ }
995
+ const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
996
+ if (orchestrationError) {
997
+ return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
998
+ }
999
+ try {
1000
+ remove();
1001
+ } catch (error) {
1002
+ return commitIntentFailed(
1003
+ 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL',
1004
+ error instanceof Error ? error.message : String(error),
1005
+ resolved.taskId
1006
+ );
1007
+ }
1008
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1009
+ }
1010
+
1011
+ function completeCommitIntent(
1012
+ taskRef: string,
1013
+ input: Readonly<{ token: string; agent: string }>,
1014
+ options: OrchestrationOptions = {}
1015
+ ): CommitIntentResult {
1016
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1017
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1018
+ try {
1019
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
1020
+ readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1021
+ const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1022
+ return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1023
+ removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1024
+ });
1025
+ });
1026
+ } catch (error) {
1027
+ return mapCommitIntentError(error, resolved.taskId);
1028
+ }
1029
+ }
1030
+
1031
+ function recoverCommitIntent(
1032
+ taskRef: string,
1033
+ input: Readonly<{ agent: string }>,
1034
+ options: OrchestrationOptions = {}
1035
+ ): CommitIntentResult {
1036
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1037
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1038
+ try {
1039
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
1040
+ const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1041
+ if (inspection.disposition === 'prepared' && inspection.intentDigest) {
1042
+ removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
1043
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1044
+ }
1045
+ if (!inspection.intentDigest) {
1046
+ return commitIntentFailed(
1047
+ inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
1048
+ inspection.message,
1049
+ resolved.taskId
1050
+ );
1051
+ }
1052
+ return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1053
+ removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest!);
1054
+ });
1055
+ });
1056
+ } catch (error) {
1057
+ return mapCommitIntentError(error, resolved.taskId);
1058
+ }
1059
+ }
1060
+
1061
+ function abortCommitIntent(
1062
+ taskRef: string,
1063
+ input: Readonly<{ token: string; expectedHead: string }>,
1064
+ options: OrchestrationOptions = {}
1065
+ ): CommitIntentResult {
1066
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1067
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1068
+ try {
1069
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
1070
+ const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1071
+ const currentHead = repositoryHead(resolved.repoRoot);
1072
+ if (
1073
+ intent.phase !== 'prepared'
1074
+ || intent.pushEvidence !== null
1075
+ || input.expectedHead !== intent.baselineHead
1076
+ || currentHead !== intent.baselineHead
1077
+ ) {
1078
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
1079
+ }
1080
+ removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1081
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1082
+ });
1083
+ } catch (error) {
1084
+ return mapCommitIntentError(error, resolved.taskId);
1085
+ }
1086
+ }
1087
+
1088
+ function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
1089
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1090
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1091
+ const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1092
+ return {
1093
+ status: 'ready',
1094
+ changed: false,
1095
+ taskId: resolved.taskId,
1096
+ intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
1097
+ finalization: commitFinalizationView(inspection),
1098
+ error: null
1099
+ };
235
1100
  }
236
1101
 
237
1102
  function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
@@ -242,24 +1107,33 @@ function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}
242
1107
  return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
243
1108
  }
244
1109
 
245
- function prepareOrchestrationDelegation(
1110
+ function prepareOrchestrationDelegationUnlocked(
246
1111
  taskRef: string,
247
- input: Readonly<{ client: AgentClientId; requestedModel?: string }>,
1112
+ input: Readonly<{ client: AgentClientId; requestedModel?: string; requestedReasoningEffort?: string }>,
248
1113
  options: OrchestrationOptions = {}
249
1114
  ): OrchestrationResult {
250
1115
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
251
1116
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
252
- if (
253
- getAgentClientCapability(input.client, 'subagents').level === 'unsupported'
254
- || getAgentClientCapability(input.client, 'orchestration').level === 'unsupported'
255
- ) {
1117
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
1118
+ try {
1119
+ readCommitIntent(resolved.taskDir, resolved.taskId);
1120
+ } catch (error) {
1121
+ return failed(
1122
+ 'ORCHESTRATION_COMMIT_INTENT_INVALID',
1123
+ error instanceof Error ? error.message : String(error),
1124
+ resolved.taskId
1125
+ );
1126
+ }
1127
+ return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
1128
+ }
1129
+ if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
256
1130
  return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
257
1131
  }
258
1132
  const run = readRun(resolved.taskDir);
259
1133
  if (!run || run.status !== 'running') return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
1134
+ if (!isV2Policy(run.modelPolicy)) return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
260
1135
  if (run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
261
- const repositoryPending = (['claude-code', 'codex'] as AgentClientId[])
262
- .flatMap((client) => matchingDelegations(client, () => true, options));
1136
+ const repositoryPending = matchingDelegations(() => true, options);
263
1137
  if (repositoryPending.length > 0) {
264
1138
  return failed('ORCHESTRATION_DELEGATION_BUSY', 'the repository already has a pending lifecycle delegation', resolved.taskId);
265
1139
  }
@@ -267,9 +1141,22 @@ function prepareOrchestrationDelegation(
267
1141
  const routed = routeOrchestration(taskRef, options);
268
1142
  if (!routed.next) return routed;
269
1143
  const next = routed.next;
1144
+ if (!validModel(input.requestedModel)) {
1145
+ return failed('ORCHESTRATION_REQUESTED_MODEL_REQUIRED', 'prepare requires the exact requested model identity', resolved.taskId);
1146
+ }
1147
+ if (!validModel(input.requestedReasoningEffort)) {
1148
+ return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_REQUIRED', 'prepare requires the exact requested reasoning effort', resolved.taskId);
1149
+ }
1150
+ const expectedPolicy = run.modelPolicy[next.role];
1151
+ if (input.requestedModel !== expectedPolicy.model) {
1152
+ return failed('ORCHESTRATION_REQUESTED_MODEL_MISMATCH', `requested model does not match the persisted ${next.role} model`, resolved.taskId);
1153
+ }
1154
+ if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
1155
+ return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
1156
+ }
270
1157
  let beforeFingerprint: string;
271
1158
  try {
272
- beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(resolved.repoRoot);
1159
+ beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(resolved.repoRoot, resolved.taskId);
273
1160
  } catch (error) {
274
1161
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
275
1162
  }
@@ -281,7 +1168,9 @@ function prepareOrchestrationDelegation(
281
1168
  round: next.round,
282
1169
  artifact: next.artifact,
283
1170
  client: input.client,
284
- requestedModel: input.requestedModel ?? null,
1171
+ requestedModel: input.requestedModel,
1172
+ requestedReasoningEffort: input.requestedReasoningEffort,
1173
+ workspaceSnapshotScope: 'task',
285
1174
  beforeFingerprint
286
1175
  }, { id: options.id, now: options.now });
287
1176
  const updated = withUpdatedRun(run, {
@@ -295,8 +1184,32 @@ function prepareOrchestrationDelegation(
295
1184
  return { status: 'running', changed: true, taskId: resolved.taskId, run: updated, next, error: null };
296
1185
  }
297
1186
 
1187
+ function prepareOrchestrationDelegation(
1188
+ taskRef: string,
1189
+ input: Readonly<{ client: AgentClientId; requestedModel?: string; requestedReasoningEffort?: string }>,
1190
+ options: OrchestrationOptions = {}
1191
+ ): OrchestrationResult {
1192
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1193
+ if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1194
+ try {
1195
+ return withTaskExecutionLock(
1196
+ resolved.repoRoot,
1197
+ '__repository__',
1198
+ 'task-orchestration.prepare.repository',
1199
+ () => withTaskExecutionLock(
1200
+ resolved.repoRoot,
1201
+ resolved.taskId,
1202
+ 'task-orchestration.prepare.task',
1203
+ () => prepareOrchestrationDelegationUnlocked(taskRef, input, options)
1204
+ )
1205
+ );
1206
+ } catch (error) {
1207
+ if (error instanceof TaskExecutionLockError) return failed(error.code, error.message, resolved.taskId);
1208
+ throw error;
1209
+ }
1210
+ }
1211
+
298
1212
  function matchingDelegations(
299
- client: AgentClientId,
300
1213
  predicate: (receipt: DelegationReceipt) => boolean,
301
1214
  options: OrchestrationOptions
302
1215
  ): Array<{ taskId: string; run: OrchestrationRun }> {
@@ -309,7 +1222,7 @@ function matchingDelegations(
309
1222
  const run = readRun(path.join(activeRoot, entry.name));
310
1223
  if (!run?.pendingDelegation) return [];
311
1224
  const receipt = run.pendingDelegation;
312
- return receipt.client === client && predicate(receipt)
1225
+ return predicate(receipt)
313
1226
  ? [{ taskId: entry.name, run }]
314
1227
  : [];
315
1228
  });
@@ -320,7 +1233,10 @@ function uniqueMatchingDelegation(
320
1233
  predicate: (receipt: DelegationReceipt) => boolean,
321
1234
  options: OrchestrationOptions
322
1235
  ): { taskId: string; run: OrchestrationRun } | OrchestrationResult {
323
- const matches = matchingDelegations(client, predicate, options);
1236
+ const matches = matchingDelegations(
1237
+ (receipt) => receipt.client === client && predicate(receipt),
1238
+ options
1239
+ );
324
1240
  if (matches.length === 0) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no matching lifecycle delegation exists');
325
1241
  if (matches.length > 1) return failed('ORCHESTRATION_DELEGATION_AMBIGUOUS', 'multiple lifecycle delegations match the native hook event');
326
1242
  return matches[0]!;
@@ -364,7 +1280,8 @@ function sealMatchingOrchestrationDelegation(
364
1280
  }
365
1281
  const repoRoot = options.repoRoot ?? process.cwd();
366
1282
  try {
367
- const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(repoRoot);
1283
+ const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
1284
+ const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(repoRoot, snapshotTaskId);
368
1285
  const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(repoRoot, receipt.beforeFingerprint, afterFingerprint);
369
1286
  return sealOrchestrationDelegation(matched.taskId, {
370
1287
  childId: event.childId,
@@ -419,36 +1336,20 @@ function completeOrchestrationStage(
419
1336
  return { status: 'running', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
420
1337
  }
421
1338
 
422
- function completeCommitOrchestrationStage(
1339
+ function inspectOrchestrationStage(
423
1340
  taskRef: string,
424
- agent: string,
1341
+ event: OrchestrationStageIdentity,
425
1342
  options: OrchestrationOptions = {}
426
1343
  ): OrchestrationResult {
427
1344
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
428
1345
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
429
- const run = readRun(resolved.taskDir);
430
- if (!run) return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
431
- const receipt = run.pendingDelegation;
432
- if (!receipt || receipt.stage !== 'commit') {
433
- return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending commit delegation', resolved.taskId);
1346
+ let run: OrchestrationRun | null;
1347
+ try {
1348
+ run = readRun(resolved.taskDir);
1349
+ } catch (error) {
1350
+ return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
434
1351
  }
435
- return completeOrchestrationStage(taskRef, {
436
- stage: receipt.stage,
437
- round: receipt.round,
438
- artifact: receipt.artifact,
439
- agent
440
- }, options);
441
- }
442
-
443
- function validateOrchestrationStage(
444
- taskRef: string,
445
- event: Readonly<{ stage: DelegationStage; round: number; artifact: string; role: DelegationRole }>,
446
- options: OrchestrationOptions = {}
447
- ): OrchestrationResult {
448
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
449
- if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
450
- const run = readRun(resolved.taskDir);
451
- if (!run) return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
1352
+ if (!run) return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
452
1353
  const receipt = run.pendingDelegation;
453
1354
  if (
454
1355
  !receipt
@@ -458,17 +1359,46 @@ function validateOrchestrationStage(
458
1359
  || receipt.artifact !== event.artifact
459
1360
  || receipt.role !== event.role
460
1361
  ) {
461
- return pauseOrchestration(
462
- taskRef,
1362
+ return failed(
463
1363
  'ORCHESTRATION_PROVENANCE_MISMATCH',
464
1364
  'active run does not have one matching activated delegation',
465
- true,
466
- options
1365
+ resolved.taskId
467
1366
  );
468
1367
  }
469
1368
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next: null, error: null };
470
1369
  }
471
1370
 
1371
+ function planOrchestrationStageCompletion(
1372
+ taskRef: string,
1373
+ event: Readonly<{ stage: DelegationStage; round: number; artifact: string; role: DelegationRole; agent: string }>,
1374
+ options: OrchestrationOptions = {}
1375
+ ): OrchestrationCompletionPlanResult {
1376
+ const inspected = inspectOrchestrationStage(taskRef, event, options);
1377
+ if (inspected.status === 'failed' || !inspected.run?.pendingDelegation) {
1378
+ return { result: inspected, plan: null };
1379
+ }
1380
+ const receipt = inspected.run.pendingDelegation;
1381
+ const completed = completeDelegationStage(receipt, event);
1382
+ if (!completed.ok) {
1383
+ return { result: failed(completed.code, completed.message, inspected.taskId), plan: null };
1384
+ }
1385
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1386
+ if (!resolved.ok) return { result: failed(resolved.code, resolved.message, resolved.taskId), plan: null };
1387
+ const updatedRun = withUpdatedRun(inspected.run, { pendingDelegation: completed.receipt }, options.now);
1388
+ return {
1389
+ result: { status: 'running', changed: true, taskId: resolved.taskId, run: updatedRun, next: null, error: null },
1390
+ plan: {
1391
+ taskId: resolved.taskId,
1392
+ taskDir: resolved.taskDir,
1393
+ updatedRun
1394
+ }
1395
+ };
1396
+ }
1397
+
1398
+ function commitOrchestrationStageCompletion(plan: OrchestrationStageCompletion): void {
1399
+ saveRun(plan.taskDir, plan.updatedRun);
1400
+ }
1401
+
472
1402
  function sealOrchestrationDelegation(
473
1403
  taskRef: string,
474
1404
  event: Parameters<typeof sealDelegation>[1],
@@ -527,17 +1457,34 @@ export {
527
1457
  activateMatchingOrchestrationDelegation,
528
1458
  activateOrchestrationDelegation,
529
1459
  advanceOrchestration,
1460
+ abortCommitIntent,
1461
+ beginCommitIntent,
530
1462
  beginOrResumeOrchestration,
531
- completeCommitOrchestrationStage,
1463
+ checkpointCommitIntent,
1464
+ commitOrchestrationStageCompletion,
1465
+ completeCommitIntent,
532
1466
  completeOrchestrationStage,
1467
+ inspectOrchestrationStage,
533
1468
  orchestrationPath,
534
1469
  pauseOrchestration,
1470
+ planOrchestrationStageCompletion,
535
1471
  prepareOrchestrationDelegation,
536
1472
  readRun,
1473
+ recoverCommitIntent,
537
1474
  routeOrchestration,
538
1475
  sealMatchingOrchestrationDelegation,
539
1476
  sealOrchestrationDelegation,
540
- statusOrchestration,
541
- validateOrchestrationStage
1477
+ statusCommitIntent,
1478
+ statusOrchestration
1479
+ };
1480
+ export type {
1481
+ CommitIntentResult,
1482
+ OrchestrationCompletionPlanResult,
1483
+ OrchestrationModelPolicy,
1484
+ OrchestrationNext,
1485
+ OrchestrationResult,
1486
+ OrchestrationRun,
1487
+ OrchestrationStageCompletion,
1488
+ OrchestrationStageIdentity,
1489
+ OrchestrationStatus
542
1490
  };
543
- export type { OrchestrationNext, OrchestrationResult, OrchestrationRun, OrchestrationStatus };