@intentius/chant 0.124.27 → 0.124.28

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 (450) hide show
  1. package/README.md +1 -1
  2. package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
  3. package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
  4. package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  6. package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
  7. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  8. package/dist/__golden__/wave-loop/op-held-on.json +2 -0
  9. package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
  10. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  11. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  12. package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
  13. package/dist/__golden__/wave-loop/op-per-run.json +7 -0
  14. package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
  15. package/dist/__golden__/wave-loop/op-sealed.json +3 -0
  16. package/dist/__golden__/wave-loop/op-spend.json +3 -0
  17. package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
  18. package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
  19. package/dist/change-set-graph.d.ts +56 -0
  20. package/dist/change-set-graph.d.ts.map +1 -0
  21. package/dist/change-set-graph.js +128 -0
  22. package/dist/change-set.d.ts +52 -3
  23. package/dist/change-set.d.ts.map +1 -1
  24. package/dist/change-set.js +35 -4
  25. package/dist/chant-version.d.ts +9 -0
  26. package/dist/chant-version.d.ts.map +1 -0
  27. package/dist/chant-version.js +16 -0
  28. package/dist/cli/commands/build.d.ts +8 -0
  29. package/dist/cli/commands/build.d.ts.map +1 -1
  30. package/dist/cli/commands/build.js +9 -8
  31. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  32. package/dist/cli/commands/check-lexicon-docs.js +30 -2
  33. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  34. package/dist/cli/commands/check-lexicon.js +13 -2
  35. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
  36. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
  38. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon.js +17 -8
  40. package/dist/cli/handlers/approve.d.ts.map +1 -1
  41. package/dist/cli/handlers/approve.js +19 -4
  42. package/dist/cli/handlers/build.d.ts.map +1 -1
  43. package/dist/cli/handlers/build.js +28 -0
  44. package/dist/cli/handlers/change-set.d.ts +10 -0
  45. package/dist/cli/handlers/change-set.d.ts.map +1 -1
  46. package/dist/cli/handlers/change-set.js +51 -1
  47. package/dist/cli/handlers/fan-out.d.ts +1 -1
  48. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  49. package/dist/cli/handlers/fan-out.js +14 -3
  50. package/dist/cli/handlers/pr.d.ts +8 -2
  51. package/dist/cli/handlers/pr.d.ts.map +1 -1
  52. package/dist/cli/handlers/pr.js +148 -24
  53. package/dist/cli/handlers/promote.d.ts.map +1 -1
  54. package/dist/cli/handlers/promote.js +256 -21
  55. package/dist/cli/handlers/release.d.ts +34 -0
  56. package/dist/cli/handlers/release.d.ts.map +1 -0
  57. package/dist/cli/handlers/release.js +203 -0
  58. package/dist/cli/handlers/run.d.ts +28 -2
  59. package/dist/cli/handlers/run.d.ts.map +1 -1
  60. package/dist/cli/handlers/run.js +170 -13
  61. package/dist/cli/main.d.ts.map +1 -1
  62. package/dist/cli/main.js +88 -13
  63. package/dist/cli/mcp/workspace-tools.js +1 -1
  64. package/dist/cli/parse-args.d.ts.map +1 -1
  65. package/dist/cli/parse-args.js +49 -3
  66. package/dist/cli/registry.d.ts +29 -3
  67. package/dist/cli/registry.d.ts.map +1 -1
  68. package/dist/cli/run-events.d.ts +29 -0
  69. package/dist/cli/run-events.d.ts.map +1 -0
  70. package/dist/cli/run-events.js +47 -0
  71. package/dist/cli/version.d.ts +3 -5
  72. package/dist/cli/version.d.ts.map +1 -1
  73. package/dist/cli/version.js +3 -13
  74. package/dist/codegen/docs-in-site.d.ts +71 -0
  75. package/dist/codegen/docs-in-site.d.ts.map +1 -0
  76. package/dist/codegen/docs-in-site.js +177 -0
  77. package/dist/codegen/docs-types.d.ts +14 -1
  78. package/dist/codegen/docs-types.d.ts.map +1 -1
  79. package/dist/codegen/docs.d.ts +18 -1
  80. package/dist/codegen/docs.d.ts.map +1 -1
  81. package/dist/codegen/docs.js +40 -7
  82. package/dist/codegen/rehype-base-url.mjs +15 -9
  83. package/dist/components/auto-release.d.ts +4 -1
  84. package/dist/components/auto-release.d.ts.map +1 -1
  85. package/dist/components/auto-release.js +2 -0
  86. package/dist/components/cli-support.d.ts +6 -0
  87. package/dist/components/cli-support.d.ts.map +1 -1
  88. package/dist/components/cli-support.js +10 -9
  89. package/dist/components/driver.d.ts +42 -9
  90. package/dist/components/driver.d.ts.map +1 -1
  91. package/dist/components/driver.js +81 -54
  92. package/dist/components/fan-out-run.d.ts +7 -0
  93. package/dist/components/fan-out-run.d.ts.map +1 -1
  94. package/dist/components/fan-out-run.js +35 -26
  95. package/dist/components/pr-run.d.ts +11 -0
  96. package/dist/components/pr-run.d.ts.map +1 -1
  97. package/dist/components/pr-run.js +20 -1
  98. package/dist/components/promote-order.d.ts +38 -0
  99. package/dist/components/promote-order.d.ts.map +1 -0
  100. package/dist/components/promote-order.js +62 -0
  101. package/dist/components/promote.d.ts +15 -2
  102. package/dist/components/promote.d.ts.map +1 -1
  103. package/dist/components/promote.js +17 -0
  104. package/dist/components/release-compile.d.ts +133 -0
  105. package/dist/components/release-compile.d.ts.map +1 -0
  106. package/dist/components/release-compile.js +376 -0
  107. package/dist/components/release-gate.d.ts +125 -0
  108. package/dist/components/release-gate.d.ts.map +1 -0
  109. package/dist/components/release-gate.js +282 -0
  110. package/dist/components/release-promote.d.ts +96 -0
  111. package/dist/components/release-promote.d.ts.map +1 -0
  112. package/dist/components/release-promote.js +204 -0
  113. package/dist/components/run-progress.d.ts +7 -0
  114. package/dist/components/run-progress.d.ts.map +1 -1
  115. package/dist/components/wave-plan.d.ts +3 -1
  116. package/dist/components/wave-plan.d.ts.map +1 -1
  117. package/dist/components/wave-plan.js +1 -1
  118. package/dist/config.d.ts +7 -0
  119. package/dist/config.d.ts.map +1 -1
  120. package/dist/config.js +1 -0
  121. package/dist/index.d.ts +1 -0
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +2 -0
  124. package/dist/lifecycle/change-set-store.d.ts +67 -0
  125. package/dist/lifecycle/change-set-store.d.ts.map +1 -0
  126. package/dist/lifecycle/change-set-store.js +96 -0
  127. package/dist/lifecycle/commit-sha.d.ts +13 -0
  128. package/dist/lifecycle/commit-sha.d.ts.map +1 -0
  129. package/dist/lifecycle/commit-sha.js +21 -0
  130. package/dist/lifecycle/gate-change-set.d.ts +69 -0
  131. package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
  132. package/dist/lifecycle/gate-change-set.js +106 -0
  133. package/dist/lifecycle/gate-ledger.d.ts +49 -1
  134. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  135. package/dist/lifecycle/gate-ledger.js +18 -0
  136. package/dist/lifecycle/release-cut.d.ts +107 -0
  137. package/dist/lifecycle/release-cut.d.ts.map +1 -0
  138. package/dist/lifecycle/release-cut.js +273 -0
  139. package/dist/lifecycle/release-ledger.d.ts +54 -0
  140. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  141. package/dist/lifecycle/release-ledger.js +53 -20
  142. package/dist/lifecycle/release-store.d.ts +190 -0
  143. package/dist/lifecycle/release-store.d.ts.map +1 -0
  144. package/dist/lifecycle/release-store.js +318 -0
  145. package/dist/lifecycle/run-ledger.d.ts +4 -0
  146. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  147. package/dist/lifecycle/run-ledger.js +2 -0
  148. package/dist/op/activities/change-coverage.d.ts.map +1 -1
  149. package/dist/op/activities/change-coverage.js +4 -2
  150. package/dist/op/activities/source-release.d.ts.map +1 -1
  151. package/dist/op/activities/source-release.js +7 -3
  152. package/dist/op/activities/source-rollback.d.ts.map +1 -1
  153. package/dist/op/activities/source-rollback.js +7 -3
  154. package/dist/op/gate-name.d.ts +11 -2
  155. package/dist/op/gate-name.d.ts.map +1 -1
  156. package/dist/op/gate-name.js +12 -3
  157. package/dist/op/gate-run-locator.d.ts +22 -0
  158. package/dist/op/gate-run-locator.d.ts.map +1 -1
  159. package/dist/op/gate-run-locator.js +31 -0
  160. package/dist/op/gate.d.ts +27 -3
  161. package/dist/op/gate.d.ts.map +1 -1
  162. package/dist/op/gate.js +57 -3
  163. package/dist/op/local-executor.d.ts +36 -1
  164. package/dist/op/local-executor.d.ts.map +1 -1
  165. package/dist/op/local-executor.js +158 -52
  166. package/dist/op/op-waves-run.d.ts.map +1 -1
  167. package/dist/op/op-waves-run.js +4 -0
  168. package/dist/op/run-live.d.ts +2 -0
  169. package/dist/op/run-live.d.ts.map +1 -1
  170. package/dist/op/run-live.js +1 -1
  171. package/dist/op/runtime.d.ts +46 -0
  172. package/dist/op/runtime.d.ts.map +1 -1
  173. package/dist/op/runtimes/local.d.ts.map +1 -1
  174. package/dist/op/runtimes/local.js +8 -0
  175. package/dist/point-decision.d.ts +2 -0
  176. package/dist/point-decision.d.ts.map +1 -1
  177. package/dist/point-decision.js +9 -2
  178. package/dist/run-event-store.d.ts +77 -0
  179. package/dist/run-event-store.d.ts.map +1 -0
  180. package/dist/run-event-store.js +204 -0
  181. package/dist/run-event-stream.d.ts +132 -0
  182. package/dist/run-event-stream.d.ts.map +1 -0
  183. package/dist/run-event-stream.js +238 -0
  184. package/dist/run-event.d.ts +267 -0
  185. package/dist/run-event.d.ts.map +1 -0
  186. package/dist/run-event.js +207 -0
  187. package/dist/wave-runtime.mjs +204 -199
  188. package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
  189. package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
  190. package/dist/workspace/__fixtures__/run-event-check.js +59 -0
  191. package/dist/workspace/change-set-show.d.ts +56 -0
  192. package/dist/workspace/change-set-show.d.ts.map +1 -0
  193. package/dist/workspace/change-set-show.js +54 -0
  194. package/dist/workspace/change-set-show.schema.json +102 -0
  195. package/dist/workspace/change-set.schema.json +28 -1
  196. package/dist/workspace/changes-cli.d.ts.map +1 -1
  197. package/dist/workspace/changes-cli.js +10 -1
  198. package/dist/workspace/changes.d.ts +37 -0
  199. package/dist/workspace/changes.d.ts.map +1 -1
  200. package/dist/workspace/changes.js +49 -2
  201. package/dist/workspace/changes.schema.json +135 -0
  202. package/dist/workspace/ci-green-cli.d.ts +19 -4
  203. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  204. package/dist/workspace/ci-green-cli.js +75 -12
  205. package/dist/workspace/ci-green-forge.d.ts +20 -3
  206. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  207. package/dist/workspace/ci-green-forge.js +6 -4
  208. package/dist/workspace/ci-green-workflow.d.ts +11 -0
  209. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  210. package/dist/workspace/ci-green-workflow.js +6 -0
  211. package/dist/workspace/ci-green.d.ts +273 -13
  212. package/dist/workspace/ci-green.d.ts.map +1 -1
  213. package/dist/workspace/ci-green.js +449 -38
  214. package/dist/workspace/ci-last-green.schema.json +32 -2
  215. package/dist/workspace/ci-list.schema.json +525 -0
  216. package/dist/workspace/commit-links.d.ts +85 -0
  217. package/dist/workspace/commit-links.d.ts.map +1 -0
  218. package/dist/workspace/commit-links.js +163 -0
  219. package/dist/workspace/declaration.d.ts +19 -0
  220. package/dist/workspace/declaration.d.ts.map +1 -1
  221. package/dist/workspace/declaration.js +1 -1
  222. package/dist/workspace/declaration.schema.json +12 -0
  223. package/dist/workspace/identity-gate-rule.d.ts +37 -0
  224. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  225. package/dist/workspace/identity-gate-rule.js +72 -7
  226. package/dist/workspace/identity.d.ts +12 -0
  227. package/dist/workspace/identity.d.ts.map +1 -1
  228. package/dist/workspace/identity.js +13 -1
  229. package/dist/workspace/intent.d.ts +6 -1
  230. package/dist/workspace/intent.d.ts.map +1 -1
  231. package/dist/workspace/intent.js +51 -1
  232. package/dist/workspace/ls.d.ts +4 -0
  233. package/dist/workspace/ls.d.ts.map +1 -1
  234. package/dist/workspace/ls.js +2 -0
  235. package/dist/workspace/ls.schema.json +3 -1
  236. package/dist/workspace/member-pipeline.d.ts +6 -0
  237. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  238. package/dist/workspace/member-pipeline.js +6 -0
  239. package/dist/workspace/promotion-order.d.ts +41 -0
  240. package/dist/workspace/promotion-order.d.ts.map +1 -0
  241. package/dist/workspace/promotion-order.js +95 -0
  242. package/dist/workspace/reason-codes.d.ts +2 -0
  243. package/dist/workspace/reason-codes.d.ts.map +1 -1
  244. package/dist/workspace/reason-codes.js +4 -1
  245. package/dist/workspace/releases.d.ts +72 -0
  246. package/dist/workspace/releases.d.ts.map +1 -0
  247. package/dist/workspace/releases.js +94 -0
  248. package/dist/workspace/releases.schema.json +391 -0
  249. package/dist/workspace/run-event.schema.json +288 -0
  250. package/dist/workspace/status-gates.d.ts +26 -1
  251. package/dist/workspace/status-gates.d.ts.map +1 -1
  252. package/dist/workspace/status-gates.js +31 -2
  253. package/dist/workspace/status-stewards.d.ts +4 -0
  254. package/dist/workspace/status-stewards.d.ts.map +1 -1
  255. package/dist/workspace/status-stewards.js +2 -0
  256. package/dist/workspace/status.d.ts +39 -0
  257. package/dist/workspace/status.d.ts.map +1 -1
  258. package/dist/workspace/status.js +48 -1
  259. package/dist/workspace/status.schema.json +70 -1
  260. package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
  261. package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
  262. package/dist/workspace/trust/ci-tag-statement.js +123 -0
  263. package/dist/workspace/trust/op-run-statement.d.ts +91 -0
  264. package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
  265. package/dist/workspace/trust/op-run-statement.js +143 -0
  266. package/dist/workspace/trust/rotation.d.ts +18 -2
  267. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  268. package/dist/workspace/trust/rotation.js +65 -16
  269. package/dist/workspace/trust/seal.d.ts +11 -0
  270. package/dist/workspace/trust/seal.d.ts.map +1 -1
  271. package/dist/workspace/trust/seal.js +9 -0
  272. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  273. package/dist/workspace/trust/ssh-commit.js +10 -10
  274. package/package.json +1 -1
  275. package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
  276. package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
  277. package/src/__golden__/wave-loop/fan-out-only.json +6 -0
  278. package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  279. package/src/__golden__/wave-loop/op-cost-member.json +3 -0
  280. package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  281. package/src/__golden__/wave-loop/op-held-on.json +2 -0
  282. package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
  283. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  284. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  285. package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
  286. package/src/__golden__/wave-loop/op-per-run.json +7 -0
  287. package/src/__golden__/wave-loop/op-pr-review.json +7 -0
  288. package/src/__golden__/wave-loop/op-sealed.json +3 -0
  289. package/src/__golden__/wave-loop/op-spend.json +3 -0
  290. package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
  291. package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
  292. package/src/change-set-graph.test.ts +72 -0
  293. package/src/change-set-graph.ts +133 -0
  294. package/src/change-set.ts +76 -6
  295. package/src/chant-version.ts +16 -0
  296. package/src/cli/commands/build.ts +19 -8
  297. package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
  298. package/src/cli/commands/check-lexicon-docs.ts +39 -2
  299. package/src/cli/commands/check-lexicon.ts +14 -2
  300. package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
  301. package/src/cli/commands/init-lexicon.test.ts +25 -0
  302. package/src/cli/commands/init-lexicon.ts +18 -8
  303. package/src/cli/handlers/approve-release.test.ts +80 -0
  304. package/src/cli/handlers/approve.ts +18 -2
  305. package/src/cli/handlers/build.test.ts +52 -0
  306. package/src/cli/handlers/build.ts +29 -0
  307. package/src/cli/handlers/change-set.ts +49 -1
  308. package/src/cli/handlers/fan-out.test.ts +48 -0
  309. package/src/cli/handlers/fan-out.ts +15 -3
  310. package/src/cli/handlers/operator.test.ts +37 -0
  311. package/src/cli/handlers/pr.test.ts +194 -0
  312. package/src/cli/handlers/pr.ts +168 -34
  313. package/src/cli/handlers/promote.test.ts +389 -5
  314. package/src/cli/handlers/promote.ts +279 -20
  315. package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
  316. package/src/cli/handlers/release.e2e.test.ts +85 -0
  317. package/src/cli/handlers/release.ts +206 -0
  318. package/src/cli/handlers/run-auto-release.test.ts +71 -0
  319. package/src/cli/handlers/run-events-cmd.test.ts +92 -0
  320. package/src/cli/handlers/run.test.ts +30 -0
  321. package/src/cli/handlers/run.ts +176 -15
  322. package/src/cli/main.test.ts +7 -0
  323. package/src/cli/main.ts +88 -13
  324. package/src/cli/mcp/never-over-mcp.test.ts +2 -0
  325. package/src/cli/mcp/workspace-tools.ts +1 -1
  326. package/src/cli/parse-args.ts +37 -3
  327. package/src/cli/registry.ts +29 -3
  328. package/src/cli/run-events.ts +54 -0
  329. package/src/cli/version.ts +3 -13
  330. package/src/codegen/docs-in-site.test.ts +166 -0
  331. package/src/codegen/docs-in-site.ts +182 -0
  332. package/src/codegen/docs-site/AgentPrompt.astro +26 -1
  333. package/src/codegen/docs-types.ts +14 -1
  334. package/src/codegen/docs.ts +60 -7
  335. package/src/codegen/rehype-base-url.d.mts +3 -1
  336. package/src/codegen/rehype-base-url.mjs +15 -9
  337. package/src/codegen/rehype-base-url.test.ts +34 -0
  338. package/src/components/auto-release.test.ts +21 -0
  339. package/src/components/auto-release.ts +6 -1
  340. package/src/components/cli-support.test.ts +82 -0
  341. package/src/components/cli-support.ts +16 -9
  342. package/src/components/driver.ts +129 -56
  343. package/src/components/fan-out-run.ts +56 -35
  344. package/src/components/pr-run.ts +30 -1
  345. package/src/components/promote-order.test.ts +90 -0
  346. package/src/components/promote-order.ts +75 -0
  347. package/src/components/promote.test.ts +10 -1
  348. package/src/components/promote.ts +26 -1
  349. package/src/components/release-compile.test.ts +147 -0
  350. package/src/components/release-compile.ts +445 -0
  351. package/src/components/release-gate.test.ts +215 -0
  352. package/src/components/release-gate.ts +341 -0
  353. package/src/components/release-promote.test.ts +159 -0
  354. package/src/components/release-promote.ts +232 -0
  355. package/src/components/run-progress.ts +7 -0
  356. package/src/components/wave-plan.ts +1 -1
  357. package/src/config.ts +7 -1
  358. package/src/index.ts +2 -0
  359. package/src/lifecycle/change-set-store.test.ts +217 -0
  360. package/src/lifecycle/change-set-store.ts +112 -0
  361. package/src/lifecycle/commit-sha.ts +23 -0
  362. package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
  363. package/src/lifecycle/gate-change-set.ts +133 -0
  364. package/src/lifecycle/gate-ledger.test.ts +33 -0
  365. package/src/lifecycle/gate-ledger.ts +66 -1
  366. package/src/lifecycle/release-cut-signed.test.ts +180 -0
  367. package/src/lifecycle/release-cut.test.ts +411 -0
  368. package/src/lifecycle/release-cut.ts +367 -0
  369. package/src/lifecycle/release-ledger.test.ts +14 -0
  370. package/src/lifecycle/release-ledger.ts +85 -17
  371. package/src/lifecycle/release-store.ts +423 -0
  372. package/src/lifecycle/run-ledger.test.ts +10 -0
  373. package/src/lifecycle/run-ledger.ts +6 -0
  374. package/src/op/activities/change-coverage.ts +4 -1
  375. package/src/op/activities/source-release.ts +8 -3
  376. package/src/op/activities/source-rollback.ts +8 -4
  377. package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
  378. package/src/op/gate-name.ts +13 -3
  379. package/src/op/gate-release.test.ts +126 -0
  380. package/src/op/gate-run-locator.test.ts +110 -0
  381. package/src/op/gate-run-locator.ts +35 -0
  382. package/src/op/gate.ts +88 -6
  383. package/src/op/local-executor.ts +207 -52
  384. package/src/op/op-waves-run.ts +4 -0
  385. package/src/op/run-events.test.ts +166 -0
  386. package/src/op/run-live.ts +1 -1
  387. package/src/op/runtime.ts +39 -0
  388. package/src/op/runtimes/local.test.ts +16 -0
  389. package/src/op/runtimes/local.ts +9 -0
  390. package/src/point-decision.ts +12 -2
  391. package/src/run-event-store.test.ts +144 -0
  392. package/src/run-event-store.ts +213 -0
  393. package/src/run-event-stream.test.ts +175 -0
  394. package/src/run-event-stream.ts +306 -0
  395. package/src/run-event.test.ts +277 -0
  396. package/src/run-event.ts +451 -0
  397. package/src/wave-loop-golden.test.ts +9 -0
  398. package/src/workspace/__fixtures__/run-event-check.ts +58 -0
  399. package/src/workspace/ancestry-index.test.ts +93 -0
  400. package/src/workspace/change-set-show.schema.json +102 -0
  401. package/src/workspace/change-set-show.ts +72 -0
  402. package/src/workspace/change-set.schema.json +28 -1
  403. package/src/workspace/changes-cli.ts +10 -1
  404. package/src/workspace/changes.schema.json +135 -0
  405. package/src/workspace/changes.test.ts +1 -1
  406. package/src/workspace/changes.ts +84 -2
  407. package/src/workspace/ci-green-cli.ts +75 -13
  408. package/src/workspace/ci-green-forge.ts +24 -4
  409. package/src/workspace/ci-green-runs.test.ts +242 -0
  410. package/src/workspace/ci-green-signed.test.ts +302 -0
  411. package/src/workspace/ci-green-workflow.test.ts +16 -0
  412. package/src/workspace/ci-green-workflow.ts +12 -0
  413. package/src/workspace/ci-green.test.ts +131 -2
  414. package/src/workspace/ci-green.ts +624 -42
  415. package/src/workspace/ci-last-green.schema.json +32 -2
  416. package/src/workspace/ci-list.schema.json +525 -0
  417. package/src/workspace/commit-links.test.ts +279 -0
  418. package/src/workspace/commit-links.ts +183 -0
  419. package/src/workspace/declaration.schema.json +12 -0
  420. package/src/workspace/declaration.ts +22 -2
  421. package/src/workspace/identity-gate-rule.ts +91 -7
  422. package/src/workspace/identity.ts +14 -1
  423. package/src/workspace/intent.ts +48 -1
  424. package/src/workspace/ls.schema.json +3 -1
  425. package/src/workspace/ls.ts +6 -0
  426. package/src/workspace/member-pipeline.test.ts +17 -0
  427. package/src/workspace/member-pipeline.ts +9 -0
  428. package/src/workspace/promotion-order.ts +97 -0
  429. package/src/workspace/read-contract.e2e.test.ts +4 -1
  430. package/src/workspace/reason-codes.test.ts +2 -1
  431. package/src/workspace/reason-codes.ts +4 -1
  432. package/src/workspace/releases.schema.json +391 -0
  433. package/src/workspace/releases.test.ts +108 -0
  434. package/src/workspace/releases.ts +122 -0
  435. package/src/workspace/run-event.schema.json +288 -0
  436. package/src/workspace/status-contract.test.ts +52 -1
  437. package/src/workspace/status-gates.ts +56 -1
  438. package/src/workspace/status-stewards.ts +6 -0
  439. package/src/workspace/status.schema.json +70 -1
  440. package/src/workspace/status.ts +80 -1
  441. package/src/workspace/trust/ci-tag-statement.ts +151 -0
  442. package/src/workspace/trust/op-run-statement.ts +148 -0
  443. package/src/workspace/trust/rotation.ts +61 -14
  444. package/src/workspace/trust/seal.ts +10 -0
  445. package/src/workspace/trust/ssh-commit.ts +10 -9
  446. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
  447. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
  448. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
  449. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
  450. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
@@ -0,0 +1,166 @@
1
+ /**
2
+ * #4041: the local executor's run-event stream (`chant run <op>
3
+ * --events-json`). Every line passes the schema; each step's start comes
4
+ * before its settle under the same step id; and the cases an old
5
+ * `StepRecord` could not tell apart come out as `gated`, `skip: "receipt"`
6
+ * and `skip: "not-run"`.
7
+ */
8
+
9
+ import { describe, expect, test, vi } from "vitest";
10
+ import type { ActivityFn, ActivityProfile } from "./activity-registry";
11
+ import type { OpConfig } from "./types";
12
+ import { OpRunFailure, runOpLocally, type RunOpOptions } from "./local-executor";
13
+ import { memoryGateLedgerPort } from "./gate";
14
+ import { receiptActivities, type ReceiptStore } from "./receipt-store";
15
+ import { EffectReceipt, receiptExpectation } from "../effect-receipt";
16
+ import { effect } from "./builders";
17
+ import type { RunEvent } from "../run-event";
18
+ import { checkRunEventLines, stepKey, stepTrail } from "../workspace/__fixtures__/run-event-check";
19
+
20
+ vi.mock("./gate", async () => {
21
+ const actual = await vi.importActual<typeof import("./gate")>("./gate");
22
+ return { ...actual, gateIsSealed: async () => false };
23
+ });
24
+
25
+ const PROFILES: Record<string, ActivityProfile> = { fastIdempotent: { timeout: "5m", retry: { maximumAttempts: 1 } } };
26
+ const NOW = "2026-10-16T12:00:00.000Z";
27
+ const SHA = "0123456789abcdef0123456789abcdef01234567";
28
+
29
+ const op = (partial: Partial<OpConfig>): OpConfig => ({ name: "deploy-api", overview: "", phases: [], ...partial });
30
+ const ok: ActivityFn = async () => ({ done: true });
31
+
32
+ /** Run an Op with its events captured, settling whether it resolves or rejects. */
33
+ async function run(config: OpConfig, activities: Map<string, ActivityFn>, options: RunOpOptions = {}) {
34
+ const lines: string[] = [];
35
+ let status: string;
36
+ try {
37
+ status = (await runOpLocally(config, activities, PROFILES, undefined, {
38
+ gates: memoryGateLedgerPort(),
39
+ now: NOW,
40
+ runId: "local-1",
41
+ commit: SHA,
42
+ runEvents: { write: (l) => lines.push(l) },
43
+ ...options,
44
+ })).status;
45
+ } catch (err) {
46
+ if (!(err instanceof OpRunFailure)) throw err;
47
+ status = err.result.status;
48
+ }
49
+ return { status, events: checkRunEventLines(lines) };
50
+ }
51
+
52
+ describe("the local runtime's run events", () => {
53
+ test("a run that passes: run, phases and steps, each step started before it settles, named as the run record names it", async () => {
54
+ const { status, events } = await run(
55
+ op({ phases: [{ name: "Build", steps: [{ kind: "activity", fn: "make" }] }, { name: "Ship", steps: [{ kind: "activity", fn: "push" }] }] }),
56
+ new Map([["make", ok], ["push", ok]]),
57
+ );
58
+ expect(status).toBe("ok");
59
+ // In a CI job the run keeps its own id and names the CI run as its origin, as its run record does (#4020).
60
+ const ci = Boolean(process.env.GITHUB_RUN_ID || process.env.CI_PIPELINE_ID);
61
+ expect(events[0]).toMatchObject({ runId: "local-1", kind: "op", name: "deploy-api", gitSha: SHA, runtime: ci ? "ci" : "local" });
62
+ expect(events[0]!.runOrigin.forge).toBe(process.env.GITHUB_RUN_ID ? "github" : process.env.CI_PIPELINE_ID ? "gitlab" : "local");
63
+ expect(events.map((e) => e.type)).toEqual([
64
+ "run-start",
65
+ "phase-start", "step-start", "step-settle", "phase-settle",
66
+ "phase-start", "step-start", "step-settle", "phase-settle",
67
+ "run-settle",
68
+ ]);
69
+ expect(events.at(-1)).toMatchObject({ status: "ok" });
70
+ });
71
+
72
+ test("a step's start is emitted before its settle, with the same step id", async () => {
73
+ const { events } = await run(
74
+ op({ phases: [{ name: "Build", steps: [{ kind: "activity", fn: "make" }, { kind: "activity", fn: "make" }] }] }),
75
+ new Map([["make", ok]]),
76
+ );
77
+ const steps = events.filter((e): e is Extract<RunEvent, { type: "step-start" | "step-settle" }> => e.type === "step-start" || e.type === "step-settle");
78
+ // Two steps of one phase share a name; the index keeps them apart.
79
+ expect(steps.map((e) => [e.type, stepKey(e)])).toEqual([
80
+ ["step-start", JSON.stringify([null, "Build", "make", 0])],
81
+ ["step-settle", JSON.stringify([null, "Build", "make", 0])],
82
+ ["step-start", JSON.stringify([null, "Build", "make", 1])],
83
+ ["step-settle", JSON.stringify([null, "Build", "make", 1])],
84
+ ]);
85
+ expect(steps[0]!.seq).toBeLessThan(steps[1]!.seq);
86
+ });
87
+
88
+ test("a parallel phase starts each step before any settles, and tells two steps of one name apart by index", async () => {
89
+ const { events } = await run(
90
+ op({ phases: [{ name: "Fan", parallel: true, steps: [{ kind: "activity", fn: "probe", args: { at: 1 } }, { kind: "activity", fn: "probe", args: { at: 2 } }] }] }),
91
+ new Map([["probe", ok]]),
92
+ );
93
+ expect(stepTrail(events)).toEqual([
94
+ ["step-start", "probe"], ["step-start", "probe"], ["step-settle", "probe", "ok"], ["step-settle", "probe", "ok"],
95
+ ]);
96
+ expect(events.filter((e) => e.type === "step-settle").map((e) => (e as { index?: number }).index)).toEqual([0, 1]);
97
+ });
98
+
99
+ test("an unapproved gate settles gated after gate-waiting; the steps after it settle skipped as not-run", async () => {
100
+ const { status, events } = await run(
101
+ op({ phases: [{ name: "Ship", steps: [{ kind: "activity", fn: "before" }, { kind: "gate", gate: "approve-prod" }, { kind: "activity", fn: "after" }] }] }),
102
+ new Map([["before", ok], ["after", ok]]),
103
+ );
104
+ expect(status).toBe("gated");
105
+ expect(stepTrail(events)).toEqual([
106
+ ["step-start", "before"], ["step-settle", "before", "ok"],
107
+ ["step-start", "gate:approve-prod"], ["gate-waiting", "gate:approve-prod"], ["step-settle", "gate:approve-prod", "gated"],
108
+ ["step-settle", "after", "skipped:not-run"],
109
+ ]);
110
+ expect(events.find((e) => e.type === "gate-waiting")).toMatchObject({ op: "deploy-api", gate: "approve-prod", point: null, index: 1 });
111
+ expect(events.find((e) => e.type === "phase-settle")).toMatchObject({ status: "gated" });
112
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "gated" });
113
+ });
114
+
115
+ test("an approved gate resolves, naming who approved it", async () => {
116
+ const gates = memoryGateLedgerPort({
117
+ resolutions: [{ version: 1, op: "deploy-api", gate: "approve-prod", resolvedBy: "alex", timestamp: "2026-10-16T11:00:00.000Z" }],
118
+ });
119
+ const { status, events } = await run(op({ phases: [{ name: "Ship", steps: [{ kind: "gate", gate: "approve-prod" }] }] }), new Map(), { gates });
120
+ expect(status).toBe("ok");
121
+ expect(events.find((e) => e.type === "gate-resolved")).toMatchObject({ gate: "approve-prod", resolvedBy: "alex", via: null, index: 0 });
122
+ });
123
+
124
+ test("a failed step fails its phase and the run, with its error, and the rest of its phase is not-run", async () => {
125
+ const boom: ActivityFn = async () => { throw new Error("disk full"); };
126
+ const { status, events } = await run(
127
+ op({ phases: [{ name: "Build", steps: [{ kind: "activity", fn: "make" }, { kind: "activity", fn: "pack" }] }] }),
128
+ new Map([["make", boom], ["pack", ok]]),
129
+ );
130
+ expect(status).toBe("fail");
131
+ expect(stepTrail(events)).toEqual([["step-start", "make"], ["step-settle", "make", "failed"], ["step-settle", "pack", "skipped:not-run"]]);
132
+ expect(events.find((e) => e.type === "phase-settle")).toMatchObject({ status: "failed" });
133
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed", error: "disk full" });
134
+ });
135
+
136
+ test("an effect whose receipt matched settles its steps skipped with skip: receipt", async () => {
137
+ const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { version: 3 } });
138
+ const values = new Map<string, string>([["seeded", receiptExpectation(seeded)]]);
139
+ const store: ReceiptStore = {
140
+ read: async (r) => values.get(r.name),
141
+ write: async (r, e) => void values.set(r.name, e),
142
+ };
143
+ const { receiptRead, receiptWrite } = receiptActivities(store);
144
+ const { status, events } = await run(
145
+ op({ phases: [{ name: "Seed", steps: [effect(seeded, [{ kind: "activity", fn: "seed" }])] }] }),
146
+ new Map<string, ActivityFn>([["seed", ok], ["receiptRead", receiptRead as unknown as ActivityFn], ["receiptWrite", receiptWrite as unknown as ActivityFn]]),
147
+ );
148
+ expect(status).toBe("ok");
149
+ expect(stepTrail(events)).toEqual([
150
+ ["step-start", "receiptRead"], ["step-settle", "receiptRead", "ok"],
151
+ ["step-settle", "seed", "skipped:receipt"],
152
+ ]);
153
+ });
154
+
155
+ test("a run that throws before its first phase still ends its stream, failed with the reason", async () => {
156
+ const lines: string[] = [];
157
+ await expect(
158
+ runOpLocally(op({ phases: [{ name: "P", parallel: true, steps: [effect(EffectReceipt("r", { effect: "e", flavor: "existence", inputs: {} }), [])] }] }), new Map(), PROFILES, undefined, {
159
+ gates: memoryGateLedgerPort(), now: NOW, commit: SHA, runEvents: { write: (l) => lines.push(l) },
160
+ }),
161
+ ).rejects.toThrow(/cannot run in a parallel phase/);
162
+ const events = checkRunEventLines(lines);
163
+ expect(events.map((e) => e.type)).toEqual(["run-start", "run-settle"]);
164
+ expect(events.at(-1)).toMatchObject({ status: "failed", error: expect.stringMatching(/cannot run in a parallel phase/) });
165
+ });
166
+ });
@@ -86,7 +86,7 @@ function keyOf(prefix: string, env: string, op: string): string {
86
86
  }
87
87
 
88
88
  /** The checkout's shared git directory (the same for every worktree), or null outside a checkout. */
89
- async function gitCommonDir(cwd: string): Promise<string | null> {
89
+ export async function gitCommonDir(cwd: string): Promise<string | null> {
90
90
  try {
91
91
  const { stdout } = await execFileAsync("git", ["rev-parse", "--git-common-dir"], { cwd });
92
92
  const dir = stdout.trim();
package/src/op/runtime.ts CHANGED
@@ -20,6 +20,7 @@ import type { OpConfig } from "./types";
20
20
  import type { StepRecord, OpRunResult } from "./local-executor";
21
21
  import type { WaitingPoint } from "./steward-points";
22
22
  import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
23
+ import type { RunOrigin } from "../lifecycle/release-ledger";
23
24
  import type { RunComponentsOptions, RunComponentsResult } from "../components/cli-support";
24
25
 
25
26
  /**
@@ -159,6 +160,29 @@ export interface OpRunRecord {
159
160
  * first phase.
160
161
  */
161
162
  resumed?: { gate: string; run: string; checkpoint: string };
163
+ /**
164
+ * The commit the run built from (#4020): the checkout's `HEAD`, else the
165
+ * commit the CI job started for. Gate facts the run writes carry the same
166
+ * commit, so a run and its gate join on it. Absent on a record written
167
+ * before 0.124.28 and when the run knew no commit.
168
+ */
169
+ gitSha?: string;
170
+ /**
171
+ * The CI run this Op ran in (#4020), as a deploy record's `runOrigin` names
172
+ * its run, so the two name a run the same way. `id` is still the Op's own
173
+ * run id. Absent outside CI and on a record written before 0.124.28.
174
+ */
175
+ runOrigin?: RunOrigin;
176
+ /**
177
+ * The run's statement (#4044): a DSSE envelope over an in-toto statement
178
+ * (`https://intentius.io/chant/op-run/v1`) a runner key signed at the end
179
+ * of the run, naming the run, its commit and the SHA-256 of this record
180
+ * without the statement (`../workspace/trust/op-run-statement.ts`).
181
+ * `chant ci tick` with `ci.green.source: "runs"` counts only a run that
182
+ * carries one. Absent on a run given no runner key, and on a record
183
+ * written before 0.124.28.
184
+ */
185
+ statement?: { payloadType: string; payload: string; signatures: { keyid: string; sig: string }[] };
162
186
  }
163
187
 
164
188
  /** An {@link OpRunRecord} before the ledger stamps its version and mints an id. */
@@ -187,6 +211,14 @@ export interface OpRunStartOptions {
187
211
  profile?: string;
188
212
  /** Called once per settled step, so `--progress-json` can stream. */
189
213
  progress?: (record: StepRecord) => void;
214
+ /**
215
+ * Where the run's event stream goes (#4041, `--events-json`): one
216
+ * `run-event.schema.json` line at a time, written through the one writer
217
+ * (`createRunEventWriter`). The local runtime writes `runtime: "local"`
218
+ * events (`ci` in a CI job); a hosted runtime writes its own, as fountain
219
+ * writes `runtime: "steward"` ones. A runtime that cannot stream omits it.
220
+ */
221
+ runEvents?: (line: string) => void;
190
222
  /** Aborts in-flight work (Ctrl-C). */
191
223
  signal?: AbortSignal;
192
224
  /**
@@ -205,6 +237,13 @@ export interface OpRunStartOptions {
205
237
  * and the run `chant run status --on fountain` reports carry the same id.
206
238
  */
207
239
  runId?: string;
240
+ /**
241
+ * Signs the run's record when it settles (#4044), for `chant run <op>
242
+ * --key` or `CHANT_RUNNER_KEY`: the local runtime stores what it returns
243
+ * on the record as `statement`. A runtime that writes no record here
244
+ * ignores it.
245
+ */
246
+ signRecord?: (record: OpRunRecord) => NonNullable<OpRunRecord["statement"]>;
208
247
  }
209
248
 
210
249
  /** What a caller hands {@link OpRuntimeProvider.resolveGate} besides the resolution. */
@@ -51,6 +51,7 @@ vi.mock("../../lifecycle/run-ledger", async () => {
51
51
  });
52
52
 
53
53
  const { createLocalOpRuntime } = await import("./local");
54
+ const { checkRunEventLines, stepTrail } = await import("../../workspace/__fixtures__/run-event-check");
54
55
 
55
56
  function op(name: string, steps: unknown[]): OpConfig {
56
57
  return {
@@ -145,6 +146,21 @@ describe("the local op runtime", () => {
145
146
  expect(seen).toEqual(["ok", "ok"]);
146
147
  });
147
148
 
149
+ test("runEvents gets the run's event stream, named by the handle's run id (#4041)", async () => {
150
+ loadActivitiesMock.mockResolvedValue(new Map([["ok", vi.fn(async () => ({}))]]));
151
+ const runtime = createLocalOpRuntime();
152
+ const lines: string[] = [];
153
+
154
+ const handle = await runtime.start(op("hello", [{ kind: "activity", fn: "ok", args: {} }]), { runEvents: (l) => lines.push(l) });
155
+ await handle.result();
156
+
157
+ const events = checkRunEventLines(lines);
158
+ expect(events[0]).toMatchObject({ type: "run-start", runId: handle.runId, kind: "op", name: "hello" });
159
+ expect(events[0]!.runtime).toBe(process.env.GITHUB_RUN_ID || process.env.CI_PIPELINE_ID ? "ci" : "local");
160
+ expect(stepTrail(events)).toEqual([["step-start", "ok"], ["step-settle", "ok", "ok"]]);
161
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "ok" });
162
+ });
163
+
148
164
  test("a gated Op settles as gated, naming the gate, and runs nothing after it (#2119)", async () => {
149
165
  const after = vi.fn(async () => ({}));
150
166
  loadActivitiesMock.mockResolvedValue(new Map([["after", after]]));
@@ -176,6 +176,15 @@ export function createLocalOpRuntime(opts: { projectPath?: string } = {}): OpRun
176
176
  `appended to the run ledger: ${err instanceof Error ? err.message : String(err)}\n`,
177
177
  ),
178
178
  ...(startOpts.progress ? { onRecord: startOpts.progress } : {}),
179
+ ...(startOpts.runEvents ? { runEvents: { write: startOpts.runEvents } } : {}),
180
+ // #4044: a runner key signs the record as the run settles.
181
+ ...(startOpts.signRecord
182
+ ? {
183
+ signRecord: startOpts.signRecord,
184
+ onSignError: (err: unknown) =>
185
+ process.stderr.write(`warning: run "${runId}" of "${op.name}" was not signed: ${err instanceof Error ? err.message : String(err)}\n`),
186
+ }
187
+ : {}),
179
188
  // #3956: a running step's output, and a still-running line while
180
189
  // it is quiet, on stderr as they happen; stdout stays the
181
190
  // `--json` record's and `--progress-json`'s.
@@ -36,7 +36,7 @@ import { parseDuration } from "./op/duration";
36
36
  import type { GateLedgerPort } from "./op/gate";
37
37
  import { DEFAULT_GATE_POINT_PASS, defaultGatePointStore, gatePointInputs, gateSubject, type GatePoint, type GatePointFacts, type GatePointStore } from "./op/gate-point";
38
38
  import type { WaitingPoint } from "./op/steward-points";
39
- import { resolveGateRunLocator, type GateRunLocator } from "./op/gate-run-locator";
39
+ import { ciCommitFromEnv, ciRunIdFromEnv, resolveGateRunLocator, type GateRunLocator } from "./op/gate-run-locator";
40
40
  import { refuseGateWithoutRecording } from "./lifecycle/no-record";
41
41
 
42
42
  export {
@@ -102,6 +102,13 @@ export interface GatePointAsker {
102
102
  ask(request: GatePointRequest): Promise<GatePointQuestion>;
103
103
  }
104
104
 
105
+ /** The run and commit a gate record names (#4020): the input's own, else the CI job's. */
106
+ function runStamp(input: { runId?: string; gitSha?: string }): { runId?: string; gitSha?: string } {
107
+ const runId = input.runId || ciRunIdFromEnv();
108
+ const gitSha = input.gitSha || ciCommitFromEnv();
109
+ return { ...(runId ? { runId } : {}), ...(gitSha ? { gitSha } : {}) };
110
+ }
111
+
105
112
  /** What the run needs to decide a gate that asks a point. */
106
113
  export interface PointGateInput {
107
114
  op: string;
@@ -112,6 +119,8 @@ export interface PointGateInput {
112
119
  description?: string;
113
120
  timeout?: string;
114
121
  runId?: string;
122
+ /** The commit the run is at (#4020). Default: the commit the CI job started for, when it runs in CI. */
123
+ gitSha?: string;
115
124
  planDigest?: string;
116
125
  /** The environment the run was started for: the `gate.env` input. Op gate facts on the ledger record none. */
117
126
  env?: string;
@@ -202,6 +211,7 @@ export async function evaluatePointGate(port: PointGateLedgerPort, asker: GatePo
202
211
  resolvedBy: resolvedByOf(q),
203
212
  timestamp: now,
204
213
  note: `answered through decision point ${q.point} (${q.id})`,
214
+ ...runStamp(input),
205
215
  ...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
206
216
  approver: { kind: q.decider === "quorum" || q.answeredBy.length > 0 ? "human" : "agent" },
207
217
  answer,
@@ -228,7 +238,7 @@ export async function evaluatePointGate(port: PointGateLedgerPort, asker: GatePo
228
238
  timestamp: now,
229
239
  expiresAt: new Date(new Date(now).getTime() + parseDuration(input.timeout ?? DEFAULT_GATE_EXPIRY)).toISOString(),
230
240
  ...(input.description ? { description: input.description } : {}),
231
- ...(input.runId ? { runId: input.runId } : {}),
241
+ ...runStamp(input),
232
242
  ...(url ? { url } : {}),
233
243
  ...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
234
244
  ...(resume ? { resume } : {}),
@@ -0,0 +1,144 @@
1
+ /**
2
+ * #4042: a run's events kept on this host, replayed, and followed while the
3
+ * run is in progress. No servers: a temp git checkout, the real executor and
4
+ * the real writer, and a short poll.
5
+ */
6
+
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, appendFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
12
+ import { runOpLocally } from "./op/local-executor";
13
+ import { memoryGateLedgerPort } from "./op/gate";
14
+ import type { ActivityFn } from "./op/activity-registry";
15
+ import type { OpConfig } from "./op/types";
16
+ import type { RunEvent } from "./run-event";
17
+ import { openRunEventStream, runEventRunOf } from "./run-event-stream";
18
+ import { findStoredRun, listStoredRuns, openRunEventStore, readRunEvents, runEventsDir } from "./run-event-store";
19
+ import { checkRunEventLines } from "./workspace/__fixtures__/run-event-check";
20
+
21
+ const dirs: string[] = [];
22
+ // These tests read the store a temp checkout keeps under its own git directory.
23
+ beforeEach(() => void vi.stubEnv("CHANT_RUN_EVENTS_DIR", ""));
24
+ afterEach(() => {
25
+ vi.unstubAllEnvs();
26
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
27
+ });
28
+
29
+ function checkout(): string {
30
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "run-event-store-")));
31
+ dirs.push(dir);
32
+ execFileSync("git", ["init", "-q", "-b", "main"], { cwd: dir });
33
+ execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "--allow-empty", "-m", "init"], { cwd: dir });
34
+ return dir;
35
+ }
36
+
37
+ const op: OpConfig = {
38
+ name: "deploy-api",
39
+ overview: "",
40
+ phases: [{ name: "Build", steps: [{ kind: "activity", fn: "make" }] }],
41
+ };
42
+ const ok: ActivityFn = async () => ({ done: true });
43
+ const PROFILES = { fastIdempotent: { timeout: "5m", retry: { maximumAttempts: 1 } } };
44
+
45
+ describe("a settled run's events", () => {
46
+ test("a run that records to the ledger keeps its stream, and it replays as the writer wrote it", async () => {
47
+ const dir = checkout();
48
+ await runOpLocally(op, new Map([["make", ok]]), PROFILES, undefined, {
49
+ runId: "r-settled",
50
+ ledger: { cwd: dir },
51
+ cwd: dir,
52
+ gates: memoryGateLedgerPort(),
53
+ });
54
+ const stored = (await findStoredRun(dir, "r-settled"))!;
55
+ expect(stored.file.startsWith(join(dir, ".git", "chant", "run-events"))).toBe(true);
56
+ expect(stored.name).toBe("deploy-api");
57
+
58
+ const lines: string[] = [];
59
+ const settle = await readRunEvents(stored.file, (_e, line) => lines.push(line));
60
+ const events = checkRunEventLines(lines);
61
+ expect(settle?.type).toBe("run-settle");
62
+ expect(events.map((e) => e.type)).toContain("step-settle");
63
+ expect(lines.join("")).toBe(readFileSync(stored.file, "utf8"));
64
+ });
65
+
66
+ test("an Op name stands for its newest run", async () => {
67
+ const dir = checkout();
68
+ for (const runId of ["r-1", "r-2"]) {
69
+ await runOpLocally(op, new Map([["make", ok]]), PROFILES, undefined, { runId, ledger: { cwd: dir }, cwd: dir, gates: memoryGateLedgerPort() });
70
+ await new Promise((r) => setTimeout(r, 15));
71
+ }
72
+ expect((await findStoredRun(dir, "deploy-api"))?.runId).toBe("r-2");
73
+ expect((await listStoredRuns(dir)).map((r) => r.runId)).toEqual(["r-2", "r-1"]);
74
+ expect(await findStoredRun(dir, "nothing")).toBeNull();
75
+ });
76
+
77
+ test("a run with no ledger (--no-record) keeps nothing", async () => {
78
+ const dir = checkout();
79
+ await runOpLocally(op, new Map([["make", ok]]), PROFILES, undefined, { runId: "r-none", cwd: dir, gates: memoryGateLedgerPort() });
80
+ const events = await runEventsDir(dir);
81
+ expect(events === null || !existsSync(events)).toBe(true);
82
+ });
83
+ });
84
+
85
+ describe("a run in progress", () => {
86
+ async function inFlight() {
87
+ const dir = checkout();
88
+ const store = (await openRunEventStore(dir, "r-live"))!;
89
+ const stream = openRunEventStream(runEventRunOf({ kind: "op", name: "deploy-api", runId: "r-live" }), store.write);
90
+ return { dir, store, stream };
91
+ }
92
+
93
+ test("replays what has been written so far, and not a half-written last line", async () => {
94
+ const { store, stream } = await inFlight();
95
+ stream.emit({ type: "run-start", waves: null });
96
+ stream.emit({ type: "phase-start", component: null, phase: "Build" });
97
+ appendFileSync(store.path, '{"type":"step-st');
98
+ const seen: RunEvent[] = [];
99
+ const settle = await readRunEvents(store.path, (e) => seen.push(e));
100
+ expect(settle).toBeUndefined();
101
+ expect(seen.map((e) => e.type)).toEqual(["run-start", "phase-start"]);
102
+ });
103
+
104
+ test("--follow streams each event as it is written and ends at run-settle", async () => {
105
+ const { store, stream } = await inFlight();
106
+ stream.emit({ type: "run-start", waves: null });
107
+ const seen: string[] = [];
108
+ const follow = readRunEvents(store.path, (e) => seen.push(`${e.seq}:${e.type}`), { follow: true, pollMs: 5, idleMs: 3000 });
109
+
110
+ await new Promise((r) => setTimeout(r, 30));
111
+ expect(seen).toEqual(["0:run-start"]);
112
+ stream.emit({ type: "phase-start", component: null, phase: "Build" });
113
+ stream.emit({ type: "step-start", component: null, phase: "Build", step: "make" });
114
+ await new Promise((r) => setTimeout(r, 30));
115
+ expect(seen).toEqual(["0:run-start", "1:phase-start", "2:step-start"]);
116
+ stream.settle("ok");
117
+
118
+ const settle = await follow;
119
+ expect(settle?.type).toBe("run-settle");
120
+ expect(seen.at(-1)).toBe("3:run-settle");
121
+ });
122
+
123
+ test("--follow waits for a file that is not there yet", async () => {
124
+ const dir = checkout();
125
+ const file = join((await runEventsDir(dir))!, "later.jsonl");
126
+ const seen: string[] = [];
127
+ const follow = readRunEvents(file, (e) => seen.push(e.type), { follow: true, pollMs: 5, idleMs: 3000 });
128
+ await new Promise((r) => setTimeout(r, 20));
129
+ const store = (await openRunEventStore(dir, "later"))!;
130
+ const stream = openRunEventStream(runEventRunOf({ kind: "op", name: "x", runId: "later" }), store.write);
131
+ stream.settle("failed", "boom");
132
+ expect((await follow)?.type).toBe("run-settle");
133
+ expect(seen).toEqual(["run-start", "run-settle"]);
134
+ });
135
+
136
+ test("an aborted follow returns without a verdict", async () => {
137
+ const { store, stream } = await inFlight();
138
+ stream.emit({ type: "run-start", waves: null });
139
+ const ac = new AbortController();
140
+ const follow = readRunEvents(store.path, () => {}, { follow: true, pollMs: 5, signal: ac.signal });
141
+ setTimeout(() => ac.abort(), 20);
142
+ expect(await follow).toBeUndefined();
143
+ });
144
+ });