@iowarp/clio-coder 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -6
  3. package/dist/{acp-2BEHC4DL.js → acp-U67UHUK2.js} +14 -14
  4. package/dist/{agents-LNNFTM53.js → agents-YU6SGALZ.js} +42 -35
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-KXXFI2VS.js → auth-5ZPJOIVG.js} +32 -24
  7. package/dist/builtins-C6JMZVV6.js +17 -0
  8. package/dist/{chunk-E25LMLRW.js → chunk-26LEYJZH.js} +2 -2
  9. package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
  10. package/dist/{chunk-PBTHKCPN.js → chunk-2HFZQUHL.js} +7 -7
  11. package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
  12. package/dist/{chunk-4OC57DA6.js → chunk-4DGYLA73.js} +53 -2
  13. package/dist/{chunk-CKXWIANG.js → chunk-4SPRNWDE.js} +18 -16
  14. package/dist/{chunk-E2ER4LJF.js → chunk-5C3AQNDW.js} +25 -1
  15. package/dist/{chunk-XF5N4U5A.js → chunk-5DHKRSMQ.js} +9 -8
  16. package/dist/{chunk-43AOLP7E.js → chunk-5FR74PWO.js} +2 -1
  17. package/dist/{chunk-EKY57CSP.js → chunk-5H3GB5BO.js} +68 -772
  18. package/dist/{chunk-6XXKFVSN.js → chunk-5Q2VVUKB.js} +4 -4
  19. package/dist/{chunk-DJVECN66.js → chunk-7RFXX52T.js} +295 -3705
  20. package/dist/{chunk-ZXF4XRKW.js → chunk-7RGZWPB6.js} +158 -8
  21. package/dist/{chunk-FYYLNIL5.js → chunk-A2NJGIB3.js} +2 -2
  22. package/dist/{chunk-XXQNGV4M.js → chunk-A3WNZD3P.js} +548 -72
  23. package/dist/{chunk-VEZEGCGW.js → chunk-B5CSFE7B.js} +23 -21
  24. package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
  25. package/dist/chunk-DR52UMZW.js +21 -0
  26. package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
  27. package/dist/{chunk-QKMUKYO7.js → chunk-E77JEWSD.js} +270 -125
  28. package/dist/{chunk-LYF7OHWH.js → chunk-EMYUUSFG.js} +12 -467
  29. package/dist/{chunk-4VP4KH3K.js → chunk-EQ63NRB7.js} +8 -8
  30. package/dist/chunk-FBVTI2TJ.js +518 -0
  31. package/dist/{chunk-R46L2BIR.js → chunk-FHJEP5SW.js} +19 -25
  32. package/dist/{chunk-WR67VIZY.js → chunk-GN57SG4G.js} +66 -8
  33. package/dist/{chunk-RD5U66HV.js → chunk-GPIEI3LY.js} +9 -9
  34. package/dist/{chunk-PCZJO5TI.js → chunk-GU2UIAFZ.js} +13 -178
  35. package/dist/chunk-GWS3VEIW.js +195 -0
  36. package/dist/chunk-H7IXIC72.js +103 -0
  37. package/dist/{chunk-3BPUFZDL.js → chunk-HLE42MG7.js} +3 -3
  38. package/dist/{chunk-24I7BN55.js → chunk-IFBNV6H6.js} +3 -3
  39. package/dist/{chunk-QNQHSOLF.js → chunk-IGWKHNIQ.js} +114 -55
  40. package/dist/chunk-IIZWH4XA.js +172 -0
  41. package/dist/{chunk-AD2SYQYC.js → chunk-IJ7RPIYJ.js} +124 -6
  42. package/dist/chunk-J3YUBZWY.js +382 -0
  43. package/dist/{chunk-5JGRAMKL.js → chunk-JOZYP4GM.js} +8 -6
  44. package/dist/{chunk-OH3TOQTB.js → chunk-K4XHGFR5.js} +751 -15
  45. package/dist/{chunk-EYPA3EGJ.js → chunk-KTYTFRMB.js} +178 -14
  46. package/dist/chunk-LU7P4LHA.js +33 -0
  47. package/dist/chunk-ME6CCNFO.js +108 -0
  48. package/dist/chunk-MXKJU4JB.js +1100 -0
  49. package/dist/{chunk-RY3LY4J5.js → chunk-N22QMJKY.js} +21 -18
  50. package/dist/chunk-NMPKI6XL.js +3006 -0
  51. package/dist/chunk-NUGM5KR6.js +165 -0
  52. package/dist/{chunk-22NAGB7X.js → chunk-P43ETTHK.js} +5 -94
  53. package/dist/{chunk-NILBFAPG.js → chunk-PMDBGQSJ.js} +2 -2
  54. package/dist/chunk-PT7HYKEM.js +165 -0
  55. package/dist/chunk-RVG5JXAL.js +41 -0
  56. package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
  57. package/dist/{chunk-4BPJXDWC.js → chunk-TANS5ZJS.js} +35 -19
  58. package/dist/{verifiers-NCBTHHN2.js → chunk-TB5666IT.js} +67 -324
  59. package/dist/{chunk-QM3F2GKX.js → chunk-TLQJPP24.js} +7927 -8088
  60. package/dist/{chunk-G7MUEIGA.js → chunk-TT36MB5S.js} +2 -1
  61. package/dist/chunk-TTHACPOM.js +961 -0
  62. package/dist/{chunk-6US73PDB.js → chunk-TYPGUK6W.js} +7 -7
  63. package/dist/{chunk-MFFY33HR.js → chunk-U6MBIEMB.js} +554 -209
  64. package/dist/chunk-VAWNZU7Z.js +242 -0
  65. package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
  66. package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
  67. package/dist/{chunk-XYDYPRZI.js → chunk-VWZOAB7K.js} +10 -10
  68. package/dist/{chunk-ZRGEBJ4T.js → chunk-WLFILSD5.js} +48 -48
  69. package/dist/{chunk-K7T3E2SR.js → chunk-WNIJTQQK.js} +12 -11
  70. package/dist/{chunk-CYQKWTG3.js → chunk-WSB3FPX7.js} +68 -20
  71. package/dist/{chunk-KHSFENX2.js → chunk-WWCZ5F23.js} +116 -14
  72. package/dist/{chunk-XE2VEJHX.js → chunk-WXY7KU3G.js} +2 -2
  73. package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
  74. package/dist/{chunk-KOHPCX4K.js → chunk-XWSF374K.js} +5 -5
  75. package/dist/{chunk-CJUB2JJ2.js → chunk-YS5VLNH5.js} +10 -10
  76. package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
  77. package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
  78. package/dist/cli/index.js +33 -31
  79. package/dist/{clio-M2KGYUFZ.js → clio-QVTYJ57A.js} +10 -10
  80. package/dist/{code-nav-GQNL7XA6.js → code-nav-FGGFIE7L.js} +5 -5
  81. package/dist/codewiki/build-worker.js +4 -4
  82. package/dist/{components-5TTYYX6G.js → components-ZFA3SAER.js} +9 -9
  83. package/dist/{config-XUUYQIWO.js → config-LW5IJFQN.js} +65 -55
  84. package/dist/{configure-IHJ7YOMV.js → configure-7XIZCOU4.js} +29 -24
  85. package/dist/{context-75MIWW3U.js → context-L3WL3X7K.js} +54 -43
  86. package/dist/{context-ZQ7SIFJV.js → context-N52ZA626.js} +30 -14
  87. package/dist/{context-74JLXAWD.js → context-Y6Y7QPR6.js} +12 -12
  88. package/dist/{context-clear-GYKWNUML.js → context-clear-MBQRLSDQ.js} +54 -43
  89. package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
  90. package/dist/{context-working-set-UX5KEP4J.js → context-working-set-GS6DSO7F.js} +18 -18
  91. package/dist/{dispatch-runner-GIJBHNFL.js → dispatch-runner-22ZCNOM3.js} +367 -79
  92. package/dist/{docs-6FZSCG5B.js → docs-7LQ23DLM.js} +9 -9
  93. package/dist/{doctor-SVJ5BZCW.js → doctor-M7YEDGAE.js} +27 -23
  94. package/dist/{eval-CG6LLBLD.js → eval-BEC2WHDA.js} +73 -20
  95. package/dist/{evidence-ZYFIEN42.js → evidence-REJUMSKM.js} +70 -59
  96. package/dist/{evolve-QGEXEMDW.js → evolve-PY5ZBA5K.js} +50 -39
  97. package/dist/{extensions-ADGNCJJD.js → extensions-HVKU65YU.js} +7 -7
  98. package/dist/{fleet-S5R4ZOQY.js → fleet-7WZEWRFA.js} +236 -377
  99. package/dist/fleet-commands-UVHWM76J.js +70 -0
  100. package/dist/fleet-graph-6ULH7PES.js +125 -0
  101. package/dist/fleet-new-RDVJLHHH.js +48 -0
  102. package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
  103. package/dist/fleet-validate-72PC4SLA.js +79 -0
  104. package/dist/{init-5DRU55YR.js → init-OG3TPGQG.js} +69 -57
  105. package/dist/library-CNTMPLRF.js +217 -0
  106. package/dist/{memory-7YKKR6UC.js → memory-6IS7F275.js} +52 -41
  107. package/dist/{models-ZPOLRU2C.js → models-ENRJDA5W.js} +37 -31
  108. package/dist/{monitor-US5F5YGZ.js → monitor-XLDVO7TN.js} +79 -42
  109. package/dist/{orchestrator-E2AL4T5N.js → orchestrator-6KSPYRHA.js} +4126 -762
  110. package/dist/{paths-E7KYAQWE.js → paths-DBXMZMDU.js} +6 -6
  111. package/dist/registry-LG64LTF4.js +11 -0
  112. package/dist/{reset-KZ652EK6.js → reset-RZ4ER727.js} +12 -12
  113. package/dist/{run-SRNBKDWD.js → run-Y2CNK5RU.js} +89 -76
  114. package/dist/{share-CGZE33UP.js → share-A55GYP6Z.js} +37 -13
  115. package/dist/{skills-S2X4DLY5.js → skills-ALC5J6AT.js} +29 -13
  116. package/dist/{skills-eval-W2GGIC4R.js → skills-eval-JPBEBYQU.js} +60 -48
  117. package/dist/support-MIETYA5E.js +38 -0
  118. package/dist/{targets-54SWINWB.js → targets-VGNXIR3S.js} +46 -36
  119. package/dist/{terminal-lease-SAIF2OGY.js → terminal-lease-WOBR64YA.js} +4 -4
  120. package/dist/{uninstall-BVLWXKBT.js → uninstall-ZJF5H5ZN.js} +9 -9
  121. package/dist/{upgrade-JKAR27XC.js → upgrade-FUSUAGHR.js} +31 -28
  122. package/dist/{usage-MSAWCLX4.js → usage-N4MKVHKD.js} +140 -68
  123. package/dist/verifiers-YAWOJ3H2.js +336 -0
  124. package/dist/{verify-X5HDROLA.js → verify-LTDHYBGY.js} +10 -9
  125. package/dist/{wiki-generate-GUSOQ6ZP.js → wiki-generate-6M7GHTBJ.js} +75 -62
  126. package/dist/worker/entry.js +67 -57
  127. package/docs/README.md +3 -2
  128. package/docs/acp.md +1 -1
  129. package/docs/alcf-provider.md +1 -1
  130. package/docs/architecture.md +2 -2
  131. package/docs/artifact-versions.md +11 -6
  132. package/docs/built-in-agents.md +26 -2
  133. package/docs/capacity-and-scheduling.md +1 -1
  134. package/docs/commands-and-modes.md +83 -3
  135. package/docs/configuration-and-targets.md +86 -4
  136. package/docs/context-engine.md +1 -1
  137. package/docs/development-pipeline.md +1 -1
  138. package/docs/dispatch-architecture-rationale.md +1 -1
  139. package/docs/documentation-coverage.md +3 -3
  140. package/docs/documentation-guide.md +3 -3
  141. package/docs/eval-runner.md +1 -1
  142. package/docs/evals-internal.md +1 -1
  143. package/docs/evidence-and-memory.md +74 -10
  144. package/docs/evolution.md +1 -1
  145. package/docs/exit-codes-and-output.md +4 -1
  146. package/docs/extensions-and-sharing.md +6 -2
  147. package/docs/fleet-demo-runbook.md +2 -2
  148. package/docs/fleet-dispatch.md +228 -16
  149. package/docs/git-commit-provenance.md +2 -2
  150. package/docs/glossary.md +22 -2
  151. package/docs/installation-and-lifecycle.md +2 -2
  152. package/docs/middleware-and-components.md +2 -1
  153. package/docs/model-catalog.md +1 -1
  154. package/docs/observability.md +56 -9
  155. package/docs/proactive-memory.md +1 -1
  156. package/docs/prompt-envelope-and-tools.md +4 -2
  157. package/docs/provider-adapter-cookbook.md +1 -1
  158. package/docs/release-cut-checklist.md +83 -65
  159. package/docs/resource-library.md +59 -0
  160. package/docs/safety-model.md +2 -2
  161. package/docs/scientific-validation.md +3 -3
  162. package/docs/session-lifecycle.md +37 -1
  163. package/docs/skills-marketplace.md +16 -3
  164. package/docs/tool-usage.md +14 -7
  165. package/docs/trace-store.md +1 -1
  166. package/docs/troubleshooting.md +1 -1
  167. package/docs/tui-design.md +1 -1
  168. package/docs/worker-dispatch-mechanics.md +3 -3
  169. package/package.json +1 -2
  170. package/src/cli/argv.ts +5 -0
  171. package/src/cli/configure.ts +107 -23
  172. package/src/cli/doctor.ts +5 -1
  173. package/src/cli/evidence.ts +30 -25
  174. package/src/cli/fleet-commands.ts +37 -0
  175. package/src/cli/fleet-graph.ts +102 -0
  176. package/src/cli/fleet-new.ts +36 -0
  177. package/src/cli/fleet-preflight.ts +111 -0
  178. package/src/cli/fleet-validate.ts +30 -0
  179. package/src/cli/fleet.ts +173 -335
  180. package/src/cli/index.ts +3 -1
  181. package/src/cli/library.ts +190 -0
  182. package/src/cli/share.ts +13 -1
  183. package/src/cli/shared.ts +1 -0
  184. package/src/cli/targets.ts +4 -1
  185. package/src/cli/usage.ts +111 -19
  186. package/src/cli/validate-model.ts +60 -5
  187. package/src/core/bus-events.ts +29 -0
  188. package/src/core/commit-attribution.ts +4 -4
  189. package/src/core/config.ts +130 -0
  190. package/src/core/defaults.ts +81 -0
  191. package/src/core/path-boundary.ts +100 -0
  192. package/src/domains/agents/builtins/architect.md +1 -0
  193. package/src/domains/agents/builtins/oracle.md +33 -0
  194. package/src/domains/agents/catalog.ts +13 -1
  195. package/src/domains/agents/extension.ts +2 -11
  196. package/src/domains/agents/fleet-contract.ts +304 -24
  197. package/src/domains/agents/index.ts +14 -0
  198. package/src/domains/agents/recipe.ts +7 -1
  199. package/src/domains/agents/registry.ts +73 -5
  200. package/src/domains/agents/result-contract.ts +360 -15
  201. package/src/domains/agents/write-boundary.ts +15 -50
  202. package/src/domains/config/classify.ts +4 -0
  203. package/src/domains/context/project-rules.ts +51 -1
  204. package/src/domains/dispatch/active-route-planner.ts +14 -0
  205. package/src/domains/dispatch/assignment-reconcile.ts +22 -5
  206. package/src/domains/dispatch/assignment-store.ts +151 -14
  207. package/src/domains/dispatch/backoff.ts +2 -1
  208. package/src/domains/dispatch/capability-match.ts +1 -0
  209. package/src/domains/dispatch/checkout-writer-lease.ts +175 -0
  210. package/src/domains/dispatch/contract.ts +48 -0
  211. package/src/domains/dispatch/delegation-plan.ts +164 -0
  212. package/src/domains/dispatch/execution-plan.ts +76 -5
  213. package/src/domains/dispatch/execution-role.ts +12 -2
  214. package/src/domains/dispatch/execution-scheduler.ts +183 -67
  215. package/src/domains/dispatch/extension.ts +412 -74
  216. package/src/domains/dispatch/fleet-gate.ts +14 -0
  217. package/src/domains/dispatch/fleet-plan.ts +63 -3
  218. package/src/domains/dispatch/fleet-run.ts +791 -0
  219. package/src/domains/dispatch/gate-role-prompts.ts +47 -0
  220. package/src/domains/dispatch/host-verification.ts +178 -0
  221. package/src/domains/dispatch/index.ts +41 -1
  222. package/src/domains/dispatch/intent-requirements.ts +40 -0
  223. package/src/domains/dispatch/intent.ts +235 -0
  224. package/src/domains/dispatch/path-scope.ts +370 -0
  225. package/src/domains/dispatch/receipt-integrity.ts +9 -4
  226. package/src/domains/dispatch/state.ts +36 -3
  227. package/src/domains/dispatch/types.ts +64 -12
  228. package/src/domains/dispatch/validation.ts +69 -6
  229. package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
  230. package/src/domains/dispatch/write-boundary.ts +201 -22
  231. package/src/domains/eval/metrics/evidence.ts +79 -2
  232. package/src/domains/eval/runners/clio-run.ts +12 -2
  233. package/src/domains/evidence/build.ts +69 -11
  234. package/src/domains/evidence/index.ts +21 -0
  235. package/src/domains/evidence/provenance.ts +46 -11
  236. package/src/domains/evidence/trust-projection.ts +274 -0
  237. package/src/domains/evidence/trust-status.ts +155 -18
  238. package/src/domains/evidence/types.ts +4 -0
  239. package/src/domains/extensions/discovery.ts +88 -1
  240. package/src/domains/extensions/resources.ts +20 -8
  241. package/src/domains/extensions/state.ts +6 -2
  242. package/src/domains/extensions/types.ts +4 -1
  243. package/src/domains/lifecycle/doctor.ts +140 -1
  244. package/src/domains/middleware/index.ts +15 -0
  245. package/src/domains/middleware/watchdog.ts +281 -0
  246. package/src/domains/observability/contract.ts +3 -1
  247. package/src/domains/observability/cost.ts +12 -1
  248. package/src/domains/observability/extension.ts +2 -2
  249. package/src/domains/observability/index.ts +10 -0
  250. package/src/domains/observability/out-of-turn-usage.ts +223 -0
  251. package/src/domains/prompts/contract.ts +3 -5
  252. package/src/domains/providers/extension.ts +30 -2
  253. package/src/domains/resources/common-loader.ts +3 -0
  254. package/src/domains/resources/index.ts +20 -0
  255. package/src/domains/resources/library.ts +326 -0
  256. package/src/domains/resources/prompts/loader.ts +119 -14
  257. package/src/domains/resources/skills/marketplace.ts +37 -12
  258. package/src/domains/safety/policy-engine.ts +5 -5
  259. package/src/domains/safety/run-effects.ts +64 -1
  260. package/src/domains/safety/scope.ts +7 -12
  261. package/src/domains/session/handoff.ts +629 -0
  262. package/src/domains/share/archive.ts +67 -2
  263. package/src/engine/acp/server.ts +4 -1
  264. package/src/engine/prompt-templates.ts +18 -1
  265. package/src/engine/worker-runtime.ts +6 -3
  266. package/src/entry/orchestrator.ts +37 -0
  267. package/src/interactive/bus-notices.ts +26 -0
  268. package/src/interactive/chat-loop.ts +235 -1
  269. package/src/interactive/chat-renderer.ts +22 -0
  270. package/src/interactive/cost-overlay.ts +31 -3
  271. package/src/interactive/council-dispatch.ts +30 -0
  272. package/src/interactive/council-grid.ts +213 -0
  273. package/src/interactive/council.ts +99 -0
  274. package/src/interactive/dispatch-board.ts +311 -17
  275. package/src/interactive/fleet-run-preview.ts +307 -0
  276. package/src/interactive/footer/notifications.ts +219 -0
  277. package/src/interactive/handoff-round.ts +56 -0
  278. package/src/interactive/interactive-application.ts +43 -1
  279. package/src/interactive/interactive-event-projection.ts +23 -1
  280. package/src/interactive/interactive-slash-runtime.ts +52 -2
  281. package/src/interactive/interactive-subscriptions.ts +14 -2
  282. package/src/interactive/oracle.ts +179 -0
  283. package/src/interactive/overlay-ask-user-lifecycle.ts +6 -0
  284. package/src/interactive/overlay-general-openers.ts +190 -1
  285. package/src/interactive/overlay-key-routing.ts +17 -1
  286. package/src/interactive/overlay-lifecycle.ts +41 -1
  287. package/src/interactive/overlay-permission-lifecycle.ts +10 -0
  288. package/src/interactive/overlay-resource-openers.ts +11 -3
  289. package/src/interactive/overlay-session-lifecycle.ts +234 -2
  290. package/src/interactive/overlays/fleet-run-approval.ts +208 -0
  291. package/src/interactive/overlays/handoff-review.ts +185 -0
  292. package/src/interactive/overlays/library-install-confirm.ts +151 -0
  293. package/src/interactive/overlays/list-overlay.ts +168 -2
  294. package/src/interactive/overlays/settings.ts +221 -30
  295. package/src/interactive/overlays/side-question.ts +139 -0
  296. package/src/interactive/overlays/skills-hub.ts +401 -15
  297. package/src/interactive/side-question.ts +171 -0
  298. package/src/interactive/slash-commands.ts +439 -7
  299. package/src/interactive/slash-spec.ts +19 -6
  300. package/src/interactive/theme/tokens.ts +30 -0
  301. package/src/interactive/turn-middleware.ts +15 -1
  302. package/src/interactive/view/artifacts.ts +42 -9
  303. package/src/interactive/view/view-overlay.ts +15 -3
  304. package/src/interactive/watchdog-run.ts +75 -0
  305. package/src/interactive/worker-receipts.ts +14 -2
  306. package/src/interactive/worker-share.ts +56 -1
  307. package/src/interactive/worker-stream.ts +15 -0
  308. package/src/tools/bootstrap.ts +3 -0
  309. package/src/tools/compete-worktrees.ts +13 -79
  310. package/src/tools/dispatch-admission.ts +251 -18
  311. package/src/tools/dispatch-arguments.ts +84 -1
  312. package/src/tools/dispatch-plan.ts +165 -6
  313. package/src/tools/dispatch-runner.ts +364 -23
  314. package/src/tools/dispatch-types.ts +20 -1
  315. package/src/tools/dispatch.ts +72 -2
  316. package/src/tools/monitor.ts +29 -0
  317. package/src/tools/profiles.ts +18 -4
  318. package/src/tools/task-worktree.ts +238 -0
  319. package/src/tools/verify/authoring.ts +61 -1
  320. package/src/tools/verify/scripts.ts +62 -0
  321. package/src/tools/worker-evidence.ts +21 -14
  322. package/src/worker/spec-contract.ts +3 -1
  323. package/dist/chunk-HC4CLZ2Y.js +0 -68
@@ -1,5 +1,8 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { resolvePackageRoot } from "../../core/package-root.js";
4
+ import { clioConfigDir } from "../../core/xdg.js";
5
+ import { enabledExtensionResourceRoots } from "../extensions/index.js";
3
6
  import { loadSkills } from "../resources/skills/loader.js";
4
7
  import { parseFrontmatter } from "./frontmatter.js";
5
8
  import { type AgentRecipe, type RecipeSource, recipeIdFromPath } from "./recipe.js";
@@ -20,10 +23,29 @@ function resolveBoundSkills(recipe: AgentRecipe, source: RecipeSource): AgentRec
20
23
  // only the operator's discovered skill roots; a recipe cannot smuggle an
21
24
  // arbitrary filesystem path into worker context through a skill name.
22
25
  const packageSkills = path.resolve(source.dir, "..", "..", "..", "..", "skills");
23
- const skills = loadSkills({
24
- cwd: process.cwd(),
25
- ...(source.source === "builtin" && existsSync(packageSkills) ? { explicitSkillPaths: [packageSkills] } : {}),
26
- });
26
+ if (source.source === "extension" && source.skillRoot === undefined) {
27
+ throw new Error(`agent recipe: ${recipe.filepath}: extension declares bound skills but no skills resource root`);
28
+ }
29
+ const skills =
30
+ source.source === "extension"
31
+ ? loadSkills({
32
+ cwd: source.cwd ?? process.cwd(),
33
+ disableDiscovery: true,
34
+ explicitSkillPaths: source.skillRoot === undefined ? [] : [source.skillRoot],
35
+ })
36
+ : loadSkills({
37
+ cwd: source.cwd ?? process.cwd(),
38
+ ...(source.source === "builtin" && existsSync(packageSkills) ? { explicitSkillPaths: [packageSkills] } : {}),
39
+ });
40
+ if (source.source === "extension" && source.skillRoot !== undefined) {
41
+ const canonicalRoot = realpathSync(source.skillRoot);
42
+ for (const skill of skills.items) {
43
+ const relative = path.relative(canonicalRoot, realpathSync(skill.filePath));
44
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
45
+ throw new Error(`agent recipe: ${recipe.filepath}: bound skill escapes its extension: ${skill.filePath}`);
46
+ }
47
+ }
48
+ }
27
49
  const byName = new Map(skills.items.map((skill) => [skill.name, skill.filePath]));
28
50
  const missing = recipe.skills.filter((skill) => !byName.has(skill));
29
51
  if (missing.length > 0) {
@@ -95,6 +117,10 @@ export function mergeRecipes(...sources: ReadonlyArray<ReadonlyArray<AgentRecipe
95
117
  continue;
96
118
  }
97
119
  const builtin = builtinById.get(recipe.id);
120
+ if (recipe.source === "extension" && builtin) {
121
+ process.stderr.write(`[clio:agents] ignore override id=${recipe.id} by=extension reason=reserved-builtin\n`);
122
+ continue;
123
+ }
98
124
  if (recipe.source === "user" && builtin && isShadowAgent(normalizeAgentSpec(builtin))) {
99
125
  process.stderr.write(`[clio:agents] ignore override id=${recipe.id} by=user reason=reserved-shadow\n`);
100
126
  continue;
@@ -109,3 +135,45 @@ export function mergeRecipes(...sources: ReadonlyArray<ReadonlyArray<AgentRecipe
109
135
  }
110
136
  return Array.from(byId.values()).sort((left, right) => left.id.localeCompare(right.id));
111
137
  }
138
+
139
+ /**
140
+ * Discover the one native-agent catalog used by listing, fleet admission, and
141
+ * dispatch. Extension recipes occupy the same precedence slot as extension
142
+ * fleets: after builtins and before operator/user and project recipes. Their
143
+ * skill bindings remain constrained to the declaring extension's skill root.
144
+ */
145
+ export function discoverAgentRecipes(
146
+ cwd = process.cwd(),
147
+ diagnostics: AgentRecipeDiagnostic[] = [],
148
+ ): ReadonlyArray<AgentRecipe> {
149
+ const builtin = loadRecipesFromDir(
150
+ {
151
+ dir: path.join(resolvePackageRoot(), "src", "domains", "agents", "builtins"),
152
+ source: "builtin",
153
+ cwd,
154
+ },
155
+ diagnostics,
156
+ );
157
+ const skillRoots = new Map(enabledExtensionResourceRoots("skills", cwd).map((root) => [root.source, root.path]));
158
+ const extensionRecipes = enabledExtensionResourceRoots("agents", cwd)
159
+ .sort((left, right) => left.source.localeCompare(right.source))
160
+ .map((root) => {
161
+ const skillRoot = skillRoots.get(root.source);
162
+ return loadRecipesFromDir(
163
+ {
164
+ dir: root.path,
165
+ source: "extension",
166
+ cwd,
167
+ origin: root.source,
168
+ ...(skillRoot === undefined ? {} : { skillRoot }),
169
+ },
170
+ diagnostics,
171
+ );
172
+ });
173
+ const user = loadRecipesFromDir({ dir: path.join(clioConfigDir(), "agents"), source: "user", cwd }, diagnostics);
174
+ const project = loadRecipesFromDir(
175
+ { dir: path.join(cwd, ".clio-coder", "agents"), source: "project", cwd },
176
+ diagnostics,
177
+ );
178
+ return mergeRecipes(builtin, ...extensionRecipes, user, project);
179
+ }
@@ -7,10 +7,42 @@ export type ResultContract =
7
7
  | { kind: "architect-plan"; path: string }
8
8
  | { kind: "scout-report" }
9
9
  | { kind: "verifier-report" }
10
+ | { kind: "council-report" }
11
+ /**
12
+ * One council member's ballot under `synthesis: "vote"`.
13
+ *
14
+ * The vote is a strict majority over the members' `verdict` fields, computed
15
+ * by the coordinator with no model call. Nothing asked a member for that
16
+ * field before this kind existed, and nothing could: an unnamed council
17
+ * seats the builtin `researcher`, whose `research-report` accepts only
18
+ * `source` and `findings`, so a member that emitted a verdict failed its own
19
+ * postcondition and a member that obeyed its postcondition emitted no
20
+ * verdict. Every vote therefore resolved to `no_verdict_field` with an empty
21
+ * tally, on every input (#230).
22
+ *
23
+ * The ballot is the coordinator's postcondition for that slot, not the
24
+ * recipe's, in the same way `reviewer` and the compete `judge` answer the
25
+ * gate that dispatched them. It rides in as a `resultContractOverride` on
26
+ * the member request, so the seated recipe keeps its persona and any recipe
27
+ * can be voted with. No recipe may declare it: `parseResultContract` rejects
28
+ * the kind, and a Scout subtask may not request it, because it is a
29
+ * topology's ballot rather than an agent's work product.
30
+ */
31
+ | { kind: "council-ballot" }
10
32
  | { kind: "debugger-report" }
11
33
  | { kind: "research-report" }
12
34
  | { kind: "mutation-report" }
13
35
  | { kind: "provenance-report" }
36
+ | { kind: "delegation-plan" }
37
+ /**
38
+ * The advisory answer `/oracle` asks for. It exists as its own kind because
39
+ * the answer shape is the whole point of the command: a verdict, the
40
+ * strongest challenge the advisor can mount, the evidence that would change
41
+ * its mind, and the prior decisions it read. Folding it onto
42
+ * `research-report` would ask for claims and evidence instead, and the
43
+ * challenge and the reversal condition would have nowhere to go.
44
+ */
45
+ | { kind: "oracle-report" }
14
46
  | { kind: "external-delegation" }
15
47
  /**
16
48
  * The run's postcondition is the artifact it left on disk, so its terminal
@@ -58,6 +90,7 @@ export interface ResultContractValidation {
58
90
  scout?: ScoutResult;
59
91
  /** Parsed Verifier data for the dispatch review-gate projection. */
60
92
  verifier?: VerifierResult;
93
+ delegationPlan?: DelegationPlanResult;
61
94
  reason?: string;
62
95
  }
63
96
 
@@ -211,6 +244,220 @@ export interface VerifierResult {
211
244
  checks: ReadonlyArray<VerifierCheck>;
212
245
  }
213
246
 
247
+ export interface DelegationPlanResultTask {
248
+ id: string;
249
+ agent: string;
250
+ description: string;
251
+ depends_on: ReadonlyArray<string>;
252
+ writes: ReadonlyArray<string>;
253
+ mode?: "sequential" | "parallel";
254
+ }
255
+
256
+ export interface DelegationPlanResult {
257
+ tasks: ReadonlyArray<DelegationPlanResultTask>;
258
+ }
259
+
260
+ export function parseDelegationPlanResult(output: string | null): DelegationPlanResult | null {
261
+ const parsed = parseJson(output);
262
+ if (
263
+ !parsed.ok ||
264
+ !hasOnlyKeys(parsed.value, ["tasks"]) ||
265
+ !Array.isArray(parsed.value.tasks) ||
266
+ parsed.value.tasks.length === 0
267
+ )
268
+ return null;
269
+ const tasks: DelegationPlanResultTask[] = [];
270
+ for (const raw of parsed.value.tasks) {
271
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
272
+ const task = raw as Record<string, unknown>;
273
+ if (
274
+ !hasOnlyKeys(task, ["id", "agent", "description", "depends_on", "writes", "mode"]) ||
275
+ !string(task.id) ||
276
+ !string(task.agent) ||
277
+ !string(task.description) ||
278
+ !Array.isArray(task.depends_on) ||
279
+ task.depends_on.some((entry) => !string(entry)) ||
280
+ !Array.isArray(task.writes) ||
281
+ task.writes.some((entry) => !string(entry)) ||
282
+ (task.mode !== undefined && task.mode !== "sequential" && task.mode !== "parallel")
283
+ )
284
+ return null;
285
+ tasks.push({
286
+ id: task.id,
287
+ agent: task.agent,
288
+ description: task.description,
289
+ depends_on: [...task.depends_on] as string[],
290
+ writes: [...task.writes] as string[],
291
+ ...(task.mode !== undefined ? { mode: task.mode as "sequential" | "parallel" } : {}),
292
+ });
293
+ }
294
+ return { tasks };
295
+ }
296
+
297
+ /**
298
+ * Longest verdict token a ballot may cast, in bytes.
299
+ *
300
+ * A tally groups members by their exact verdict string, so a verdict that is a
301
+ * sentence can only ever tie with itself and a council of any size reports
302
+ * `no_majority`. The bound is what makes a majority reachable: it forces the
303
+ * decision into a token members can match (`yes`, `no`, `keep`, an option
304
+ * name) and pushes the reasoning into `text`, where the report already reads
305
+ * it. A newline is refused for the same reason.
306
+ */
307
+ export const COUNCIL_BALLOT_VERDICT_MAX_BYTES = 64;
308
+
309
+ /**
310
+ * The ballot's wire shape, quoted verbatim to the member that must cast it and
311
+ * by the repair round that corrects it. Exported because the council composes
312
+ * the member's ask from it, which is the rule this module already states: a
313
+ * contract's example is written once so a prompt cannot drift from its
314
+ * validator.
315
+ */
316
+ export const COUNCIL_BALLOT_SHAPE = '{"verdict":"yes","text":"the answer and the evidence behind it"}';
317
+
318
+ interface CouncilBallot {
319
+ /** The comparable token the tally counts. */
320
+ verdict: string;
321
+ /** The member's answer in prose, which is what the council report shows. */
322
+ text: string;
323
+ }
324
+
325
+ /**
326
+ * Read a council member's ballot. The council reads a verdict out of the
327
+ * member's terminal text with its own reader, because the judge synthesis
328
+ * carries the same two fields under no contract at all; this one is what
329
+ * decides whether the member is allowed to seal that text as its result.
330
+ */
331
+ function parseCouncilBallot(output: string | null): CouncilBallot | null {
332
+ const parsed = parseJson(output);
333
+ if (!parsed.ok) return null;
334
+ const value = parsed.value;
335
+ if (!hasOnlyKeys(value, ["verdict", "text"]) || !string(value.verdict) || !string(value.text)) return null;
336
+ const verdict = value.verdict.trim();
337
+ if (verdict.includes("\n") || Buffer.byteLength(verdict, "utf8") > COUNCIL_BALLOT_VERDICT_MAX_BYTES) return null;
338
+ return { verdict, text: value.text.trim() };
339
+ }
340
+
341
+ function validateCouncilBallot(contract: ResultContract, output: string | null): ResultContractValidation {
342
+ const parsed = parseJson(output);
343
+ if (!parsed.ok) return failure(contract, "unmeasured", parsed.reason);
344
+ const ballot = parseCouncilBallot(output);
345
+ if (ballot === null) {
346
+ return failure(
347
+ contract,
348
+ "unmeasured",
349
+ `A council ballot must carry only verdict and text, both non-empty strings, with verdict a single line of at most ${COUNCIL_BALLOT_VERDICT_MAX_BYTES} bytes so the tally can match it against the other members`,
350
+ );
351
+ }
352
+ // Unmeasured, like every other contract whose shape carries no correctness
353
+ // claim: casting a well-formed ballot says nothing about whether the member
354
+ // was right, and the tally is a count of opinions rather than a check.
355
+ return success(contract, "unmeasured", ballot);
356
+ }
357
+
358
+ export interface CouncilReportMember {
359
+ label: string;
360
+ runId: string;
361
+ round: number;
362
+ answer: string;
363
+ verdict?: string;
364
+ failed?: { reason: string };
365
+ }
366
+
367
+ export interface CouncilReport {
368
+ members: CouncilReportMember[];
369
+ synthesis: {
370
+ kind: "none" | "judge" | "vote";
371
+ text?: string;
372
+ verdict?: string;
373
+ tally?: Record<string, number>;
374
+ judgeRunId?: string;
375
+ };
376
+ }
377
+
378
+ export function parseCouncilReport(output: string | null): CouncilReport | null {
379
+ if (output === null) return null;
380
+ const parsed = parseJsonObjectPayload(output);
381
+ if (!parsed.ok) return null;
382
+ const value = parsed.value;
383
+ if (!hasOnlyKeys(value, ["members", "synthesis"]) || !Array.isArray(value.members) || value.members.length === 0) {
384
+ return null;
385
+ }
386
+ const members: CouncilReportMember[] = [];
387
+ for (const raw of value.members) {
388
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
389
+ const member = raw as Record<string, unknown>;
390
+ if (
391
+ !hasOnlyKeys(member, ["label", "runId", "round", "answer", "verdict", "failed"]) ||
392
+ !string(member.label) ||
393
+ !string(member.runId) ||
394
+ !Number.isInteger(member.round) ||
395
+ Number(member.round) < 1 ||
396
+ typeof member.answer !== "string" ||
397
+ (member.verdict !== undefined && !string(member.verdict))
398
+ )
399
+ return null;
400
+ let failed: { reason: string } | undefined;
401
+ if (member.failed !== undefined) {
402
+ if (
403
+ member.failed === null ||
404
+ typeof member.failed !== "object" ||
405
+ Array.isArray(member.failed) ||
406
+ !hasOnlyKeys(member.failed as Record<string, unknown>, ["reason"]) ||
407
+ !string((member.failed as Record<string, unknown>).reason)
408
+ )
409
+ return null;
410
+ failed = { reason: (member.failed as { reason: string }).reason };
411
+ }
412
+ members.push({
413
+ label: member.label,
414
+ runId: member.runId,
415
+ round: Number(member.round),
416
+ answer: member.answer,
417
+ ...(member.verdict !== undefined ? { verdict: member.verdict as string } : {}),
418
+ ...(failed !== undefined ? { failed } : {}),
419
+ });
420
+ }
421
+ if (value.synthesis === null || typeof value.synthesis !== "object" || Array.isArray(value.synthesis)) return null;
422
+ const rawSynthesis = value.synthesis as Record<string, unknown>;
423
+ if (!hasOnlyKeys(rawSynthesis, ["kind", "text", "verdict", "tally", "judgeRunId"])) return null;
424
+ if (rawSynthesis.kind !== "none" && rawSynthesis.kind !== "judge" && rawSynthesis.kind !== "vote") return null;
425
+ for (const key of ["text", "verdict", "judgeRunId"] as const) {
426
+ if (rawSynthesis[key] !== undefined && !string(rawSynthesis[key])) return null;
427
+ }
428
+ let tally: Record<string, number> | undefined;
429
+ if (rawSynthesis.tally !== undefined) {
430
+ if (rawSynthesis.tally === null || typeof rawSynthesis.tally !== "object" || Array.isArray(rawSynthesis.tally))
431
+ return null;
432
+ tally = {};
433
+ for (const [key, count] of Object.entries(rawSynthesis.tally as Record<string, unknown>)) {
434
+ if (!key || !Number.isInteger(count) || Number(count) < 0) return null;
435
+ tally[key] = Number(count);
436
+ }
437
+ }
438
+ return {
439
+ members,
440
+ synthesis: {
441
+ kind: rawSynthesis.kind,
442
+ ...(rawSynthesis.text !== undefined ? { text: rawSynthesis.text as string } : {}),
443
+ ...(rawSynthesis.verdict !== undefined ? { verdict: rawSynthesis.verdict as string } : {}),
444
+ ...(tally !== undefined ? { tally } : {}),
445
+ ...(rawSynthesis.judgeRunId !== undefined ? { judgeRunId: rawSynthesis.judgeRunId as string } : {}),
446
+ },
447
+ };
448
+ }
449
+
450
+ /**
451
+ * Parsed `oracle-report` payload. `/oracle` renders these four fields into the
452
+ * operator note it hands the main agent, so the shape is the answer shape.
453
+ */
454
+ export interface OracleResult {
455
+ verdict: string;
456
+ challenge: string;
457
+ changesMyMind: string;
458
+ citedDecisions: ReadonlyArray<string>;
459
+ }
460
+
214
461
  /**
215
462
  * Parsed `code-report` payload. `passed` restates `exitCode === 0` so a
216
463
  * downstream reader never has to know a runner's exit conventions, and
@@ -269,7 +516,7 @@ function success(
269
516
  contract: ResultContract,
270
517
  quality: ResultContractQuality,
271
518
  value: unknown,
272
- extra: Pick<ResultContractValidation, "scout" | "verifier" | "reason"> = {},
519
+ extra: Pick<ResultContractValidation, "scout" | "verifier" | "delegationPlan" | "reason"> = {},
273
520
  ): ResultContractValidation {
274
521
  return {
275
522
  conformance: "pass",
@@ -335,6 +582,7 @@ const RESULT_CONTRACT_KINDS: ReadonlyArray<ResultContract["kind"]> = [
335
582
  "architect-plan",
336
583
  "scout-report",
337
584
  "verifier-report",
585
+ "council-report",
338
586
  "debugger-report",
339
587
  "research-report",
340
588
  "mutation-report",
@@ -664,6 +912,47 @@ export function parseVerifierResult(output: string | null): VerifierResult | nul
664
912
  return validation.verifier ?? null;
665
913
  }
666
914
 
915
+ /**
916
+ * An advisory opinion is never a correctness signal about the route that
917
+ * produced it, so conformance is judged on the shape alone and quality stays
918
+ * `unmeasured`. `citedDecisions` may be empty: an honest advisor that found no
919
+ * settled decision bearing on the question must be able to say so.
920
+ */
921
+ function validateOracle(contract: ResultContract, output: string | null): ResultContractValidation {
922
+ const parsed = parseJson(output);
923
+ if (!parsed.ok) return failure(contract, "unmeasured", parsed.reason);
924
+ const value = parsed.value;
925
+ if (
926
+ !hasOnlyKeys(value, ["verdict", "challenge", "changesMyMind", "citedDecisions"]) ||
927
+ !string(value.verdict) ||
928
+ !string(value.challenge) ||
929
+ !string(value.changesMyMind) ||
930
+ !Array.isArray(value.citedDecisions) ||
931
+ value.citedDecisions.some((entry) => !string(entry))
932
+ ) {
933
+ return failure(
934
+ contract,
935
+ "unmeasured",
936
+ 'Oracle result must carry verdict, challenge, changesMyMind, and a citedDecisions array of strings, for example {"verdict":"consistent with decision routing.target","challenge":"...","changesMyMind":"...","citedDecisions":["routing.target"]}',
937
+ );
938
+ }
939
+ return success(contract, "unmeasured", value);
940
+ }
941
+
942
+ /** Parse an oracle answer under the same schema its recipe contract enforces. */
943
+ export function parseOracleResult(output: string | null): OracleResult | null {
944
+ const validation = validateOracle({ kind: "oracle-report" }, output);
945
+ if (validation.conformance !== "pass") return null;
946
+ const parsed = parseJson(output);
947
+ if (!parsed.ok) return null;
948
+ return {
949
+ verdict: String(parsed.value.verdict),
950
+ challenge: String(parsed.value.challenge),
951
+ changesMyMind: String(parsed.value.changesMyMind),
952
+ citedDecisions: (parsed.value.citedDecisions as ReadonlyArray<string>).map((entry) => String(entry)),
953
+ };
954
+ }
955
+
667
956
  /**
668
957
  * A code step's result is always `unmeasured` for routing quality. A red suite
669
958
  * is evidence about the repository, not about the route that ran the command,
@@ -1036,6 +1325,14 @@ export function validateResultContract(input: ResultContractValidationInput): Re
1036
1325
  }
1037
1326
  case "verifier-report":
1038
1327
  return validateVerifier(input.contract, input.output);
1328
+ case "council-report": {
1329
+ const report = parseCouncilReport(input.output);
1330
+ return report === null
1331
+ ? failure(input.contract, "fail", "council-report is malformed")
1332
+ : success(input.contract, "unmeasured", { council: report });
1333
+ }
1334
+ case "council-ballot":
1335
+ return validateCouncilBallot(input.contract, input.output);
1039
1336
  case "debugger-report":
1040
1337
  return validateDebugger(input.contract, input.output);
1041
1338
  case "research-report":
@@ -1044,6 +1341,18 @@ export function validateResultContract(input: ResultContractValidationInput): Re
1044
1341
  return validateMutation(input.contract, input);
1045
1342
  case "provenance-report":
1046
1343
  return validateProvenance(input.contract, input.output);
1344
+ case "delegation-plan": {
1345
+ const plan = parseDelegationPlanResult(input.output);
1346
+ return plan === null
1347
+ ? failure(
1348
+ input.contract,
1349
+ "fail",
1350
+ "delegation-plan must carry tasks with id, agent, description, depends_on, writes, and optional mode",
1351
+ )
1352
+ : success(input.contract, "unmeasured", plan, { delegationPlan: plan });
1353
+ }
1354
+ case "oracle-report":
1355
+ return validateOracle(input.contract, input.output);
1047
1356
  case "external-delegation":
1048
1357
  return success(input.contract, "unmeasured", { external: true });
1049
1358
  case "artifact-report":
@@ -1120,6 +1429,10 @@ function resultContractShape(contract: ResultContract): string {
1120
1429
  return '{"findings":[{"claim":"what you observed","path":"src/file.ts","line":1}],"needsSplit":false,"proposedSubtasks":[]} or {"findings":[],"needsSplit":true,"proposedSubtasks":[{"id":"inspect","task":"Inspect the boundary","dependencies":[],"expectedResultContract":"scout-report","requestedAuthority":"read-only"}]} or, if you cannot produce a citation you are sure of, at least {"findings":[{"claim":"what you observed"}]}, which is accepted as an ungrounded lead rather than losing the run';
1121
1430
  case "verifier-report":
1122
1431
  return '{"verdict":"pass","checks":[{"name":"npm run typecheck","passed":true,"evidence":"exit 0"}]}';
1432
+ case "council-report":
1433
+ return '{"members":[{"label":"alpha","runId":"run-1","round":1,"answer":"..."}],"synthesis":{"kind":"none"}}';
1434
+ case "council-ballot":
1435
+ return COUNCIL_BALLOT_SHAPE;
1123
1436
  case "debugger-report":
1124
1437
  return '{"diagnosis":"...","reproduction":"reproduced","evidence":["..."]}';
1125
1438
  case "research-report":
@@ -1128,6 +1441,10 @@ function resultContractShape(contract: ResultContract): string {
1128
1441
  return '{"mutatedPaths":["src/file.ts"],"validations":[{"name":"npm test","passed":true,"evidence":"exit 0"}],"commitMessage":"optional: the commit message for this change","summary":"optional: one line"}';
1129
1442
  case "provenance-report":
1130
1443
  return '{"confirmedFacts":["..."],"missingEvidence":["..."],"nextInspections":["..."]}';
1444
+ case "delegation-plan":
1445
+ return '{"tasks":[{"id":"build","agent":"coder","description":"Implement the change","depends_on":[],"writes":["src"],"mode":"parallel"}]}';
1446
+ case "oracle-report":
1447
+ return '{"verdict":"one line","challenge":"the strongest objection you can mount","changesMyMind":"the evidence that would reverse the verdict","citedDecisions":["decision key or task id you relied on"]}';
1131
1448
  case "external-delegation":
1132
1449
  return "any final text";
1133
1450
  case "artifact-report":
@@ -1232,8 +1549,31 @@ export function resultContractRepairMessages(
1232
1549
  ] as const;
1233
1550
  }
1234
1551
 
1235
- /** Strict frontmatter parser for the one recipe result-contract schema. */
1236
- export function parseResultContract(value: unknown, sourcePath: string): ResultContract {
1552
+ /** Kinds an agent recipe may declare in its frontmatter. */
1553
+ const RECIPE_DECLARABLE_KINDS = [
1554
+ "scout-report",
1555
+ "verifier-report",
1556
+ "council-report",
1557
+ "debugger-report",
1558
+ "research-report",
1559
+ "mutation-report",
1560
+ "provenance-report",
1561
+ "oracle-report",
1562
+ "external-delegation",
1563
+ "artifact-report",
1564
+ "context-handbook",
1565
+ "delegation-plan",
1566
+ ] as const;
1567
+
1568
+ /**
1569
+ * Kinds the coordinator authors and hands to a worker on the wire, which no
1570
+ * recipe may declare. A council ballot is the topology's postcondition for a
1571
+ * vote member's slot, so it arrives as a `resultContractOverride` on the
1572
+ * request and never out of frontmatter.
1573
+ */
1574
+ const COORDINATOR_AUTHORED_KINDS = ["council-ballot"] as const;
1575
+
1576
+ function parseContract(value: unknown, sourcePath: string, kinds: ReadonlyArray<string>): ResultContract {
1237
1577
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
1238
1578
  throw new Error(`agent recipe: ${sourcePath}: resultContract must be an object`);
1239
1579
  }
@@ -1250,19 +1590,24 @@ export function parseResultContract(value: unknown, sourcePath: string): ResultC
1250
1590
  }
1251
1591
  return { kind: "architect-plan", path: record.path };
1252
1592
  }
1253
- const kinds = [
1254
- "scout-report",
1255
- "verifier-report",
1256
- "debugger-report",
1257
- "research-report",
1258
- "mutation-report",
1259
- "provenance-report",
1260
- "external-delegation",
1261
- "artifact-report",
1262
- "context-handbook",
1263
- ] as const;
1264
- if (!(kinds as ReadonlyArray<string>).includes(record.kind) || !only("kind")) {
1593
+ if (!kinds.includes(record.kind) || !only("kind")) {
1265
1594
  throw new Error(`agent recipe: ${sourcePath}: resultContract.kind is unsupported or has unknown keys`);
1266
1595
  }
1267
1596
  return { kind: record.kind as Exclude<ResultContract["kind"], "architect-plan"> };
1268
1597
  }
1598
+
1599
+ /** Strict frontmatter parser for the one recipe result-contract schema. */
1600
+ export function parseResultContract(value: unknown, sourcePath: string): ResultContract {
1601
+ return parseContract(value, sourcePath, RECIPE_DECLARABLE_KINDS);
1602
+ }
1603
+
1604
+ /**
1605
+ * The same parser for a contract that arrived on a WorkerSpec. It admits what a
1606
+ * recipe may declare plus what the coordinator may author, because a dispatch
1607
+ * request can override the seated recipe's postcondition and the worker has to
1608
+ * be able to read the contract it will be validated against. Keeping the recipe
1609
+ * parser strict is the point: a recipe still cannot claim a coordinator's kind.
1610
+ */
1611
+ export function parseWorkerResultContract(value: unknown, sourcePath: string): ResultContract {
1612
+ return parseContract(value, sourcePath, [...RECIPE_DECLARABLE_KINDS, ...COORDINATOR_AUTHORED_KINDS]);
1613
+ }
@@ -1,3 +1,12 @@
1
+ import {
2
+ normalizePathBoundary,
3
+ normalizePathBoundaryEntry,
4
+ PATH_BOUNDARY_MAX_ENTRIES,
5
+ type PathBoundary,
6
+ pathBoundariesOverlap,
7
+ pathBoundaryCovers,
8
+ } from "../../core/path-boundary.js";
9
+
1
10
  /**
2
11
  * Declared write boundaries: the repo paths a fleet step is allowed to change.
3
12
  *
@@ -30,37 +39,14 @@
30
39
  * a step that needs more of the tree than that should say `workspace` and mean
31
40
  * it in a contract that does not claim a boundary.
32
41
  */
33
- export const WRITE_BOUNDARY_MAX_ENTRIES = 32;
42
+ export const WRITE_BOUNDARY_MAX_ENTRIES = PATH_BOUNDARY_MAX_ENTRIES;
34
43
 
35
44
  /** A normalized allowlist. Empty means "changes nothing", which is `readonly`. */
36
- export type WriteBoundary = ReadonlyArray<string>;
37
-
38
- const GLOB_CHARACTERS = /[*?[\]{}]/u;
45
+ export type WriteBoundary = PathBoundary;
39
46
 
40
47
  /** Normalize one declared entry, or throw explaining exactly what is wrong. */
41
48
  export function normalizeWriteBoundaryEntry(entry: string): string {
42
- const raw = entry.trim();
43
- if (raw.length === 0) throw new Error("write boundary: entry must be a non-empty path");
44
- if (raw.includes("\\")) {
45
- throw new Error(`write boundary: entry '${entry}' must use '/' separators`);
46
- }
47
- if (GLOB_CHARACTERS.test(raw)) {
48
- throw new Error(`write boundary: entry '${entry}' looks like a glob, which v1 does not support`);
49
- }
50
- if (raw.startsWith("/") || /^[A-Za-z]:/u.test(raw)) {
51
- throw new Error(`write boundary: entry '${entry}' must be repository-relative, not absolute`);
52
- }
53
- const subtree = raw.endsWith("/");
54
- const segments = raw.split("/").filter((segment) => segment.length > 0);
55
- if (segments.length === 0) {
56
- throw new Error(`write boundary: entry '${entry}' must name a path inside the repository`);
57
- }
58
- for (const segment of segments) {
59
- if (segment === "..") throw new Error(`write boundary: entry '${entry}' must not contain '..'`);
60
- if (segment === ".") throw new Error(`write boundary: entry '${entry}' must not contain '.' segments`);
61
- }
62
- const joined = segments.join("/");
63
- return subtree ? `${joined}/` : joined;
49
+ return normalizePathBoundaryEntry(entry);
64
50
  }
65
51
 
66
52
  /**
@@ -69,38 +55,17 @@ export function normalizeWriteBoundaryEntry(entry: string): string {
69
55
  * and a sealed verdict can both carry.
70
56
  */
71
57
  export function normalizeWriteBoundary(entries: ReadonlyArray<string>): string[] {
72
- if (entries.length > WRITE_BOUNDARY_MAX_ENTRIES) {
73
- throw new Error(`write boundary: at most ${WRITE_BOUNDARY_MAX_ENTRIES} entries may be declared`);
74
- }
75
- const normalized = new Set(entries.map(normalizeWriteBoundaryEntry));
76
- // A file entry covered by a declared subtree is redundant, not an error: the
77
- // operator said the directory, and saying one of its files again changes
78
- // nothing about what is allowed.
79
- return [...normalized].sort();
80
- }
81
-
82
- /** Repo-relative path with the separators this grammar speaks. */
83
- function toRepoRelativePosix(path: string): string {
84
- return path.split("\\").join("/");
58
+ return normalizePathBoundary(entries);
85
59
  }
86
60
 
87
61
  /** Whether the boundary permits a change to one repo-relative path. */
88
62
  export function writeBoundaryCovers(boundary: WriteBoundary, path: string): boolean {
89
- const candidate = toRepoRelativePosix(path);
90
- return boundary.some((entry) => (entry.endsWith("/") ? candidate.startsWith(entry) : candidate === entry));
91
- }
92
-
93
- /** Whether two declared entries can describe the same path. */
94
- function entriesOverlap(left: string, right: string): boolean {
95
- if (left === right) return true;
96
- if (left.endsWith("/") && right.startsWith(left)) return true;
97
- if (right.endsWith("/") && left.startsWith(right)) return true;
98
- return false;
63
+ return pathBoundaryCovers(boundary, path);
99
64
  }
100
65
 
101
66
  /** Whether two boundaries share any path. Empty boundaries never overlap. */
102
67
  export function writeBoundariesOverlap(left: WriteBoundary, right: WriteBoundary): boolean {
103
- return left.some((entry) => right.some((other) => entriesOverlap(entry, other)));
68
+ return pathBoundariesOverlap(left, right);
104
69
  }
105
70
 
106
71
  /** Operator-facing rendering, used verbatim in violation messages. */
@@ -28,6 +28,10 @@ const HOT_RELOAD_FIELDS = new Set<string>([
28
28
  "modelSelector",
29
29
  "terminal.smoothStreaming",
30
30
  "attribution.gitCommits",
31
+ // The watchdog registration reads its settings live on every trigger, so
32
+ // enabling it, retargeting it, or changing its cadence takes effect on the
33
+ // next turn boundary without a restart or a session-routing patch.
34
+ "watchdog",
31
35
  ]);
32
36
 
33
37
  const NEXT_TURN_FIELDS = new Set<string>([