@iowarp/clio-coder 0.3.6 → 0.3.8

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 (323) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -6
  3. package/dist/{acp-2BEHC4DL.js → acp-U67UHUK2.js} +14 -14
  4. package/dist/{agents-LNNFTM53.js → agents-YU6SGALZ.js} +42 -35
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-KXXFI2VS.js → auth-5ZPJOIVG.js} +32 -24
  7. package/dist/builtins-C6JMZVV6.js +17 -0
  8. package/dist/{chunk-E25LMLRW.js → chunk-26LEYJZH.js} +2 -2
  9. package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
  10. package/dist/{chunk-PBTHKCPN.js → chunk-2HFZQUHL.js} +7 -7
  11. package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
  12. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  13. package/dist/{chunk-CKXWIANG.js → chunk-4SPRNWDE.js} +18 -16
  14. package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
  15. package/dist/{chunk-XF5N4U5A.js → chunk-5DHKRSMQ.js} +9 -8
  16. package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
  17. package/dist/{chunk-EKY57CSP.js → chunk-5H3GB5BO.js} +68 -772
  18. package/dist/{chunk-6XXKFVSN.js → chunk-5Q2VVUKB.js} +4 -4
  19. package/dist/{chunk-DJVECN66.js → chunk-7RFXX52T.js} +295 -3705
  20. package/dist/{chunk-ZXF4XRKW.js → chunk-7RGZWPB6.js} +158 -8
  21. package/dist/{chunk-FYYLNIL5.js → chunk-A2NJGIB3.js} +2 -2
  22. package/dist/{chunk-XXQNGV4M.js → chunk-A3WNZD3P.js} +548 -72
  23. package/dist/{chunk-VEZEGCGW.js → chunk-B5CSFE7B.js} +23 -21
  24. package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
  25. package/dist/chunk-DR52UMZW.js +21 -0
  26. package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
  27. package/dist/{chunk-QKMUKYO7.js → chunk-E77JEWSD.js} +270 -125
  28. package/dist/{chunk-LYF7OHWH.js → chunk-EMYUUSFG.js} +12 -467
  29. package/dist/{chunk-4VP4KH3K.js → chunk-EQ63NRB7.js} +8 -8
  30. package/dist/chunk-FBVTI2TJ.js +518 -0
  31. package/dist/{chunk-R46L2BIR.js → chunk-FHJEP5SW.js} +19 -25
  32. package/dist/{chunk-WR67VIZY.js → chunk-GN57SG4G.js} +66 -8
  33. package/dist/{chunk-RD5U66HV.js → chunk-GPIEI3LY.js} +9 -9
  34. package/dist/{chunk-PCZJO5TI.js → chunk-GU2UIAFZ.js} +13 -178
  35. package/dist/chunk-GWS3VEIW.js +195 -0
  36. package/dist/chunk-H7IXIC72.js +103 -0
  37. package/dist/{chunk-3BPUFZDL.js → chunk-HLE42MG7.js} +3 -3
  38. package/dist/{chunk-24I7BN55.js → chunk-IFBNV6H6.js} +3 -3
  39. package/dist/{chunk-QNQHSOLF.js → chunk-IGWKHNIQ.js} +114 -55
  40. package/dist/chunk-IIZWH4XA.js +172 -0
  41. package/dist/{chunk-AD2SYQYC.js → chunk-IJ7RPIYJ.js} +124 -6
  42. package/dist/chunk-J3YUBZWY.js +382 -0
  43. package/dist/{chunk-5JGRAMKL.js → chunk-JOZYP4GM.js} +8 -6
  44. package/dist/{chunk-OH3TOQTB.js → chunk-K4XHGFR5.js} +751 -15
  45. package/dist/{chunk-EYPA3EGJ.js → chunk-KTYTFRMB.js} +178 -14
  46. package/dist/chunk-LU7P4LHA.js +33 -0
  47. package/dist/chunk-ME6CCNFO.js +108 -0
  48. package/dist/chunk-MXKJU4JB.js +1100 -0
  49. package/dist/{chunk-RY3LY4J5.js → chunk-N22QMJKY.js} +21 -18
  50. package/dist/chunk-NMPKI6XL.js +3006 -0
  51. package/dist/chunk-NUGM5KR6.js +165 -0
  52. package/dist/{chunk-22NAGB7X.js → chunk-P43ETTHK.js} +5 -94
  53. package/dist/{chunk-NILBFAPG.js → chunk-PMDBGQSJ.js} +2 -2
  54. package/dist/chunk-PT7HYKEM.js +165 -0
  55. package/dist/chunk-RVG5JXAL.js +41 -0
  56. package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
  57. package/dist/{chunk-4BPJXDWC.js → chunk-TANS5ZJS.js} +35 -19
  58. package/dist/{verifiers-NCBTHHN2.js → chunk-TB5666IT.js} +67 -324
  59. package/dist/{chunk-QM3F2GKX.js → chunk-TLQJPP24.js} +7927 -8088
  60. package/dist/{chunk-G7MUEIGA.js → chunk-TT36MB5S.js} +2 -1
  61. package/dist/chunk-TTHACPOM.js +961 -0
  62. package/dist/{chunk-6US73PDB.js → chunk-TYPGUK6W.js} +7 -7
  63. package/dist/{chunk-MFFY33HR.js → chunk-U6MBIEMB.js} +554 -209
  64. package/dist/chunk-VAWNZU7Z.js +242 -0
  65. package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
  66. package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
  67. package/dist/{chunk-XYDYPRZI.js → chunk-VWZOAB7K.js} +10 -10
  68. package/dist/{chunk-ZRGEBJ4T.js → chunk-WLFILSD5.js} +48 -48
  69. package/dist/{chunk-K7T3E2SR.js → chunk-WNIJTQQK.js} +12 -11
  70. package/dist/{chunk-CYQKWTG3.js → chunk-WSB3FPX7.js} +68 -20
  71. package/dist/{chunk-KHSFENX2.js → chunk-WWCZ5F23.js} +116 -14
  72. package/dist/{chunk-XE2VEJHX.js → chunk-WXY7KU3G.js} +2 -2
  73. package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
  74. package/dist/{chunk-KOHPCX4K.js → chunk-XWSF374K.js} +5 -5
  75. package/dist/{chunk-CJUB2JJ2.js → chunk-YS5VLNH5.js} +10 -10
  76. package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
  77. package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
  78. package/dist/cli/index.js +33 -31
  79. package/dist/{clio-M2KGYUFZ.js → clio-QVTYJ57A.js} +10 -10
  80. package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +5 -5
  81. package/dist/codewiki/build-worker.js +4 -4
  82. package/dist/{components-5TTYYX6G.js → components-ZFA3SAER.js} +9 -9
  83. package/dist/{config-XUUYQIWO.js → config-LW5IJFQN.js} +65 -55
  84. package/dist/{configure-IHJ7YOMV.js → configure-7XIZCOU4.js} +29 -24
  85. package/dist/{context-75MIWW3U.js → context-L3WL3X7K.js} +54 -43
  86. package/dist/{context-ZQ7SIFJV.js → context-N52ZA626.js} +30 -14
  87. package/dist/{context-74JLXAWD.js → context-Y6Y7QPR6.js} +12 -12
  88. package/dist/{context-clear-GYKWNUML.js → context-clear-MBQRLSDQ.js} +54 -43
  89. package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
  90. package/dist/{context-working-set-UX5KEP4J.js → context-working-set-GS6DSO7F.js} +18 -18
  91. package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-22ZCNOM3.js} +367 -79
  92. package/dist/{docs-6FZSCG5B.js → docs-7LQ23DLM.js} +9 -9
  93. package/dist/{doctor-SVJ5BZCW.js → doctor-M7YEDGAE.js} +27 -23
  94. package/dist/{eval-CG6LLBLD.js → eval-BEC2WHDA.js} +73 -20
  95. package/dist/{evidence-ZYFIEN42.js → evidence-REJUMSKM.js} +70 -59
  96. package/dist/{evolve-QGEXEMDW.js → evolve-PY5ZBA5K.js} +50 -39
  97. package/dist/{extensions-ADGNCJJD.js → extensions-HVKU65YU.js} +7 -7
  98. package/dist/{fleet-S5R4ZOQY.js → fleet-7WZEWRFA.js} +236 -377
  99. package/dist/fleet-commands-UVHWM76J.js +70 -0
  100. package/dist/fleet-graph-6ULH7PES.js +125 -0
  101. package/dist/fleet-new-RDVJLHHH.js +48 -0
  102. package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
  103. package/dist/fleet-validate-72PC4SLA.js +79 -0
  104. package/dist/{init-5DRU55YR.js → init-OG3TPGQG.js} +69 -57
  105. package/dist/library-CNTMPLRF.js +217 -0
  106. package/dist/{memory-7YKKR6UC.js → memory-6IS7F275.js} +52 -41
  107. package/dist/{models-ZPOLRU2C.js → models-ENRJDA5W.js} +37 -31
  108. package/dist/{monitor-US5F5YGZ.js → monitor-XLDVO7TN.js} +79 -42
  109. package/dist/{orchestrator-E2AL4T5N.js → orchestrator-6KSPYRHA.js} +4126 -762
  110. package/dist/{paths-E7KYAQWE.js → paths-DBXMZMDU.js} +6 -6
  111. package/dist/registry-LG64LTF4.js +11 -0
  112. package/dist/{reset-KZ652EK6.js → reset-RZ4ER727.js} +12 -12
  113. package/dist/{run-SRNBKDWD.js → run-Y2CNK5RU.js} +89 -76
  114. package/dist/{share-CGZE33UP.js → share-A55GYP6Z.js} +37 -13
  115. package/dist/{skills-S2X4DLY5.js → skills-ALC5J6AT.js} +29 -13
  116. package/dist/{skills-eval-W2GGIC4R.js → skills-eval-JPBEBYQU.js} +60 -48
  117. package/dist/support-MIETYA5E.js +38 -0
  118. package/dist/{targets-54SWINWB.js → targets-VGNXIR3S.js} +46 -36
  119. package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WOBR64YA.js} +4 -4
  120. package/dist/{uninstall-BVLWXKBT.js → uninstall-ZJF5H5ZN.js} +9 -9
  121. package/dist/{upgrade-JKAR27XC.js → upgrade-FUSUAGHR.js} +31 -28
  122. package/dist/{usage-MSAWCLX4.js → usage-N4MKVHKD.js} +140 -68
  123. package/dist/verifiers-YAWOJ3H2.js +336 -0
  124. package/dist/{verify-X5HDROLA.js → verify-LTDHYBGY.js} +10 -9
  125. package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-6M7GHTBJ.js} +75 -62
  126. package/dist/worker/entry.js +67 -57
  127. package/docs/README.md +3 -2
  128. package/docs/acp.md +1 -1
  129. package/docs/alcf-provider.md +1 -1
  130. package/docs/architecture.md +2 -2
  131. package/docs/artifact-versions.md +11 -6
  132. package/docs/built-in-agents.md +26 -2
  133. package/docs/capacity-and-scheduling.md +1 -1
  134. package/docs/commands-and-modes.md +83 -3
  135. package/docs/configuration-and-targets.md +86 -4
  136. package/docs/context-engine.md +1 -1
  137. package/docs/development-pipeline.md +1 -1
  138. package/docs/dispatch-architecture-rationale.md +1 -1
  139. package/docs/documentation-coverage.md +3 -3
  140. package/docs/documentation-guide.md +3 -3
  141. package/docs/eval-runner.md +1 -1
  142. package/docs/evals-internal.md +1 -1
  143. package/docs/evidence-and-memory.md +74 -10
  144. package/docs/evolution.md +1 -1
  145. package/docs/exit-codes-and-output.md +4 -1
  146. package/docs/extensions-and-sharing.md +6 -2
  147. package/docs/fleet-demo-runbook.md +2 -2
  148. package/docs/fleet-dispatch.md +228 -16
  149. package/docs/git-commit-provenance.md +2 -2
  150. package/docs/glossary.md +22 -2
  151. package/docs/installation-and-lifecycle.md +2 -2
  152. package/docs/middleware-and-components.md +2 -1
  153. package/docs/model-catalog.md +1 -1
  154. package/docs/observability.md +56 -9
  155. package/docs/proactive-memory.md +1 -1
  156. package/docs/prompt-envelope-and-tools.md +4 -2
  157. package/docs/provider-adapter-cookbook.md +1 -1
  158. package/docs/release-cut-checklist.md +83 -65
  159. package/docs/resource-library.md +59 -0
  160. package/docs/safety-model.md +2 -2
  161. package/docs/scientific-validation.md +3 -3
  162. package/docs/session-lifecycle.md +37 -1
  163. package/docs/skills-marketplace.md +16 -3
  164. package/docs/tool-usage.md +14 -7
  165. package/docs/trace-store.md +1 -1
  166. package/docs/troubleshooting.md +1 -1
  167. package/docs/tui-design.md +1 -1
  168. package/docs/worker-dispatch-mechanics.md +3 -3
  169. package/package.json +1 -2
  170. package/src/cli/argv.ts +5 -0
  171. package/src/cli/configure.ts +107 -23
  172. package/src/cli/doctor.ts +5 -1
  173. package/src/cli/evidence.ts +30 -25
  174. package/src/cli/fleet-commands.ts +37 -0
  175. package/src/cli/fleet-graph.ts +102 -0
  176. package/src/cli/fleet-new.ts +36 -0
  177. package/src/cli/fleet-preflight.ts +111 -0
  178. package/src/cli/fleet-validate.ts +30 -0
  179. package/src/cli/fleet.ts +173 -335
  180. package/src/cli/index.ts +3 -1
  181. package/src/cli/library.ts +190 -0
  182. package/src/cli/share.ts +13 -1
  183. package/src/cli/shared.ts +1 -0
  184. package/src/cli/targets.ts +4 -1
  185. package/src/cli/usage.ts +111 -19
  186. package/src/cli/validate-model.ts +60 -5
  187. package/src/core/bus-events.ts +29 -0
  188. package/src/core/commit-attribution.ts +4 -4
  189. package/src/core/config.ts +130 -0
  190. package/src/core/defaults.ts +81 -0
  191. package/src/core/path-boundary.ts +100 -0
  192. package/src/domains/agents/builtins/architect.md +1 -0
  193. package/src/domains/agents/builtins/oracle.md +33 -0
  194. package/src/domains/agents/catalog.ts +13 -1
  195. package/src/domains/agents/extension.ts +2 -11
  196. package/src/domains/agents/fleet-contract.ts +304 -24
  197. package/src/domains/agents/index.ts +14 -0
  198. package/src/domains/agents/recipe.ts +7 -1
  199. package/src/domains/agents/registry.ts +73 -5
  200. package/src/domains/agents/result-contract.ts +360 -15
  201. package/src/domains/agents/write-boundary.ts +15 -50
  202. package/src/domains/config/classify.ts +4 -0
  203. package/src/domains/context/project-rules.ts +51 -1
  204. package/src/domains/dispatch/active-route-planner.ts +14 -0
  205. package/src/domains/dispatch/assignment-reconcile.ts +22 -5
  206. package/src/domains/dispatch/assignment-store.ts +151 -14
  207. package/src/domains/dispatch/backoff.ts +2 -1
  208. package/src/domains/dispatch/capability-match.ts +1 -0
  209. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  210. package/src/domains/dispatch/contract.ts +48 -0
  211. package/src/domains/dispatch/delegation-plan.ts +164 -0
  212. package/src/domains/dispatch/execution-plan.ts +76 -5
  213. package/src/domains/dispatch/execution-role.ts +12 -2
  214. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  215. package/src/domains/dispatch/extension.ts +412 -74
  216. package/src/domains/dispatch/fleet-gate.ts +14 -0
  217. package/src/domains/dispatch/fleet-plan.ts +63 -3
  218. package/src/domains/dispatch/fleet-run.ts +791 -0
  219. package/src/domains/dispatch/gate-role-prompts.ts +47 -0
  220. package/src/domains/dispatch/host-verification.ts +178 -0
  221. package/src/domains/dispatch/index.ts +41 -1
  222. package/src/domains/dispatch/intent-requirements.ts +40 -0
  223. package/src/domains/dispatch/intent.ts +235 -0
  224. package/src/domains/dispatch/path-scope.ts +370 -0
  225. package/src/domains/dispatch/receipt-integrity.ts +9 -4
  226. package/src/domains/dispatch/state.ts +36 -3
  227. package/src/domains/dispatch/types.ts +64 -12
  228. package/src/domains/dispatch/validation.ts +69 -6
  229. package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
  230. package/src/domains/dispatch/write-boundary.ts +201 -22
  231. package/src/domains/eval/metrics/evidence.ts +79 -2
  232. package/src/domains/eval/runners/clio-run.ts +12 -2
  233. package/src/domains/evidence/build.ts +69 -11
  234. package/src/domains/evidence/index.ts +21 -0
  235. package/src/domains/evidence/provenance.ts +46 -11
  236. package/src/domains/evidence/trust-projection.ts +274 -0
  237. package/src/domains/evidence/trust-status.ts +155 -18
  238. package/src/domains/evidence/types.ts +4 -0
  239. package/src/domains/extensions/discovery.ts +88 -1
  240. package/src/domains/extensions/resources.ts +20 -8
  241. package/src/domains/extensions/state.ts +6 -2
  242. package/src/domains/extensions/types.ts +4 -1
  243. package/src/domains/lifecycle/doctor.ts +140 -1
  244. package/src/domains/middleware/index.ts +15 -0
  245. package/src/domains/middleware/watchdog.ts +281 -0
  246. package/src/domains/observability/contract.ts +3 -1
  247. package/src/domains/observability/cost.ts +12 -1
  248. package/src/domains/observability/extension.ts +2 -2
  249. package/src/domains/observability/index.ts +10 -0
  250. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  251. package/src/domains/prompts/contract.ts +3 -5
  252. package/src/domains/providers/extension.ts +30 -2
  253. package/src/domains/resources/common-loader.ts +3 -0
  254. package/src/domains/resources/index.ts +20 -0
  255. package/src/domains/resources/library.ts +326 -0
  256. package/src/domains/resources/prompts/loader.ts +119 -14
  257. package/src/domains/resources/skills/marketplace.ts +37 -12
  258. package/src/domains/safety/policy-engine.ts +5 -5
  259. package/src/domains/safety/run-effects.ts +64 -1
  260. package/src/domains/safety/scope.ts +7 -12
  261. package/src/domains/session/handoff.ts +629 -0
  262. package/src/domains/share/archive.ts +67 -2
  263. package/src/engine/acp/server.ts +4 -1
  264. package/src/engine/prompt-templates.ts +18 -1
  265. package/src/engine/worker-runtime.ts +6 -3
  266. package/src/entry/orchestrator.ts +37 -0
  267. package/src/interactive/bus-notices.ts +26 -0
  268. package/src/interactive/chat-loop.ts +235 -1
  269. package/src/interactive/chat-renderer.ts +22 -0
  270. package/src/interactive/cost-overlay.ts +31 -3
  271. package/src/interactive/council-dispatch.ts +30 -0
  272. package/src/interactive/council-grid.ts +213 -0
  273. package/src/interactive/council.ts +99 -0
  274. package/src/interactive/dispatch-board.ts +311 -17
  275. package/src/interactive/fleet-run-preview.ts +307 -0
  276. package/src/interactive/footer/notifications.ts +219 -0
  277. package/src/interactive/handoff-round.ts +56 -0
  278. package/src/interactive/interactive-application.ts +43 -1
  279. package/src/interactive/interactive-event-projection.ts +23 -1
  280. package/src/interactive/interactive-slash-runtime.ts +52 -2
  281. package/src/interactive/interactive-subscriptions.ts +14 -2
  282. package/src/interactive/oracle.ts +179 -0
  283. package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
  284. package/src/interactive/overlay-general-openers.ts +190 -1
  285. package/src/interactive/overlay-key-routing.ts +17 -1
  286. package/src/interactive/overlay-lifecycle.ts +41 -1
  287. package/src/interactive/overlay-permission-lifecycle.ts +10 -0
  288. package/src/interactive/overlay-resource-openers.ts +11 -3
  289. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  290. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  291. package/src/interactive/overlays/handoff-review.ts +185 -0
  292. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  293. package/src/interactive/overlays/list-overlay.ts +168 -2
  294. package/src/interactive/overlays/settings.ts +221 -30
  295. package/src/interactive/overlays/side-question.ts +139 -0
  296. package/src/interactive/overlays/skills-hub.ts +401 -15
  297. package/src/interactive/side-question.ts +171 -0
  298. package/src/interactive/slash-commands.ts +439 -7
  299. package/src/interactive/slash-spec.ts +19 -6
  300. package/src/interactive/theme/tokens.ts +30 -0
  301. package/src/interactive/turn-middleware.ts +15 -1
  302. package/src/interactive/view/artifacts.ts +42 -9
  303. package/src/interactive/view/view-overlay.ts +15 -3
  304. package/src/interactive/watchdog-run.ts +75 -0
  305. package/src/interactive/worker-receipts.ts +14 -2
  306. package/src/interactive/worker-share.ts +56 -1
  307. package/src/interactive/worker-stream.ts +15 -0
  308. package/src/tools/bootstrap.ts +3 -0
  309. package/src/tools/compete-worktrees.ts +13 -79
  310. package/src/tools/dispatch-admission.ts +251 -18
  311. package/src/tools/dispatch-arguments.ts +84 -1
  312. package/src/tools/dispatch-plan.ts +165 -6
  313. package/src/tools/dispatch-runner.ts +364 -23
  314. package/src/tools/dispatch-types.ts +20 -1
  315. package/src/tools/dispatch.ts +72 -2
  316. package/src/tools/monitor.ts +29 -0
  317. package/src/tools/profiles.ts +18 -4
  318. package/src/tools/task-worktree.ts +238 -0
  319. package/src/tools/verify/authoring.ts +61 -1
  320. package/src/tools/verify/scripts.ts +62 -0
  321. package/src/tools/worker-evidence.ts +21 -14
  322. package/src/worker/spec-contract.ts +3 -1
  323. package/dist/chunk-HC4CLZ2Y.js +0 -68
@@ -203,12 +203,62 @@ function ruleMatchesAnyPath(rule: ProjectRule, workingPaths: ReadonlyArray<strin
203
203
  continue;
204
204
  }
205
205
  for (const path of workingPaths) {
206
- if (regex.test(normalizeGlobInput(path))) return true;
206
+ const normalized = normalizeGlobInput(path);
207
+ if (regex.test(normalized)) return true;
208
+ if (normalized.endsWith("/") && globMatchesInsideDirectory(regex, pattern, normalized)) return true;
207
209
  }
208
210
  }
209
211
  return false;
210
212
  }
211
213
 
214
+ function materializedGlobVariants(pattern: string): string[] {
215
+ let variants = [""];
216
+ const normalized = normalizeGlobInput(pattern);
217
+ for (let index = 0; index < normalized.length; index += 1) {
218
+ const character = normalized[index];
219
+ if (character === undefined) break;
220
+ if (character === "*" && normalized[index + 1] === "*" && normalized[index + 2] === "/") {
221
+ variants = variants.flatMap((prefix) => [prefix, `${prefix}x/`]).slice(0, 64);
222
+ index += 2;
223
+ continue;
224
+ }
225
+ if (character === "*" && normalized[index + 1] === "*") {
226
+ variants = variants.map((prefix) => `${prefix}x`);
227
+ index += 1;
228
+ continue;
229
+ }
230
+ if (character === "*" || character === "?") {
231
+ variants = variants.map((prefix) => `${prefix}x`);
232
+ continue;
233
+ }
234
+ if (character === "[") {
235
+ const end = normalized.indexOf("]", index + 1);
236
+ const selected = end === -1 ? "x" : (normalized[index + 1] ?? "x");
237
+ variants = variants.map((prefix) => `${prefix}${selected}`);
238
+ index = end === -1 ? index : end;
239
+ continue;
240
+ }
241
+ variants = variants.map((prefix) => `${prefix}${character}`);
242
+ }
243
+ return variants;
244
+ }
245
+
246
+ function globMatchesInsideDirectory(regex: RegExp, pattern: string, directory: string): boolean {
247
+ const candidates = new Set<string>([`${directory}x`, `${directory}x/x`]);
248
+ for (const materialized of materializedGlobVariants(pattern)) {
249
+ if (materialized.startsWith(directory)) candidates.add(materialized);
250
+ const segments = materialized.split("/");
251
+ for (let index = 0; index < segments.length; index += 1) {
252
+ const suffix = segments.slice(index).join("/");
253
+ if (suffix.length > 0) candidates.add(`${directory}${suffix}`);
254
+ }
255
+ for (let index = 0; index < materialized.length; index += 1) {
256
+ candidates.add(`${directory}${materialized.slice(index)}`);
257
+ }
258
+ }
259
+ return [...candidates].some((candidate) => regex.test(candidate));
260
+ }
261
+
212
262
  /**
213
263
  * Select the rules that should load now. Unconditional rules (no `paths`) always
214
264
  * load; a path-scoped rule loads only when one of its globs matches a path
@@ -37,6 +37,12 @@ export function routeValidationProjection(
37
37
  // Presentation-only parentage stamped by the calling tool, on the same
38
38
  // terms: a model must not be able to author it.
39
39
  parentToolCallId,
40
+ resolvedVerification,
41
+ resultContractOverride,
42
+ fleetGateReceipt,
43
+ taskWorktree,
44
+ runIdHint,
45
+ taskWorktreeDestination,
40
46
  ...raw
41
47
  } = request;
42
48
  const awaitingEnvelope =
@@ -54,6 +60,14 @@ export function routeValidationProjection(
54
60
  ...(assignmentDeadlineAt !== undefined ? { assignmentDeadlineAt } : {}),
55
61
  ...(ledger !== undefined ? { ledger } : {}),
56
62
  ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
63
+ ...(resolvedVerification !== undefined
64
+ ? { resolvedVerification: resolvedVerification.map((check) => ({ ...check, argv: [...check.argv] })) }
65
+ : {}),
66
+ ...(resultContractOverride !== undefined ? { resultContractOverride } : {}),
67
+ ...(fleetGateReceipt !== undefined ? { fleetGateReceipt: { ...fleetGateReceipt } } : {}),
68
+ ...(taskWorktree !== undefined ? { taskWorktree } : {}),
69
+ ...(runIdHint !== undefined ? { runIdHint } : {}),
70
+ ...(taskWorktreeDestination !== undefined ? { taskWorktreeDestination } : {}),
57
71
  ...(awaitingEnvelope ? { failover: "approved" as const } : {}),
58
72
  }),
59
73
  };
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Startup reconciliation for durable assignments. The in-memory retry queue and
3
- * assignment registry do not survive a restart (Symphony §14.3), so every
4
- * durable record still marked `running` after a crash is orphaned: no active
5
- * attempt, no queued retry, and no attached terminal promise can ever settle it.
3
+ * assignment registry do not survive a restart (Symphony §14.3), so a durable
4
+ * record whose process owner is gone is orphaned: no active attempt, no queued
5
+ * retry, and no attached terminal promise can ever settle it. Running records
6
+ * owned by a live sibling process are deliberately left alone.
6
7
  *
7
8
  * This pass resolves each orphan deterministically against durable ledger
8
9
  * evidence so a later `wait`/`collect` can never observe a stuck `running`
@@ -11,7 +12,7 @@
11
12
  */
12
13
 
13
14
  import type { AssignmentStatus } from "./assignment.js";
14
- import type { DurableAssignmentRecord } from "./assignment-store.js";
15
+ import type { AssignmentVerdictOwner, DurableAssignmentRecord } from "./assignment-store.js";
15
16
 
16
17
  /** Minimal terminal view of one attempt's durable ledger row. */
17
18
  export interface ReconcileAttemptView {
@@ -25,13 +26,16 @@ export interface ReconcileAttemptView {
25
26
  export interface AssignmentReconcileDeps {
26
27
  /** Durable records currently marked `running`. */
27
28
  listRunning: () => ReadonlyArray<DurableAssignmentRecord>;
29
+ /** Decide liveness from the record's durable pid and process birth token. */
30
+ ownerAlive: (record: DurableAssignmentRecord) => boolean;
28
31
  /** Resolve one attempt's terminal view from the ledger, or null if pruned. */
29
32
  lookupAttempt: (runId: string) => ReconcileAttemptView | null;
30
- /** Persist the reconciled terminal status. */
33
+ /** Persist the reconciled terminal status, as the record's verdict owner when it has one. */
31
34
  settle: (
32
35
  assignmentId: string,
33
36
  terminalRunId: string,
34
37
  status: Exclude<AssignmentStatus, "running">,
38
+ owner?: AssignmentVerdictOwner,
35
39
  ) => Promise<unknown>;
36
40
  }
37
41
 
@@ -44,6 +48,10 @@ export interface AssignmentReconcileSummary {
44
48
 
45
49
  /**
46
50
  * Deterministic rule, in priority order:
51
+ * -1. a record owned by a live process is not an orphan and remains running;
52
+ * 0. a record whose verdict belongs to its opener is failed (abandoned): the
53
+ * opener died before reaching a verdict, and a green attempt under it is
54
+ * one step of a run, never the run's answer;
47
55
  * 1. a recovered succeeded terminal attempt wins (`succeeded`);
48
56
  * 2. otherwise the last terminal attempt marks the assignment `failed`;
49
57
  * 3. otherwise no attempt is recoverable and the assignment is failed
@@ -53,6 +61,15 @@ export async function reconcileOrphanAssignments(deps: AssignmentReconcileDeps):
53
61
  const summary: AssignmentReconcileSummary = { recovered: 0, abandoned: 0 };
54
62
  for (const record of deps.listRunning()) {
55
63
  if (record.status !== "running") continue;
64
+ if (deps.ownerAlive(record)) continue;
65
+ if (record.verdictOwner !== undefined) {
66
+ // Attempts under a claimed row are fleet steps, not the fleet verdict.
67
+ // Filing a green step as the terminal id of an abandoned fleet would
68
+ // contradict that step's immutable ledger result.
69
+ await deps.settle(record.assignmentId, record.assignmentId, "failed", record.verdictOwner);
70
+ summary.abandoned += 1;
71
+ continue;
72
+ }
56
73
  const views = record.attempts
57
74
  .map((runId) => deps.lookupAttempt(runId))
58
75
  .filter((view): view is ReconcileAttemptView => view !== null);
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
+ import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from "../../core/process-identity.js";
8
9
  import { withStateFileLock } from "../../core/state-file-lock.js";
9
10
  import { clioStateDir } from "../../core/xdg.js";
10
11
  import { atomicWrite } from "../../engine/session.js";
@@ -12,11 +13,54 @@ import type { AssignmentStatus } from "./assignment.js";
12
13
 
13
14
  const MAX_ASSIGNMENT_RECORDS = 1_000;
14
15
 
16
+ /**
17
+ * Who decides a record's status.
18
+ *
19
+ * Absent is the ordinary case: the record is one logical dispatch, so the
20
+ * attempts filed under it are the only thing that can settle it. A fleet claims
21
+ * the verdict instead, because a fleet root gathers every step of a run under
22
+ * one id and no single step is the run's answer. Without the claim the last
23
+ * step to settle writes the row, which reports a green step of an aborted run
24
+ * as the run's outcome.
25
+ */
26
+ export type AssignmentVerdictOwner = "fleet";
27
+
28
+ /**
29
+ * Durable identity of the process executing a running assignment. A pid alone
30
+ * is not enough: the OS may recycle it after a crash, so the process birth
31
+ * token must still match before another extension treats the owner as live.
32
+ */
33
+ export interface AssignmentProcessOwner {
34
+ pid: number;
35
+ processBirthToken: string;
36
+ acquiredAt: string;
37
+ }
38
+
39
+ export interface AssignmentProcessOwnerProbe {
40
+ birthToken(pid: number): string | null;
41
+ /** True when a missing birth token proves that the process is gone. */
42
+ tokenProvesDeath?: boolean;
43
+ /** Liveness fallback for platforms whose birth token is synthetic. */
44
+ alive?(pid: number): boolean;
45
+ }
46
+
47
+ /**
48
+ * One durable logical-dispatch row.
49
+ *
50
+ * `attempts` holds terminal run ids: an agent attempt's receipt id, and for a
51
+ * fleet also each deterministic step's `code-*` run id, whose evidence lives
52
+ * under `code-steps/<assignmentId>/`. `status` answers for the whole record,
53
+ * so for a fleet it answers for the whole run rather than for its last step.
54
+ */
15
55
  export interface DurableAssignmentRecord {
16
56
  assignmentId: string;
17
57
  attempts: string[];
18
58
  terminalRunId: string | null;
19
59
  status: AssignmentStatus;
60
+ /** Set when something other than the attempts decides `status`. */
61
+ verdictOwner?: AssignmentVerdictOwner;
62
+ /** Process lease held only while this record is running. */
63
+ processOwner?: AssignmentProcessOwner;
20
64
  }
21
65
 
22
66
  interface AssignmentStoreFile {
@@ -28,6 +72,18 @@ function storePath(): string {
28
72
  return join(clioStateDir(), "assignments.json");
29
73
  }
30
74
 
75
+ function validProcessOwner(value: unknown): value is AssignmentProcessOwner {
76
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
77
+ const owner = value as Partial<AssignmentProcessOwner>;
78
+ return (
79
+ Number.isSafeInteger(owner.pid) &&
80
+ (owner.pid ?? 0) > 0 &&
81
+ typeof owner.processBirthToken === "string" &&
82
+ typeof owner.acquiredAt === "string" &&
83
+ Number.isFinite(Date.parse(owner.acquiredAt))
84
+ );
85
+ }
86
+
31
87
  function validRecord(value: unknown): value is DurableAssignmentRecord {
32
88
  if (typeof value !== "object" || value === null) return false;
33
89
  const record = value as Partial<DurableAssignmentRecord>;
@@ -36,6 +92,8 @@ function validRecord(value: unknown): value is DurableAssignmentRecord {
36
92
  Array.isArray(record.attempts) &&
37
93
  record.attempts.every((runId) => typeof runId === "string") &&
38
94
  (record.terminalRunId === null || typeof record.terminalRunId === "string") &&
95
+ (record.verdictOwner === undefined || record.verdictOwner === "fleet") &&
96
+ (record.processOwner === undefined || validProcessOwner(record.processOwner)) &&
39
97
  (record.status === "running" ||
40
98
  record.status === "succeeded" ||
41
99
  record.status === "failed" ||
@@ -57,7 +115,32 @@ function readStore(): DurableAssignmentRecord[] {
57
115
  }
58
116
 
59
117
  function copyRecord(record: DurableAssignmentRecord): DurableAssignmentRecord {
60
- return { ...record, attempts: [...record.attempts] };
118
+ return {
119
+ ...record,
120
+ attempts: [...record.attempts],
121
+ ...(record.processOwner === undefined ? {} : { processOwner: { ...record.processOwner } }),
122
+ };
123
+ }
124
+
125
+ const defaultProcessOwnerProbe: AssignmentProcessOwnerProbe = {
126
+ birthToken: processBirthToken,
127
+ tokenProvesDeath: BIRTH_TOKEN_SOURCE_AVAILABLE,
128
+ alive: processAlive,
129
+ };
130
+
131
+ /** True only when the durable identity still names the same live process. */
132
+ export function assignmentProcessOwnerAlive(
133
+ record: Pick<DurableAssignmentRecord, "processOwner">,
134
+ probe: AssignmentProcessOwnerProbe = defaultProcessOwnerProbe,
135
+ ): boolean {
136
+ const owner = record.processOwner;
137
+ if (owner === undefined) return false;
138
+ const current = probe.birthToken(owner.pid);
139
+ if (current === null || current !== owner.processBirthToken) return false;
140
+ // A synthetic `pid-N` token cannot distinguish pid reuse, so require an
141
+ // independent liveness signal on platforms without a real birth-token source.
142
+ if (probe.tokenProvesDeath === false) return probe.alive?.(owner.pid) ?? false;
143
+ return true;
61
144
  }
62
145
 
63
146
  function writeStore(assignments: ReadonlyArray<DurableAssignmentRecord>): void {
@@ -114,49 +197,103 @@ export async function renameStoredAssignment(
114
197
  return copyRecord(result);
115
198
  }
116
199
 
117
- export function registerAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
118
- return updateRecord(
200
+ function currentProcessOwner(): AssignmentProcessOwner {
201
+ const token = processBirthToken();
202
+ if (token === null) throw new Error("dispatch: cannot establish assignment owner process birth token");
203
+ return {
204
+ pid: process.pid,
205
+ processBirthToken: token,
206
+ acquiredAt: new Date().toISOString(),
207
+ };
208
+ }
209
+
210
+ function openRecord(assignmentId: string): DurableAssignmentRecord {
211
+ return {
119
212
  assignmentId,
120
- (current) => current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" },
121
- );
213
+ attempts: [],
214
+ terminalRunId: null,
215
+ status: "running",
216
+ processOwner: currentProcessOwner(),
217
+ };
218
+ }
219
+
220
+ function ensureRunningProcessOwner(record: DurableAssignmentRecord): DurableAssignmentRecord {
221
+ if (record.status !== "running" || record.processOwner !== undefined) return record;
222
+ return { ...record, processOwner: currentProcessOwner() };
223
+ }
224
+
225
+ function clearProcessOwner(record: DurableAssignmentRecord): DurableAssignmentRecord {
226
+ const result = { ...record };
227
+ delete result.processOwner;
228
+ return result;
229
+ }
230
+
231
+ export function registerAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
232
+ return updateRecord(assignmentId, (current) => ensureRunningProcessOwner(current ?? openRecord(assignmentId)));
233
+ }
234
+
235
+ /**
236
+ * Reserve this record's status for `owner`, opening the record if it is new.
237
+ * Attempts filed by anyone are still recorded; only the verdict is reserved,
238
+ * and until the owner settles it the record stays `running`.
239
+ */
240
+ export function claimAssignmentVerdict(
241
+ assignmentId: string,
242
+ owner: AssignmentVerdictOwner,
243
+ ): Promise<DurableAssignmentRecord> {
244
+ return updateRecord(assignmentId, (current) => ({
245
+ ...ensureRunningProcessOwner(current ?? openRecord(assignmentId)),
246
+ verdictOwner: owner,
247
+ }));
122
248
  }
123
249
 
124
250
  export function recordAssignmentAttempt(assignmentId: string, runId: string): Promise<DurableAssignmentRecord> {
125
251
  return updateRecord(assignmentId, (current) => {
126
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
252
+ const base = ensureRunningProcessOwner(current ?? openRecord(assignmentId));
127
253
  return base.attempts.includes(runId) ? base : { ...base, attempts: [...base.attempts, runId] };
128
254
  });
129
255
  }
130
256
 
131
257
  export function failQueuedAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
132
258
  return updateRecord(assignmentId, (current) => {
133
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
134
- return base.status === "running" ? { ...base, status: "failed" } : base;
259
+ const base = current ?? openRecord(assignmentId);
260
+ if (base.verdictOwner !== undefined) return base;
261
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "failed" }) : base;
135
262
  });
136
263
  }
137
264
 
138
265
  export function timeoutStoredAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
139
266
  return updateRecord(assignmentId, (current) => {
140
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
141
- return base.status === "running" ? { ...base, status: "timed_out" } : base;
267
+ const base = current ?? openRecord(assignmentId);
268
+ if (base.verdictOwner !== undefined) return base;
269
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "timed_out" }) : base;
142
270
  });
143
271
  }
144
272
 
145
273
  export function cancelStoredAssignment(assignmentId: string): Promise<DurableAssignmentRecord> {
146
274
  return updateRecord(assignmentId, (current) => {
147
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
148
- return base.status === "running" ? { ...base, status: "canceled" } : base;
275
+ const base = current ?? openRecord(assignmentId);
276
+ if (base.verdictOwner !== undefined) return base;
277
+ return base.status === "running" ? clearProcessOwner({ ...base, status: "canceled" }) : base;
149
278
  });
150
279
  }
151
280
 
281
+ /**
282
+ * File `terminalRunId` as an attempt and, when the caller owns the verdict, set
283
+ * the record's status. An attempt settling under a claimed record settles
284
+ * itself and not the record: one fleet step reaching a green receipt says
285
+ * nothing about the six steps behind it that never ran.
286
+ */
152
287
  export function settleStoredAssignment(
153
288
  assignmentId: string,
154
289
  terminalRunId: string,
155
290
  status: Exclude<AssignmentStatus, "running">,
291
+ owner?: AssignmentVerdictOwner,
156
292
  ): Promise<DurableAssignmentRecord> {
157
293
  return updateRecord(assignmentId, (current) => {
158
- const base = current ?? { assignmentId, attempts: [], terminalRunId: null, status: "running" as const };
294
+ const base = current ?? openRecord(assignmentId);
159
295
  const attempts = base.attempts.includes(terminalRunId) ? base.attempts : [...base.attempts, terminalRunId];
160
- return { ...base, attempts, terminalRunId, status };
296
+ if (base.verdictOwner !== owner) return { ...base, attempts };
297
+ return clearProcessOwner({ ...base, attempts, terminalRunId, status });
161
298
  });
162
299
  }
@@ -54,6 +54,7 @@ export function isDeterministicOutcomeCode(code: RunOutcomeCode | null | undefin
54
54
  code === "worker_tool_call_cap_exhausted" ||
55
55
  code === "loop_guard_tools_disabled_exhausted" ||
56
56
  code === "result_contract_exhausted" ||
57
- code === "worker_final_output_missing"
57
+ code === "worker_final_output_missing" ||
58
+ code === "host_verification_rejected"
58
59
  );
59
60
  }
@@ -77,6 +77,7 @@ const REPORTING_CONTRACT_KINDS: ReadonlySet<ResultContract["kind"]> = new Set([
77
77
  "research-report",
78
78
  "provenance-report",
79
79
  "debugger-report",
80
+ "oracle-report",
80
81
  ]);
81
82
 
82
83
  /** Preferred mutating specialist per task shape, best first. */
@@ -0,0 +1,175 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { BIRTH_TOKEN_SOURCE_AVAILABLE, processAlive, processBirthToken } from "../../core/process-identity.js";
5
+ import { withStateFileLockSync } from "../../core/state-file-lock.js";
6
+ import { clioStateDir } from "../../core/xdg.js";
7
+ import { atomicWrite } from "../../engine/session.js";
8
+
9
+ export interface CheckoutWriterLeaseRecord {
10
+ version: 1;
11
+ checkout: string;
12
+ pid: number;
13
+ processBirthToken: string;
14
+ acquiredAt: string;
15
+ }
16
+
17
+ export interface CheckoutWriterLease {
18
+ checkout: string;
19
+ path: string;
20
+ release(): void;
21
+ }
22
+
23
+ export interface CheckoutWriterLeaseProbe {
24
+ birthToken(pid: number): string | null;
25
+ tokenProvesDeath?: boolean;
26
+ alive?(pid: number): boolean;
27
+ }
28
+
29
+ const held = new Map<string, { count: number; path: string }>();
30
+ let exitHooksInstalled = false;
31
+
32
+ function canonicalCheckout(checkout: string): string {
33
+ return realpathSync(checkout);
34
+ }
35
+
36
+ function checkoutWriterLeasePath(checkout: string): string {
37
+ const canonical = canonicalCheckout(checkout);
38
+ const key = createHash("sha256").update(canonical, "utf8").digest("hex");
39
+ return join(clioStateDir(), "checkout-writer-leases", `${key}.json`);
40
+ }
41
+
42
+ function validRecord(value: unknown): value is CheckoutWriterLeaseRecord {
43
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
44
+ const record = value as Partial<CheckoutWriterLeaseRecord>;
45
+ return (
46
+ record.version === 1 &&
47
+ typeof record.checkout === "string" &&
48
+ Number.isSafeInteger(record.pid) &&
49
+ (record.pid ?? 0) > 0 &&
50
+ typeof record.processBirthToken === "string" &&
51
+ typeof record.acquiredAt === "string" &&
52
+ Number.isFinite(Date.parse(record.acquiredAt))
53
+ );
54
+ }
55
+
56
+ function readRecord(path: string): CheckoutWriterLeaseRecord | null {
57
+ if (!existsSync(path)) return null;
58
+ try {
59
+ const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
60
+ if (!validRecord(value)) throw new Error("invalid schema");
61
+ return value;
62
+ } catch (error) {
63
+ throw new Error(
64
+ `checkout writer lease is unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`,
65
+ );
66
+ }
67
+ }
68
+
69
+ function ownerAlive(record: CheckoutWriterLeaseRecord, probe: CheckoutWriterLeaseProbe): boolean {
70
+ const current = probe.birthToken(record.pid);
71
+ if (current === null || current !== record.processBirthToken) return false;
72
+ if (probe.tokenProvesDeath === false) return probe.alive?.(record.pid) ?? false;
73
+ return true;
74
+ }
75
+
76
+ const defaultProbe: CheckoutWriterLeaseProbe = {
77
+ birthToken: processBirthToken,
78
+ tokenProvesDeath: BIRTH_TOKEN_SOURCE_AVAILABLE,
79
+ alive: processAlive,
80
+ };
81
+
82
+ function releaseAllOnExit(): void {
83
+ for (const [checkout, entry] of held) {
84
+ try {
85
+ withStateFileLockSync(entry.path, () => {
86
+ const record = readRecord(entry.path);
87
+ if (record?.checkout === checkout && record.pid === process.pid) unlinkSync(entry.path);
88
+ });
89
+ } catch {
90
+ // Process exit is best effort. A stale record is reclaimed by the next admission.
91
+ }
92
+ }
93
+ held.clear();
94
+ }
95
+
96
+ function installExitHooks(): void {
97
+ if (exitHooksInstalled) return;
98
+ exitHooksInstalled = true;
99
+ process.once("exit", releaseAllOnExit);
100
+ }
101
+
102
+ export function acquireCheckoutWriterLease(input: {
103
+ checkout: string;
104
+ nowMs?: number;
105
+ pid?: number;
106
+ processBirthToken?: string;
107
+ probe?: CheckoutWriterLeaseProbe;
108
+ }): CheckoutWriterLease {
109
+ const checkout = canonicalCheckout(input.checkout);
110
+ const path = checkoutWriterLeasePath(checkout);
111
+ const local = held.get(checkout);
112
+ if (local !== undefined && (input.pid === undefined || input.pid === process.pid)) {
113
+ local.count += 1;
114
+ let released = false;
115
+ return {
116
+ checkout,
117
+ path,
118
+ release: () => {
119
+ if (released) return;
120
+ released = true;
121
+ releaseCheckoutWriterLease(checkout);
122
+ },
123
+ };
124
+ }
125
+ mkdirSync(dirname(path), { recursive: true });
126
+ const ownerPid = input.pid ?? process.pid;
127
+ const token = input.processBirthToken ?? processBirthToken(ownerPid);
128
+ if (token === null) throw new Error("dispatch: cannot establish checkout writer process birth token");
129
+ withStateFileLockSync(path, () => {
130
+ const existing = readRecord(path);
131
+ if (existing !== null && ownerAlive(existing, input.probe ?? defaultProbe)) {
132
+ throw new Error(`checkout_writer_lease_held: checkout writer lease is held by pid ${existing.pid}`);
133
+ }
134
+ const record: CheckoutWriterLeaseRecord = {
135
+ version: 1,
136
+ checkout,
137
+ pid: ownerPid,
138
+ processBirthToken: token,
139
+ acquiredAt: new Date(input.nowMs ?? Date.now()).toISOString(),
140
+ };
141
+ atomicWrite(path, JSON.stringify(record, null, 2));
142
+ });
143
+ if (ownerPid === process.pid) {
144
+ held.set(checkout, { count: 1, path });
145
+ installExitHooks();
146
+ }
147
+ let released = false;
148
+ return {
149
+ checkout,
150
+ path,
151
+ release: () => {
152
+ if (released) return;
153
+ released = true;
154
+ if (ownerPid === process.pid) releaseCheckoutWriterLease(checkout);
155
+ else releaseLeaseRecord(path, checkout, ownerPid, token);
156
+ },
157
+ };
158
+ }
159
+
160
+ function releaseLeaseRecord(path: string, checkout: string, pid: number, token: string): void {
161
+ withStateFileLockSync(path, () => {
162
+ const record = readRecord(path);
163
+ if (record?.checkout === checkout && record.pid === pid && record.processBirthToken === token) unlinkSync(path);
164
+ });
165
+ }
166
+
167
+ function releaseCheckoutWriterLease(checkout: string): void {
168
+ const entry = held.get(checkout);
169
+ if (entry === undefined) return;
170
+ entry.count -= 1;
171
+ if (entry.count > 0) return;
172
+ held.delete(checkout);
173
+ const token = processBirthToken(process.pid);
174
+ if (token !== null) releaseLeaseRecord(entry.path, checkout, process.pid, token);
175
+ }
@@ -15,6 +15,30 @@ import type { RunEnvelope, RunLineage, RunNodeIdentity, RunPhaseDurations, RunRe
15
15
  import type { DispatchFailoverCandidate, JobSpec } from "./validation.js";
16
16
 
17
17
  export interface DispatchRequest extends JobSpec {
18
+ /** Coordinator-owned result contract override for a fleet plan step. */
19
+ resultContractOverride?: ResultContract;
20
+ /** Gate path whose post-run hash is sealed into this step's receipt. */
21
+ fleetGateReceipt?: { path: string };
22
+ /** Orchestrator-prepared task worktree state. Model arguments cannot author it. */
23
+ taskWorktree?: {
24
+ root: string;
25
+ runId: string;
26
+ path: string;
27
+ branch: string;
28
+ base: string;
29
+ ownerToken: string;
30
+ };
31
+ /** Run identity allocated before task worktree creation. Model arguments cannot author it. */
32
+ runIdHint?: string;
33
+ /** Parent checkout frozen by plan approval for task worktree application. */
34
+ taskWorktreeDestination?: string;
35
+ /** Admission-resolved commands. Model text never enters these argv vectors. */
36
+ resolvedVerification?: ReadonlyArray<{
37
+ check: string;
38
+ argv: ReadonlyArray<string>;
39
+ cwd: string;
40
+ timeoutMs: number;
41
+ }>;
18
42
  /**
19
43
  * Semantic role this request's first attempt runs under. Derived once by
20
44
  * `execution-role.ts` at request construction, never authored by a model, and
@@ -212,12 +236,36 @@ export interface DispatchContract {
212
236
  finalPromise: Promise<ReadonlyArray<RunReceipt>>;
213
237
  }>;
214
238
 
239
+ /** Seal a coordinator-only council synthesis receipt without invoking a model. */
240
+ sealCouncilSynthesis?(input: {
241
+ group: string;
242
+ round: number;
243
+ kind: "none" | "vote" | "judge";
244
+ text: string;
245
+ subjects: ReadonlyArray<{ runId: string; digest: string | null }>;
246
+ template: RunReceipt;
247
+ }): Promise<RunReceipt>;
248
+
215
249
  /** List runs from the ledger. */
216
250
  listRuns(status?: RunStatus): ReadonlyArray<RunEnvelope>;
217
251
 
218
252
  /** Get a specific immutable attempt envelope. */
219
253
  getRun(runId: string): RunEnvelope | null;
220
254
 
255
+ /**
256
+ * Absolute paths this run's own successful tool calls aimed a mutation at,
257
+ * or null when the run kept no usable record: an unknown or evicted run, a
258
+ * runtime that publishes no tool telemetry, telemetry that came back
259
+ * incomplete, or a run that called a tool able to write a path its arguments
260
+ * do not name. Null is not an empty list, and a caller that cannot tell them
261
+ * apart will read "wrote nothing" off a run nobody watched.
262
+ *
263
+ * The write boundary reads this to separate what a step changed from what
264
+ * changed under it, so the answer must stay a record of observation rather
265
+ * than an inference from the checkout.
266
+ */
267
+ observedRunWrites?(runId: string): ReadonlyArray<string> | null;
268
+
221
269
  /** In-memory logical assignment state and complete finalized-attempt history. */
222
270
  readonly assignments?: {
223
271
  /** Live assignment, including its terminal promise and detailed attempt refs. */