@iowarp/clio-coder 0.3.3 → 0.3.6

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 (370) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/CONTRIBUTING.md +7 -7
  3. package/README.md +3 -3
  4. package/dist/{acp-P2AQILE2.js → acp-2BEHC4DL.js} +9 -8
  5. package/dist/{agents-72W3BI7I.js → agents-LNNFTM53.js} +29 -24
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-5TWEIYDN.js → auth-KXXFI2VS.js} +14 -10
  8. package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
  9. package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
  10. package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
  11. package/dist/chunk-3BPUFZDL.js +37 -0
  12. package/dist/chunk-43AOLP7E.js +375 -0
  13. package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
  14. package/dist/{chunk-6SGHMWE3.js → chunk-5JGRAMKL.js} +5 -5
  15. package/dist/{chunk-V6RTAOC2.js → chunk-6US73PDB.js} +572 -51
  16. package/dist/{chunk-5UFT4SUX.js → chunk-6XXKFVSN.js} +3 -3
  17. package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
  18. package/dist/chunk-AOCYTWAV.js +449 -0
  19. package/dist/chunk-CFGTUFWB.js +67 -0
  20. package/dist/chunk-CJUB2JJ2.js +1478 -0
  21. package/dist/{chunk-FNTMWMX5.js → chunk-CKXWIANG.js} +14 -12
  22. package/dist/{chunk-PIWWS5BL.js → chunk-CYQKWTG3.js} +63 -78
  23. package/dist/{chunk-LZSJBIVT.js → chunk-DJVECN66.js} +271 -762
  24. package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
  25. package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
  26. package/dist/{chunk-STBPMHSX.js → chunk-EKY57CSP.js} +51 -84
  27. package/dist/{chunk-DUYJ5IO6.js → chunk-EYPA3EGJ.js} +12 -4
  28. package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
  29. package/dist/chunk-FYYLNIL5.js +313 -0
  30. package/dist/{chunk-OQ33BKR3.js → chunk-G7MUEIGA.js} +3 -60
  31. package/dist/chunk-GEYXPTRF.js +613 -0
  32. package/dist/chunk-GOXNB3AO.js +261 -0
  33. package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
  34. package/dist/chunk-HWUFFB6L.js +83 -0
  35. package/dist/{chunk-4XUGQOHA.js → chunk-K7T3E2SR.js} +15 -8
  36. package/dist/chunk-K7VKOLQQ.js +15 -0
  37. package/dist/{chunk-UFIIWP2H.js → chunk-KHSFENX2.js} +8 -8
  38. package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
  39. package/dist/chunk-LCGCVYZ4.js +57 -0
  40. package/dist/chunk-LL4KHSZI.js +22 -0
  41. package/dist/{chunk-PAJK6MAQ.js → chunk-LYF7OHWH.js} +42 -15
  42. package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
  43. package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
  44. package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
  45. package/dist/{chunk-TZK7PACC.js → chunk-NILBFAPG.js} +14 -8
  46. package/dist/chunk-ODFEOB4F.js +1082 -0
  47. package/dist/{chunk-AGYYIBLL.js → chunk-OH3TOQTB.js} +6 -2
  48. package/dist/chunk-OZNBF4L3.js +23 -0
  49. package/dist/{chunk-DSELYM6W.js → chunk-PBTHKCPN.js} +30 -10
  50. package/dist/{verify-375KUB3Y.js → chunk-PCZJO5TI.js} +127 -42
  51. package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
  52. package/dist/{chunk-SRDMMSEP.js → chunk-QM3F2GKX.js} +1063 -1645
  53. package/dist/{chunk-X6IAEBZR.js → chunk-QNQHSOLF.js} +7 -7
  54. package/dist/{chunk-OC7FIQPC.js → chunk-R46L2BIR.js} +10 -7
  55. package/dist/{chunk-2TLUCQVG.js → chunk-RD5U66HV.js} +3 -3
  56. package/dist/{chunk-6N5PTWMY.js → chunk-RY3LY4J5.js} +50 -13
  57. package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
  58. package/dist/{chunk-OOJYHWRB.js → chunk-TSHXZTOQ.js} +6 -5
  59. package/dist/chunk-TZSKNMZG.js +434 -0
  60. package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
  61. package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
  62. package/dist/{chunk-COU2UHX6.js → chunk-VEZEGCGW.js} +170 -2
  63. package/dist/chunk-W6GROXXM.js +69 -0
  64. package/dist/{chunk-OAO4GE4M.js → chunk-WHGPSPT5.js} +2 -2
  65. package/dist/chunk-WHJYKASB.js +677 -0
  66. package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
  67. package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
  68. package/dist/{chunk-TZTZS7QK.js → chunk-XE2VEJHX.js} +5 -3
  69. package/dist/{chunk-LM5TQCJZ.js → chunk-XF5N4U5A.js} +8 -7
  70. package/dist/{chunk-LWLEKMDQ.js → chunk-XXQNGV4M.js} +1073 -552
  71. package/dist/{chunk-KZWTDYJF.js → chunk-XYDYPRZI.js} +7 -7
  72. package/dist/chunk-ZGVHUX3M.js +66 -0
  73. package/dist/{chunk-LW6DSM3M.js → chunk-ZRGEBJ4T.js} +1192 -1119
  74. package/dist/{chunk-2DJ2KNFG.js → chunk-ZXF4XRKW.js} +202 -40
  75. package/dist/chunk-ZZMN5OM4.js +122 -0
  76. package/dist/cli/index.js +34 -30
  77. package/dist/{clio-JOU4FXVA.js → clio-M2KGYUFZ.js} +7 -6
  78. package/dist/{code-nav-7AX6FYE6.js → code-nav-GQNL7XA6.js} +8 -6
  79. package/dist/codewiki/build-worker.js +4 -4
  80. package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
  81. package/dist/{config-XCDVKR23.js → config-XUUYQIWO.js} +47 -35
  82. package/dist/{configure-4GAP54ZW.js → configure-IHJ7YOMV.js} +18 -15
  83. package/dist/{context-77FM5DV5.js → context-74JLXAWD.js} +18 -10
  84. package/dist/{context-4UOGGLQ5.js → context-75MIWW3U.js} +41 -29
  85. package/dist/{context-5VKGUVJJ.js → context-ZQ7SIFJV.js} +85 -9
  86. package/dist/{context-clear-XXJRLCJJ.js → context-clear-GYKWNUML.js} +41 -29
  87. package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
  88. package/dist/context-working-set-UX5KEP4J.js +1553 -0
  89. package/dist/{dispatch-runner-QPRDDBDX.js → dispatch-runner-GIJBHNFL.js} +47 -32
  90. package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
  91. package/dist/{doctor-HR46URBJ.js → doctor-SVJ5BZCW.js} +12 -12
  92. package/dist/{eval-XSSNATB4.js → eval-CG6LLBLD.js} +54 -238
  93. package/dist/{evidence-6HG2PY2B.js → evidence-ZYFIEN42.js} +57 -28
  94. package/dist/{evolve-K7YU3NCY.js → evolve-QGEXEMDW.js} +36 -25
  95. package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
  96. package/dist/{fleet-VY3HHKN6.js → fleet-S5R4ZOQY.js} +73 -44
  97. package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-BHSNPBMH.js} +3 -3
  98. package/dist/{init-JYGXI3FK.js → init-5DRU55YR.js} +49 -37
  99. package/dist/memory-7YKKR6UC.js +467 -0
  100. package/dist/{models-I5QWSEOM.js → models-ZPOLRU2C.js} +24 -21
  101. package/dist/{monitor-GE4ID3IA.js → monitor-US5F5YGZ.js} +73 -46
  102. package/dist/{orchestrator-EM5MC3HM.js → orchestrator-E2AL4T5N.js} +1624 -1007
  103. package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
  104. package/dist/{reset-L2FQEE3E.js → reset-KZ652EK6.js} +6 -5
  105. package/dist/{run-ZU3QMZPZ.js → run-SRNBKDWD.js} +76 -54
  106. package/dist/{share-S5BZQC5I.js → share-CGZE33UP.js} +7 -6
  107. package/dist/{skills-X5VXCRNQ.js → skills-S2X4DLY5.js} +4 -4
  108. package/dist/{skills-eval-WKIHWTHR.js → skills-eval-W2GGIC4R.js} +40 -29
  109. package/dist/{targets-SNCPI2NR.js → targets-54SWINWB.js} +28 -23
  110. package/dist/{terminal-lease-BNAHVHBS.js → terminal-lease-SAIF2OGY.js} +6 -4
  111. package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
  112. package/dist/{upgrade-JQHHPQ4K.js → upgrade-JKAR27XC.js} +20 -19
  113. package/dist/{usage-OR4O5SMZ.js → usage-MSAWCLX4.js} +79 -36
  114. package/dist/verifiers-NCBTHHN2.js +1220 -0
  115. package/dist/verify-X5HDROLA.js +25 -0
  116. package/dist/{wiki-generate-UEXP2ARI.js → wiki-generate-GUSOQ6ZP.js} +50 -37
  117. package/dist/worker/entry.js +90 -70
  118. package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
  119. package/docs/README.md +8 -7
  120. package/docs/acp.md +1 -1
  121. package/docs/alcf-provider.md +1 -1
  122. package/docs/architecture.md +2 -2
  123. package/docs/artifact-placement.md +1 -2
  124. package/docs/artifact-versions.md +1 -1
  125. package/docs/built-in-agents.md +1 -1
  126. package/docs/capacity-and-scheduling.md +1 -1
  127. package/docs/commands-and-modes.md +60 -26
  128. package/docs/config-knobs-audit.md +1 -2
  129. package/docs/configuration-and-targets.md +26 -1
  130. package/docs/context-engine.md +67 -13
  131. package/docs/context-working-set.md +194 -0
  132. package/docs/development-pipeline.md +1 -1
  133. package/docs/documentation-coverage.md +6 -6
  134. package/docs/documentation-guide.md +7 -6
  135. package/docs/environment-variables.md +2 -1
  136. package/docs/eval-runner.md +1 -1
  137. package/docs/evals-internal.md +4 -32
  138. package/docs/evidence-and-memory.md +139 -7
  139. package/docs/evolution.md +1 -1
  140. package/docs/exit-codes-and-output.md +1 -1
  141. package/docs/extensions-and-sharing.md +2 -2
  142. package/docs/fleet-dispatch.md +49 -8
  143. package/docs/glossary.md +21 -1
  144. package/docs/installation-and-lifecycle.md +2 -2
  145. package/docs/middleware-and-components.md +19 -2
  146. package/docs/model-catalog.md +7 -9
  147. package/docs/observability.md +4 -4
  148. package/docs/proactive-memory.md +26 -16
  149. package/docs/prompt-envelope-and-tools.md +7 -5
  150. package/docs/provider-adapter-cookbook.md +1 -1
  151. package/docs/release-cut-checklist.md +43 -40
  152. package/docs/safety-model.md +49 -8
  153. package/docs/scientific-validation.md +21 -3
  154. package/docs/session-lifecycle.md +3 -3
  155. package/docs/skills-marketplace.md +1 -1
  156. package/docs/tool-usage.md +79 -12
  157. package/docs/trace-store.md +1 -1
  158. package/docs/troubleshooting.md +1 -1
  159. package/docs/tui-design.md +38 -4
  160. package/docs/worker-dispatch-mechanics.md +11 -1
  161. package/package.json +13 -13
  162. package/skills/meta/clio-test/SKILL.md +20 -17
  163. package/skills/meta/clio-test/evals.md +3 -3
  164. package/skills/meta/clio-test/references/harness.md +35 -6
  165. package/skills/meta/clio-test/references/test-map.md +20 -10
  166. package/skills/registry.yaml +2 -2
  167. package/skills/skill-marketplace.json +1 -1
  168. package/src/cli/agents.ts +2 -3
  169. package/src/cli/argv.ts +14 -1
  170. package/src/cli/context-working-set.ts +513 -0
  171. package/src/cli/context.ts +8 -0
  172. package/src/cli/evidence.ts +20 -2
  173. package/src/cli/fleet.ts +15 -0
  174. package/src/cli/index.ts +5 -1
  175. package/src/cli/memory.ts +272 -10
  176. package/src/cli/modes/json-stream.ts +2 -2
  177. package/src/cli/modes/print.ts +12 -1
  178. package/src/cli/run.ts +22 -2
  179. package/src/cli/targets.ts +12 -3
  180. package/src/cli/usage.ts +55 -7
  181. package/src/cli/verifiers.ts +325 -0
  182. package/src/core/bash-exec.ts +39 -14
  183. package/src/core/bus-events.ts +22 -4
  184. package/src/core/config.ts +54 -0
  185. package/src/core/defaults.ts +50 -3
  186. package/src/core/response-model-id.ts +134 -0
  187. package/src/core/toml.ts +62 -0
  188. package/src/core/verification-scripts.ts +6 -0
  189. package/src/core/workspace-files.ts +0 -1
  190. package/src/domains/agents/builtins/architect.md +1 -1
  191. package/src/domains/agents/builtins/verifier.md +3 -0
  192. package/src/domains/agents/catalog.ts +5 -4
  193. package/src/domains/agents/recipe.ts +54 -14
  194. package/src/domains/agents/result-contract.ts +7 -4
  195. package/src/domains/config/classify.ts +1 -0
  196. package/src/domains/context/bootstrap.ts +36 -27
  197. package/src/domains/context/project-metadata.ts +19 -63
  198. package/src/domains/context/prompt-context.ts +8 -0
  199. package/src/domains/context/working-set/contract.ts +161 -0
  200. package/src/domains/context/working-set/defaults.ts +28 -0
  201. package/src/domains/context/working-set/engine.ts +203 -0
  202. package/src/domains/context/working-set/fold.ts +62 -0
  203. package/src/domains/context/working-set/horizon.ts +38 -0
  204. package/src/domains/context/working-set/marker.ts +103 -0
  205. package/src/domains/context/working-set/path-index.ts +436 -0
  206. package/src/domains/context/working-set/payload.ts +152 -0
  207. package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
  208. package/src/domains/context/working-set/policies/index.ts +20 -0
  209. package/src/domains/context/working-set/policies/structural.ts +160 -0
  210. package/src/domains/context/working-set/project.ts +132 -0
  211. package/src/domains/context/working-set/protect.ts +109 -0
  212. package/src/domains/context/working-set/recall.ts +177 -0
  213. package/src/domains/context/working-set/replay/controls.ts +112 -0
  214. package/src/domains/context/working-set/replay/load-clio.ts +199 -0
  215. package/src/domains/context/working-set/replay/metrics.ts +185 -0
  216. package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
  217. package/src/domains/context/working-set/replay/report.ts +139 -0
  218. package/src/domains/context/working-set/replay/runner.ts +325 -0
  219. package/src/domains/context/working-set/replay/synthetic.ts +422 -0
  220. package/src/domains/context/working-set/replay/trace.ts +21 -0
  221. package/src/domains/context/working-set/visible.ts +54 -0
  222. package/src/domains/dispatch/budget-envelope.ts +396 -0
  223. package/src/domains/dispatch/contract.ts +2 -0
  224. package/src/domains/dispatch/extension.ts +81 -27
  225. package/src/domains/dispatch/orphan-recovery.ts +1 -0
  226. package/src/domains/dispatch/receipt-integrity.ts +4 -0
  227. package/src/domains/dispatch/state.ts +1 -0
  228. package/src/domains/dispatch/types.ts +10 -3
  229. package/src/domains/dispatch/validation.ts +14 -0
  230. package/src/domains/dispatch/worker-spawn.ts +14 -3
  231. package/src/domains/eval/metrics/evidence.ts +0 -116
  232. package/src/domains/eval/metrics/invariants.ts +1 -1
  233. package/src/domains/eval/runners/clio-run.ts +1 -10
  234. package/src/domains/eval/runners/external-command.ts +2 -29
  235. package/src/domains/eval/schema/suite.ts +0 -7
  236. package/src/domains/eval/suites/run.ts +1 -7
  237. package/src/domains/evidence/build.ts +112 -45
  238. package/src/domains/evidence/eval.ts +24 -7
  239. package/src/domains/evidence/index.ts +53 -0
  240. package/src/domains/evidence/ordering.ts +12 -0
  241. package/src/domains/evidence/run-trust.ts +221 -0
  242. package/src/domains/evidence/store.ts +46 -6
  243. package/src/domains/evidence/trust-status.ts +854 -0
  244. package/src/domains/evidence/types.ts +26 -0
  245. package/src/domains/memory/index.ts +22 -0
  246. package/src/domains/memory/operations.ts +58 -1
  247. package/src/domains/memory/promotion.ts +281 -0
  248. package/src/domains/memory/prompt-section.ts +25 -5
  249. package/src/domains/memory/proposal.ts +51 -7
  250. package/src/domains/memory/task-bank.ts +3 -2
  251. package/src/domains/memory/task-memory-handoff.ts +181 -24
  252. package/src/domains/memory/task-memory-policy.ts +3 -1
  253. package/src/domains/memory/types.ts +37 -0
  254. package/src/domains/memory/validate.ts +178 -0
  255. package/src/domains/middleware/memory-intervention.ts +38 -25
  256. package/src/domains/middleware/runtime.ts +6 -0
  257. package/src/domains/middleware/skills-reminder.ts +19 -4
  258. package/src/domains/middleware/stalled-turn.ts +208 -5
  259. package/src/domains/middleware/types.ts +10 -0
  260. package/src/domains/observability/contract.ts +6 -1
  261. package/src/domains/observability/cost.ts +20 -4
  262. package/src/domains/observability/extension.ts +2 -2
  263. package/src/domains/providers/index.ts +3 -0
  264. package/src/domains/providers/model-discovery.ts +9 -0
  265. package/src/domains/providers/runtime-resolution.ts +38 -1
  266. package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
  267. package/src/domains/providers/types/context-window-slots.ts +18 -0
  268. package/src/domains/providers/types/runtime-descriptor.ts +3 -1
  269. package/src/domains/safety/autonomy.ts +1 -1
  270. package/src/domains/safety/call-target.ts +211 -14
  271. package/src/domains/safety/decision-presentation.ts +268 -0
  272. package/src/domains/safety/default-path-policy.ts +8 -0
  273. package/src/domains/safety/finish-contract.ts +4 -3
  274. package/src/domains/safety/policy-engine.ts +48 -6
  275. package/src/domains/safety/redaction.ts +73 -0
  276. package/src/domains/session/compaction/compact.ts +23 -1
  277. package/src/domains/session/compaction/cut-point.ts +2 -0
  278. package/src/domains/session/compaction/tokens.ts +16 -1
  279. package/src/domains/session/context-ledger.ts +12 -1
  280. package/src/domains/session/decision-board.ts +4 -0
  281. package/src/domains/session/entries.ts +110 -1
  282. package/src/domains/session/history.ts +68 -19
  283. package/src/domains/session/manager.ts +9 -2
  284. package/src/domains/session/migrations/index.ts +22 -3
  285. package/src/domains/session/usage.ts +24 -7
  286. package/src/engine/acp/event-mapper.ts +7 -0
  287. package/src/engine/acp/server.ts +32 -2
  288. package/src/engine/agent.ts +18 -1
  289. package/src/engine/apis/lmstudio.ts +25 -4
  290. package/src/engine/apis/openai-completions.ts +147 -22
  291. package/src/engine/claude/sdk-runtime.ts +8 -2
  292. package/src/engine/claude/tool-safety.ts +13 -0
  293. package/src/engine/loop-guard.ts +27 -3
  294. package/src/engine/session.ts +9 -3
  295. package/src/engine/worker-events.ts +4 -3
  296. package/src/engine/worker-runtime.ts +59 -54
  297. package/src/entry/orchestrator.ts +34 -5
  298. package/src/interactive/chat-loop-messages.ts +40 -6
  299. package/src/interactive/chat-loop.ts +13 -0
  300. package/src/interactive/chat-panel.ts +17 -1
  301. package/src/interactive/chat-renderer.ts +49 -24
  302. package/src/interactive/clio-editor.ts +44 -7
  303. package/src/interactive/context-meter.ts +10 -0
  304. package/src/interactive/context-overlay.ts +120 -7
  305. package/src/interactive/context-recall-command.ts +110 -0
  306. package/src/interactive/cost-overlay.ts +39 -8
  307. package/src/interactive/dispatch-board.ts +212 -35
  308. package/src/interactive/footer/widgets.ts +13 -0
  309. package/src/interactive/interactive-application.ts +6 -1
  310. package/src/interactive/interactive-input-runtime.ts +11 -1
  311. package/src/interactive/interactive-presentation.ts +11 -1
  312. package/src/interactive/interactive-slash-runtime.ts +37 -1
  313. package/src/interactive/memory-overlay.ts +89 -4
  314. package/src/interactive/model-session-replay.ts +21 -0
  315. package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
  316. package/src/interactive/overlay-frame.ts +5 -2
  317. package/src/interactive/overlay-general-openers.ts +46 -1
  318. package/src/interactive/overlay-key-routing.ts +41 -1
  319. package/src/interactive/overlay-lifecycle.ts +11 -4
  320. package/src/interactive/overlay-permission-lifecycle.ts +23 -8
  321. package/src/interactive/overlay-session-lifecycle.ts +8 -4
  322. package/src/interactive/overlay-transitions.ts +11 -0
  323. package/src/interactive/overlays/ask-user.ts +74 -30
  324. package/src/interactive/overlays/decisions.ts +3 -1
  325. package/src/interactive/permission-hint.ts +35 -0
  326. package/src/interactive/permission-overlay.ts +95 -45
  327. package/src/interactive/renderers/tool-execution.ts +37 -51
  328. package/src/interactive/session-last-turn.ts +8 -1
  329. package/src/interactive/session-transcript.ts +2 -2
  330. package/src/interactive/session-usage-reseed.ts +36 -10
  331. package/src/interactive/slash-commands.ts +31 -4
  332. package/src/interactive/status/summary.ts +5 -0
  333. package/src/interactive/status/types.ts +5 -0
  334. package/src/interactive/terminal-lease.ts +1 -0
  335. package/src/interactive/turn-context.ts +333 -110
  336. package/src/interactive/turn-middleware.ts +7 -6
  337. package/src/interactive/turn-runtime.ts +37 -8
  338. package/src/interactive/turn-state.ts +3 -0
  339. package/src/interactive/worker-progress.ts +440 -0
  340. package/src/interactive/worker-stream.ts +51 -110
  341. package/src/tools/agent-tools.ts +39 -7
  342. package/src/tools/ask-user.ts +21 -1
  343. package/src/tools/bash.ts +144 -82
  344. package/src/tools/builtin-tool-catalog.ts +11 -5
  345. package/src/tools/context/index.ts +107 -5
  346. package/src/tools/context/surface.ts +3 -2
  347. package/src/tools/core-bootstrap.ts +21 -0
  348. package/src/tools/dispatch-arguments.ts +8 -0
  349. package/src/tools/dispatch-event-text.ts +19 -0
  350. package/src/tools/dispatch-runner.ts +9 -7
  351. package/src/tools/dispatch.ts +24 -1
  352. package/src/tools/monitor.ts +43 -20
  353. package/src/tools/registry.ts +72 -10
  354. package/src/tools/result-disposition.ts +706 -0
  355. package/src/tools/result-shaping.ts +321 -20
  356. package/src/tools/safe-exec.ts +2 -0
  357. package/src/tools/verify/authoring.ts +1120 -0
  358. package/src/tools/verify/catalog.ts +346 -0
  359. package/src/tools/verify/index.ts +13 -3
  360. package/src/tools/verify/scripts.ts +135 -37
  361. package/src/tools/verify/surface.ts +9 -5
  362. package/src/tools/worker-evidence.ts +54 -12
  363. package/src/worker/spec-contract.ts +43 -3
  364. package/dist/chunk-J7CWMCQD.js +0 -255
  365. package/dist/chunk-T6YILFSB.js +0 -80
  366. package/dist/chunk-VAKQQHWR.js +0 -434
  367. package/dist/chunk-VPAYEGVX.js +0 -184
  368. package/dist/chunk-XBXAASKX.js +0 -18
  369. package/dist/memory-WFZMGYHX.js +0 -236
  370. package/src/domains/eval/metrics/chaos-stream.ts +0 -93
@@ -0,0 +1,1120 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ renameSync,
8
+ rmSync,
9
+ statSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import path from "node:path";
13
+ import { isMap, isSeq, parseDocument, stringify, type YAMLMap } from "yaml";
14
+ import { resolveSafeCwd, SAFE_EXEC_DEFAULT_TIMEOUT_MS } from "../../core/safe-exec.js";
15
+ import { parseTomlDocument, tomlTableAt } from "../../core/toml.js";
16
+ import { isVerificationScriptName } from "../../core/verification-scripts.js";
17
+ import { compareCodepoints } from "../../domains/evidence/ordering.js";
18
+ import type { ToolResult } from "../registry.js";
19
+ import {
20
+ type DeclaredCheck,
21
+ PROJECT_VERIFIER_CATALOG_CAPS,
22
+ PROJECT_VERIFIER_CATALOG_RELATIVE_PATH,
23
+ PROJECT_VERIFIER_CATALOG_VERSION,
24
+ parseProjectVerifierCatalogText,
25
+ } from "./catalog.js";
26
+ import { verifyTool } from "./index.js";
27
+ import { discoverDeclaredChecksAtRoot } from "./scripts.js";
28
+
29
+ export type VerifierProposalAuthority = "project-declared" | "toolchain-defined";
30
+
31
+ export type VerifierSignalKind =
32
+ | "package-script"
33
+ | "project-catalog"
34
+ | "cargo"
35
+ | "cmake-preset"
36
+ | "python-runner"
37
+ | "go-module"
38
+ | "validation-contract"
39
+ | "manual-entry";
40
+
41
+ export interface VerifierProvenance {
42
+ kind: VerifierSignalKind;
43
+ path: string;
44
+ detail: string;
45
+ authority: VerifierProposalAuthority;
46
+ }
47
+
48
+ export interface AuthoringCheck {
49
+ id: string;
50
+ description: string;
51
+ command: string[];
52
+ cwd: string;
53
+ timeoutMs: number;
54
+ tags: string[];
55
+ provenance: VerifierProvenance;
56
+ state: "active" | "existing" | "proposed" | "manual";
57
+ /** The id this check has in the on-disk catalog, so a rename still edits its node in place. */
58
+ catalogId?: string;
59
+ }
60
+
61
+ export interface VerifierAuthoringDiscovery {
62
+ ok: true;
63
+ workspaceRoot: string;
64
+ catalogPath: string;
65
+ activeChecks: AuthoringCheck[];
66
+ existingChecks: AuthoringCheck[];
67
+ proposals: AuthoringCheck[];
68
+ diagnostics: string[];
69
+ manualEntry: string;
70
+ /** The catalog file as found, when one exists; revisions are written into it in place. */
71
+ catalogText?: string;
72
+ }
73
+
74
+ export interface VerifierAuthoringDiscoveryError {
75
+ ok: false;
76
+ reason: string;
77
+ manualEntry: string;
78
+ }
79
+
80
+ export type VerifierAuthoringDiscoveryResult = VerifierAuthoringDiscovery | VerifierAuthoringDiscoveryError;
81
+
82
+ export interface VerifierDraft {
83
+ workspaceRoot: string;
84
+ catalogPath: string;
85
+ activeChecks: AuthoringCheck[];
86
+ checks: AuthoringCheck[];
87
+ diagnostics: string[];
88
+ manualEntry: string;
89
+ catalogText?: string;
90
+ }
91
+
92
+ export type VerifierRevision =
93
+ | { kind: "add"; check: Omit<AuthoringCheck, "state" | "provenance">; provenance?: VerifierProvenance }
94
+ | {
95
+ kind: "edit";
96
+ id: string;
97
+ changes: Partial<Pick<AuthoringCheck, "description" | "command" | "cwd" | "timeoutMs" | "tags">>;
98
+ }
99
+ | { kind: "rename"; id: string; newId: string }
100
+ | { kind: "remove"; id: string };
101
+
102
+ export type VerifierDraftValidation =
103
+ | { ok: true; text: string; checks: AuthoringCheck[] }
104
+ | { ok: false; reason: string; text: string };
105
+
106
+ export type VerifierRevisionResult =
107
+ | { ok: true; draft: VerifierDraft; diagnostics: string[] }
108
+ | { ok: false; draft: VerifierDraft; reason: string };
109
+
110
+ export type VerifierAuthoringDecision =
111
+ | { kind: "confirm"; dryRunCheckIds?: string[] }
112
+ | { kind: "reject" }
113
+ | { kind: "revise"; revisions: VerifierRevision[] };
114
+
115
+ export interface VerifierAuthoringDecisionContext {
116
+ draft: VerifierDraft;
117
+ preview: string;
118
+ validation: VerifierDraftValidation;
119
+ revision: number;
120
+ }
121
+
122
+ export interface VerifierDryRunResult {
123
+ id: string;
124
+ result: ToolResult;
125
+ }
126
+
127
+ export type VerifierAuthoringWorkflowResult =
128
+ | { status: "invalid"; reason: string; wrote: boolean; path?: string; preview?: string }
129
+ | { status: "rejected"; wrote: false; preview: string; diagnostics: string[] }
130
+ | {
131
+ status: "written";
132
+ wrote: true;
133
+ path: string;
134
+ preview: string;
135
+ diagnostics: string[];
136
+ dryRuns: VerifierDryRunResult[];
137
+ };
138
+
139
+ export interface VerifierAuthoringWorkflowOptions {
140
+ workspaceRoot?: string;
141
+ includeProposals?: boolean;
142
+ initialRevisions?: VerifierRevision[];
143
+ /**
144
+ * The operator already authorized writing (`--yes`), so the review is a
145
+ * pre-write authority preview and must not claim nothing will be written.
146
+ */
147
+ confirmed?: boolean;
148
+ decide: (context: VerifierAuthoringDecisionContext) => VerifierAuthoringDecision | Promise<VerifierAuthoringDecision>;
149
+ runCheck?: (id: string) => Promise<ToolResult>;
150
+ }
151
+
152
+ interface RawProposal {
153
+ preferredId: string;
154
+ description: string;
155
+ command: string[];
156
+ cwd: string;
157
+ timeoutMs: number;
158
+ tags: string[];
159
+ provenance: VerifierProvenance;
160
+ }
161
+
162
+ const DECLARED_FILE_CAP_BYTES = 1024 * 1024;
163
+ const VALIDATION_CONTRACT_PATHS = [
164
+ ".clio-coder/validation.yaml",
165
+ ".clio-coder/validation.yml",
166
+ "validation.yaml",
167
+ "validation.yml",
168
+ ] as const;
169
+
170
+ function manualEntryInstruction(): string {
171
+ return (
172
+ "No unambiguous declared command was found. Add one explicitly with " +
173
+ '`clio-coder verifiers add --id <id> --description <text> --command \'["executable","arg"]\'`; `--command` is an exact JSON argv array. Review the preview, then rerun with `--yes`.'
174
+ );
175
+ }
176
+
177
+ function cloneCheck(check: AuthoringCheck): AuthoringCheck {
178
+ return {
179
+ ...check,
180
+ command: [...check.command],
181
+ tags: [...check.tags],
182
+ provenance: { ...check.provenance },
183
+ };
184
+ }
185
+
186
+ function relativePath(workspaceRoot: string, filePath: string): string {
187
+ const relative = path.relative(workspaceRoot, filePath);
188
+ return relative.length === 0 ? "." : relative.split(path.sep).join("/");
189
+ }
190
+
191
+ function regularFileText(filePath: string, workspaceRoot: string): string | null | Error {
192
+ if (!existsSync(filePath)) return null;
193
+ try {
194
+ const realRoot = realpathSync(workspaceRoot);
195
+ const realFilePath = realpathSync(filePath);
196
+ resolveSafeCwd(realFilePath, realRoot);
197
+ const stats = statSync(realFilePath);
198
+ if (!stats.isFile()) return new Error("path is not a regular file");
199
+ if (stats.size > DECLARED_FILE_CAP_BYTES) {
200
+ return new Error(`file exceeds the ${DECLARED_FILE_CAP_BYTES}-byte discovery cap`);
201
+ }
202
+ return readFileSync(realFilePath, "utf8");
203
+ } catch (error) {
204
+ return error instanceof Error ? error : new Error(String(error));
205
+ }
206
+ }
207
+
208
+ function slug(value: string, fallback: string): string {
209
+ const normalized = value
210
+ .toLowerCase()
211
+ .replace(/[^a-z0-9._:-]+/gu, "-")
212
+ .replace(/^[^a-z0-9]+/u, "")
213
+ .replace(/[-.:]+$/u, "");
214
+ return normalized.length > 0 ? normalized : fallback;
215
+ }
216
+
217
+ function boundedId(base: string, suffix = ""): string {
218
+ const cap = PROJECT_VERIFIER_CATALOG_CAPS.idBytes - Buffer.byteLength(suffix, "utf8");
219
+ let result = "";
220
+ for (const character of base) {
221
+ if (Buffer.byteLength(result + character, "utf8") > cap) break;
222
+ result += character;
223
+ }
224
+ result = result.replace(/[-.:]+$/u, "");
225
+ return `${result.length > 0 ? result : "check"}${suffix}`;
226
+ }
227
+
228
+ /** Allocate the same stable suffix for the same ordered set of occupied IDs. */
229
+ export function deterministicVerifierId(preferredId: string, occupied: ReadonlySet<string>): string {
230
+ const base = boundedId(slug(preferredId, "check"));
231
+ if (!occupied.has(base) && base !== "frontend") return base;
232
+ for (let index = 2; index <= PROJECT_VERIFIER_CATALOG_CAPS.checks + 2; index += 1) {
233
+ const suffix = `-${index}`;
234
+ const candidate = boundedId(base, suffix);
235
+ if (!occupied.has(candidate) && candidate !== "frontend") return candidate;
236
+ }
237
+ throw new Error(`cannot allocate a deterministic ID for '${preferredId}' within the catalog cap`);
238
+ }
239
+
240
+ function rawProposalSort(left: RawProposal, right: RawProposal): number {
241
+ return (
242
+ compareCodepoints(left.provenance.path, right.provenance.path) ||
243
+ compareCodepoints(left.provenance.detail, right.provenance.detail) ||
244
+ compareCodepoints(left.preferredId, right.preferredId) ||
245
+ compareCodepoints(JSON.stringify(left.command), JSON.stringify(right.command))
246
+ );
247
+ }
248
+
249
+ function checkIdentity(check: Pick<AuthoringCheck, "command" | "cwd">): string {
250
+ return JSON.stringify([check.cwd, check.command]);
251
+ }
252
+
253
+ function sourceProvenance(workspaceRoot: string, check: DeclaredCheck): VerifierProvenance {
254
+ if (check.source.kind === "package.json") {
255
+ return {
256
+ kind: "package-script",
257
+ path: relativePath(workspaceRoot, check.source.path),
258
+ detail: `package.json script '${check.id}'`,
259
+ authority: "project-declared",
260
+ };
261
+ }
262
+ return {
263
+ kind: "project-catalog",
264
+ path: PROJECT_VERIFIER_CATALOG_RELATIVE_PATH,
265
+ detail: `existing catalog check '${check.id}'`,
266
+ authority: "project-declared",
267
+ };
268
+ }
269
+
270
+ function projectedCheck(workspaceRoot: string, check: DeclaredCheck): AuthoringCheck {
271
+ return {
272
+ id: check.id,
273
+ description: check.description,
274
+ command: [...check.command],
275
+ cwd: check.cwd,
276
+ timeoutMs: check.timeoutMs,
277
+ tags: [...check.tags],
278
+ provenance: sourceProvenance(workspaceRoot, check),
279
+ state: check.source.kind === "package.json" ? "active" : "existing",
280
+ ...(check.source.kind === "package.json" ? {} : { catalogId: check.id }),
281
+ };
282
+ }
283
+
284
+ function cargoProposals(workspaceRoot: string, diagnostics: string[]): RawProposal[] {
285
+ const relative = "Cargo.toml";
286
+ const text = regularFileText(path.join(workspaceRoot, relative), workspaceRoot);
287
+ if (text === null) return [];
288
+ if (text instanceof Error) {
289
+ diagnostics.push(`${relative}: ${text.message}; Cargo discovery skipped.`);
290
+ return [];
291
+ }
292
+ const document = parseTomlDocument(text);
293
+ if (document === null) {
294
+ diagnostics.push(`${relative}: invalid TOML; Cargo discovery skipped.`);
295
+ return [];
296
+ }
297
+ const workspace = tomlTableAt(document, ["workspace"]) !== null;
298
+ const packageManifest = tomlTableAt(document, ["package"]) !== null;
299
+ if (!workspace && !packageManifest) {
300
+ diagnostics.push(`${relative}: no [package] or [workspace] declaration was found; Cargo discovery skipped.`);
301
+ return [];
302
+ }
303
+ return [
304
+ {
305
+ preferredId: "cargo-test",
306
+ description: workspace ? "Run the Cargo workspace tests" : "Run the Cargo package tests",
307
+ command: workspace ? ["cargo", "test", "--workspace"] : ["cargo", "test"],
308
+ cwd: ".",
309
+ timeoutMs: SAFE_EXEC_DEFAULT_TIMEOUT_MS,
310
+ tags: ["rust", "test"],
311
+ provenance: {
312
+ kind: "cargo",
313
+ path: relative,
314
+ detail: workspace ? "Cargo [workspace] manifest" : "Cargo package manifest",
315
+ authority: "toolchain-defined",
316
+ },
317
+ },
318
+ ];
319
+ }
320
+
321
+ function cmakeProposals(workspaceRoot: string, diagnostics: string[]): RawProposal[] {
322
+ const relative = "CMakePresets.json";
323
+ const text = regularFileText(path.join(workspaceRoot, relative), workspaceRoot);
324
+ if (text === null) return [];
325
+ if (text instanceof Error) {
326
+ diagnostics.push(`${relative}: ${text.message}; CMake preset discovery skipped.`);
327
+ return [];
328
+ }
329
+ let parsed: unknown;
330
+ try {
331
+ parsed = JSON.parse(text) as unknown;
332
+ } catch (error) {
333
+ diagnostics.push(`${relative}: invalid JSON (${error instanceof Error ? error.message : String(error)}).`);
334
+ return [];
335
+ }
336
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
337
+ diagnostics.push(`${relative}: root is not an object; CMake preset discovery skipped.`);
338
+ return [];
339
+ }
340
+ const record = parsed as Record<string, unknown>;
341
+ const proposals: RawProposal[] = [];
342
+ for (const [field, executable, args, label, tag] of [
343
+ ["testPresets", "ctest", ["--preset"], "test", "test"],
344
+ ["buildPresets", "cmake", ["--build", "--preset"], "build", "build"],
345
+ ] as const) {
346
+ const presets = record[field];
347
+ if (!Array.isArray(presets)) continue;
348
+ for (const preset of presets) {
349
+ if (preset === null || typeof preset !== "object" || Array.isArray(preset)) continue;
350
+ const value = preset as Record<string, unknown>;
351
+ if (value.hidden === true || typeof value.name !== "string" || value.name.length === 0) continue;
352
+ const name = value.name;
353
+ proposals.push({
354
+ preferredId: `cmake-${label}-${slug(name, "preset")}`,
355
+ description: `Run CMake ${label} preset '${name}'`,
356
+ command: [executable, ...args, name],
357
+ cwd: ".",
358
+ timeoutMs: SAFE_EXEC_DEFAULT_TIMEOUT_MS,
359
+ tags: ["cmake", tag],
360
+ provenance: {
361
+ kind: "cmake-preset",
362
+ path: relative,
363
+ detail: `${field} entry '${name}'`,
364
+ authority: "toolchain-defined",
365
+ },
366
+ });
367
+ }
368
+ }
369
+ if (proposals.length === 0) diagnostics.push(`${relative}: no visible buildPresets or testPresets were declared.`);
370
+ return proposals;
371
+ }
372
+
373
+ function pythonProposals(workspaceRoot: string, diagnostics: string[]): RawProposal[] {
374
+ const proposals: RawProposal[] = [];
375
+ const pyprojectPath = "pyproject.toml";
376
+ const pyproject = regularFileText(path.join(workspaceRoot, pyprojectPath), workspaceRoot);
377
+ if (pyproject instanceof Error) diagnostics.push(`${pyprojectPath}: ${pyproject.message}; Python discovery skipped.`);
378
+ if (typeof pyproject === "string") {
379
+ const document = parseTomlDocument(pyproject);
380
+ if (document === null) {
381
+ diagnostics.push(`${pyprojectPath}: invalid TOML; Python discovery skipped.`);
382
+ } else {
383
+ for (const [path, section, module, id, description, tags] of [
384
+ [
385
+ ["tool", "pytest", "ini_options"],
386
+ "tool.pytest.ini_options",
387
+ "pytest",
388
+ "python-pytest",
389
+ "Run the declared pytest suite",
390
+ ["python", "test"],
391
+ ],
392
+ [["tool", "tox"], "tool.tox", "tox", "python-tox", "Run the declared tox environments", ["python", "test"]],
393
+ [["tool", "nox"], "tool.nox", "nox", "python-nox", "Run the declared nox sessions", ["python", "test"]],
394
+ ] as const) {
395
+ if (tomlTableAt(document, path) === null) continue;
396
+ proposals.push({
397
+ preferredId: id,
398
+ description,
399
+ command: ["python", "-m", module],
400
+ cwd: ".",
401
+ timeoutMs: SAFE_EXEC_DEFAULT_TIMEOUT_MS,
402
+ tags: [...tags],
403
+ provenance: {
404
+ kind: "python-runner",
405
+ path: pyprojectPath,
406
+ detail: `[${section}]`,
407
+ authority: "toolchain-defined",
408
+ },
409
+ });
410
+ }
411
+ for (const [path, section] of [
412
+ [["project", "scripts"], "project.scripts"],
413
+ [["tool", "poetry", "scripts"], "tool.poetry.scripts"],
414
+ ] as const) {
415
+ const scripts = tomlTableAt(document, path);
416
+ if (scripts === null) continue;
417
+ for (const [name, target] of Object.entries(scripts)) {
418
+ if (typeof target !== "string" || !isVerificationScriptName(name)) continue;
419
+ proposals.push({
420
+ preferredId: `python-${slug(name, "check")}`,
421
+ description: `Run declared Python entry point '${name}'`,
422
+ command: [name],
423
+ cwd: ".",
424
+ timeoutMs: SAFE_EXEC_DEFAULT_TIMEOUT_MS,
425
+ tags: [slug(name.split(/[:.-]/u)[0] ?? "python", "python"), "python"],
426
+ provenance: {
427
+ kind: "python-runner",
428
+ path: pyprojectPath,
429
+ detail: `[${section}] entry '${name}'`,
430
+ authority: "project-declared",
431
+ },
432
+ });
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ for (const [relative, marker, module, id, description] of [
439
+ ["pytest.ini", null, "pytest", "python-pytest", "Run the declared pytest suite"],
440
+ ["tox.ini", null, "tox", "python-tox", "Run the declared tox environments"],
441
+ ["noxfile.py", null, "nox", "python-nox", "Run the declared nox sessions"],
442
+ ["setup.cfg", /^\s*\[tool:pytest\]/mu, "pytest", "python-pytest", "Run the declared pytest suite"],
443
+ ] as const) {
444
+ const text = regularFileText(path.join(workspaceRoot, relative), workspaceRoot);
445
+ if (text === null) continue;
446
+ if (text instanceof Error) {
447
+ diagnostics.push(`${relative}: ${text.message}; Python discovery skipped.`);
448
+ continue;
449
+ }
450
+ if (marker !== null && !marker.test(text)) continue;
451
+ proposals.push({
452
+ preferredId: id,
453
+ description,
454
+ command: ["python", "-m", module],
455
+ cwd: ".",
456
+ timeoutMs: SAFE_EXEC_DEFAULT_TIMEOUT_MS,
457
+ tags: ["python", "test"],
458
+ provenance: {
459
+ kind: "python-runner",
460
+ path: relative,
461
+ detail: `${module} configuration file`,
462
+ authority: "toolchain-defined",
463
+ },
464
+ });
465
+ }
466
+ return proposals;
467
+ }
468
+
469
+ function goProposals(workspaceRoot: string, diagnostics: string[]): RawProposal[] {
470
+ const relative = "go.mod";
471
+ const text = regularFileText(path.join(workspaceRoot, relative), workspaceRoot);
472
+ if (text === null) return [];
473
+ if (text instanceof Error) {
474
+ diagnostics.push(`${relative}: ${text.message}; Go discovery skipped.`);
475
+ return [];
476
+ }
477
+ if (!/^\s*module\s+\S+/mu.test(text)) {
478
+ diagnostics.push(`${relative}: no module directive was found; Go discovery skipped.`);
479
+ return [];
480
+ }
481
+ return [
482
+ {
483
+ preferredId: "go-test",
484
+ description: "Run all Go module tests",
485
+ command: ["go", "test", "./..."],
486
+ cwd: ".",
487
+ timeoutMs: SAFE_EXEC_DEFAULT_TIMEOUT_MS,
488
+ tags: ["go", "test"],
489
+ provenance: {
490
+ kind: "go-module",
491
+ path: relative,
492
+ detail: "Go module directive",
493
+ authority: "toolchain-defined",
494
+ },
495
+ },
496
+ ];
497
+ }
498
+
499
+ function shellLikeArgv(command: string): string[] | Error {
500
+ const argv: string[] = [];
501
+ let token = "";
502
+ let quote: "single" | "double" | null = null;
503
+ let tokenStarted = false;
504
+ for (let index = 0; index < command.length; index += 1) {
505
+ const character = command[index] ?? "";
506
+ if (quote === "single") {
507
+ if (character === "'") quote = null;
508
+ else token += character;
509
+ tokenStarted = true;
510
+ continue;
511
+ }
512
+ if (quote === "double") {
513
+ if (character === '"') {
514
+ quote = null;
515
+ continue;
516
+ }
517
+ if (character === "\\") {
518
+ const next = command[index + 1];
519
+ if (next === undefined) return new Error("trailing escape");
520
+ token += next;
521
+ index += 1;
522
+ } else if (character === "$" || character === "`") {
523
+ return new Error(`shell expansion '${character}' is ambiguous`);
524
+ } else {
525
+ token += character;
526
+ }
527
+ tokenStarted = true;
528
+ continue;
529
+ }
530
+ if (/\s/u.test(character)) {
531
+ if (tokenStarted) {
532
+ argv.push(token);
533
+ token = "";
534
+ tokenStarted = false;
535
+ }
536
+ continue;
537
+ }
538
+ if (character === "'") {
539
+ quote = "single";
540
+ tokenStarted = true;
541
+ continue;
542
+ }
543
+ if (character === '"') {
544
+ quote = "double";
545
+ tokenStarted = true;
546
+ continue;
547
+ }
548
+ if (character === "\\") {
549
+ const next = command[index + 1];
550
+ if (next === undefined) return new Error("trailing escape");
551
+ token += next;
552
+ tokenStarted = true;
553
+ index += 1;
554
+ continue;
555
+ }
556
+ if ("|&;<>()`$\n\r".includes(character)) {
557
+ return new Error(`shell operator or expansion '${character}' is ambiguous`);
558
+ }
559
+ token += character;
560
+ tokenStarted = true;
561
+ }
562
+ if (quote !== null) return new Error(`unterminated ${quote}-quoted argument`);
563
+ if (tokenStarted) argv.push(token);
564
+ if (argv.length === 0) return new Error("empty command");
565
+ if ((argv[0] ?? "").includes("=")) return new Error("environment assignments are not argv executables");
566
+ return argv;
567
+ }
568
+
569
+ function validatorArgv(value: unknown): string[] | Error {
570
+ if (typeof value === "string") return shellLikeArgv(value);
571
+ if (
572
+ !Array.isArray(value) ||
573
+ value.length === 0 ||
574
+ value.some((entry) => typeof entry !== "string" || entry.length === 0)
575
+ ) {
576
+ return new Error("validator must be a command string or a non-empty argv string array");
577
+ }
578
+ return [...(value as string[])];
579
+ }
580
+
581
+ function validationPreferredId(argv: ReadonlyArray<string>): string {
582
+ const executable = path.basename(argv[0] ?? "check").replace(/\.[^.]+$/u, "");
583
+ const script = argv.find((entry, index) => index > 0 && /\.(?:py|js|mjs|sh)$/u.test(entry));
584
+ const subject = script === undefined ? executable : path.basename(script).replace(/\.[^.]+$/u, "");
585
+ return `validation-${slug(subject, "check")}`;
586
+ }
587
+
588
+ function validationContractProposals(workspaceRoot: string, diagnostics: string[]): RawProposal[] {
589
+ const proposals: RawProposal[] = [];
590
+ for (const relative of VALIDATION_CONTRACT_PATHS) {
591
+ const text = regularFileText(path.join(workspaceRoot, relative), workspaceRoot);
592
+ if (text === null) continue;
593
+ if (text instanceof Error) {
594
+ diagnostics.push(`${relative}: ${text.message}; validation command discovery skipped.`);
595
+ continue;
596
+ }
597
+ const document = parseDocument(text, { prettyErrors: false, strict: true, uniqueKeys: true });
598
+ if (document.errors.length > 0) {
599
+ diagnostics.push(`${relative}: invalid YAML; validation command discovery skipped.`);
600
+ continue;
601
+ }
602
+ let parsed: unknown;
603
+ try {
604
+ parsed = document.toJS({ maxAliasCount: 0 }) as unknown;
605
+ } catch (error) {
606
+ diagnostics.push(`${relative}: invalid YAML (${error instanceof Error ? error.message : String(error)}).`);
607
+ continue;
608
+ }
609
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) continue;
610
+ const validators = (parsed as Record<string, unknown>).validators;
611
+ if (!Array.isArray(validators)) continue;
612
+ for (const [index, validator] of validators.entries()) {
613
+ const record =
614
+ validator !== null && typeof validator === "object" && !Array.isArray(validator)
615
+ ? (validator as Record<string, unknown>)
616
+ : null;
617
+ if (record !== null) {
618
+ const typedFields: ReadonlyArray<readonly [string, (value: unknown) => boolean]> = [
619
+ ["id", (value: unknown) => typeof value === "string"],
620
+ ["description", (value: unknown) => typeof value === "string"],
621
+ ["cwd", (value: unknown) => typeof value === "string"],
622
+ ["timeoutMs", (value: unknown) => typeof value === "number"],
623
+ ["tags", (value: unknown) => Array.isArray(value) && value.every((tag) => typeof tag === "string")],
624
+ ];
625
+ const invalidField = typedFields.find(([field, valid]) => Object.hasOwn(record, field) && !valid(record[field]));
626
+ if (invalidField !== undefined) {
627
+ diagnostics.push(
628
+ `${relative}: validators[${index}].${String(invalidField[0])} has an ambiguous type; use manual argv entry.`,
629
+ );
630
+ continue;
631
+ }
632
+ }
633
+ const commandValue = record === null ? validator : record.command;
634
+ const argv = validatorArgv(commandValue);
635
+ if (argv instanceof Error) {
636
+ diagnostics.push(`${relative}: validators[${index}] is ambiguous (${argv.message}); use manual argv entry.`);
637
+ continue;
638
+ }
639
+ const declaredId = typeof record?.id === "string" ? record.id : validationPreferredId(argv);
640
+ const description =
641
+ typeof record?.description === "string"
642
+ ? record.description
643
+ : `Run validation contract command ${JSON.stringify(argv)}`;
644
+ const cwd = typeof record?.cwd === "string" ? record.cwd : ".";
645
+ const timeoutMs = typeof record?.timeoutMs === "number" ? record.timeoutMs : SAFE_EXEC_DEFAULT_TIMEOUT_MS;
646
+ const tags = Array.isArray(record?.tags) ? record.tags.filter((tag): tag is string => typeof tag === "string") : [];
647
+ proposals.push({
648
+ preferredId: declaredId,
649
+ description,
650
+ command: argv,
651
+ cwd,
652
+ timeoutMs,
653
+ tags: tags.length > 0 ? tags : ["scientific", "validation"],
654
+ provenance: {
655
+ kind: "validation-contract",
656
+ path: relative,
657
+ detail: `validators[${index}]`,
658
+ authority: "project-declared",
659
+ },
660
+ });
661
+ }
662
+ }
663
+ if (existsSync(path.join(workspaceRoot, "VALIDATION.md"))) {
664
+ diagnostics.push(
665
+ "VALIDATION.md is advisory prose; enter an exact argv vector manually instead of inferring a command.",
666
+ );
667
+ }
668
+ return proposals;
669
+ }
670
+
671
+ function deduplicatedRawProposals(proposals: RawProposal[], diagnostics: string[]): RawProposal[] {
672
+ const seen = new Map<string, RawProposal>();
673
+ for (const proposal of proposals.sort(rawProposalSort)) {
674
+ const identity = checkIdentity(proposal);
675
+ const prior = seen.get(identity);
676
+ if (prior !== undefined) {
677
+ diagnostics.push(
678
+ `${proposal.provenance.path} (${proposal.provenance.detail}) repeats ${prior.provenance.path} (${prior.provenance.detail}); one proposal was kept.`,
679
+ );
680
+ continue;
681
+ }
682
+ seen.set(identity, proposal);
683
+ }
684
+ return [...seen.values()];
685
+ }
686
+
687
+ /** Inspect declared manifests/configuration without writing files or running commands. */
688
+ export function discoverVerifierAuthoring(workspaceRoot = process.cwd()): VerifierAuthoringDiscoveryResult {
689
+ const manualEntry = manualEntryInstruction();
690
+ const productionDiscovery = discoverDeclaredChecksAtRoot(workspaceRoot, undefined);
691
+ if (!productionDiscovery.ok) {
692
+ return {
693
+ ok: false,
694
+ reason: `Cannot author over the existing verifier authority: ${productionDiscovery.reason}`,
695
+ manualEntry,
696
+ };
697
+ }
698
+ const activeChecks: AuthoringCheck[] = [];
699
+ const existingChecks: AuthoringCheck[] = [];
700
+ for (const source of productionDiscovery.sources) {
701
+ for (const check of source.checks) {
702
+ const projected = projectedCheck(workspaceRoot, check);
703
+ if (projected.state === "active") activeChecks.push(projected);
704
+ else existingChecks.push(projected);
705
+ }
706
+ }
707
+ activeChecks.sort((left, right) => compareCodepoints(left.id, right.id));
708
+ existingChecks.sort((left, right) => compareCodepoints(left.id, right.id));
709
+
710
+ const diagnostics: string[] = [];
711
+ const raw = deduplicatedRawProposals(
712
+ [
713
+ ...cargoProposals(workspaceRoot, diagnostics),
714
+ ...cmakeProposals(workspaceRoot, diagnostics),
715
+ ...pythonProposals(workspaceRoot, diagnostics),
716
+ ...goProposals(workspaceRoot, diagnostics),
717
+ ...validationContractProposals(workspaceRoot, diagnostics),
718
+ ],
719
+ diagnostics,
720
+ );
721
+ const occupied = new Set([...activeChecks, ...existingChecks].map((check) => check.id));
722
+ const represented = new Map(existingChecks.map((check) => [checkIdentity(check), check.id]));
723
+ const proposals: AuthoringCheck[] = [];
724
+ for (const proposal of raw) {
725
+ const priorId = represented.get(checkIdentity(proposal));
726
+ if (priorId !== undefined) {
727
+ diagnostics.push(
728
+ `${proposal.provenance.path} (${proposal.provenance.detail}) is already represented by catalog check '${priorId}'.`,
729
+ );
730
+ continue;
731
+ }
732
+ const normalizedId = boundedId(slug(proposal.preferredId, "check"));
733
+ const id = deterministicVerifierId(proposal.preferredId, occupied);
734
+ if (normalizedId !== proposal.preferredId) {
735
+ diagnostics.push(`Proposed ID '${proposal.preferredId}' was normalized to '${normalizedId}'.`);
736
+ }
737
+ if (id !== normalizedId) {
738
+ diagnostics.push(`Proposed ID '${normalizedId}' collides with declared authority; using '${id}'.`);
739
+ }
740
+ occupied.add(id);
741
+ represented.set(checkIdentity(proposal), id);
742
+ proposals.push({
743
+ id,
744
+ description: proposal.description,
745
+ command: [...proposal.command],
746
+ cwd: proposal.cwd,
747
+ timeoutMs: proposal.timeoutMs,
748
+ tags: [...proposal.tags],
749
+ provenance: { ...proposal.provenance },
750
+ state: "proposed",
751
+ });
752
+ }
753
+ proposals.sort((left, right) => compareCodepoints(left.id, right.id));
754
+ if (activeChecks.length === 0 && existingChecks.length === 0 && proposals.length === 0) {
755
+ diagnostics.push(
756
+ "No package verification script, supported toolchain declaration, or exact validation command was found.",
757
+ );
758
+ }
759
+ const catalogPath = path.join(workspaceRoot, PROJECT_VERIFIER_CATALOG_RELATIVE_PATH);
760
+ const catalogText = regularFileText(catalogPath, workspaceRoot);
761
+ return {
762
+ ok: true,
763
+ workspaceRoot,
764
+ catalogPath,
765
+ activeChecks,
766
+ existingChecks,
767
+ proposals,
768
+ diagnostics,
769
+ manualEntry,
770
+ ...(typeof catalogText === "string" ? { catalogText } : {}),
771
+ };
772
+ }
773
+
774
+ export function createVerifierDraft(
775
+ discovery: VerifierAuthoringDiscovery,
776
+ options: { includeProposals?: boolean } = {},
777
+ ): VerifierDraft {
778
+ return {
779
+ workspaceRoot: discovery.workspaceRoot,
780
+ catalogPath: discovery.catalogPath,
781
+ activeChecks: discovery.activeChecks.map(cloneCheck),
782
+ checks: [...discovery.existingChecks, ...(options.includeProposals === false ? [] : discovery.proposals)]
783
+ .map(cloneCheck)
784
+ .sort((left, right) => compareCodepoints(left.id, right.id)),
785
+ diagnostics: [...discovery.diagnostics],
786
+ manualEntry: discovery.manualEntry,
787
+ ...(discovery.catalogText === undefined ? {} : { catalogText: discovery.catalogText }),
788
+ };
789
+ }
790
+
791
+ function catalogEntry({ id, description, command, cwd, timeoutMs, tags }: AuthoringCheck): Record<string, unknown> {
792
+ return { id, description, command: [...command], cwd, timeoutMs, tags: [...tags] };
793
+ }
794
+
795
+ /**
796
+ * A fresh catalog is serialized from the draft. An existing catalog is edited
797
+ * in place: the operator's comments and on-disk order survive, untouched
798
+ * fields keep their bytes, and only changed fields, removed checks, and
799
+ * appended checks move. The parser sorts by id on load, so order on disk is
800
+ * the operator's to keep.
801
+ */
802
+ function serializeVerifierDraft(draft: VerifierDraft): string {
803
+ const checks = [...draft.checks].sort((left, right) => compareCodepoints(left.id, right.id));
804
+ const document = draft.catalogText === undefined ? null : parseDocument(draft.catalogText);
805
+ const sequence = document?.get("checks");
806
+ if (document === null || !isSeq(sequence)) {
807
+ return stringify({ version: PROJECT_VERIFIER_CATALOG_VERSION, checks: checks.map(catalogEntry) }, { lineWidth: 0 });
808
+ }
809
+ const nodes = new Map<unknown, YAMLMap>();
810
+ for (const node of sequence.items) if (isMap(node)) nodes.set(node.get("id"), node);
811
+ const kept = new Set<YAMLMap>();
812
+ const appended: unknown[] = [];
813
+ let changed = false;
814
+ for (const check of checks) {
815
+ const entry = catalogEntry(check);
816
+ const node = nodes.get(check.catalogId ?? check.id);
817
+ if (node === undefined) {
818
+ appended.push(document.createNode(entry));
819
+ continue;
820
+ }
821
+ kept.add(node);
822
+ const current = node.toJSON() as Record<string, unknown>;
823
+ for (const [field, value] of Object.entries(entry)) {
824
+ if (JSON.stringify(current[field]) === JSON.stringify(value)) continue;
825
+ node.set(field, value);
826
+ changed = true;
827
+ }
828
+ }
829
+ if (!changed && appended.length === 0 && kept.size === sequence.items.length) return draft.catalogText as string;
830
+ sequence.items = [...sequence.items.filter((node) => isMap(node) && kept.has(node)), ...appended];
831
+ return document.toString({ lineWidth: 0 });
832
+ }
833
+
834
+ /** Validate with the same production parser used by verify(). */
835
+ export function validateVerifierDraft(draft: VerifierDraft): VerifierDraftValidation {
836
+ const text = serializeVerifierDraft(draft);
837
+ const parsed = parseProjectVerifierCatalogText(text, draft.workspaceRoot, draft.catalogPath);
838
+ if (!parsed.ok) return { ok: false, reason: parsed.reason, text };
839
+ const activeId = new Map(draft.activeChecks.map((check) => [check.id, check]));
840
+ for (const check of parsed.source?.checks ?? []) {
841
+ const collision = activeId.get(check.id);
842
+ if (collision !== undefined) {
843
+ return {
844
+ ok: false,
845
+ reason:
846
+ `duplicate declared check id '${check.id}' from ${collision.provenance.kind} ` +
847
+ `(${collision.provenance.path}) and project-catalog (${PROJECT_VERIFIER_CATALOG_RELATIVE_PATH})`,
848
+ text,
849
+ };
850
+ }
851
+ }
852
+ const byId = new Map(draft.checks.map((check) => [check.id, check]));
853
+ const checks = (parsed.source?.checks ?? []).map((check) => {
854
+ const original = byId.get(check.id);
855
+ return {
856
+ id: check.id,
857
+ description: check.description,
858
+ command: [...check.command],
859
+ cwd: check.cwd,
860
+ timeoutMs: check.timeoutMs,
861
+ tags: [...check.tags],
862
+ provenance:
863
+ original?.provenance ??
864
+ ({
865
+ kind: "manual-entry",
866
+ path: PROJECT_VERIFIER_CATALOG_RELATIVE_PATH,
867
+ detail: `manual check '${check.id}'`,
868
+ authority: "project-declared",
869
+ } satisfies VerifierProvenance),
870
+ state: original?.state ?? "manual",
871
+ ...(original?.catalogId === undefined ? {} : { catalogId: original.catalogId }),
872
+ } satisfies AuthoringCheck;
873
+ });
874
+ return { ok: true, text, checks };
875
+ }
876
+
877
+ function cloneDraft(draft: VerifierDraft): VerifierDraft {
878
+ return {
879
+ ...draft,
880
+ activeChecks: draft.activeChecks.map(cloneCheck),
881
+ checks: draft.checks.map(cloneCheck),
882
+ diagnostics: [...draft.diagnostics],
883
+ };
884
+ }
885
+
886
+ /** Apply edits to a copy so a collision or schema failure leaves the prior draft intact. */
887
+ export function reviseVerifierDraft(
888
+ draft: VerifierDraft,
889
+ revisions: ReadonlyArray<VerifierRevision>,
890
+ ): VerifierRevisionResult {
891
+ const next = cloneDraft(draft);
892
+ const diagnostics: string[] = [];
893
+ for (const revision of revisions) {
894
+ if (revision.kind === "add") {
895
+ if (next.checks.some((check) => check.id === revision.check.id)) {
896
+ return { ok: false, draft, reason: `Cannot add '${revision.check.id}': that ID already exists.` };
897
+ }
898
+ if (next.activeChecks.some((check) => check.id === revision.check.id)) {
899
+ return { ok: false, draft, reason: `Cannot add '${revision.check.id}': it collides with an active package check.` };
900
+ }
901
+ next.checks.push({
902
+ ...revision.check,
903
+ command: [...revision.check.command],
904
+ tags: [...revision.check.tags],
905
+ provenance:
906
+ revision.provenance ??
907
+ ({
908
+ kind: "manual-entry",
909
+ path: PROJECT_VERIFIER_CATALOG_RELATIVE_PATH,
910
+ detail: `manual check '${revision.check.id}'`,
911
+ authority: "project-declared",
912
+ } satisfies VerifierProvenance),
913
+ state: "manual",
914
+ });
915
+ diagnostics.push(`Added check '${revision.check.id}'.`);
916
+ continue;
917
+ }
918
+ const index = next.checks.findIndex((check) => check.id === revision.id);
919
+ if (index === -1) return { ok: false, draft, reason: `Check '${revision.id}' does not exist.` };
920
+ const check = next.checks[index];
921
+ if (check === undefined) return { ok: false, draft, reason: `Check '${revision.id}' does not exist.` };
922
+ if (revision.kind === "remove") {
923
+ next.checks.splice(index, 1);
924
+ diagnostics.push(`Removed check '${revision.id}'; its command is no longer executable through the catalog.`);
925
+ continue;
926
+ }
927
+ if (revision.kind === "rename") {
928
+ if (next.checks.some((candidate, candidateIndex) => candidateIndex !== index && candidate.id === revision.newId)) {
929
+ return {
930
+ ok: false,
931
+ draft,
932
+ reason: `Cannot rename '${revision.id}' to '${revision.newId}': that ID already exists.`,
933
+ };
934
+ }
935
+ if (next.activeChecks.some((candidate) => candidate.id === revision.newId)) {
936
+ return {
937
+ ok: false,
938
+ draft,
939
+ reason: `Cannot rename '${revision.id}' to '${revision.newId}': it collides with an active package check.`,
940
+ };
941
+ }
942
+ check.id = revision.newId;
943
+ diagnostics.push(`Renamed check '${revision.id}' to '${revision.newId}'.`);
944
+ continue;
945
+ }
946
+ if (revision.changes.description !== undefined) check.description = revision.changes.description;
947
+ if (revision.changes.command !== undefined) check.command = [...revision.changes.command];
948
+ if (revision.changes.cwd !== undefined) check.cwd = revision.changes.cwd;
949
+ if (revision.changes.timeoutMs !== undefined) check.timeoutMs = revision.changes.timeoutMs;
950
+ if (revision.changes.tags !== undefined) check.tags = [...revision.changes.tags];
951
+ diagnostics.push(`Edited check '${revision.id}' without changing its deterministic ID.`);
952
+ }
953
+ next.checks.sort((left, right) => compareCodepoints(left.id, right.id));
954
+ const validation = validateVerifierDraft(next);
955
+ if (!validation.ok) return { ok: false, draft, reason: validation.reason };
956
+ next.checks = validation.checks;
957
+ next.diagnostics.push(...diagnostics);
958
+ return { ok: true, draft: next, diagnostics };
959
+ }
960
+
961
+ function authorityDescription(check: AuthoringCheck): string {
962
+ if (check.state === "active") {
963
+ return "active package-script authority; verify runs npm with the declared script name and may accept explicit extra argv";
964
+ }
965
+ return (
966
+ `exact catalog authority after confirmation; verify fixes argv, cwd, and timeout through safe-exec ` +
967
+ `(proposal origin: ${check.provenance.authority})`
968
+ );
969
+ }
970
+
971
+ function renderCheck(check: AuthoringCheck, destinationPath: string): string[] {
972
+ return [
973
+ `- ${check.id}`,
974
+ ` path: ${check.state === "active" ? check.provenance.path : destinationPath}`,
975
+ ` source: ${check.provenance.path} (${check.provenance.detail}; ${check.provenance.authority})`,
976
+ ` argv: ${JSON.stringify(check.command)}`,
977
+ ` cwd: ${check.cwd}`,
978
+ ` timeoutMs: ${check.timeoutMs}`,
979
+ ` tags: ${JSON.stringify(check.tags)}`,
980
+ ` effective execution authority: ${authorityDescription(check)}`,
981
+ ];
982
+ }
983
+
984
+ export interface VerifierPreviewOptions {
985
+ /** Suppress the unconfirmed-preview footer once the write is authorized. */
986
+ confirmed?: boolean;
987
+ }
988
+
989
+ /** Render an authority review. This function is pure and never executes or writes. */
990
+ export function previewVerifierDraft(draft: VerifierDraft, options: VerifierPreviewOptions = {}): string {
991
+ const validation = validateVerifierDraft(draft);
992
+ const lines = [
993
+ "Project verifier authority preview",
994
+ `Catalog path: ${draft.catalogPath}`,
995
+ `Schema validation: ${validation.ok ? "accepted by the production catalog parser" : `rejected (${validation.reason})`}`,
996
+ ];
997
+ if (draft.activeChecks.length > 0) {
998
+ lines.push("", "Already executable package checks (not duplicated into the catalog):");
999
+ for (const check of draft.activeChecks) lines.push(...renderCheck(check, draft.catalogPath));
1000
+ }
1001
+ lines.push("", "Catalog checks after confirmation:");
1002
+ if (draft.checks.length === 0) lines.push("(none)");
1003
+ else for (const check of draft.checks) lines.push(...renderCheck(check, draft.catalogPath));
1004
+ if (draft.diagnostics.length > 0) {
1005
+ lines.push("", "Diagnostics:");
1006
+ for (const diagnostic of draft.diagnostics) lines.push(`- ${diagnostic}`);
1007
+ }
1008
+ if (draft.checks.length === 0) {
1009
+ lines.push(
1010
+ "",
1011
+ draft.activeChecks.length === 0
1012
+ ? draft.manualEntry
1013
+ : "No catalog checks are proposed; the package checks above are already executable without a catalog entry.",
1014
+ );
1015
+ }
1016
+ if (options.confirmed !== true) {
1017
+ lines.push("", "Preview only: no file has been written and no check has been executed.");
1018
+ }
1019
+ return lines.join("\n");
1020
+ }
1021
+
1022
+ function writeValidatedDraft(draft: VerifierDraft, validation: Extract<VerifierDraftValidation, { ok: true }>): void {
1023
+ const directory = path.dirname(draft.catalogPath);
1024
+ mkdirSync(directory, { recursive: true });
1025
+ const realRoot = realpathSync(draft.workspaceRoot);
1026
+ const realDirectory = realpathSync(directory);
1027
+ resolveSafeCwd(realDirectory, realRoot);
1028
+ const realCatalogPath = path.join(realDirectory, path.basename(draft.catalogPath));
1029
+ const temporaryPath = path.join(
1030
+ realDirectory,
1031
+ `.${path.basename(draft.catalogPath)}.${process.pid}.${randomUUID()}.tmp`,
1032
+ );
1033
+ try {
1034
+ writeFileSync(temporaryPath, validation.text, { encoding: "utf8", flag: "wx", mode: 0o644 });
1035
+ renameSync(temporaryPath, realCatalogPath);
1036
+ } catch (error) {
1037
+ rmSync(temporaryPath, { force: true });
1038
+ throw error;
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * Run discovery, any number of pure revisions, confirmation, atomic write, and
1044
+ * optional checks. Only the confirm branch can reach the writer or executor.
1045
+ */
1046
+ export async function runVerifierAuthoringWorkflow(
1047
+ options: VerifierAuthoringWorkflowOptions,
1048
+ ): Promise<VerifierAuthoringWorkflowResult> {
1049
+ const workspaceRoot = options.workspaceRoot ?? process.cwd();
1050
+ const discovery = discoverVerifierAuthoring(workspaceRoot);
1051
+ if (!discovery.ok) return { status: "invalid", reason: discovery.reason, wrote: false };
1052
+ let draft = createVerifierDraft(
1053
+ discovery,
1054
+ options.includeProposals === undefined ? {} : { includeProposals: options.includeProposals },
1055
+ );
1056
+ if (options.initialRevisions !== undefined && options.initialRevisions.length > 0) {
1057
+ const revised = reviseVerifierDraft(draft, options.initialRevisions);
1058
+ if (!revised.ok) return { status: "invalid", reason: revised.reason, wrote: false };
1059
+ draft = revised.draft;
1060
+ }
1061
+ for (let revision = 0; revision <= PROJECT_VERIFIER_CATALOG_CAPS.checks; revision += 1) {
1062
+ const validation = validateVerifierDraft(draft);
1063
+ const preview = previewVerifierDraft(draft, { confirmed: options.confirmed === true });
1064
+ const decision = await options.decide({ draft: cloneDraft(draft), preview, validation, revision });
1065
+ if (decision.kind === "reject") {
1066
+ return { status: "rejected", wrote: false, preview, diagnostics: [...draft.diagnostics] };
1067
+ }
1068
+ if (decision.kind === "revise") {
1069
+ const revised = reviseVerifierDraft(draft, decision.revisions);
1070
+ if (!revised.ok) {
1071
+ draft.diagnostics.push(`Revision rejected: ${revised.reason}`);
1072
+ continue;
1073
+ }
1074
+ draft = revised.draft;
1075
+ continue;
1076
+ }
1077
+ if (!validation.ok) return { status: "invalid", reason: validation.reason, wrote: false, preview };
1078
+ // Past this point the operator has authorized the write, so the returned
1079
+ // review may never claim that no file has been written.
1080
+ const authorizedPreview = previewVerifierDraft(draft, { confirmed: true });
1081
+ try {
1082
+ writeValidatedDraft(draft, validation);
1083
+ } catch (error) {
1084
+ return {
1085
+ status: "invalid",
1086
+ reason: `Could not write ${draft.catalogPath}: ${error instanceof Error ? error.message : String(error)}`,
1087
+ wrote: false,
1088
+ preview: authorizedPreview,
1089
+ };
1090
+ }
1091
+ const loaded = discoverDeclaredChecksAtRoot(workspaceRoot, undefined);
1092
+ if (!loaded.ok) {
1093
+ return {
1094
+ status: "invalid",
1095
+ reason: `Catalog was written but production discovery rejected it: ${loaded.reason}`,
1096
+ wrote: true,
1097
+ path: draft.catalogPath,
1098
+ preview: authorizedPreview,
1099
+ };
1100
+ }
1101
+ const dryRuns: VerifierDryRunResult[] = [];
1102
+ const runCheck = options.runCheck ?? ((id: string) => verifyTool.run({ check: id }));
1103
+ for (const id of decision.dryRunCheckIds ?? []) {
1104
+ dryRuns.push({ id, result: await runCheck(id) });
1105
+ }
1106
+ return {
1107
+ status: "written",
1108
+ wrote: true,
1109
+ path: draft.catalogPath,
1110
+ preview: authorizedPreview,
1111
+ diagnostics: [...draft.diagnostics],
1112
+ dryRuns,
1113
+ };
1114
+ }
1115
+ return {
1116
+ status: "invalid",
1117
+ reason: `Revision limit of ${PROJECT_VERIFIER_CATALOG_CAPS.checks} exceeded without an operator decision.`,
1118
+ wrote: false,
1119
+ };
1120
+ }