@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
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { ChatMessage, ToolDef } from "../../providers/types.js";
7
7
  import type { ChannelVisibility } from "../authz/types.js";
8
+ import type { RunProfile } from "../../config/profile.js";
8
9
  import type { RunEvent } from "../runEvents.js";
9
10
 
10
11
  /** How long a generation's claim on a run lasts without a heartbeat. */
@@ -55,6 +56,19 @@ export interface LiveRunMeta {
55
56
  headSha?: string;
56
57
  pr?: number;
57
58
  readonly?: boolean;
59
+ /** The effective profile the run was admitted with — its class, identity,
60
+ * the budget it runs on and what clipped it — so a resume keeps the clipped
61
+ * budget instead of re-reading the preset's. Absent on rows written before
62
+ * profiles existed. */
63
+ profile?: RunProfile;
64
+ /** The run that spawned this one (item 46), so a reclaimed child's record
65
+ * still names its parent. Absent on every run a person or a schedule started. */
66
+ parentRunId?: string;
67
+ /** The coordinator instance this run is a child of, and the key its spawn
68
+ * carried (item 48) — stored at the claim, so a reclaimed child's record
69
+ * still sends the parent its event and a retried spawn finds its run. */
70
+ parentInstanceId?: string;
71
+ idempotencyKey?: string;
58
72
  /** Which executor the run attached: what `makeExecutor` chose. */
59
73
  selection?: "resident" | "sandbox" | "local" | "none";
60
74
  /** The worktree path the system prompt names. */
@@ -137,8 +151,16 @@ export interface ClaimRequest {
137
151
  phase?: "attaching" | "live";
138
152
  }
139
153
 
154
+ /** A refused claim names the run holding the thread — and the coordinator key
155
+ * its row carries (item 48), so a retried spawn can tell its own child from
156
+ * a busy thread. */
140
157
  export type ClaimResult =
141
- { ok: true } | { ok: false; reason: "thread-live"; live: { runId: string; agent?: string; startedAt: number } };
158
+ | { ok: true }
159
+ | {
160
+ ok: false;
161
+ reason: "thread-live";
162
+ live: { runId: string; agent?: string; startedAt: number; idempotencyKey?: string };
163
+ };
142
164
 
143
165
  export type FenceResult = { ok: true } | { ok: false; reason: "fenced" | "unknown-run" };
144
166
 
@@ -1,7 +1,15 @@
1
1
  import type { ChannelVisibility, Predicate } from "./authz/types.js";
2
+ import type { BoundaryScope, Identity, MachineClass, RunProfile } from "../config/profile.js";
2
3
  import type { RunEvent } from "./runEvents.js";
3
4
  import { isHeadMaterial, isSpanRecord } from "./runEvents.js";
4
5
  import { isHandoffShape, type Handoff } from "./ship/handoff.js";
6
+ import {
7
+ isFindingDispositionsShape,
8
+ isReviewVerdictShape,
9
+ type FindingDisposition,
10
+ type ReviewVerdict,
11
+ } from "./reviewVerdict.js";
12
+ import { IDEMPOTENCY_KEY_PATTERN, INSTANCE_ID_PATTERN } from "./coordinator/contract.js";
5
13
  import {
6
14
  FRICTION_CATEGORIES,
7
15
  type CategoryTotals,
@@ -31,6 +39,8 @@ const RUN_STATUSES: readonly RunStatus[] = ["completed", "stopped_soft", "stoppe
31
39
 
32
40
  /** Every `runs.*` id: checked before any store call. */
33
41
  export const RUN_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
42
+ /** A reviewed head as the record stores it: the normalized sha (`normalizeHead`), 7 to 40 lowercase hex. */
43
+ const REVIEW_HEAD_PATTERN = /^[0-9a-f]{7,40}$/;
34
44
 
35
45
  /** One finished run as the store keeps it. Events are already redacted and
36
46
  * capped upstream (`runEvents.ts`); this layer adds no data. */
@@ -97,6 +107,73 @@ export interface RunRecord {
97
107
  * three empty lists, distinguishable from none); absent on every other run
98
108
  * and on records written before it existed. */
99
109
  handoff?: Handoff;
110
+ /** The verdict a review run submitted through `submit_verdict` (docs/reference/specs/agent-review.md),
111
+ * redacted like every stored string — what a coordinator's `read-record`
112
+ * answers for a review child. Present only on a run that submitted one. */
113
+ verdict?: ReviewVerdict;
114
+ /** The head a review run reviewed and posted against (7 to 40 lowercase hex),
115
+ * after the settle; present only on a review run that pinned one. */
116
+ reviewHead?: string;
117
+ /** The dispositions a fix round submitted through `submit_dispositions`
118
+ * (docs/reference/specs/agent-ship.md item 6), the last call's set, redacted;
119
+ * present only on a coding run dispatched as a fix round that submitted one. */
120
+ dispositions?: FindingDisposition[];
121
+ /** The effective profile the run was admitted with (docs/decisions/0026-capability-profiles-and-request-routing.md):
122
+ * the preset, its machine class and identity, the minutes it ran on and —
123
+ * when a boundary clipped the budget — the scope that did, so a reader can
124
+ * tell a clipped budget from a declared one. Absent on records written
125
+ * before profiles existed. */
126
+ profile?: RunProfileRecord;
127
+ /** The run that spawned this one (item 46): a child started through
128
+ * `spawnChild()` names its parent, so a listing can draw the tree. Absent
129
+ * on every run a person or a schedule started. */
130
+ parentRunId?: string;
131
+ /** The coordinator instance this run is a child of (item 48): the state
132
+ * Worker's finish sends that instance `run finished:<id>` for a record
133
+ * carrying it, and `read-record` answers only for a matching instance.
134
+ * Absent on every run no coordinator spawned. */
135
+ parentInstanceId?: string;
136
+ /** The key the coordinator's spawn carried (`<parentInstanceId>:<step>`,
137
+ * item 48), stored at the claim so a retried spawn finds its run. Present
138
+ * exactly when `parentInstanceId` is — both or neither, never one alone. */
139
+ idempotencyKey?: string;
140
+ }
141
+
142
+ /** The profile as the record stores it: the run's effective profile plus the preset it came from. */
143
+ export type RunProfileRecord = RunProfile & { preset: string };
144
+
145
+ // The vocabularies the profile is checked against, typed against the unions
146
+ // (a class or identity added without a row here fails to compile) — the record
147
+ // contract stays node-free and imports the registry's types only.
148
+ const MACHINE_CLASSES_IN_RECORD: Record<MachineClass, true> = {
149
+ none: true,
150
+ blank: true,
151
+ "repo-cold": true,
152
+ "repo-resident": true,
153
+ };
154
+ const IDENTITIES_IN_RECORD: Record<Identity, true> = { none: true, read: true, write: true };
155
+ const BOUNDARY_SCOPES_IN_RECORD: Record<BoundaryScope, true> = {
156
+ defaults: true,
157
+ channel: true,
158
+ user: true,
159
+ directive: true,
160
+ parent: true,
161
+ };
162
+
163
+ /** Structural check on a stored profile (item 3's rule for the field). */
164
+ function isRunProfileRecord(v: unknown): v is RunProfileRecord {
165
+ if (typeof v !== "object" || v === null) return false;
166
+ const p = v as Record<string, unknown>;
167
+ if (typeof p.preset !== "string") return false;
168
+ if (typeof p.machine !== "string" || !Object.hasOwn(MACHINE_CLASSES_IN_RECORD, p.machine)) return false;
169
+ if (typeof p.identity !== "string" || !Object.hasOwn(IDENTITIES_IN_RECORD, p.identity)) return false;
170
+ if (!isFiniteNumber(p.minutes) || p.minutes <= 0) return false;
171
+ if (
172
+ p.boundedBy !== undefined &&
173
+ (typeof p.boundedBy !== "string" || !Object.hasOwn(BOUNDARY_SCOPES_IN_RECORD, p.boundedBy))
174
+ )
175
+ return false;
176
+ return true;
100
177
  }
101
178
 
102
179
  /** A run as a listing shows it: the record minus its events. `diagnosis` stays —
@@ -419,6 +496,29 @@ export function isRunRecord(v: unknown): v is RunRecord {
419
496
  // The handoff is checked for shape, not bounds (docs/reference/specs/agent-ship.md
420
497
  // item 14): redaction may lengthen a stored string past the tool's limit.
421
498
  if (r.handoff !== undefined && !isHandoffShape(r.handoff)) return false;
499
+ // The review's verdict and head and the fix round's dispositions (item 2):
500
+ // shape only, like the handoff — redaction may lengthen a stored string.
501
+ if (r.verdict !== undefined && !isReviewVerdictShape(r.verdict)) return false;
502
+ if (r.reviewHead !== undefined && (typeof r.reviewHead !== "string" || !REVIEW_HEAD_PATTERN.test(r.reviewHead)))
503
+ return false;
504
+ if (r.dispositions !== undefined && !isFindingDispositionsShape(r.dispositions)) return false;
505
+ if (r.profile !== undefined && !isRunProfileRecord(r.profile)) return false;
506
+ // A parent is named by a run id (item 46): the same shape as the record's own.
507
+ if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
508
+ return false;
509
+ // A coordinator's child (item 48): the instance id in the platform's alphabet
510
+ // and the key `<instance>:<step>` — both or neither; one alone is no tag.
511
+ if ((r.parentInstanceId === undefined) !== (r.idempotencyKey === undefined)) return false;
512
+ if (
513
+ r.parentInstanceId !== undefined &&
514
+ (typeof r.parentInstanceId !== "string" || !INSTANCE_ID_PATTERN.test(r.parentInstanceId))
515
+ )
516
+ return false;
517
+ if (
518
+ r.idempotencyKey !== undefined &&
519
+ (typeof r.idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(r.idempotencyKey))
520
+ )
521
+ return false;
422
522
  if (typeof r.channelId !== "string" || typeof r.userId !== "string" || typeof r.threadKey !== "string") return false;
423
523
  // Absent on records written before the stamp existed (read as `unknown`); present → a known value.
424
524
  if (r.channelVisibility !== undefined && !CHANNEL_VISIBILITIES.includes(r.channelVisibility as ChannelVisibility))
@@ -0,0 +1,502 @@
1
+ // The child contract (docs/decisions/0031-the-coordinator-runs-a-plan-not-a-pull-request.md,
2
+ // docs/reference/specs/agent-ship.md item 13): what a coding child is handed
3
+ // for a plan unit, and what the review child checks the diff against. Both
4
+ // published accounts of agent-run development find that the instructions
5
+ // handed to a worker decide the outcome, and the plan already knows more than
6
+ // a task string can carry — so the contract is the unit's own section
7
+ // verbatim, the spec rows the unit names with their current proof bindings,
8
+ // the target repository's agent rules, and the guards the child may not
9
+ // weaken, with the rebase onto the merged parent as its first instruction.
10
+ //
11
+ // It is a typed object the runner (by hand: `contract render`) builds and
12
+ // renders — never assembled by the child, which would choose what to leave
13
+ // out — under fixed headings, so the review child can be handed the same
14
+ // object and name what the diff did not add. This module is pure: the plan
15
+ // and the specs arrive as text through the caller, nothing here reads a disk
16
+ // or the network. Over-length is cut by dropping Approach text before spec
17
+ // rows, never the other way, and the render says what it dropped.
18
+
19
+ /** One plan unit: its `### U<n>.` heading to the next `###`/`##` heading. */
20
+ export interface ContractUnit {
21
+ /** `U<n>` as the plan's heading spells it; `task` on the compatibility path. */
22
+ id: string;
23
+ title: string;
24
+ /** The section verbatim, heading line included, trailing blank lines and a
25
+ * closing thematic break dropped. */
26
+ section: string;
27
+ /** The section's `- **Key**:` bullets by key, each body whole (continuation
28
+ * lines joined with newlines, their common indentation removed). */
29
+ bullets: Readonly<Record<string, string>>;
30
+ }
31
+
32
+ /** A spec row as a unit names it: `<spec>.md item <n>`. */
33
+ export interface SpecItemRef {
34
+ spec: string;
35
+ item: number;
36
+ }
37
+
38
+ export interface ContractValidationRow {
39
+ criterion: string;
40
+ proof: string;
41
+ }
42
+
43
+ /** A named spec row resolved against the spec's current text. */
44
+ export interface ContractSpecRow extends SpecItemRef {
45
+ /** Whether the spec's text was there to read; false → nothing below is known. */
46
+ specRead: boolean;
47
+ /** The numbered behavior statement verbatim; undefined when the spec was not
48
+ * readable or has no such item — said in the render, never invented. */
49
+ text: string | undefined;
50
+ /** The validation-table rows whose criterion names this item, with their proof bindings. */
51
+ validation: ContractValidationRow[];
52
+ }
53
+
54
+ export interface AgentRules {
55
+ /** `AGENTS.md`, else `CLAUDE.md` — whichever the repository has at the head. */
56
+ file: string;
57
+ text: string;
58
+ }
59
+
60
+ export interface Guard {
61
+ name: string;
62
+ /** One line: what the guard refuses. */
63
+ refuses: string;
64
+ }
65
+
66
+ export interface ChildContract {
67
+ unit: ContractUnit;
68
+ specRows: ContractSpecRow[];
69
+ /** Absent when the repository has neither rules file. */
70
+ agentRules: AgentRules | undefined;
71
+ guards: readonly Guard[];
72
+ /** The first instruction's names, when the caller knows them: the unit's
73
+ * branch and the merged parent it is rebased onto. */
74
+ rebase: { branch: string | undefined; onto: string | undefined };
75
+ /** The unit's board issue, when the caller knows it: where the parent posts
76
+ * the child's handoff (handoff.ts). Absent on a task-string pipeline and on
77
+ * the by-hand render, whose handoff is recorded on the run only. Never
78
+ * rendered — the block is the child's brief, and the post is the parent's. */
79
+ issue: { repo: string; number: number } | undefined;
80
+ }
81
+
82
+ /** The guards a child may not weaken (AGENTS.md's Commands table), one line each on what they refuse. */
83
+ export const GUARDS: readonly Guard[] = [
84
+ {
85
+ name: "specs:check",
86
+ refuses:
87
+ "a spec whose `file::describe::it` proof names a test that does not exist, or whose Code/Tests header path is gone — a test cannot be renamed or deleted out from under its spec row",
88
+ },
89
+ {
90
+ name: "specs:coverage --test-guard",
91
+ refuses:
92
+ "a deleted test file, a removed test title or an added skip marker in a test file unless a spec covering that file changes in the same range; a lower assertion count or a rename is printed for the reviewer to dispose of",
93
+ },
94
+ {
95
+ name: "hygiene:check",
96
+ refuses:
97
+ "a new imprint in the public tree (a company, a person, a tracker reference, a plan id, a platform id, a date); the recorded list only shrinks",
98
+ },
99
+ {
100
+ name: "decisions:check",
101
+ refuses:
102
+ "a record under docs/decisions or docs/plans without a valid status, a superseded one that names no successor, and any edit to an accepted record's body",
103
+ },
104
+ ];
105
+
106
+ export const CONTRACT_HEADING = "## Contract";
107
+
108
+ /** The fixed sub-headings, in render order. The prompts name them (src/agents/registry.ts). */
109
+ export const CONTRACT_SECTION_HEADINGS = {
110
+ firstInstruction: "### First instruction",
111
+ unit: "### Unit",
112
+ specRows: "### Spec rows",
113
+ agentRules: "### Agent rules",
114
+ guards: "### Guards",
115
+ } as const;
116
+
117
+ /** The budget the children render with. Record 0031 lacks the measured
118
+ * number; this is the backstop until `contract render` has measured real
119
+ * units: roughly 12k tokens, a fraction of a first turn, with this
120
+ * repository's rules file (~15k characters) and a long unit both inside it. */
121
+ export const DEFAULT_CONTRACT_MAX_CHARS = 48_000;
122
+
123
+ const UNIT_HEADING = /^### (U\d+)\. (.+?)\s*$/;
124
+ const ANY_HEADING = /^#{2,3} /;
125
+ const BULLET = /^- \*\*([^*]+)\*\*:(?:\s?(.*))?$/;
126
+
127
+ /** The unit's section: from its heading line to the line before the next
128
+ * `##`/`###` heading, trailing blank lines and a closing `---` dropped. */
129
+ export function parsePlanUnit(planMarkdown: string, unitId: string): ContractUnit | undefined {
130
+ const lines = planMarkdown.split("\n");
131
+ const start = lines.findIndex((l) => {
132
+ const m = UNIT_HEADING.exec(l);
133
+ return m !== null && m[1] === unitId;
134
+ });
135
+ if (start < 0) return undefined;
136
+ let end = lines.findIndex((l, i) => i > start && ANY_HEADING.test(l));
137
+ if (end < 0) end = lines.length;
138
+ const body = lines.slice(start, end);
139
+ while (body.length > 1 && /^\s*$/.test(body[body.length - 1]!)) body.pop();
140
+ if (body.length > 1 && /^---\s*$/.test(body[body.length - 1]!)) body.pop();
141
+ while (body.length > 1 && /^\s*$/.test(body[body.length - 1]!)) body.pop();
142
+ const heading = UNIT_HEADING.exec(body[0]!)!;
143
+ return { id: heading[1]!, title: heading[2]!, section: body.join("\n"), bullets: parseBullets(body.slice(1)) };
144
+ }
145
+
146
+ /** The ids of every unit heading in the plan, in order. */
147
+ export function planUnitIds(planMarkdown: string): string[] {
148
+ return planMarkdown
149
+ .split("\n")
150
+ .map((l) => UNIT_HEADING.exec(l)?.[1])
151
+ .filter((id): id is string => id !== undefined);
152
+ }
153
+
154
+ interface BulletSpan {
155
+ key: string;
156
+ /** Line indices into the section, heading line included: [start, end). */
157
+ start: number;
158
+ end: number;
159
+ }
160
+
161
+ /** Where each `- **Key**:` bullet sits in the section's lines (heading at index 0). */
162
+ function bulletSpans(sectionLines: readonly string[]): BulletSpan[] {
163
+ const spans: BulletSpan[] = [];
164
+ for (let i = 1; i < sectionLines.length; i++) {
165
+ const m = BULLET.exec(sectionLines[i]!);
166
+ if (!m) continue;
167
+ if (spans.length > 0) spans[spans.length - 1]!.end = i;
168
+ spans.push({ key: m[1]!, start: i, end: sectionLines.length });
169
+ }
170
+ return spans;
171
+ }
172
+
173
+ function bulletBody(lines: readonly string[]): string {
174
+ const first = BULLET.exec(lines[0]!)!;
175
+ const head = first[2] ?? "";
176
+ const rest = lines.slice(1);
177
+ while (rest.length > 0 && /^\s*$/.test(rest[rest.length - 1]!)) rest.pop();
178
+ if (rest.length === 0) return head.trim();
179
+ const indent = Math.min(...rest.filter((l) => l.trim() !== "").map((l) => /^\s*/.exec(l)![0].length));
180
+ const dedented = rest.map((l) => l.slice(Math.min(indent, /^\s*/.exec(l)![0].length)));
181
+ return [head.trim(), ...dedented].filter((l, i) => i > 0 || l !== "").join("\n");
182
+ }
183
+
184
+ function parseBullets(bodyLines: readonly string[]): Record<string, string> {
185
+ const withHeading = ["", ...bodyLines];
186
+ const out: Record<string, string> = {};
187
+ for (const span of bulletSpans(withHeading)) out[span.key] = bulletBody(withHeading.slice(span.start, span.end));
188
+ return out;
189
+ }
190
+
191
+ // ---- the spec rows a unit names -------------------------------------------------------------------
192
+
193
+ /** A spec file as a unit may spell it: backticked, path-prefixed, a markdown link, or bare. */
194
+ const SPEC_MENTION =
195
+ /`(?:docs\/reference\/specs\/)?([a-z0-9-]+\.md)`|\[([a-z0-9-]+\.md)\]\([^)]*\)|(?<![\w/.-])(?:docs\/reference\/specs\/)?([a-z0-9-]+\.md)(?![\w/])/g;
196
+ /** `item 22`, `items 3 and 7`, `items 3, 5, 7`, `items 4–6`, `items 7-9`, `items 2 to 4` (a criterion may
197
+ * capitalize the word; a spec name never is — `AGENTS.md` is not a spec, so no `i` flag). */
198
+ const ITEM_LIST = /\b[Ii]tems?\s+(\d+(?:\s*(?:,|and|&|–|-|to|\/)\s*\d+)*)/g;
199
+ /** The mention immediately followed by its item list — an optional parenthetical between. */
200
+ const NAMED_REF = new RegExp(`(?:${SPEC_MENTION.source})\\s+(?:\\([^)]*\\)\\s+)?${ITEM_LIST.source}`, "g");
201
+
202
+ /** `3, 5 and 7` → [3, 5, 7]; `4–6` → [4, 5, 6]. */
203
+ function expandItemList(list: string): number[] {
204
+ const out: number[] = [];
205
+ const ranged = /(\d+)\s*(?:–|-|to)\s*(\d+)/g;
206
+ let rest = list;
207
+ for (const m of list.matchAll(ranged)) {
208
+ const from = Number(m[1]);
209
+ const to = Number(m[2]);
210
+ if (to >= from && to - from <= 100) for (let n = from; n <= to; n++) out.push(n);
211
+ rest = rest.replace(m[0], " ");
212
+ }
213
+ for (const m of rest.matchAll(/\d+/g)) out.push(Number(m[0]));
214
+ return out;
215
+ }
216
+
217
+ /** Any markdown file the section mentions — a spec or not (`AGENTS.md`, `README.md`, a how-to). */
218
+ const ANY_MD_MENTION = /(?<![\w/.-])(?:[\w.-]+\/)*([\w.-]+\.md)(?![\w/])/g;
219
+
220
+ /** The spec name a file mention resolves to: a bare or `docs/reference/specs/`-prefixed
221
+ * lowercase `<name>.md`; anything else (`AGENTS.md`, `docs/how-to/x.md`) is not a spec. */
222
+ function specNameOf(mention: string): string | undefined {
223
+ const m = /^(?:docs\/reference\/specs\/)?([a-z0-9-]+\.md)$/.exec(mention);
224
+ return m?.[1];
225
+ }
226
+
227
+ /** The spec rows the section names, once each, in order of first appearance.
228
+ * A bare `item N` with no spec beside it is attributed to the nearest
229
+ * preceding file mention in the section when that is a spec; with none, or
230
+ * with another file (`AGENTS.md`) nearer, it names nothing. */
231
+ export function specItemRefs(section: string): SpecItemRef[] {
232
+ const seen = new Set<string>();
233
+ const refs: SpecItemRef[] = [];
234
+ const add = (spec: string, item: number) => {
235
+ const key = `${spec}#${item}`;
236
+ if (seen.has(key)) return;
237
+ seen.add(key);
238
+ refs.push({ spec, item });
239
+ };
240
+ const named: Array<{ index: number; end: number; spec: string; items: number[] }> = [];
241
+ for (const m of section.matchAll(NAMED_REF)) {
242
+ const spec = m[1] ?? m[2] ?? m[3]!;
243
+ named.push({ index: m.index, end: m.index + m[0].length, spec, items: expandItemList(m[4]!) });
244
+ }
245
+ const mentions = [...section.matchAll(ANY_MD_MENTION)].map((m) => ({ index: m.index, spec: specNameOf(m[0]) }));
246
+ // Every item list in the section, in order: a list inside a named reference
247
+ // belongs to that spec; a bare one to the nearest spec mentioned before it.
248
+ const bare: Array<{ index: number; items: number[] }> = [];
249
+ for (const m of section.matchAll(ITEM_LIST)) {
250
+ if (named.some((n) => m.index >= n.index && m.index < n.end)) continue;
251
+ bare.push({ index: m.index, items: expandItemList(m[1]!) });
252
+ }
253
+ const all = [
254
+ ...named.map((n) => ({ index: n.index, spec: n.spec as string | undefined, items: n.items })),
255
+ ...bare.map((b) => ({
256
+ index: b.index,
257
+ spec: mentions.filter((x) => x.index < b.index).at(-1)?.spec,
258
+ items: b.items,
259
+ })),
260
+ ].sort((a, b) => a.index - b.index);
261
+ for (const ref of all) if (ref.spec !== undefined) for (const item of ref.items) add(ref.spec, item);
262
+ return refs;
263
+ }
264
+
265
+ // ---- a spec's items and validation rows -----------------------------------------------------------
266
+
267
+ const SPEC_ITEM = /^(\d+)\. /;
268
+
269
+ /** A numbered item: its `N.` line to the next item or heading, trailing blank lines dropped. */
270
+ export function parseSpecItem(specMarkdown: string, item: number): string | undefined {
271
+ const lines = specMarkdown.split("\n");
272
+ const start = lines.findIndex((l) => Number(SPEC_ITEM.exec(l)?.[1]) === item);
273
+ if (start < 0) return undefined;
274
+ let end = lines.findIndex((l, i) => i > start && (SPEC_ITEM.test(l) || /^#{1,3} /.test(l)));
275
+ if (end < 0) end = lines.length;
276
+ const body = lines.slice(start, end);
277
+ while (body.length > 1 && /^\s*$/.test(body[body.length - 1]!)) body.pop();
278
+ return body.join("\n");
279
+ }
280
+
281
+ /** The rows of the spec's validation table: `| criterion | proof |`, header and separator excluded. */
282
+ export function parseValidationRows(specMarkdown: string): ContractValidationRow[] {
283
+ const rows: ContractValidationRow[] = [];
284
+ for (const line of specMarkdown.split("\n")) {
285
+ if (!line.startsWith("|")) continue;
286
+ const cells = line
287
+ .trim()
288
+ .replace(/^\||\|$/g, "")
289
+ .split(/(?<!\\)\|/)
290
+ .map((c) => c.trim());
291
+ if (cells.length < 2) continue;
292
+ if (/^-+$/.test(cells[0]!) || (cells[0] === "Criterion" && cells[1] === "Proof")) continue;
293
+ rows.push({ criterion: cells[0]!, proof: cells.slice(1).join(" | ") });
294
+ }
295
+ return rows;
296
+ }
297
+
298
+ /** The item numbers a criterion names: a leading `N:`, `item N`, `items A, B and C`, an `A–B` range. */
299
+ export function itemNumbersNamed(criterion: string): Set<number> {
300
+ const out = new Set<number>();
301
+ const lead = /^(\d+(?:\s*[,/]\s*\d+)*)\s*:/.exec(criterion);
302
+ if (lead) for (const n of expandItemList(lead[1]!)) out.add(n);
303
+ for (const m of criterion.matchAll(ITEM_LIST)) for (const n of expandItemList(m[1]!)) out.add(n);
304
+ return out;
305
+ }
306
+
307
+ /** One named row against the spec's text (undefined when the spec was not readable). */
308
+ export function resolveSpecRow(ref: SpecItemRef, specMarkdown: string | undefined): ContractSpecRow {
309
+ if (specMarkdown === undefined) return { ...ref, specRead: false, text: undefined, validation: [] };
310
+ return {
311
+ ...ref,
312
+ specRead: true,
313
+ text: parseSpecItem(specMarkdown, ref.item),
314
+ validation: parseValidationRows(specMarkdown).filter((row) => itemNumbersNamed(row.criterion).has(ref.item)),
315
+ };
316
+ }
317
+
318
+ // ---- the builders ----------------------------------------------------------------------------------
319
+
320
+ export interface ContractFromPlanInput {
321
+ planMarkdown: string;
322
+ unitId: string;
323
+ /** The spec's text by file name (`resident-repos.md`); undefined when it is not there. */
324
+ readSpec: (spec: string) => string | undefined;
325
+ agentRules?: AgentRules;
326
+ rebase?: { branch?: string; onto?: string };
327
+ /** The unit's board issue, when the caller knows it (the runner; a test). */
328
+ issue?: { repo: string; number: number };
329
+ }
330
+
331
+ /** The contract for one unit of a plan. Throws when the plan has no such unit. */
332
+ export function contractFromPlan(input: ContractFromPlanInput): ChildContract {
333
+ const unit = parsePlanUnit(input.planMarkdown, input.unitId);
334
+ if (!unit) {
335
+ const ids = planUnitIds(input.planMarkdown);
336
+ throw new Error(
337
+ `the plan has no unit ${input.unitId} (its units: ${ids.length > 0 ? ids.join(", ") : "none — no `### U<n>.` heading"})`,
338
+ );
339
+ }
340
+ return {
341
+ unit,
342
+ specRows: specItemRefs(unit.section).map((ref) => resolveSpecRow(ref, input.readSpec(ref.spec))),
343
+ agentRules: input.agentRules,
344
+ guards: GUARDS,
345
+ rebase: { branch: input.rebase?.branch, onto: input.rebase?.onto },
346
+ issue: input.issue,
347
+ };
348
+ }
349
+
350
+ const TITLE_MAX = 80;
351
+
352
+ /** The compatibility path record 0031 names: a task string is a plan of one
353
+ * unit with no spec rows. The task is the unit's whole section. */
354
+ export function contractFromTask(input: {
355
+ task: string;
356
+ agentRules?: AgentRules;
357
+ rebase?: { branch?: string; onto?: string };
358
+ issue?: { repo: string; number: number };
359
+ }): ChildContract {
360
+ const firstLine =
361
+ input.task
362
+ .split("\n")
363
+ .find((l) => l.trim() !== "")
364
+ ?.trim() ?? "";
365
+ const title = firstLine.length > TITLE_MAX ? `${firstLine.slice(0, TITLE_MAX - 1).trimEnd()}…` : firstLine;
366
+ return {
367
+ unit: { id: "task", title, section: input.task, bullets: {} },
368
+ specRows: [],
369
+ agentRules: input.agentRules,
370
+ guards: GUARDS,
371
+ rebase: { branch: input.rebase?.branch, onto: input.rebase?.onto },
372
+ issue: input.issue,
373
+ };
374
+ }
375
+
376
+ // ---- the render --------------------------------------------------------------------------------------
377
+
378
+ /** What a cut can drop, in the order it drops them: the unit's Approach text,
379
+ * the agent rules, the spec items' behavior text (their proof rows stay),
380
+ * then the proof rows too (the rows' names stay). */
381
+ export type DroppedPart = "approach" | "agentRules" | "specText" | "specRows";
382
+
383
+ export interface RenderedContract {
384
+ text: string;
385
+ /** `text.length` — the number record 0031 lacks, measured per render. */
386
+ chars: number;
387
+ dropped: DroppedPart[];
388
+ /** True when the text is still longer than the budget after every cut; the
389
+ * render never truncates the unit's own text or the guards. */
390
+ overBudget: boolean;
391
+ }
392
+
393
+ const APPROACH_DROPPED = "- **Approach**: (dropped to fit the contract's budget — read it in the plan)";
394
+
395
+ /** The unit's body without its heading line; with `dropApproach`, the
396
+ * Approach bullet's lines replaced by one placeholder line. */
397
+ function unitBody(unit: ContractUnit, dropApproach: boolean): string {
398
+ const lines = unit.section.split("\n");
399
+ if (dropApproach) {
400
+ const span = bulletSpans(lines).find((s) => s.key === "Approach");
401
+ if (span) lines.splice(span.start, span.end - span.start, APPROACH_DROPPED);
402
+ }
403
+ return lines.slice(1).join("\n").trim();
404
+ }
405
+
406
+ function hasApproach(unit: ContractUnit): boolean {
407
+ return "Approach" in unit.bullets;
408
+ }
409
+
410
+ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
411
+ const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
412
+ const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
413
+ return (
414
+ `Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
415
+ `the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop.`
416
+ );
417
+ }
418
+
419
+ function renderSpecRow(row: ContractSpecRow, dropText: boolean): string {
420
+ const head = `#### ${row.spec} item ${row.item}`;
421
+ if (!row.specRead) return `${head}\n\n(not found: no spec ${row.spec} was readable)`;
422
+ if (row.text === undefined) return `${head}\n\n(not found: ${row.spec} has no item ${row.item})`;
423
+ const text = dropText ? `(text dropped — read item ${row.item} in the spec)` : row.text;
424
+ const rows =
425
+ row.validation.length === 0
426
+ ? `(no validation row names item ${row.item})`
427
+ : ["| Criterion | Proof |", "|---|---|", ...row.validation.map((r) => `| ${r.criterion} | ${r.proof} |`)].join(
428
+ "\n",
429
+ );
430
+ return `${head}\n\n${text}\n\n${rows}`;
431
+ }
432
+
433
+ function renderSpecRows(rows: ContractSpecRow[], dropped: { text: boolean; rows: boolean }): string {
434
+ if (rows.length === 0)
435
+ return "(none — the unit names no spec rows; until decided, a unit naming none receives none, not the covering specs' rows by path)";
436
+ if (dropped.rows) return rows.map((r) => `- ${r.spec} item ${r.item}`).join("\n");
437
+ return rows.map((r) => renderSpecRow(r, dropped.text)).join("\n\n");
438
+ }
439
+
440
+ function renderAgentRules(rules: AgentRules | undefined, dropped: boolean): string {
441
+ if (!rules) return "(none — the repository has neither AGENTS.md nor CLAUDE.md)";
442
+ if (dropped)
443
+ return `Source: ${rules.file}\n\n(dropped to fit the contract's budget — read ${rules.file} at the head)`;
444
+ return `Source: ${rules.file}\n\n${rules.text.trim()}`;
445
+ }
446
+
447
+ function renderGuards(guards: readonly Guard[]): string {
448
+ return [
449
+ "Never weaken a guard: each refuses one class of change, and the review runs them over your range.",
450
+ ...guards.map((g) => `- \`${g.name}\` — ${g.refuses}`),
451
+ ].join("\n");
452
+ }
453
+
454
+ const DROP_NOTE: Readonly<Record<DroppedPart, string>> = {
455
+ approach: "the unit's Approach text was dropped (read it in the plan)",
456
+ agentRules: "the agent rules were dropped (read the file at the head)",
457
+ specText: "the spec items' text was dropped (read them in the specs; their proof rows stay)",
458
+ specRows: "the spec rows' proof rows were dropped too (their names stay above)",
459
+ };
460
+
461
+ /**
462
+ * The block a child's turn carries. `maxChars` (default
463
+ * `DEFAULT_CONTRACT_MAX_CHARS`) is the budget: over it, the Approach text goes
464
+ * first, then the agent rules, then the spec items' behavior text, then their
465
+ * proof rows — never spec rows before Approach — each leaving a placeholder,
466
+ * and a closing line says what was dropped. The unit's other bullets (its test
467
+ * scenarios above all), the spec rows' names and the guards are never cut;
468
+ * `overBudget` says when they alone exceed the budget.
469
+ */
470
+ export function renderContract(contract: ChildContract, opts: { maxChars?: number }): RenderedContract {
471
+ const maxChars = opts.maxChars ?? DEFAULT_CONTRACT_MAX_CHARS;
472
+ const hasSpecText = contract.specRows.some((r) => r.text !== undefined);
473
+ const ladder: Array<{ part: DroppedPart; present: boolean }> = [
474
+ { part: "approach", present: hasApproach(contract.unit) },
475
+ { part: "agentRules", present: contract.agentRules !== undefined },
476
+ { part: "specText", present: hasSpecText },
477
+ { part: "specRows", present: contract.specRows.length > 0 },
478
+ ];
479
+ const compose = (dropped: DroppedPart[]): string => {
480
+ const has = (p: DroppedPart) => dropped.includes(p);
481
+ const parts = [
482
+ CONTRACT_HEADING,
483
+ `${CONTRACT_SECTION_HEADINGS.firstInstruction}\n\n${renderFirstInstruction(contract.rebase)}`,
484
+ `${CONTRACT_SECTION_HEADINGS.unit} ${contract.unit.id} — ${contract.unit.title}\n\n${unitBody(contract.unit, has("approach"))}`,
485
+ `${CONTRACT_SECTION_HEADINGS.specRows}\n\n${renderSpecRows(contract.specRows, { text: has("specText"), rows: has("specRows") })}`,
486
+ `${CONTRACT_SECTION_HEADINGS.agentRules}\n\n${renderAgentRules(contract.agentRules, has("agentRules"))}`,
487
+ `${CONTRACT_SECTION_HEADINGS.guards}\n\n${renderGuards(contract.guards)}`,
488
+ ];
489
+ if (dropped.length > 0)
490
+ parts.push(`Cut to fit ${maxChars} characters: ${dropped.map((p) => DROP_NOTE[p]).join("; ")}.`);
491
+ return parts.join("\n\n");
492
+ };
493
+ const dropped: DroppedPart[] = [];
494
+ let text = compose(dropped);
495
+ for (const step of ladder) {
496
+ if (text.length <= maxChars) break;
497
+ if (!step.present) continue;
498
+ dropped.push(step.part);
499
+ text = compose(dropped);
500
+ }
501
+ return { text, chars: text.length, dropped, overBudget: text.length > maxChars };
502
+ }