@coreplane/switchboard 1.204.0 → 1.206.0

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 (389) hide show
  1. package/dist/assets/Dockerfile +6 -3
  2. package/dist/assets/config/config.example.yaml +56 -4
  3. package/dist/assets/deploy/cloudflare/coordinator.ts +86 -0
  4. package/dist/assets/deploy/cloudflare/shared.ts +9 -0
  5. package/dist/assets/deploy/cloudflare/worker.ts +142 -6
  6. package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +9 -0
  7. package/dist/assets/deploy/cloudflare-memory/worker.ts +381 -7
  8. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +22 -2
  9. package/dist/assets/deploy/cloudflare-resident/Dockerfile +89 -3
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +81 -10
  11. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +4 -1
  12. package/dist/assets/deploy/secrets.manifest.json +1 -1
  13. package/dist/assets/package-lock.json +291 -97
  14. package/dist/assets/package.json +1 -1
  15. package/dist/assets/source.json +3 -3
  16. package/dist/assets/src/agents/registry.ts +517 -0
  17. package/dist/assets/src/config/profile.ts +229 -0
  18. package/dist/assets/src/core/authz/policy.ts +15 -0
  19. package/dist/assets/src/core/coordinator/contract.ts +239 -0
  20. package/dist/assets/src/core/coordinator/driver.ts +501 -0
  21. package/dist/assets/src/core/coordinator/instancesRoute.ts +181 -0
  22. package/dist/assets/src/core/delivery.ts +829 -0
  23. package/dist/assets/src/core/deliverySnapshotStore.ts +260 -0
  24. package/dist/assets/src/core/reviewVerdict.ts +296 -0
  25. package/dist/assets/src/core/reviewedHead.ts +77 -0
  26. package/dist/assets/src/core/runEvents.ts +4 -2
  27. package/dist/assets/src/core/runLedger/decisions.ts +2 -1
  28. package/dist/assets/src/core/runLedger/types.ts +23 -1
  29. package/dist/assets/src/core/runRecord.ts +100 -0
  30. package/dist/assets/src/core/ship/contract.ts +502 -0
  31. package/dist/assets/src/core/ship/coordinator.ts +1039 -0
  32. package/dist/assets/src/core/workerError.ts +12 -0
  33. package/dist/assets/web/dist/.vite/manifest.json +2770 -70
  34. package/dist/assets/web/dist/assets/AppShell-lYVcj-k7.js +1 -0
  35. package/dist/assets/web/dist/assets/{CostsPage-XQ-fQzdz.js → CostsPage-DGSE48l1.js} +1 -1
  36. package/dist/assets/web/dist/assets/DeliveryPage-CvlWP7Eq.js +1 -0
  37. package/dist/assets/web/dist/assets/NotFoundPage-BzWS4Ca1.js +1 -0
  38. package/dist/assets/web/dist/assets/ResidentDetailPage-D3P21yeI.js +1 -0
  39. package/dist/assets/web/dist/assets/ResidentsIndexPage-DOYqnZ1q.js +1 -0
  40. package/dist/assets/web/dist/assets/RunRoutePage-OmvrvPXY.js +12 -0
  41. package/dist/assets/web/dist/assets/RunRoutePage-f6TSNPeW.css +1 -0
  42. package/dist/assets/web/dist/assets/RunsIndexPage-DWbSQtL4.js +1 -0
  43. package/dist/assets/web/dist/assets/RunsTabs-DEICQ4FY.js +1 -0
  44. package/dist/assets/web/dist/assets/ScheduledPage-CPKfJ4mR.js +1 -0
  45. package/dist/assets/web/dist/assets/StatusDot-COr8jTyM.js +1 -0
  46. package/dist/assets/web/dist/assets/Tooltip-fOqTZkNT.js +1 -0
  47. package/dist/assets/web/dist/assets/abap-CLvhMVsD.js +1 -0
  48. package/dist/assets/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
  49. package/dist/assets/web/dist/assets/ada-C5qYipkI.js +1 -0
  50. package/dist/assets/web/dist/assets/ahk-CA9gdOdF.js +1 -0
  51. package/dist/assets/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
  52. package/dist/assets/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
  53. package/dist/assets/web/dist/assets/angular-html-Cw130Zyi.js +1 -0
  54. package/dist/assets/web/dist/assets/angular-ts-DY1m4C2s.js +1 -0
  55. package/dist/assets/web/dist/assets/apache-U0d_L8uA.js +1 -0
  56. package/dist/assets/web/dist/assets/apex-KFGFyKwG.js +1 -0
  57. package/dist/assets/web/dist/assets/apl-W6Ty7v05.js +1 -0
  58. package/dist/assets/web/dist/assets/applescript-CCn79oCD.js +1 -0
  59. package/dist/assets/web/dist/assets/ara-4CJ0cIlV.js +1 -0
  60. package/dist/assets/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
  61. package/dist/assets/web/dist/assets/asm-Cmm7eHzH.js +1 -0
  62. package/dist/assets/web/dist/assets/astro-DlLVHwVk.js +1 -0
  63. package/dist/assets/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
  64. package/dist/assets/web/dist/assets/awk-BWXHIvNe.js +1 -0
  65. package/dist/assets/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
  66. package/dist/assets/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
  67. package/dist/assets/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  68. package/dist/assets/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
  69. package/dist/assets/web/dist/assets/bat-qdxAdBWQ.js +1 -0
  70. package/dist/assets/web/dist/assets/beancount-D-usSTwE.js +1 -0
  71. package/dist/assets/web/dist/assets/berry-DKpUyyne.js +1 -0
  72. package/dist/assets/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
  73. package/dist/assets/web/dist/assets/bicep-CUHmPFLl.js +1 -0
  74. package/dist/assets/web/dist/assets/bird2-CYIUgYqB.js +1 -0
  75. package/dist/assets/web/dist/assets/blade-Bco3tDh4.js +1 -0
  76. package/dist/assets/web/dist/assets/bsl-_VjlB9mv.js +1 -0
  77. package/dist/assets/web/dist/assets/c-1LaFY_cj.js +1 -0
  78. package/dist/assets/web/dist/assets/c3-D0apG41V.js +1 -0
  79. package/dist/assets/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
  80. package/dist/assets/web/dist/assets/cairo-DLTphjLi.js +1 -0
  81. package/dist/assets/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  82. package/dist/assets/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  83. package/dist/assets/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  84. package/dist/assets/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  85. package/dist/assets/web/dist/assets/chapel-B-OvrOL5.js +1 -0
  86. package/dist/assets/web/dist/assets/clarity-CtfBBspg.js +1 -0
  87. package/dist/assets/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
  88. package/dist/assets/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
  89. package/dist/assets/web/dist/assets/cobol-Ch3EPlHu.js +1 -0
  90. package/dist/assets/web/dist/assets/codeowners-C8r90Shi.js +1 -0
  91. package/dist/assets/web/dist/assets/codeql-oeQT6MSM.js +1 -0
  92. package/dist/assets/web/dist/assets/coffee-hZuITnto.js +1 -0
  93. package/dist/assets/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
  94. package/dist/assets/web/dist/assets/coq-DuBRPxYg.js +1 -0
  95. package/dist/assets/web/dist/assets/cpp-B8iRVo8m.js +1 -0
  96. package/dist/assets/web/dist/assets/crystal-DKveI7lr.js +1 -0
  97. package/dist/assets/web/dist/assets/csharp-oqKa8noW.js +1 -0
  98. package/dist/assets/web/dist/assets/css-_2_CD-rr.js +1 -0
  99. package/dist/assets/web/dist/assets/csv-Dx-8-gkx.js +1 -0
  100. package/dist/assets/web/dist/assets/cue-CE9AQfxI.js +1 -0
  101. package/dist/assets/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
  102. package/dist/assets/web/dist/assets/d-qD-0Kul2.js +1 -0
  103. package/dist/assets/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
  104. package/dist/assets/web/dist/assets/dart-CnvKMtbv.js +1 -0
  105. package/dist/assets/web/dist/assets/dax-BkyTk9wS.js +1 -0
  106. package/dist/assets/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
  107. package/dist/assets/web/dist/assets/diff-woXpYk--.js +1 -0
  108. package/dist/assets/web/dist/assets/dist-BVjAWgkb.js +1547 -0
  109. package/dist/assets/web/dist/assets/docker-IyjqRm3v.js +1 -0
  110. package/dist/assets/web/dist/assets/dotenv-_5a1GRtc.js +1 -0
  111. package/dist/assets/web/dist/assets/dracula-BHWKrbxM.js +1 -0
  112. package/dist/assets/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
  113. package/dist/assets/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
  114. package/dist/assets/web/dist/assets/edge-BenMDPct.js +1 -0
  115. package/dist/assets/web/dist/assets/elixir-CBbdEqG6.js +1 -0
  116. package/dist/assets/web/dist/assets/elm-DMub_2lY.js +1 -0
  117. package/dist/assets/web/dist/assets/emacs-lisp-c_oH4hRZ.js +1 -0
  118. package/dist/assets/web/dist/assets/erb-CI9He_Up.js +1 -0
  119. package/dist/assets/web/dist/assets/erlang-Cphh6RMH.js +1 -0
  120. package/dist/assets/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
  121. package/dist/assets/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
  122. package/dist/assets/web/dist/assets/{favicon-CFfbl2AI.js → favicon-CWPcvWvp.js} +1 -1
  123. package/dist/assets/web/dist/assets/fennel-DQxkIbk2.js +1 -0
  124. package/dist/assets/web/dist/assets/fish-BJitypiv.js +1 -0
  125. package/dist/assets/web/dist/assets/fluent-C03EYrpw.js +1 -0
  126. package/dist/assets/web/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  127. package/dist/assets/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
  128. package/dist/assets/web/dist/assets/fsharp-D13ZGOAj.js +1 -0
  129. package/dist/assets/web/dist/assets/gdresource-xMC7jr7x.js +1 -0
  130. package/dist/assets/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
  131. package/dist/assets/web/dist/assets/gdshader-CBce3t8t.js +1 -0
  132. package/dist/assets/web/dist/assets/genie-CV2tkWYe.js +1 -0
  133. package/dist/assets/web/dist/assets/gherkin-DExj1W_8.js +1 -0
  134. package/dist/assets/web/dist/assets/git-commit-BSykSTBG.js +1 -0
  135. package/dist/assets/web/dist/assets/git-rebase-BBfhnyF8.js +1 -0
  136. package/dist/assets/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
  137. package/dist/assets/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
  138. package/dist/assets/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  139. package/dist/assets/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  140. package/dist/assets/web/dist/assets/github-light-EUqPIrTm.js +1 -0
  141. package/dist/assets/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
  142. package/dist/assets/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  143. package/dist/assets/web/dist/assets/gleam-CSRkHgEL.js +1 -0
  144. package/dist/assets/web/dist/assets/glimmer-js-CR9Fxrau.js +1 -0
  145. package/dist/assets/web/dist/assets/glimmer-ts-CwFwX_XS.js +1 -0
  146. package/dist/assets/web/dist/assets/glsl-Daiodp1r.js +1 -0
  147. package/dist/assets/web/dist/assets/gn-ilITqXS6.js +1 -0
  148. package/dist/assets/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
  149. package/dist/assets/web/dist/assets/go-rLFTqkRN.js +1 -0
  150. package/dist/assets/web/dist/assets/graphql-DZvrsEkB.js +1 -0
  151. package/dist/assets/web/dist/assets/groovy-CacY0gHj.js +1 -0
  152. package/dist/assets/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  153. package/dist/assets/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  154. package/dist/assets/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  155. package/dist/assets/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  156. package/dist/assets/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  157. package/dist/assets/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  158. package/dist/assets/web/dist/assets/hack-BaXOzdg7.js +1 -0
  159. package/dist/assets/web/dist/assets/haml-CMskq_VA.js +1 -0
  160. package/dist/assets/web/dist/assets/handlebars-Dhw_f7jD.js +1 -0
  161. package/dist/assets/web/dist/assets/haskell-D8IpX4py.js +1 -0
  162. package/dist/assets/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
  163. package/dist/assets/web/dist/assets/hcl-Dh228itO.js +1 -0
  164. package/dist/assets/web/dist/assets/hjson-CxZEssPk.js +1 -0
  165. package/dist/assets/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
  166. package/dist/assets/web/dist/assets/horizon-CE9ld1lL.js +1 -0
  167. package/dist/assets/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
  168. package/dist/assets/web/dist/assets/houston-CsvMBhTu.js +1 -0
  169. package/dist/assets/web/dist/assets/html-C9k99z0z.js +1 -0
  170. package/dist/assets/web/dist/assets/html-derivative-VckPItXN.js +1 -0
  171. package/dist/assets/web/dist/assets/http-DXU_3h9v.js +1 -0
  172. package/dist/assets/web/dist/assets/hurl-KU24JNdp.js +1 -0
  173. package/dist/assets/web/dist/assets/hxml-Dcm88srW.js +1 -0
  174. package/dist/assets/web/dist/assets/hy-CZbG8q4J.js +1 -0
  175. package/dist/assets/web/dist/assets/imba-DsUTQ-LC.js +1 -0
  176. package/dist/assets/web/dist/assets/indexFormat-B-pd4r7Y.js +1 -0
  177. package/dist/assets/web/dist/assets/ini-B5eOa1yu.js +1 -0
  178. package/dist/assets/web/dist/assets/java-BbZjTNgf.js +1 -0
  179. package/dist/assets/web/dist/assets/javascript-DXCdqcZZ.js +1 -0
  180. package/dist/assets/web/dist/assets/jinja-pDlsjTVp.js +1 -0
  181. package/dist/assets/web/dist/assets/jison-DsXDk66e.js +1 -0
  182. package/dist/assets/web/dist/assets/json-tCxXfRgG.js +1 -0
  183. package/dist/assets/web/dist/assets/json5-BR5RXkoi.js +1 -0
  184. package/dist/assets/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
  185. package/dist/assets/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  186. package/dist/assets/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
  187. package/dist/assets/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
  188. package/dist/assets/web/dist/assets/jsx-B0ZYkCmi.js +1 -0
  189. package/dist/assets/web/dist/assets/julia-BLzzeI6x.js +1 -0
  190. package/dist/assets/web/dist/assets/just-DvD2_60c.js +1 -0
  191. package/dist/assets/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  192. package/dist/assets/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  193. package/dist/assets/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
  194. package/dist/assets/web/dist/assets/kdl-CsD5j6eV.js +1 -0
  195. package/dist/assets/web/dist/assets/kotlin-DhhofPvG.js +1 -0
  196. package/dist/assets/web/dist/assets/kusto-C7mF5XQf.js +1 -0
  197. package/dist/assets/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
  198. package/dist/assets/web/dist/assets/latex-511zn3h7.js +1 -0
  199. package/dist/assets/web/dist/assets/lean-CewbzKMR.js +1 -0
  200. package/dist/assets/web/dist/assets/less-DVTAwKKz.js +1 -0
  201. package/dist/assets/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
  202. package/dist/assets/web/dist/assets/liquid-BD0BkMox.js +1 -0
  203. package/dist/assets/web/dist/assets/llvm-DqHOMS-s.js +1 -0
  204. package/dist/assets/web/dist/assets/log-BNLmms1o.js +1 -0
  205. package/dist/assets/web/dist/assets/logo-Cluzi2Zq.js +1 -0
  206. package/dist/assets/web/dist/assets/lua-NFicKm6N.js +1 -0
  207. package/dist/assets/web/dist/assets/luau-C2pBCSgt.js +1 -0
  208. package/dist/assets/web/dist/assets/main-CuENKPdD.css +1 -0
  209. package/dist/assets/web/dist/assets/main-DZbJaqUb.js +28 -0
  210. package/dist/assets/web/dist/assets/make-Dixweg8N.js +1 -0
  211. package/dist/assets/web/dist/assets/markdown-BYOwaDjH.js +1 -0
  212. package/dist/assets/web/dist/assets/marko-1-G_nEXK.js +1 -0
  213. package/dist/assets/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
  214. package/dist/assets/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
  215. package/dist/assets/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
  216. package/dist/assets/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  217. package/dist/assets/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  218. package/dist/assets/web/dist/assets/matlab-D7qyCx1q.js +1 -0
  219. package/dist/assets/web/dist/assets/mdc-DUe3AQLp.js +1 -0
  220. package/dist/assets/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
  221. package/dist/assets/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
  222. package/dist/assets/web/dist/assets/min-dark-BSWPekZh.js +1 -0
  223. package/dist/assets/web/dist/assets/min-light-DDpmG2fV.js +1 -0
  224. package/dist/assets/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
  225. package/dist/assets/web/dist/assets/mojo-fg40_ntQ.js +1 -0
  226. package/dist/assets/web/dist/assets/monokai-B_bV9PQ9.js +1 -0
  227. package/dist/assets/web/dist/assets/moonbit-DgjMoLWr.js +1 -0
  228. package/dist/assets/web/dist/assets/move-BYnGhZoM.js +1 -0
  229. package/dist/assets/web/dist/assets/narrat-_X_XdTYD.js +1 -0
  230. package/dist/assets/web/dist/assets/nextflow-BJtWHP5T.js +1 -0
  231. package/dist/assets/web/dist/assets/nextflow-groovy-DJMQeKeT.js +1 -0
  232. package/dist/assets/web/dist/assets/nginx-Dg379_bA.js +1 -0
  233. package/dist/assets/web/dist/assets/night-owl-DhmEMT88.js +1 -0
  234. package/dist/assets/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
  235. package/dist/assets/web/dist/assets/nim-BnFq97ZO.js +1 -0
  236. package/dist/assets/web/dist/assets/nix-IvuFDN5E.js +1 -0
  237. package/dist/assets/web/dist/assets/nord-Cb4Vim4T.js +1 -0
  238. package/dist/assets/web/dist/assets/nsis-CjNLYCLg.js +1 -0
  239. package/dist/assets/web/dist/assets/nushell-DHYfO66s.js +1 -0
  240. package/dist/assets/web/dist/assets/objective-c-D1A_Heim.js +1 -0
  241. package/dist/assets/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
  242. package/dist/assets/web/dist/assets/ocaml-O90oeIOV.js +1 -0
  243. package/dist/assets/web/dist/assets/odin-B1RWQWA5.js +1 -0
  244. package/dist/assets/web/dist/assets/one-dark-pro-CLwyXe_n.js +1 -0
  245. package/dist/assets/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
  246. package/dist/assets/web/dist/assets/openscad-BUDT5pXO.js +1 -0
  247. package/dist/assets/web/dist/assets/org-F0uiwGvq.js +1 -0
  248. package/dist/assets/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
  249. package/dist/assets/web/dist/assets/perl-B0euczkl.js +1 -0
  250. package/dist/assets/web/dist/assets/php-DkL3k_n7.js +1 -0
  251. package/dist/assets/web/dist/assets/pierre-dark-CpLgRqie.js +1 -0
  252. package/dist/assets/web/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  253. package/dist/assets/web/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  254. package/dist/assets/web/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  255. package/dist/assets/web/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  256. package/dist/assets/web/dist/assets/pierre-light-CoaEpmwp.js +1 -0
  257. package/dist/assets/web/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  258. package/dist/assets/web/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  259. package/dist/assets/web/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  260. package/dist/assets/web/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  261. package/dist/assets/web/dist/assets/pkl-ot-7Btpt.js +1 -0
  262. package/dist/assets/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
  263. package/dist/assets/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
  264. package/dist/assets/web/dist/assets/po-BiJDBrnU.js +1 -0
  265. package/dist/assets/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
  266. package/dist/assets/web/dist/assets/polar-C7UOKdEL.js +1 -0
  267. package/dist/assets/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
  268. package/dist/assets/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
  269. package/dist/assets/web/dist/assets/powershell-CCSX75yM.js +1 -0
  270. package/dist/assets/web/dist/assets/prisma-DMgNhnYS.js +1 -0
  271. package/dist/assets/web/dist/assets/prolog-iXnhIJG7.js +1 -0
  272. package/dist/assets/web/dist/assets/proto-DB4EqR-F.js +1 -0
  273. package/dist/assets/web/dist/assets/pug-L_OjjZP4.js +1 -0
  274. package/dist/assets/web/dist/assets/puppet-CDv2pdJW.js +1 -0
  275. package/dist/assets/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
  276. package/dist/assets/web/dist/assets/python-gzcpVVnB.js +1 -0
  277. package/dist/assets/web/dist/assets/qml-BMjB00Zz.js +1 -0
  278. package/dist/assets/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
  279. package/dist/assets/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
  280. package/dist/assets/web/dist/assets/r-DoeLdnqR.js +1 -0
  281. package/dist/assets/web/dist/assets/racket-DcIDlBhZ.js +1 -0
  282. package/dist/assets/web/dist/assets/raku-B3gFvitq.js +1 -0
  283. package/dist/assets/web/dist/assets/razor-BzYNkAWy.js +1 -0
  284. package/dist/assets/web/dist/assets/rbs-C0MpOrj4.js +1 -0
  285. package/dist/assets/web/dist/assets/red-DX1BQOs8.js +1 -0
  286. package/dist/assets/web/dist/assets/reg-CRGYupPL.js +1 -0
  287. package/dist/assets/web/dist/assets/regexp-CPmElMk3.js +1 -0
  288. package/dist/assets/web/dist/assets/rel-BtDbiS_P.js +1 -0
  289. package/dist/assets/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
  290. package/dist/assets/web/dist/assets/ron-VUp2lXgN.js +1 -0
  291. package/dist/assets/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
  292. package/dist/assets/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  293. package/dist/assets/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  294. package/dist/assets/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
  295. package/dist/assets/web/dist/assets/rst-DrsjgfI7.js +1 -0
  296. package/dist/assets/web/dist/assets/ruby-cB42ppvx.js +1 -0
  297. package/dist/assets/web/dist/assets/rust-Cfkwpbl8.js +1 -0
  298. package/dist/assets/web/dist/assets/sas-R5Y3NM_I.js +1 -0
  299. package/dist/assets/web/dist/assets/sass-DXrisJhu.js +1 -0
  300. package/dist/assets/web/dist/assets/scala-BiFt_7mu.js +1 -0
  301. package/dist/assets/web/dist/assets/scheme-DQCgrYNe.js +1 -0
  302. package/dist/assets/web/dist/assets/scss-C-zWxV9s.js +1 -0
  303. package/dist/assets/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
  304. package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +6 -0
  305. package/dist/assets/web/dist/assets/shaderlab-TOUzSsQk.js +1 -0
  306. package/dist/assets/web/dist/assets/shellscript-oJF96aAU.js +1 -0
  307. package/dist/assets/web/dist/assets/shellsession-CznECKyi.js +1 -0
  308. package/dist/assets/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
  309. package/dist/assets/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
  310. package/dist/assets/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
  311. package/dist/assets/web/dist/assets/smithy-Dgaz7aur.js +1 -0
  312. package/dist/assets/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
  313. package/dist/assets/web/dist/assets/solarized-dark-UdsmMDCL.js +1 -0
  314. package/dist/assets/web/dist/assets/solarized-light-C1IUL_tW.js +1 -0
  315. package/dist/assets/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
  316. package/dist/assets/web/dist/assets/soy-wjHLSRag.js +1 -0
  317. package/dist/assets/web/dist/assets/sparql-D_iOobhT.js +1 -0
  318. package/dist/assets/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
  319. package/dist/assets/web/dist/assets/sql-C_BM8IOW.js +1 -0
  320. package/dist/assets/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
  321. package/dist/assets/web/dist/assets/stata-D31HO8aQ.js +1 -0
  322. package/dist/assets/web/dist/assets/stylus-B6D30XZt.js +1 -0
  323. package/dist/assets/web/dist/assets/surrealql-CKCLyplA.js +1 -0
  324. package/dist/assets/web/dist/assets/svelte-3geWk2iu.js +1 -0
  325. package/dist/assets/web/dist/assets/swift-CyEgAFGc.js +1 -0
  326. package/dist/assets/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
  327. package/dist/assets/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
  328. package/dist/assets/web/dist/assets/systemd-BxMlprV5.js +1 -0
  329. package/dist/assets/web/dist/assets/talonscript-CohzipZa.js +1 -0
  330. package/dist/assets/web/dist/assets/tasl-DMoTqEGO.js +1 -0
  331. package/dist/assets/web/dist/assets/tcl-CZd0xW_V.js +1 -0
  332. package/dist/assets/web/dist/assets/templ-DiJr_hTD.js +1 -0
  333. package/dist/assets/web/dist/assets/terraform-DswuEJGm.js +1 -0
  334. package/dist/assets/web/dist/assets/tex-BEJGWyeF.js +1 -0
  335. package/dist/assets/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
  336. package/dist/assets/web/dist/assets/toml-CcmNWLt0.js +1 -0
  337. package/dist/assets/web/dist/assets/ts-tags-C8Mzdpxx.js +1 -0
  338. package/dist/assets/web/dist/assets/tsv-sltzmVWM.js +1 -0
  339. package/dist/assets/web/dist/assets/tsx-UQiSm_p3.js +1 -0
  340. package/dist/assets/web/dist/assets/turtle-ByJddavk.js +1 -0
  341. package/dist/assets/web/dist/assets/twig-_Pvzkeo9.js +1 -0
  342. package/dist/assets/web/dist/assets/typescript-BWMkINKK.js +1 -0
  343. package/dist/assets/web/dist/assets/typespec-BRdr0IET.js +1 -0
  344. package/dist/assets/web/dist/assets/typst-Bdg9m-e7.js +1 -0
  345. package/dist/assets/web/dist/assets/v-DBj4K9d_.js +1 -0
  346. package/dist/assets/web/dist/assets/vala-zf12oZj6.js +1 -0
  347. package/dist/assets/web/dist/assets/vb-DOwzaHZ0.js +1 -0
  348. package/dist/assets/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
  349. package/dist/assets/web/dist/assets/vesper-DdrHHSXu.js +1 -0
  350. package/dist/assets/web/dist/assets/vhdl-BroJfC0k.js +1 -0
  351. package/dist/assets/web/dist/assets/viml-DvXPmvsu.js +1 -0
  352. package/dist/assets/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
  353. package/dist/assets/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
  354. package/dist/assets/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
  355. package/dist/assets/web/dist/assets/vue-CDZrrAZ8.js +1 -0
  356. package/dist/assets/web/dist/assets/vue-html-tAlpNhfN.js +1 -0
  357. package/dist/assets/web/dist/assets/vue-vine-C27UF_rh.js +1 -0
  358. package/dist/assets/web/dist/assets/vyper-CgoNMtux.js +1 -0
  359. package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +1 -0
  360. package/dist/assets/web/dist/assets/wasm-BnjxR4X6.js +1 -0
  361. package/dist/assets/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
  362. package/dist/assets/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
  363. package/dist/assets/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
  364. package/dist/assets/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
  365. package/dist/assets/web/dist/assets/wit-DdvCle-K.js +1 -0
  366. package/dist/assets/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
  367. package/dist/assets/web/dist/assets/xml-CxeDr9Zh.js +1 -0
  368. package/dist/assets/web/dist/assets/xsl-Bpi0uUnM.js +1 -0
  369. package/dist/assets/web/dist/assets/yaml-BtUgZ1GN.js +1 -0
  370. package/dist/assets/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
  371. package/dist/assets/web/dist/assets/zig-CMLA9XwU.js +1 -0
  372. package/dist/cli.js +6936 -3347
  373. package/package.json +1 -1
  374. package/dist/assets/web/dist/assets/AppShell-Bw3-_TTE.js +0 -1
  375. package/dist/assets/web/dist/assets/DeliveryPage-BRwcQyr7.js +0 -1
  376. package/dist/assets/web/dist/assets/NotFoundPage-RjH-9dPy.js +0 -1
  377. package/dist/assets/web/dist/assets/ResidentDetailPage-BRy5wkv9.js +0 -1
  378. package/dist/assets/web/dist/assets/ResidentsIndexPage-DAEVLN8j.js +0 -1
  379. package/dist/assets/web/dist/assets/RunRoutePage-B1KHmkZ9.js +0 -126
  380. package/dist/assets/web/dist/assets/RunRoutePage-CdbrbqU2.css +0 -1
  381. package/dist/assets/web/dist/assets/RunsIndexPage-3hUWFpFV.js +0 -1
  382. package/dist/assets/web/dist/assets/RunsTabs-Qn_5TVJU.js +0 -1
  383. package/dist/assets/web/dist/assets/ScheduledPage-DysVvVm1.js +0 -1
  384. package/dist/assets/web/dist/assets/StatusDot-Bug2a6T6.js +0 -1
  385. package/dist/assets/web/dist/assets/Tooltip-C2eEUbwn.js +0 -1
  386. package/dist/assets/web/dist/assets/main-0uhp-taL.js +0 -28
  387. package/dist/assets/web/dist/assets/main-Xickfv8V.css +0 -1
  388. package/dist/assets/web/dist/assets/seed-B8mQEhhw.js +0 -6
  389. package/dist/assets/web/dist/assets/wallClock-DI4HIEN5.js +0 -1
@@ -0,0 +1,1039 @@
1
+ // The plan runner's state machine (docs/decisions/0029-durable-objects-store-workflows-schedule.md,
2
+ // docs/decisions/0031-the-coordinator-runs-a-plan-not-a-pull-request.md;
3
+ // docs/reference/specs/agent-ship.md item 15): what the ship coordinator — a
4
+ // Workflow instance in the bot's shim Worker with no model turn and no
5
+ // credential — decides between the steps it asks the bot for. The Workflow
6
+ // asks `nextAction`, performs it (a bot route, a `waitForEvent`, a sleep) and
7
+ // feeds the answer to `applyReturn`; everything the in-process round loop
8
+ // decides today (`runShipPipeline`: which round is next, what a child's end
9
+ // means, when a cap ends the pipeline, when the pull request is merge-ready)
10
+ // is decided here over the step returns instead, so the same endings hold
11
+ // with the loop's process gone.
12
+ //
13
+ // Two machines, both pure. The plan cursor walks a plan record's unit graph:
14
+ // which units are ready (their dependencies merged), which one a failure
15
+ // blocks. The unit pipeline runs one unit's coding → review → fix loop to
16
+ // approve and the merge: one thread and one head branch per unit
17
+ // (`plan/<plan-id>/<unit-slug>`), every child a `dispatch()` run the bot
18
+ // starts as the requesting user, every step retry-safe under the key
19
+ // `<instance>:<unit>/<round>/<kind>`. Nothing here reads a clock: the bot
20
+ // answers every step with its own `at`, and that is the machine's time. Nothing
21
+ // here carries a task's text or a thread's contents: a spawn's brief names the
22
+ // unit and the runs whose records the bot reads to compose the child's turn.
23
+ //
24
+ // Worker-importable: the shim Worker's Workflow drives this machine, so the
25
+ // module reaches nothing but node-free modules — the child presets' budgets
26
+ // arrive in the input rather than from the agent registry.
27
+
28
+ import type { ShipRoundOutcome } from "../runEvents.js";
29
+ import type { RunStatus } from "../runRecord.js";
30
+ import { normalizeHead, sameCommit } from "../reviewedHead.js";
31
+ import { formatFinding, type Finding, type FindingDisposition, type ReviewVerdictKind } from "../reviewVerdict.js";
32
+ import { parsePlanUnit, planUnitIds } from "./contract.js";
33
+
34
+ const MIN = 60_000;
35
+
36
+ /** What a pipeline runs under: the rounds cap from the `ship` config block, and
37
+ * the wall clock from the parent's EFFECTIVE profile — the ship preset's
38
+ * declared budget as the profile gate clipped it, never the block read again. */
39
+ export interface ShipCaps {
40
+ maxRounds: number;
41
+ maxMinutes: number;
42
+ }
43
+
44
+ /** A round is dispatched only when at least this much of the pipeline budget
45
+ * remains (the reservation check, agent-ship item 8): a child clipped below
46
+ * this cannot do useful work, so the pipeline reports the cap instead of
47
+ * burning an attach and a model turn on a doomed round. */
48
+ export const SHIP_ROUND_RESERVE_MS = 3 * MIN;
49
+
50
+ /** What a ship pipeline's thread and card say when the bot died under it (run-
51
+ * history item 36): the work it did stands on GitHub with nobody driving it,
52
+ * so the note names the PR when one was opened and the exact re-issue that
53
+ * continues the loop — the same entry the preflight's resume-at-review takes
54
+ * (agent-ship item 10). Without a PR the task itself is the re-issue: round 0
55
+ * runs again on the pipeline's own deterministic branch. The coordinator says
56
+ * the same when a child of its closed `interrupted`. */
57
+ export function shipInterruptedNote(prUrl?: string): string {
58
+ const stands = prUrl
59
+ ? `Its work stands on GitHub: ${prUrl}.`
60
+ : "Whatever it pushed stands on its pipeline branch; no PR was opened yet.";
61
+ const reissue = prUrl
62
+ ? `To continue the review loop, re-issue \`agent:ship\` in this thread with only the PR URL (${prUrl}).`
63
+ : "To continue, re-issue `agent:ship` in this thread with the task — round 0 runs again on the same branch.";
64
+ return `⚠️ The bot restarted while this ship pipeline was running, so the pipeline stopped. ${stands} ${reissue}`;
65
+ }
66
+
67
+ // ---- the plan graph --------------------------------------------------------------------------------
68
+
69
+ /** One unit of a plan as the runner sees it: its heading, the units it waits on, its branch. */
70
+ export interface PlanUnitNode {
71
+ /** `U<n>` as the plan's heading spells it. */
72
+ id: string;
73
+ title: string;
74
+ /** `u<n>-<title slug>`, the branch's last segment. */
75
+ slug: string;
76
+ /** `plan/<plan-id>/<unit-slug>` — one head branch per unit, so two plans sharing a unit name never share one. */
77
+ branch: string;
78
+ /** The unit ids its Dependencies bullet names, in order, itself excluded. */
79
+ dependsOn: string[];
80
+ }
81
+
82
+ export interface PlanGraph {
83
+ planId: string;
84
+ /** In the plan's order. */
85
+ units: PlanUnitNode[];
86
+ }
87
+
88
+ /** A plan id: the plan file's name without its extension, lowercase, in the branch alphabet. */
89
+ export const PLAN_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,79}$/;
90
+ const PLAN_BRANCH = /^plan\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/;
91
+ /** A unit id in a Dependencies bullet, or a `U20 to U22` range naming every unit between, in the bullet's order. */
92
+ const UNIT_REF = /\bU(\d+)\s+to\s+U(\d+)\b|\bU\d+\b/g;
93
+ const SLUG_MAX = 24;
94
+ const INSTANCE_ID_MAX = 100;
95
+
96
+ /** `docs/plans/<date>-<n>-feat-x-plan.md` → `<date>-<n>-feat-x-plan`. Throws naming the path when the name is not a plan id. */
97
+ export function planIdOf(planPath: string): string {
98
+ const base = planPath.split("/").pop() ?? "";
99
+ const id = base.replace(/\.md$/i, "").toLowerCase();
100
+ if (!PLAN_ID_PATTERN.test(id))
101
+ throw new Error(`the plan's file name is not a plan id (lowercase letters, digits and hyphens): ${planPath}`);
102
+ return id;
103
+ }
104
+
105
+ /** The unit's slug: its lowercase id, then a bounded slug of its title. */
106
+ export function unitSlug(unit: { id: string; title: string }): string {
107
+ const title = unit.title
108
+ .toLowerCase()
109
+ .replace(/[^a-z0-9]+/g, "-")
110
+ .replace(/^-+|-+$/g, "")
111
+ .slice(0, SLUG_MAX)
112
+ .replace(/-+$/, "");
113
+ const id = unit.id.toLowerCase();
114
+ return title ? `${id}-${title}` : id;
115
+ }
116
+
117
+ export function unitBranch(planId: string, slug: string): string {
118
+ return `plan/${planId}/${slug}`;
119
+ }
120
+
121
+ /** The plan and unit a head branch names, or undefined for any other branch — the merge grant's line. */
122
+ export function parsePlanBranch(branch: string): { planId: string; unitSlug: string } | undefined {
123
+ const m = PLAN_BRANCH.exec(branch);
124
+ return m ? { planId: m[1]!, unitSlug: m[2]! } : undefined;
125
+ }
126
+
127
+ /** The instance id for a plan: it names the plan, so a second runner for the
128
+ * same plan meets the engine's duplicate-id refusal. */
129
+ export function planInstanceId(planId: string, attempt = 1): string {
130
+ const base = `plan-${planId}`;
131
+ if (attempt <= 1) return base.slice(0, INSTANCE_ID_MAX);
132
+ // A re-issue after an earlier attempt ended: the attempt suffix keeps the
133
+ // plan's name and never trims into it.
134
+ const suffix = `-${attempt}`;
135
+ return `${base.slice(0, INSTANCE_ID_MAX - suffix.length)}${suffix}`;
136
+ }
137
+
138
+ /** The Dependencies bullet's unit ids — lists, `to` ranges — the unit itself excluded. */
139
+ function dependenciesOf(text: string, self: string): string[] {
140
+ const out: string[] = [];
141
+ const add = (id: string) => {
142
+ if (id !== self && !out.includes(id)) out.push(id);
143
+ };
144
+ for (const m of text.matchAll(UNIT_REF)) {
145
+ if (m[1] === undefined) {
146
+ add(m[0]);
147
+ continue;
148
+ }
149
+ const from = Number(m[1]);
150
+ const to = Number(m[2]);
151
+ if (to >= from && to - from <= 100) for (let n = from; n <= to; n++) add(`U${n}`);
152
+ }
153
+ return out;
154
+ }
155
+
156
+ /** Every `### U<n>.` unit of the plan with its dependencies, slug and branch. */
157
+ export function parsePlanGraph(planMarkdown: string, planId: string): PlanGraph {
158
+ const units = planUnitIds(planMarkdown).map((id): PlanUnitNode => {
159
+ const unit = parsePlanUnit(planMarkdown, id)!;
160
+ const slug = unitSlug(unit);
161
+ return {
162
+ id,
163
+ title: unit.title,
164
+ slug,
165
+ branch: unitBranch(planId, slug),
166
+ dependsOn: dependenciesOf(unit.bullets.Dependencies ?? "", id),
167
+ };
168
+ });
169
+ return { planId, units };
170
+ }
171
+
172
+ /** `plan <path>.md [units U<n>, U<m>]` — the ship request that names a plan
173
+ * instead of a task; anything else is a task string. */
174
+ export function parseShipPlanRequest(task: string): { planPath: string; units?: string[] } | undefined {
175
+ const m = /^plan\s+(\S+\.md)(?:\s+units?\s+((?:U\d+[\s,]*)+))?\s*$/i.exec(task.trim());
176
+ if (!m) return undefined;
177
+ const units = m[2] ? [...m[2].matchAll(/U\d+/gi)].map((u) => u[0].toUpperCase()) : undefined;
178
+ return { planPath: m[1]!, ...(units && units.length > 0 ? { units } : {}) };
179
+ }
180
+
181
+ // ---- the plan cursor -------------------------------------------------------------------------------
182
+
183
+ export type UnitStatus = "pending" | "running" | "done" | "failed" | "blocked";
184
+
185
+ /** The outer state: which of the plan's units are in play and where each stands. */
186
+ export interface PlanCursor {
187
+ /** The selected units in the plan's order. */
188
+ order: string[];
189
+ status: Readonly<Record<string, UnitStatus>>;
190
+ }
191
+
192
+ function nodeOf(graph: PlanGraph, id: string): PlanUnitNode {
193
+ const node = graph.units.find((u) => u.id === id);
194
+ if (!node) throw new Error(`the plan has no unit ${id} (its units: ${graph.units.map((u) => u.id).join(", ")})`);
195
+ return node;
196
+ }
197
+
198
+ /** The dependencies of `id` that are in play: one outside the selection is the
199
+ * requester's assertion that it is done. */
200
+ function selectedDependencies(graph: PlanGraph, cursor: PlanCursor, id: string): string[] {
201
+ return nodeOf(graph, id).dependsOn.filter((d) => d in cursor.status);
202
+ }
203
+
204
+ /** Open the cursor over the selected units (every unit when none are named).
205
+ * Throws on a unit the plan lacks and on a dependency cycle inside the selection. */
206
+ export function openPlanCursor(graph: PlanGraph, selected?: readonly string[]): PlanCursor {
207
+ const chosen = new Set(selected ?? graph.units.map((u) => u.id));
208
+ for (const id of chosen) nodeOf(graph, id);
209
+ const order = graph.units.map((u) => u.id).filter((id) => chosen.has(id));
210
+ const status: Record<string, UnitStatus> = {};
211
+ for (const id of order) status[id] = "pending";
212
+ const cursor: PlanCursor = { order, status };
213
+ // A cycle among the selected units would leave them pending forever.
214
+ const visiting = new Set<string>();
215
+ const done = new Set<string>();
216
+ const visit = (id: string, path: string[]) => {
217
+ if (done.has(id)) return;
218
+ if (visiting.has(id)) throw new Error(`the plan's units form a dependency cycle: ${[...path, id].join(" → ")}`);
219
+ visiting.add(id);
220
+ for (const dep of selectedDependencies(graph, cursor, id)) visit(dep, [...path, id]);
221
+ visiting.delete(id);
222
+ done.add(id);
223
+ };
224
+ for (const id of order) visit(id, []);
225
+ return cursor;
226
+ }
227
+
228
+ /** The pending units whose in-play dependencies are all done, in the plan's order. */
229
+ export function readyUnits(graph: PlanGraph, cursor: PlanCursor): string[] {
230
+ return cursor.order.filter(
231
+ (id) =>
232
+ cursor.status[id] === "pending" &&
233
+ selectedDependencies(graph, cursor, id).every((dep) => cursor.status[dep] === "done"),
234
+ );
235
+ }
236
+
237
+ /** Start a ready unit; anything else — a unit outside the selection, one already
238
+ * started or settled, one whose dependencies are not done — throws by name. */
239
+ export function startUnit(graph: PlanGraph, cursor: PlanCursor, id: string): PlanCursor {
240
+ const status = cursor.status[id];
241
+ if (status === undefined) throw new Error(`unit ${id} is not in the plan's selection`);
242
+ if (status !== "pending") throw new Error(`unit ${id} is ${status}, not pending`);
243
+ if (!readyUnits(graph, cursor).includes(id)) {
244
+ const waiting = selectedDependencies(graph, cursor, id).filter((d) => cursor.status[d] !== "done");
245
+ throw new Error(`unit ${id} is not ready: it waits on ${waiting.join(", ")}`);
246
+ }
247
+ return { ...cursor, status: { ...cursor.status, [id]: "running" } };
248
+ }
249
+
250
+ /** A unit's end: `done` (merged) frees its dependents; `failed` (anything
251
+ * else — a conflict, a cap, a stop, a refused merge) blocks them, transitively,
252
+ * and leaves every other ready unit in play. */
253
+ export function settleUnit(graph: PlanGraph, cursor: PlanCursor, id: string, ending: "done" | "failed"): PlanCursor {
254
+ if (cursor.status[id] !== "running")
255
+ throw new Error(`unit ${id} is ${cursor.status[id] ?? "not selected"}, not running`);
256
+ const status: Record<string, UnitStatus> = { ...cursor.status, [id]: ending };
257
+ if (ending === "failed") {
258
+ let changed = true;
259
+ while (changed) {
260
+ changed = false;
261
+ for (const unit of cursor.order) {
262
+ if (status[unit] !== "pending") continue;
263
+ const blocked = nodeOf(graph, unit).dependsOn.some(
264
+ (dep) => dep in status && (status[dep] === "failed" || status[dep] === "blocked"),
265
+ );
266
+ if (blocked) {
267
+ status[unit] = "blocked";
268
+ changed = true;
269
+ }
270
+ }
271
+ }
272
+ }
273
+ return { ...cursor, status };
274
+ }
275
+
276
+ export function cursorFinished(cursor: PlanCursor): boolean {
277
+ return cursor.order.every((id) => cursor.status[id] !== "pending" && cursor.status[id] !== "running");
278
+ }
279
+
280
+ // ---- the unit pipeline: shapes --------------------------------------------------------------------
281
+
282
+ export type RoundKind = "coding" | "review" | "fix";
283
+ export interface RoundRef {
284
+ /** Round 0 is the coding round; review round n and its fix round share n. */
285
+ index: number;
286
+ kind: RoundKind;
287
+ }
288
+ export type ChildPreset = "coding" | "review";
289
+
290
+ /** The preset a round's child runs as: a fix round is a coding child. */
291
+ export function presetOf(kind: RoundKind): ChildPreset {
292
+ return kind === "review" ? "review" : "coding";
293
+ }
294
+
295
+ /** How a spawn's child is briefed — ids only, never text. The bot composes the
296
+ * turn: the unit's contract, or the review turn from the pull request and the
297
+ * prior rounds' records, or the fix turn from the review run's verdict. */
298
+ export type Brief =
299
+ | { kind: "contract"; unit: string; rebase: { branch: string; onto: string } }
300
+ | {
301
+ kind: "review";
302
+ unit: string;
303
+ pr: number;
304
+ headSha?: string;
305
+ round: number;
306
+ /** The previous review round's run and the fix round that answered it, for a re-review. */
307
+ prior?: { reviewRunId: string; fixRunId?: string };
308
+ }
309
+ | { kind: "fix"; unit: string; pr: number; reviewRunId: string };
310
+
311
+ export interface PrRef {
312
+ number: number;
313
+ url: string;
314
+ }
315
+
316
+ /** What the coordinator asks for next. `step` is the Workflow step's name, and
317
+ * for a spawn the second half of the child's idempotency key. */
318
+ export type CoordinatorAction =
319
+ | { type: "branch"; step: string; branch: string; from: string }
320
+ | { type: "spawn"; step: string; preset: ChildPreset; round: RoundRef; budgetMinutes: number; brief: Brief }
321
+ | { type: "wait"; step: string; runId: string; timeoutMs: number }
322
+ | { type: "read-record"; step: string; runId: string }
323
+ | { type: "pr-check"; step: string }
324
+ | { type: "merge"; step: string; prNumber: number; headSha: string }
325
+ | { type: "sleep"; step: string; ms: number }
326
+ | { type: "end"; step: string; ending: UnitEnding };
327
+
328
+ /** A child's facts as `read-record` answers them: live, or finished with the
329
+ * typed artifacts its run recorded. */
330
+ export type ChildFacts =
331
+ | { finished: false }
332
+ | {
333
+ finished: true;
334
+ status: RunStatus;
335
+ finalReply?: string;
336
+ /** A coding child's `pr_opened`. */
337
+ pr?: { number: number; url: string; created: boolean };
338
+ /** The head the coding child left the branch at, when its run observed one. */
339
+ headSha?: string;
340
+ description?: boolean;
341
+ /** A review child's verdict. */
342
+ verdict?: { verdict: ReviewVerdictKind; summary?: string; findings: Finding[] };
343
+ /** Whether the review child's verdict landed on the pull request. */
344
+ reviewPosted?: boolean;
345
+ reviewHead?: string;
346
+ /** A fix child's dispositions. */
347
+ dispositions?: FindingDisposition[];
348
+ handoff?: boolean;
349
+ };
350
+
351
+ export type PrCheck = { state: "none" } | { state: "open"; prNumber: number; url: string; headSha?: string };
352
+
353
+ /** What a step answered. Every bot answer carries `at`, the bot's clock — the machine's time. */
354
+ export type StepReturn =
355
+ | { type: "branch"; step: string; ok: true; at: number }
356
+ | { type: "branch"; step: string; ok: false; reason: string; at: number }
357
+ | { type: "spawn"; step: string; outcome: "spawned" | "alreadySpawned"; runId: string; at: number }
358
+ | { type: "spawn"; step: string; outcome: "busy"; runId?: string; at: number }
359
+ | { type: "spawn"; step: string; outcome: "refused"; refusal: string; message?: string; at: number }
360
+ | { type: "spawn"; step: string; outcome: "failed"; reason: string; at: number }
361
+ | { type: "wait"; step: string; outcome: "event" | "timeout" }
362
+ | { type: "read-record"; step: string; run: ChildFacts; at: number }
363
+ | { type: "pr-check"; step: string; pr: PrCheck; at: number }
364
+ | { type: "merge"; step: string; outcome: "merged"; sha: string; at: number }
365
+ | { type: "merge"; step: string; outcome: "pending" | "refused"; reason: string; at: number }
366
+ | { type: "sleep"; step: string };
367
+
368
+ /** How one unit's pipeline ended — the truthful vocabulary the in-process loop
369
+ * has, plus the merge's own: `merged` by the runner, `merge_ready` for a
370
+ * person, `merge_refused` by the guards; `interrupted` a child the ledger
371
+ * closed; `refused` a child the authorize stage never started. */
372
+ export type UnitEnding =
373
+ | { kind: "merged"; pr: PrRef; sha: string; reviewRounds: number }
374
+ | { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
375
+ | { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
376
+ | { kind: "round_cap"; maxRounds: number; reviewRounds: number }
377
+ | { kind: "wall_clock_cap"; remainingMs: number; reviewRounds: number }
378
+ | {
379
+ kind: "stopped";
380
+ mode: "soft" | "hard";
381
+ round: RoundRef;
382
+ reviewRounds: number;
383
+ finalReply?: string;
384
+ /** A changes-requested review posted this round before the stop. */
385
+ postedReview?: boolean;
386
+ }
387
+ | { kind: "aborted"; reason: string; round?: RoundRef; reviewRounds: number; finalReply?: string }
388
+ | { kind: "no_verdict"; round: RoundRef; reviewRounds: number; finalReply?: string }
389
+ | { kind: "interrupted"; round: RoundRef; runId: string; reviewRounds: number }
390
+ | { kind: "refused"; refusal: string; message?: string; round: RoundRef; reviewRounds: number };
391
+
392
+ /** What a transition tells the driver beyond the next action: a round boundary
393
+ * the card draws (`shipRoundHeader`) and the run stream records, and the end. */
394
+ export type CoordinatorNote =
395
+ | { type: "round"; index: number; agent: ChildPreset; outcome: ShipRoundOutcome }
396
+ | { type: "ended"; ending: UnitEnding };
397
+
398
+ export interface UnitPipelineInput {
399
+ unit: { id: string; branch: string };
400
+ repo: string;
401
+ /** The pull request's base — the branch the unit is created from and rebased onto. */
402
+ base: string;
403
+ caps: ShipCaps;
404
+ /** Each child preset's own wall-clock budget (its `maxMinutes`), the number a
405
+ * round's budget is clipped from — supplied by the bot, which holds the registry. */
406
+ childMinutes: Readonly<Record<ChildPreset, number>>;
407
+ /** Who merges: the runner (a plan branch, under its grant) or a person (any other branch). */
408
+ merge: "runner" | "person";
409
+ /** Resume at review: an open pull request of ship's own the requester named. */
410
+ resume?: { pr: number; headSha?: string; url?: string };
411
+ }
412
+
413
+ type Phase =
414
+ | { at: "branch" }
415
+ | { at: "spawn"; round: RoundRef; busy: number }
416
+ | { at: "busy-wait"; round: RoundRef; runId?: string; n: number }
417
+ | { at: "wait"; round: RoundRef; runId: string; n: number; budgetMinutes: number }
418
+ | { at: "read"; round: RoundRef; runId: string; n: number; budgetMinutes: number }
419
+ | { at: "pr-check"; round: RoundRef; runId: string; childHead?: string; finalReply?: string }
420
+ | { at: "merge"; pr: PrRef; headSha: string; n: number; since: number }
421
+ | { at: "merge-sleep"; pr: PrRef; headSha: string; n: number; since: number }
422
+ | { at: "ended" };
423
+
424
+ export interface UnitPipelineState {
425
+ readonly input: UnitPipelineInput;
426
+ readonly startedAt: number;
427
+ /** The last `at` a step answered with. */
428
+ readonly clock: number;
429
+ readonly phase: Phase;
430
+ /** Review rounds started so far. */
431
+ readonly reviewRounds: number;
432
+ readonly pr?: PrRef;
433
+ readonly lastReviewHead?: string;
434
+ readonly lastVerdictSummary?: string;
435
+ /** Findings per review round and dispositions per fix round, keyed by the
436
+ * review round they belong to — finding ids are unique within one round only. */
437
+ readonly findingsByRound: Readonly<Record<number, Finding[]>>;
438
+ readonly dispositionsByRound: Readonly<Record<number, FindingDisposition[]>>;
439
+ readonly reviewRunByRound: Readonly<Record<number, string>>;
440
+ readonly fixRunByRound: Readonly<Record<number, string>>;
441
+ /** The last coding child (round 0 or a fix round): its record carries the unit's handoff. */
442
+ readonly lastCodingRunId?: string;
443
+ readonly ending?: UnitEnding;
444
+ }
445
+
446
+ /** Past a child's budget, the parent asks the bot instead of waiting on. */
447
+ export const WAIT_MARGIN_MS = 5 * MIN;
448
+ /** How often the runner asks for the merge while the guards are still pending, and for how long at most. */
449
+ export const MERGE_POLL_MS = 5 * MIN;
450
+ export const MERGE_WAIT_MAX_MS = 60 * MIN;
451
+ /** A `busy` without the live run's id: nothing to wait on, so a short sleep before the spawn is asked again. */
452
+ export const BUSY_RETRY_MS = 2 * MIN;
453
+
454
+ // ---- the unit pipeline: opening and the next action -----------------------------------------------
455
+
456
+ export function openUnitPipeline(input: UnitPipelineInput, at: number): UnitPipelineState {
457
+ const base: UnitPipelineState = {
458
+ input,
459
+ startedAt: at,
460
+ clock: at,
461
+ phase: { at: "branch" },
462
+ reviewRounds: 0,
463
+ findingsByRound: {},
464
+ dispositionsByRound: {},
465
+ reviewRunByRound: {},
466
+ fixRunByRound: {},
467
+ };
468
+ if (!input.resume) return base;
469
+ const url = input.resume.url ?? `https://github.com/${input.repo}/pull/${input.resume.pr}`;
470
+ const resumed: UnitPipelineState = {
471
+ ...base,
472
+ pr: { number: input.resume.pr, url },
473
+ ...(input.resume.headSha !== undefined ? { lastReviewHead: input.resume.headSha } : {}),
474
+ };
475
+ return nextReview(resumed).state;
476
+ }
477
+
478
+ const deadlineAt = (s: UnitPipelineState) => s.startedAt + s.input.caps.maxMinutes * MIN;
479
+ const remainingMs = (s: UnitPipelineState) => deadlineAt(s) - s.clock;
480
+
481
+ /** The child's budget: its preset's own, clipped to the pipeline's remaining
482
+ * wall clock (the in-process loop's `clip`), never under the two minutes a
483
+ * spawn accepts. */
484
+ function budgetMinutesFor(s: UnitPipelineState, preset: ChildPreset): number {
485
+ return Math.max(2, Math.min(s.input.childMinutes[preset], Math.floor(remainingMs(s) / MIN)));
486
+ }
487
+
488
+ const roundStep = (s: UnitPipelineState, round: RoundRef) => `${s.input.unit.id}/${round.index}/${round.kind}`;
489
+
490
+ function briefFor(s: UnitPipelineState, round: RoundRef): Brief {
491
+ const unit = s.input.unit.id;
492
+ if (round.kind === "coding")
493
+ return { kind: "contract", unit, rebase: { branch: s.input.unit.branch, onto: s.input.base } };
494
+ const pr = s.pr!.number;
495
+ if (round.kind === "fix") return { kind: "fix", unit, pr, reviewRunId: s.reviewRunByRound[round.index]! };
496
+ const priorReview = s.reviewRunByRound[round.index - 1];
497
+ const priorFix = s.fixRunByRound[round.index - 1];
498
+ return {
499
+ kind: "review",
500
+ unit,
501
+ pr,
502
+ ...(s.lastReviewHead !== undefined ? { headSha: s.lastReviewHead } : {}),
503
+ round: round.index,
504
+ ...(priorReview !== undefined
505
+ ? { prior: { reviewRunId: priorReview, ...(priorFix !== undefined ? { fixRunId: priorFix } : {}) } }
506
+ : {}),
507
+ };
508
+ }
509
+
510
+ /** The step the machine is at. Pure over the state: asked before every step
511
+ * and again after a replay, it names the same step for the same state. */
512
+ export function nextAction(s: UnitPipelineState): CoordinatorAction {
513
+ const unit = s.input.unit.id;
514
+ const p = s.phase;
515
+ switch (p.at) {
516
+ case "branch":
517
+ return { type: "branch", step: `${unit}/branch`, branch: s.input.unit.branch, from: s.input.base };
518
+ case "spawn": {
519
+ const preset = presetOf(p.round.kind);
520
+ return {
521
+ type: "spawn",
522
+ step: roundStep(s, p.round),
523
+ preset,
524
+ round: p.round,
525
+ budgetMinutes: budgetMinutesFor(s, preset),
526
+ brief: briefFor(s, p.round),
527
+ };
528
+ }
529
+ case "busy-wait": {
530
+ const step = `${roundStep(s, p.round)}/busy/${p.n}`;
531
+ if (p.runId === undefined) return { type: "sleep", step, ms: BUSY_RETRY_MS };
532
+ return { type: "wait", step, runId: p.runId, timeoutMs: Math.max(0, remainingMs(s)) + WAIT_MARGIN_MS };
533
+ }
534
+ case "wait":
535
+ return {
536
+ type: "wait",
537
+ step: `${roundStep(s, p.round)}/wait/${p.n}`,
538
+ runId: p.runId,
539
+ timeoutMs: p.budgetMinutes * MIN + WAIT_MARGIN_MS,
540
+ };
541
+ case "read":
542
+ return { type: "read-record", step: `${roundStep(s, p.round)}/read/${p.n}`, runId: p.runId };
543
+ case "pr-check":
544
+ return { type: "pr-check", step: `${roundStep(s, p.round)}/pr-check` };
545
+ case "merge":
546
+ return { type: "merge", step: `${unit}/merge/${p.n}`, prNumber: p.pr.number, headSha: p.headSha };
547
+ case "merge-sleep":
548
+ return { type: "sleep", step: `${unit}/merge/sleep/${p.n}`, ms: MERGE_POLL_MS };
549
+ case "ended":
550
+ return { type: "end", step: `${unit}/end`, ending: s.ending! };
551
+ }
552
+ }
553
+
554
+ // ---- the unit pipeline: transitions -----------------------------------------------------------------
555
+
556
+ interface Transition {
557
+ state: UnitPipelineState;
558
+ notes: CoordinatorNote[];
559
+ }
560
+
561
+ const ENDED: Phase = { at: "ended" };
562
+
563
+ function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[] = []): Transition {
564
+ return { state: { ...s, phase: ENDED, ending }, notes: [...notes, { type: "ended", ending }] };
565
+ }
566
+
567
+ const roundNote = (round: RoundRef, outcome: ShipRoundOutcome): CoordinatorNote => ({
568
+ type: "round",
569
+ index: round.index,
570
+ agent: presetOf(round.kind),
571
+ outcome,
572
+ });
573
+
574
+ /** Start a round if the reservation holds (the in-process loop's check: a
575
+ * child clipped under the reserve cannot do useful work). */
576
+ function enterRound(s: UnitPipelineState, round: RoundRef, notes: CoordinatorNote[] = []): Transition {
577
+ const remaining = remainingMs(s);
578
+ if (remaining < SHIP_ROUND_RESERVE_MS)
579
+ return end(s, { kind: "wall_clock_cap", remainingMs: remaining, reviewRounds: s.reviewRounds }, notes);
580
+ const reviewRounds = round.kind === "review" ? round.index : s.reviewRounds;
581
+ return { state: { ...s, reviewRounds, phase: { at: "spawn", round, busy: 0 } }, notes };
582
+ }
583
+
584
+ /** The next review round, or the round cap. */
585
+ function nextReview(s: UnitPipelineState, notes: CoordinatorNote[] = []): Transition {
586
+ if (s.reviewRounds >= s.input.caps.maxRounds)
587
+ return end(s, { kind: "round_cap", maxRounds: s.input.caps.maxRounds, reviewRounds: s.reviewRounds }, notes);
588
+ return enterRound(s, { index: s.reviewRounds + 1, kind: "review" }, notes);
589
+ }
590
+
591
+ const stopMode = (status: RunStatus): "soft" | "hard" | undefined =>
592
+ status === "stopped_soft" ? "soft" : status === "stopped_hard" ? "hard" : undefined;
593
+
594
+ /** A coding or fix child's confirmed end. */
595
+ function settleCoding(
596
+ s: UnitPipelineState,
597
+ round: RoundRef,
598
+ runId: string,
599
+ facts: Extract<ChildFacts, { finished: true }>,
600
+ ): Transition {
601
+ // Recorded before the checks below: a pull request is a fact a stop must
602
+ // still report, and submitted dispositions are a fact no ending erases.
603
+ let next: UnitPipelineState = {
604
+ ...s,
605
+ ...(facts.pr !== undefined ? { pr: { number: facts.pr.number, url: facts.pr.url } } : {}),
606
+ ...(round.kind === "fix" && facts.dispositions !== undefined
607
+ ? { dispositionsByRound: { ...s.dispositionsByRound, [round.index]: facts.dispositions } }
608
+ : {}),
609
+ };
610
+ const mode = stopMode(facts.status);
611
+ if (mode !== undefined)
612
+ return end(
613
+ next,
614
+ {
615
+ kind: "stopped",
616
+ mode,
617
+ round,
618
+ reviewRounds: next.reviewRounds,
619
+ ...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
620
+ },
621
+ [roundNote(round, "stopped")],
622
+ );
623
+ if (facts.status === "failed")
624
+ return end(
625
+ next,
626
+ {
627
+ kind: "aborted",
628
+ reason: `⚠️ The coding child of round ${round.index} (run ${runId}) ended \`failed\` — its run page has the error; nothing was opened or edited from it.`,
629
+ round,
630
+ reviewRounds: next.reviewRounds,
631
+ },
632
+ [roundNote(round, "aborted")],
633
+ );
634
+ next = {
635
+ ...next,
636
+ phase: {
637
+ at: "pr-check",
638
+ round,
639
+ runId,
640
+ ...(facts.headSha !== undefined ? { childHead: facts.headSha } : {}),
641
+ ...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
642
+ },
643
+ };
644
+ return { state: next, notes: [] };
645
+ }
646
+
647
+ /** A review child's confirmed end. */
648
+ function settleReview(
649
+ s: UnitPipelineState,
650
+ round: RoundRef,
651
+ facts: Extract<ChildFacts, { finished: true }>,
652
+ ): Transition {
653
+ const mode = stopMode(facts.status);
654
+ if (!facts.verdict) {
655
+ if (mode !== undefined)
656
+ return end(
657
+ s,
658
+ {
659
+ kind: "stopped",
660
+ mode,
661
+ round,
662
+ reviewRounds: s.reviewRounds,
663
+ ...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
664
+ },
665
+ [roundNote(round, "stopped")],
666
+ );
667
+ return end(
668
+ s,
669
+ {
670
+ kind: "no_verdict",
671
+ round,
672
+ reviewRounds: s.reviewRounds,
673
+ ...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
674
+ },
675
+ [roundNote(round, "no_verdict")],
676
+ );
677
+ }
678
+ const verdict = facts.verdict;
679
+ const next: UnitPipelineState = {
680
+ ...s,
681
+ findingsByRound: { ...s.findingsByRound, [round.index]: verdict.findings },
682
+ ...(facts.reviewHead !== undefined ? { lastReviewHead: facts.reviewHead } : {}),
683
+ ...(verdict.summary !== undefined ? { lastVerdictSummary: verdict.summary } : {}),
684
+ };
685
+ const notes = [roundNote(round, verdict.verdict)];
686
+ if (verdict.verdict === "approve") {
687
+ // Merge-ready stands on the POSTED approval: an approve whose post did
688
+ // not land left no approving review on the pull request.
689
+ if (facts.reviewPosted === false)
690
+ return end(
691
+ next,
692
+ {
693
+ kind: "aborted",
694
+ reason: `⚠️ The review approved, but the approval could not be posted — the pull request carries no approving review. Re-run ship with the pull request URL to retry the approval.`,
695
+ round,
696
+ reviewRounds: next.reviewRounds,
697
+ },
698
+ notes,
699
+ );
700
+ const pr = next.pr!;
701
+ if (next.input.merge !== "runner")
702
+ return end(next, { kind: "merge_ready", pr, reviewRounds: next.reviewRounds }, notes);
703
+ // The runner merges only at the head the review approved; with none known
704
+ // there is nothing to pin the merge to, and a person decides.
705
+ const headSha = next.lastReviewHead;
706
+ if (headSha === undefined)
707
+ return end(
708
+ next,
709
+ { kind: "merge_refused", pr, reason: "no approved head is known to merge at", reviewRounds: next.reviewRounds },
710
+ notes,
711
+ );
712
+ return { state: { ...next, phase: { at: "merge", pr, headSha, n: 1, since: next.clock } }, notes };
713
+ }
714
+ // request_changes: the verdict settled (and posted) — now a stop
715
+ // short-circuits the fix round, naming the review standing on the pull request.
716
+ if (mode !== undefined)
717
+ return end(
718
+ next,
719
+ {
720
+ kind: "stopped",
721
+ mode,
722
+ round,
723
+ reviewRounds: next.reviewRounds,
724
+ ...(facts.finalReply !== undefined ? { finalReply: facts.finalReply } : {}),
725
+ ...(facts.reviewPosted === true ? { postedReview: true } : {}),
726
+ },
727
+ notes,
728
+ );
729
+ if (next.reviewRounds >= next.input.caps.maxRounds)
730
+ return end(
731
+ next,
732
+ { kind: "round_cap", maxRounds: next.input.caps.maxRounds, reviewRounds: next.reviewRounds },
733
+ notes,
734
+ );
735
+ return enterRound(next, { index: round.index, kind: "fix" }, notes);
736
+ }
737
+
738
+ /** The pull request heading the branch after a coding or fix round. */
739
+ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-check" }>, pr: PrCheck): Transition {
740
+ const { round } = phase;
741
+ if (pr.state === "none") {
742
+ const reason =
743
+ round.index === 0
744
+ ? `⚠️ Ship ended at round 0: the coding round ended without opening a pull request (a clarifying question, a budget write-up, an unproven push or a description-less push ends the pipeline here). No review round ran.`
745
+ : `⚠️ Fix round ${round.index} left no open pull request heading \`${s.input.unit.branch}\` — the pull request was closed out from under the pipeline and none was reopened, so there is nothing to re-review.`;
746
+ return end(
747
+ s,
748
+ {
749
+ kind: "aborted",
750
+ reason,
751
+ round,
752
+ reviewRounds: s.reviewRounds,
753
+ ...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
754
+ },
755
+ [roundNote(round, "aborted")],
756
+ );
757
+ }
758
+ const head = pr.headSha ?? phase.childHead;
759
+ const next: UnitPipelineState = { ...s, pr: { number: pr.prNumber, url: pr.url } };
760
+ if (round.kind === "fix") {
761
+ // Nothing repushed → nothing to re-review, unless every finding of the
762
+ // last review was declined on the record: that re-review verifies the
763
+ // arguments and may concede.
764
+ const fixHead = normalizeHead(head);
765
+ const reviewedAt = normalizeHead(s.lastReviewHead);
766
+ if (fixHead !== undefined && reviewedAt !== undefined && sameCommit(fixHead, reviewedAt)) {
767
+ const findings = s.findingsByRound[round.index] ?? [];
768
+ const dispositions = s.dispositionsByRound[round.index] ?? [];
769
+ const allDeclined =
770
+ findings.length > 0 &&
771
+ findings.every((f) => dispositions.find((d) => d.findingId === f.id)?.disposition === "declined");
772
+ if (!allDeclined)
773
+ return end(
774
+ next,
775
+ {
776
+ kind: "aborted",
777
+ reason: `⚠️ Fix round ${round.index} produced no new head — the branch still sits at \`${fixHead.slice(0, 7)}\`, the commit the review already read, and not every finding was declined on the record, so there is nothing new to re-review.`,
778
+ round,
779
+ reviewRounds: next.reviewRounds,
780
+ },
781
+ [roundNote(round, "aborted")],
782
+ );
783
+ }
784
+ }
785
+ return nextReview({ ...next, ...(head !== undefined ? { lastReviewHead: head } : {}) }, [
786
+ roundNote(round, "pr_opened"),
787
+ ]);
788
+ }
789
+
790
+ /**
791
+ * Feed a step's answer to the machine. An answer for any step but the one the
792
+ * machine is at — a duplicate `run finished`, a replayed spawn — changes
793
+ * nothing, so every return is safe to apply twice.
794
+ */
795
+ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
796
+ const expected = nextAction(s);
797
+ if (expected.type === "end" || ret.step !== expected.step || ret.type !== expected.type)
798
+ return { state: s, notes: [] };
799
+ const clocked: UnitPipelineState = "at" in ret ? { ...s, clock: ret.at } : s;
800
+ const p = s.phase;
801
+ switch (p.at) {
802
+ case "branch": {
803
+ const r = ret as Extract<StepReturn, { type: "branch" }>;
804
+ if (r.ok) return enterRound(clocked, { index: 0, kind: "coding" });
805
+ return end(clocked, {
806
+ kind: "aborted",
807
+ reason: `⚠️ Could not create the pipeline branch \`${s.input.unit.branch}\` from \`${s.input.base}\` on \`${s.input.repo}\`: ${r.reason} — round 0 never started.`,
808
+ reviewRounds: s.reviewRounds,
809
+ });
810
+ }
811
+ case "spawn": {
812
+ const r = ret as Extract<StepReturn, { type: "spawn" }>;
813
+ switch (r.outcome) {
814
+ case "spawned":
815
+ case "alreadySpawned": {
816
+ const budgetMinutes = budgetMinutesFor(s, presetOf(p.round.kind));
817
+ const runs =
818
+ p.round.kind === "review"
819
+ ? { reviewRunByRound: { ...s.reviewRunByRound, [p.round.index]: r.runId } }
820
+ : p.round.kind === "fix"
821
+ ? { fixRunByRound: { ...s.fixRunByRound, [p.round.index]: r.runId }, lastCodingRunId: r.runId }
822
+ : { lastCodingRunId: r.runId };
823
+ return {
824
+ state: { ...clocked, ...runs, phase: { at: "wait", round: p.round, runId: r.runId, n: 1, budgetMinutes } },
825
+ notes: [roundNote(p.round, "started")],
826
+ };
827
+ }
828
+ case "busy": {
829
+ // Another run holds the unit's thread: wait for its end, then ask
830
+ // again — unless the pipeline's wall clock ran out meanwhile.
831
+ if (remainingMs(clocked) < SHIP_ROUND_RESERVE_MS)
832
+ return end(clocked, {
833
+ kind: "wall_clock_cap",
834
+ remainingMs: remainingMs(clocked),
835
+ reviewRounds: s.reviewRounds,
836
+ });
837
+ return {
838
+ state: {
839
+ ...clocked,
840
+ phase: {
841
+ at: "busy-wait",
842
+ round: p.round,
843
+ ...(r.runId !== undefined ? { runId: r.runId } : {}),
844
+ n: p.busy + 1,
845
+ },
846
+ },
847
+ notes: [],
848
+ };
849
+ }
850
+ case "refused":
851
+ return end(clocked, {
852
+ kind: "refused",
853
+ refusal: r.refusal,
854
+ ...(r.message !== undefined ? { message: r.message } : {}),
855
+ round: p.round,
856
+ reviewRounds: s.reviewRounds,
857
+ });
858
+ case "failed":
859
+ return end(clocked, {
860
+ kind: "aborted",
861
+ reason: `⚠️ The ${presetOf(p.round.kind)} child of round ${p.round.index} could not be started: ${r.reason}.`,
862
+ round: p.round,
863
+ reviewRounds: s.reviewRounds,
864
+ });
865
+ }
866
+ break;
867
+ }
868
+ case "busy-wait":
869
+ return { state: { ...s, phase: { at: "spawn", round: p.round, busy: p.n } }, notes: [] };
870
+ case "wait":
871
+ return {
872
+ state: { ...s, phase: { at: "read", round: p.round, runId: p.runId, n: p.n, budgetMinutes: p.budgetMinutes } },
873
+ notes: [],
874
+ };
875
+ case "read": {
876
+ const r = ret as Extract<StepReturn, { type: "read-record" }>;
877
+ if (!r.run.finished)
878
+ return {
879
+ state: {
880
+ ...clocked,
881
+ phase: { at: "wait", round: p.round, runId: p.runId, n: p.n + 1, budgetMinutes: p.budgetMinutes },
882
+ },
883
+ notes: [],
884
+ };
885
+ if (r.run.status === "interrupted")
886
+ return end(clocked, { kind: "interrupted", round: p.round, runId: p.runId, reviewRounds: s.reviewRounds }, [
887
+ roundNote(p.round, "aborted"),
888
+ ]);
889
+ return p.round.kind === "review"
890
+ ? settleReview(clocked, p.round, r.run)
891
+ : settleCoding(clocked, p.round, p.runId, r.run);
892
+ }
893
+ case "pr-check":
894
+ return settlePrCheck(clocked, p, (ret as Extract<StepReturn, { type: "pr-check" }>).pr);
895
+ case "merge": {
896
+ const r = ret as Extract<StepReturn, { type: "merge" }>;
897
+ if (r.outcome === "merged")
898
+ return end(clocked, { kind: "merged", pr: p.pr, sha: r.sha, reviewRounds: s.reviewRounds });
899
+ if (r.outcome === "refused")
900
+ return end(clocked, { kind: "merge_refused", pr: p.pr, reason: r.reason, reviewRounds: s.reviewRounds });
901
+ const waited = r.at - p.since;
902
+ if (waited >= MERGE_WAIT_MAX_MS)
903
+ return end(clocked, {
904
+ kind: "merge_refused",
905
+ pr: p.pr,
906
+ reason: `still pending after ${Math.round(waited / MIN)} minutes (${r.reason})`,
907
+ reviewRounds: s.reviewRounds,
908
+ });
909
+ return {
910
+ state: { ...clocked, phase: { at: "merge-sleep", pr: p.pr, headSha: p.headSha, n: p.n, since: p.since } },
911
+ notes: [],
912
+ };
913
+ }
914
+ case "merge-sleep":
915
+ return {
916
+ state: { ...s, phase: { at: "merge", pr: p.pr, headSha: p.headSha, n: p.n + 1, since: p.since } },
917
+ notes: [],
918
+ };
919
+ case "ended":
920
+ break;
921
+ }
922
+ return { state: s, notes: [] };
923
+ }
924
+
925
+ // ---- the report --------------------------------------------------------------------------------------
926
+
927
+ const sameFinding = (a: Finding, b: Finding) => a.severity === b.severity && a.file === b.file && a.title === b.title;
928
+
929
+ /** The disposition that answers `finding` as review round `round` listed it:
930
+ * that round's own fix round's, else one carried forward unchanged from an
931
+ * earlier round — never across a reused id, which inherits nothing. */
932
+ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number): FindingDisposition | undefined {
933
+ let cur = finding;
934
+ for (let r = round; r >= 1; r--) {
935
+ const d = s.dispositionsByRound[r]?.find((x) => x.findingId === cur.id);
936
+ if (d) return d;
937
+ const carriedFrom = s.findingsByRound[r - 1]?.find((p) => p.id === cur.id && sameFinding(p, cur));
938
+ if (!carriedFrom) return undefined;
939
+ cur = carriedFrom;
940
+ }
941
+ return undefined;
942
+ }
943
+
944
+ /** The cap report's declined-vs-unaddressed split over the last review round's findings. */
945
+ function splitReport(s: UnitPipelineState): string {
946
+ if (s.reviewRounds === 0) return "No review round ran before the cap — there are no findings to report.";
947
+ const last = s.findingsByRound[s.reviewRounds] ?? [];
948
+ const withDisposition = (f: Finding) => dispositionFor(s, f, s.reviewRounds);
949
+ const declined = last.filter((f) => withDisposition(f)?.disposition === "declined");
950
+ const unaddressed = last.filter((f) => !withDisposition(f));
951
+ const claimedFixed = last.filter((f) => withDisposition(f)?.disposition === "fixed");
952
+ const list = (items: Finding[], note?: (f: Finding) => string) =>
953
+ items.length > 0
954
+ ? items.map((f) => ` - ${formatFinding(f)}${note ? ` — ${note(f)}` : ""}`).join("\n")
955
+ : " - none";
956
+ const lines = [
957
+ `Open findings from the last review (${last.length}):`,
958
+ `Declined (disposition recorded):\n${list(declined, (f) => withDisposition(f)?.note || "no note")}`,
959
+ `Unaddressed (no disposition):\n${list(unaddressed)}`,
960
+ ];
961
+ if (claimedFixed.length > 0)
962
+ lines.push(`Claimed fixed but still flagged:\n${list(claimedFixed, (f) => withDisposition(f)?.note || "no note")}`);
963
+ return lines.join("\n");
964
+ }
965
+
966
+ /** The thread's report for a unit's ending — the in-process loop's own words
967
+ * for the endings it has, and the merge's for the ones it gains. */
968
+ export function renderUnitReport(s: UnitPipelineState): string {
969
+ const e = s.ending;
970
+ if (!e) return "";
971
+ const rounds = `${e.reviewRounds} review round${e.reviewRounds === 1 ? "" : "s"}`;
972
+ const prUrl = s.pr?.url;
973
+ const prLine = prUrl ? ` PR: ${prUrl}` : "";
974
+ const reissue =
975
+ s.input.unit.id === "task"
976
+ ? `To continue, re-issue \`agent:ship\` in this thread${prUrl ? ` and include the PR URL (${prUrl})` : " — include the PR URL if a PR exists"}.`
977
+ : `The unit's dependents in this plan stay blocked; the unit runs again when the plan is re-issued.`;
978
+ const declined = [...(s.dispositionsByRound[e.reviewRounds - 1] ?? [])].filter((d) => d.disposition === "declined");
979
+ const declinedLine = `Declined findings: ${declined.length > 0 ? declined.map((d) => `${d.findingId}${d.note ? ` — ${d.note}` : ""}`).join("; ") : "none"}`;
980
+ const verdictLine = `Verdict: LGTM${s.lastVerdictSummary ? ` — ${s.lastVerdictSummary}` : ""}`;
981
+ const join = (parts: Array<string | undefined>) => parts.filter(Boolean).join("\n\n");
982
+ switch (e.kind) {
983
+ case "merged":
984
+ return [
985
+ `✅ Merged after ${rounds}: ${e.pr.url} (squash \`${e.sha.slice(0, 7)}\`) — merged by the plan runner under \`plan:merge\`: the review approved at this head and the guards were green.`,
986
+ verdictLine,
987
+ declinedLine,
988
+ ].join("\n");
989
+ case "merge_ready":
990
+ return [
991
+ `✅ Merge-ready after ${rounds}: ${e.pr.url}`,
992
+ verdictLine,
993
+ declinedLine,
994
+ "Remaining gate: a person's merge — the runner merges only a plan branch's pull request, and ship never approves.",
995
+ ].join("\n");
996
+ case "merge_refused":
997
+ return join([
998
+ `⚠️ The review approved ${e.pr.url} but the runner did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
999
+ reissue,
1000
+ ]);
1001
+ case "round_cap":
1002
+ return join([
1003
+ `🧢 Ship stopped at a cap: the ${e.maxRounds}-round cap — no approval after ${rounds}.${prLine}`,
1004
+ splitReport(s),
1005
+ reissue,
1006
+ ]);
1007
+ case "wall_clock_cap":
1008
+ return join([
1009
+ `🧢 Ship stopped at a cap: the remaining pipeline time (~${Math.max(0, Math.round(e.remainingMs / MIN))} min of the ${s.input.caps.maxMinutes}-minute budget) cannot hold another round — no approval after ${rounds}.${prLine}`,
1010
+ splitReport(s),
1011
+ reissue,
1012
+ ]);
1013
+ case "stopped":
1014
+ return join([
1015
+ `${e.mode === "hard" ? "⛔" : "⏹"} Ship stopped by operator (${e.mode} stop) after ${rounds}.${prLine}`,
1016
+ e.finalReply,
1017
+ e.postedReview
1018
+ ? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
1019
+ : undefined,
1020
+ reissue,
1021
+ ]);
1022
+ case "aborted":
1023
+ return join([e.finalReply, e.reason, `⚠️ Ship aborted after ${rounds}.`, reissue]);
1024
+ case "no_verdict":
1025
+ return join([
1026
+ `⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no fix round ran.`,
1027
+ e.finalReply ? `Review round's final message:\n\n${e.finalReply}` : undefined,
1028
+ `⚠️ Ship aborted after ${rounds}.`,
1029
+ reissue,
1030
+ ]);
1031
+ case "interrupted":
1032
+ return shipInterruptedNote(prUrl);
1033
+ case "refused":
1034
+ return join([
1035
+ `🚫 The ${presetOf(e.round.kind)} child of round ${e.round.index} was refused by the authorize stage (${e.refusal})${e.message ? `: ${e.message}` : ""} — every child is authorized as the requesting user, so the pipeline ends here.`,
1036
+ reissue,
1037
+ ]);
1038
+ }
1039
+ }