@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,80 @@
1
+ /**
2
+ * `chant approve` on a release gate (#4045): the approval copies the release
3
+ * the pending fact names, with its plan and environment, so it counts for
4
+ * that release only. Against a temp repo; nothing is pushed.
5
+ */
6
+
7
+ import { afterEach, describe, expect, test, vi } from "vitest";
8
+ import { withTestDir } from "@intentius/chant-test-utils";
9
+ import { spawnSync } from "node:child_process";
10
+ import { writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { appendPendingGate, readGateLedger } from "../../lifecycle/gate-ledger";
13
+ import { resetGateOrigin } from "../../lifecycle/gate-origin";
14
+
15
+ vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
16
+
17
+ const { recordGateApproval } = await import("./approve");
18
+
19
+ function git(args: string[], cwd: string): void {
20
+ spawnSync("git", args, { cwd, encoding: "utf-8" });
21
+ }
22
+
23
+ const RELEASE = `jcs1-sha256:${"a".repeat(64)}`;
24
+ const DOC = `jcs1-sha256:${"d".repeat(64)}`;
25
+
26
+ async function repoWithReleaseGate(dir: string): Promise<void> {
27
+ git(["init", "-q", "-b", "main"], dir);
28
+ git(["config", "user.email", "t@chant.dev"], dir);
29
+ git(["config", "user.name", "T"], dir);
30
+ writeFileSync(join(dir, "README.md"), "x\n");
31
+ git(["add", "."], dir);
32
+ git(["commit", "-q", "-m", "init"], dir);
33
+ await appendPendingGate(
34
+ {
35
+ op: "release",
36
+ gate: "prod-release",
37
+ timestamp: "2026-10-16T00:00:00.000Z",
38
+ expiresAt: "2999-01-01T00:00:00.000Z",
39
+ planDigest: DOC,
40
+ environment: "prod",
41
+ releaseDigest: RELEASE,
42
+ },
43
+ { cwd: dir },
44
+ );
45
+ }
46
+
47
+ describe("chant approve on a release gate (#4045)", () => {
48
+ const cwd = process.cwd();
49
+ afterEach(() => {
50
+ process.chdir(cwd);
51
+ resetGateOrigin();
52
+ vi.restoreAllMocks();
53
+ });
54
+
55
+ test("the approval names the release, the environment and the change set the pending fact names", async () => {
56
+ vi.spyOn(console, "error").mockImplementation(() => {});
57
+ await withTestDir(async (dir) => {
58
+ await repoWithReleaseGate(dir);
59
+ process.chdir(dir);
60
+ const outcome = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" });
61
+ expect(outcome.ok).toBe(true);
62
+ const { resolutions } = await readGateLedger("release", { cwd: dir });
63
+ expect(resolutions).toEqual([expect.objectContaining({ resolvedBy: "alice", planDigest: DOC, environment: "prod", releaseDigest: RELEASE })]);
64
+ });
65
+ });
66
+
67
+ test("an approval of another plan names no release, so no release gate counts it", async () => {
68
+ vi.spyOn(console, "error").mockImplementation(() => {});
69
+ await withTestDir(async (dir) => {
70
+ await repoWithReleaseGate(dir);
71
+ process.chdir(dir);
72
+ const other = `jcs1-sha256:${"e".repeat(64)}`;
73
+ const outcome = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod", plan: other });
74
+ expect(outcome.ok).toBe(true);
75
+ const { resolutions } = await readGateLedger("release", { cwd: dir });
76
+ expect(resolutions[0].planDigest).toBe(other);
77
+ expect(resolutions[0].releaseDigest).toBeUndefined();
78
+ });
79
+ });
80
+ });
@@ -21,7 +21,7 @@ import {
21
21
  type GateApprover, type GatePolicyDecision, type GatePolicyEvaluator, type ResolvedGateApproval,
22
22
  } from "../../op/gate-approval";
23
23
  import { approverOf, humanQuorumSince, tallyGateApprovals } from "../../op/gate";
24
- import { FAN_OUT_GATE_OP, OVERRIDE_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "../../op/gate-name";
24
+ import { FAN_OUT_GATE_OP, OVERRIDE_GATE_OP, RELEASE_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "../../op/gate-name";
25
25
  import { PR_GATE_OP } from "../../pr-loop";
26
26
  import { pushLifecycleReport, requireLifecycleLedger } from "../../lifecycle/git";
27
27
  import { LIFECYCLE_LOCAL_NOTE, isLifecycleLocalNote } from "../../lifecycle/local-note";
@@ -112,6 +112,8 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
112
112
  timestamp: now,
113
113
  expiresAt: now,
114
114
  ...(standing.description ? { description: standing.description } : {}),
115
+ ...(standing.runId ? { runId: standing.runId } : {}),
116
+ ...(standing.gitSha ? { gitSha: standing.gitSha } : {}),
115
117
  });
116
118
  const push = await reportedPush(
117
119
  `The expiry is recorded locally on ${LIFECYCLE_BRANCH}. Until it reaches the remote, ` +
@@ -127,6 +129,8 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
127
129
  ? `The next \`chant components pr-apply\` decides this gate from scratch and records a fresh pending fact.`
128
130
  : opName === WORKSPACE_UPGRADE_GATE_OP
129
131
  ? `The next \`chant workspace upgrade ${gate}\` decides this gate from scratch and records a fresh pending fact.`
132
+ : opName === RELEASE_GATE_OP
133
+ ? `The next \`chant components promote --release\` decides this gate from scratch and records a fresh pending fact.`
130
134
  : `The next \`chant run ${opName}\` decides this gate from scratch and records a fresh pending fact.`,
131
135
  ));
132
136
  return 0;
@@ -177,6 +181,8 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
177
181
  ? `The pull request's apply (\`chant components pr-apply\`) walks through gate "${gate}" when it plans this same digest.`
178
182
  : opName === WORKSPACE_UPGRADE_GATE_OP
179
183
  ? `Repeat \`chant workspace upgrade ${gate}\` with the same target, and it applies the patch this approval names.`
184
+ : opName === RELEASE_GATE_OP
185
+ ? `Repeat the same \`chant components promote --release\`, and it walks through gate "${gate}" when it plans this same change set for this release.`
180
186
  : `Run \`chant run ${opName}\` and it walks through gate "${gate}".`),
181
187
  ));
182
188
  return 0;
@@ -283,7 +289,7 @@ export async function recordGateApproval(
283
289
  // so there is no `*.op.ts` to find and nothing is wrong when none is there.
284
290
  // #3949: a gate that lists this one as an alias, after an Op was restructured.
285
291
  const successor = aliasingGate([...ops.values()].map((o) => o.config), opName, gate);
286
- if (!successor && !ops.has(opName) && opName !== FAN_OUT_GATE_OP && opName !== WORKSPACE_UPGRADE_GATE_OP && opName !== OVERRIDE_GATE_OP) {
292
+ if (!successor && !ops.has(opName) && opName !== FAN_OUT_GATE_OP && opName !== WORKSPACE_UPGRADE_GATE_OP && opName !== OVERRIDE_GATE_OP && opName !== RELEASE_GATE_OP) {
287
293
  console.error(formatWarning({
288
294
  message: `Op "${opName}" was not found among discovered *.op.ts declarations — recording the resolution anyway`,
289
295
  }));
@@ -389,6 +395,11 @@ export async function recordGateApproval(
389
395
  standing?.checkpointDigest !== undefined && samePlanDigest(planDigest, standing.planDigest)
390
396
  ? standing.checkpointDigest
391
397
  : undefined;
398
+ // #4045: a release gate's fact names the release its change set was
399
+ // planned for. An approval of that fact's plan is for that release too; an
400
+ // approval of another plan names none, and no release gate counts it.
401
+ const releaseDigest =
402
+ standing?.releaseDigest !== undefined && samePlanDigest(planDigest, standing.planDigest) ? standing.releaseDigest : undefined;
392
403
 
393
404
  // chant#2384 — the gate's whole value is that its two halves have different
394
405
  // authors. At a shell they do: a person runs, reads the plan, and approves.
@@ -587,6 +598,10 @@ export async function recordGateApproval(
587
598
  ...(planDigest !== undefined ? { planDigest } : {}),
588
599
  ...(checkpointDigest !== undefined ? { checkpointDigest } : {}),
589
600
  ...(environment !== undefined ? { environment } : {}),
601
+ ...(releaseDigest !== undefined ? { releaseDigest } : {}),
602
+ // #4020: the commit and run the gate waited on, off the fact this approves.
603
+ ...(standing?.gitSha ? { gitSha: standing.gitSha } : {}),
604
+ ...(standing?.runId ? { runId: standing.runId } : {}),
590
605
  origin,
591
606
  ...(refusal && opts.allowSameOrigin ? { sameOriginOverride: true } : {}),
592
607
  });
@@ -607,6 +622,7 @@ export async function recordGateApproval(
607
622
  console.error(formatInfo(
608
623
  `This approves the plan ${record.planDigest}` +
609
624
  (record.environment !== undefined ? ` in environment "${record.environment}"` : "") +
625
+ (record.releaseDigest !== undefined ? ` for release ${record.releaseDigest}` : "") +
610
626
  ", and only that plan. A run whose fresh plan differs refuses rather than applying it.",
611
627
  ));
612
628
  }
@@ -70,6 +70,37 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
70
70
  vi.restoreAllMocks();
71
71
  });
72
72
 
73
+ test("--promote-to that skips a step of the declared promotion order is refused, and generates nothing (#4030)", async () => {
74
+ loadChantConfigUpwardMock.mockResolvedValue({ config: { environments: ["dev", "staging", "prod"] } });
75
+ const stderr = makeStderrSpy();
76
+ const exit = await runBuild({ args: makeArgs({ env: "dev", promoteTo: "prod" }), plugins: [], serializers: [] });
77
+ expect(exit).toBe(1);
78
+ expect(generateComponentsPipelineMock).not.toHaveBeenCalled();
79
+ const out = stderr.join("\n");
80
+ expect(out).toMatch(/\[promote-skips-environment\]/);
81
+ expect(out).toMatch(/--allow-skip/);
82
+ });
83
+
84
+ test("--promote-to the next step of the order generates as before (#4030)", async () => {
85
+ loadChantConfigUpwardMock.mockResolvedValue({ config: { environments: ["dev", "staging", "prod"] } });
86
+ generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "x", stages: [], jobs: [] });
87
+ vi.spyOn(console, "log").mockImplementation(() => {});
88
+ const exit = await runBuild({ args: makeArgs({ env: "staging", promoteTo: "prod" }), plugins: [], serializers: [] });
89
+ expect(exit).toBe(0);
90
+ expect(generateComponentsPipelineMock.mock.calls[0][2]).toEqual({ env: "staging", promoteTo: "prod" });
91
+ });
92
+
93
+ test("--allow-skip lets the skip through and the promote job passes it on (#4030)", async () => {
94
+ loadChantConfigUpwardMock.mockResolvedValue({ config: { environments: ["dev", "staging", "prod"] } });
95
+ generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "x", stages: [], jobs: [] });
96
+ vi.spyOn(console, "log").mockImplementation(() => {});
97
+ const exit = await runBuild({ args: makeArgs({ env: "dev", promoteTo: "prod", allowSkip: true }), plugins: [], serializers: [] });
98
+ expect(exit).toBe(0);
99
+ expect(generateComponentsPipelineMock.mock.calls[0][2]).toMatchObject({
100
+ promoteCommand: ["chant", "components", "promote", "--from", "dev", "--to", "prod", "--allow-skip"],
101
+ });
102
+ });
103
+
73
104
  test("--promote-to is forwarded to the generator as promoteTo (#2575)", async () => {
74
105
  generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
75
106
  vi.spyOn(console, "log").mockImplementation(() => {});
@@ -81,6 +112,27 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
81
112
  vi.restoreAllMocks();
82
113
  });
83
114
 
115
+ test("--image and each --before-script are forwarded to the generator, in order (#4002)", async () => {
116
+ generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
117
+ vi.spyOn(console, "log").mockImplementation(() => {});
118
+
119
+ const exit = await runBuild({
120
+ args: makeArgs({ generate: "forgejo", prLoop: true, image: "node:22-bookworm", beforeScript: ["npm ci", "npm run setup"] }),
121
+ plugins: [],
122
+ serializers: [],
123
+ });
124
+
125
+ expect(exit).toBe(0);
126
+ expect(generateComponentsPipelineMock).toHaveBeenCalledWith(
127
+ ".",
128
+ "forgejo",
129
+ { env: undefined, prLoop: {}, image: "node:22-bookworm", beforeScript: ["npm ci", "npm run setup"] },
130
+ undefined,
131
+ [],
132
+ );
133
+ vi.restoreAllMocks();
134
+ });
135
+
84
136
  test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
85
137
  loadChantConfigUpwardMock.mockResolvedValue({
86
138
  config: { buildParams: { tier: { type: "string", default: "light" } } },
@@ -7,6 +7,7 @@ import { generateComponentsPipeline } from "../../components/cli-support";
7
7
  import { loadChantConfigUpward, type ChantConfig } from "../../config";
8
8
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
9
9
  import { findWorkspaceRoot } from "../../project-root";
10
+ import { resolvePromotionSource } from "../../components/promote-order";
10
11
 
11
12
  /**
12
13
  * `chant build --components --generate <lexicon>` — generate mode (#563,
@@ -45,6 +46,24 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
45
46
  return 1;
46
47
  }
47
48
 
49
+ // #4030: the generated promote job runs `promote --from <env> --to <promote-to>`,
50
+ // which the member's declared promotion order refuses when it skips a step.
51
+ // Refuse here, with the same code, rather than write a job that fails.
52
+ if (args.promoteTo) {
53
+ // Loaded here, not at the top: a build without --promote-to loads no workspace module (#2526).
54
+ const { promotionOrder } = await import("../../workspace/promotion-order");
55
+ const resolved = resolvePromotionSource({
56
+ order: promotionOrder(config.environments),
57
+ to: args.promoteTo,
58
+ from: args.env ?? "production",
59
+ ...(args.allowSkip ? { allowSkip: true } : {}),
60
+ });
61
+ if ("error" in resolved) {
62
+ console.error(formatError({ message: `${resolved.error.message} [${resolved.error.code}]`, hint: resolved.error.fix }));
63
+ return 1;
64
+ }
65
+ }
66
+
48
67
  // A project inside a workspace member gets that member's pipeline (#2542):
49
68
  // path-filtered, run in the member's directory, written to a file named
50
69
  // after the member and recorded as the member's generated file. The
@@ -62,11 +81,14 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
62
81
  lexicon,
63
82
  env: args.env,
64
83
  promoteTo: args.promoteTo,
84
+ ...(args.allowSkip ? { allowSkip: true } : {}),
65
85
  output: args.output,
66
86
  params: args.param,
67
87
  paramsFile: args.paramsFile,
68
88
  ...(args.waveGate ? { waveGate: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } } : {}),
69
89
  ...(args.prLoop ? { prLoop: { ...(args.gate ? { gate: args.gate } : {}), ...(args.branch ? { branch: args.branch } : {}) } } : {}),
90
+ ...(args.image ? { image: args.image } : {}),
91
+ ...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
70
92
  });
71
93
  } catch (err) {
72
94
  console.error(formatError({ message: memberPipeline.describeError(err) }));
@@ -84,6 +106,10 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
84
106
  {
85
107
  env: args.env,
86
108
  ...(args.promoteTo ? { promoteTo: args.promoteTo } : {}),
109
+ // #4030: the job passes --allow-skip through, so it is not refused again at deploy time.
110
+ ...(args.promoteTo && args.allowSkip
111
+ ? { promoteCommand: ["chant", "components", "promote", "--from", args.env ?? "production", "--to", args.promoteTo, "--allow-skip"] }
112
+ : {}),
87
113
  ...(plan ? { member: plan.member } : {}),
88
114
  // #3049: one job per wave of a gated-wave fan-out instead of one per component.
89
115
  ...(args.waveGate
@@ -93,6 +119,9 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
93
119
  ...(args.prLoop
94
120
  ? { prLoop: { ...(args.gate ? { gate: args.gate } : {}), ...(args.branch ? { branch: args.branch } : {}) } }
95
121
  : {}),
122
+ // #4002: the job image and the lines that install what it lacks, chant itself included.
123
+ ...(args.image ? { image: args.image } : {}),
124
+ ...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
96
125
  },
97
126
  args.sandbox,
98
127
  paramsResolution.provenance,
@@ -1,10 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
2
3
  import { CHANGE_SET_CONTRACT, CHANGE_SET_SCHEMA_ID, type ChangeSetDocument } from "../../change-set";
3
4
  import { GITHUB_COMMENT_LIMIT, groupChangeSet, renderPlanSummaryMarkdown, renderPlanSummaryText } from "../../plan-summary";
4
- import { formatError } from "../format";
5
+ import { formatError, formatWarning } from "../format";
5
6
  import type { CommandContext } from "../registry";
6
7
 
7
8
  const USAGE = "chant change-set summary <change-set.json> [--format text|json|markdown] [--limit <chars>]";
9
+ const SHOW_USAGE = "chant change-set show <digest> [--member <name>] [--json]";
8
10
 
9
11
  /**
10
12
  * `chant change-set summary <file>` (#3188): the grouped plan summary of a
@@ -42,3 +44,49 @@ export async function runChangeSetSummary(ctx: CommandContext): Promise<number>
42
44
  else process.stdout.write(renderPlanSummaryText(summary));
43
45
  return 0;
44
46
  }
47
+
48
+ /**
49
+ * `chant change-set show <digest>` (#4022): the change-set document a gate
50
+ * bound, stored on `chant/lifecycle` under its digest, with each entry's
51
+ * graph node. `--json` prints the read contract's `change-set-show`
52
+ * document (`../../workspace/change-set-show.ts`); without it, the grouped
53
+ * summary and each entry with its node. `--member` reads a workspace
54
+ * member's ledger from anywhere in the checkout. Exit 1 when nothing is
55
+ * stored under the digest.
56
+ */
57
+ export async function runChangeSetShow(ctx: CommandContext): Promise<number> {
58
+ const { args } = ctx;
59
+ const digest = args.extraPositional;
60
+ const { changeSetShowDocument, CHANGE_SET_DIGEST_PATTERN } = await import("../../workspace/change-set-show");
61
+ if (!digest || !CHANGE_SET_DIGEST_PATTERN.test(digest)) {
62
+ console.error(formatError({ message: digest ? `${digest} is not a change-set digest` : "change-set show needs the digest a gate binds", hint: SHOW_USAGE }));
63
+ return 1;
64
+ }
65
+ if ((args.members?.length ?? 0) > 1) {
66
+ console.error(formatError({ message: `change-set show reads one member, not ${args.members!.join(", ")}`, hint: SHOW_USAGE }));
67
+ return 1;
68
+ }
69
+ const member = args.members?.[0]?.split("/").filter(Boolean);
70
+ const doc = await changeSetShowDocument({ cwd: resolve("."), digest, ...(member ? { member } : {}) });
71
+ if (args.json) {
72
+ console.log(JSON.stringify(doc, null, 2));
73
+ return "error" in doc || doc.changeSet === null ? 1 : 0;
74
+ }
75
+ if ("error" in doc) {
76
+ console.error(formatError({ message: doc.error.message, hint: SHOW_USAGE }));
77
+ return 1;
78
+ }
79
+ if (doc.reason) console.error(formatWarning({ message: doc.reason.message }));
80
+ if (doc.changeSet === null) {
81
+ if (!doc.reason) console.error(formatError({ message: `no change set is stored under ${digest}${doc.member ? ` for member ${doc.member}` : ""}`, hint: "A pull-request or release gate stores the change set it binds; a component gate binds a lifecycle plan and stores none." }));
82
+ return 1;
83
+ }
84
+ process.stdout.write(renderPlanSummaryText(groupChangeSet(doc.changeSet)));
85
+ const graph = doc.changeSet.graph;
86
+ console.log(graph ? `\nNodes from the graph of ${graph.member ?? "this project"}${graph.at ? ` at ${graph.at.slice(0, 12)}` : ""}:` : "\nNo graph was read, so no entry has a node.");
87
+ for (const e of doc.changeSet.entries) {
88
+ if (e.action === "no-op" || e.action === "read") continue;
89
+ console.log(` ${e.action.padEnd(8)} ${e.member} ${e.address}${e.deposed ? ` (deposed ${e.deposed})` : ""} ${e.node ?? "(no node)"}`);
90
+ }
91
+ return 0;
92
+ }
@@ -52,6 +52,7 @@ const sealedGates = new Set<string>();
52
52
  const { CapabilityRegistry } = await import("../../components/capability");
53
53
  const { memoryGateLedgerPort } = await import("../../op/gate");
54
54
  const { runComponentsFanOut } = await import("./fan-out");
55
+ const { checkRunEventLines } = await import("../../workspace/__fixtures__/run-event-check");
55
56
 
56
57
  let gateLedger = memoryGateLedgerPort();
57
58
 
@@ -622,3 +623,50 @@ describe("gated waves (#3049): one gate per wave, bound to the wave's set digest
622
623
  expect(stderr()).toContain("--wave runs one wave");
623
624
  });
624
625
  });
626
+
627
+ describe("--events-json (#4041)", () => {
628
+ const lines = () => stdoutLines.filter((l) => l.startsWith('{"$schema"'));
629
+
630
+ test("a fan-out over the set: the gate is a gate phase settled gated, and nothing after it starts", async () => {
631
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", eventsJson: true }))).toBe(3);
632
+ const events = checkRunEventLines(lines());
633
+ expect(events[0]).toMatchObject({ type: "run-start", kind: "component", waves: [["net"], ["cluster-a", "cluster-b"], ["app-one", "app-three", "app-two"]] });
634
+ expect(events.map((e) => e.type)).toEqual(["run-start", "phase-start", "step-start", "gate-waiting", "step-settle", "phase-settle", "run-settle"]);
635
+ expect(events.find((e) => e.type === "gate-waiting")).toMatchObject({ component: null, phase: "gate", step: "gate:release", op: "fan-out", gate: "release" });
636
+ expect(events.at(-1)).toMatchObject({ status: "gated" });
637
+ });
638
+
639
+ test("approved, the gate resolves and every wave and component follows, each step started before it settles", async () => {
640
+ await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release" }));
641
+ const pending = gateLedger.appended[0];
642
+ gateLedger = memoryGateLedgerPort({
643
+ pending: [pending],
644
+ resolutions: [{
645
+ version: 1, kind: "resolution", op: "fan-out", gate: "release", resolvedBy: "operator",
646
+ timestamp: new Date(Date.parse(pending.timestamp) + 1000).toISOString(), planDigest: pending.planDigest,
647
+ }],
648
+ });
649
+ stdoutLines = [];
650
+ failing = ["cluster-b"];
651
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", eventsJson: true }))).toBe(1);
652
+ const events = checkRunEventLines(lines());
653
+ expect(events.find((e) => e.type === "gate-resolved")).toMatchObject({ gate: "release", resolvedBy: "operator" });
654
+ expect(events.filter((e) => e.type === "wave-start").map((e) => (e as { wave: number }).wave)).toEqual([1, 2, 3]);
655
+ expect(events.find((e) => e.type === "component-settle" && e.component === "cluster-b")).toMatchObject({ status: "failed" });
656
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
657
+ });
658
+
659
+ test("--progress-json on a set the gate stops now prints run-start and run-done gated (a level-0 change)", async () => {
660
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", gate: "release", progressJson: true }))).toBe(3);
661
+ const progress = stdoutLines.filter((l) => l.startsWith('{"type"')).map((l) => JSON.parse(l));
662
+ expect(progress).toEqual([
663
+ { type: "run-start", waves: [["net"], ["cluster-a", "cluster-b"], ["app-one", "app-three", "app-two"]] },
664
+ { type: "run-done", status: "gated" },
665
+ ]);
666
+ });
667
+
668
+ test("beside --progress-json it is refused", async () => {
669
+ expect(await runComponentsFanOut(ctx({ base: "main", eventsJson: true, progressJson: true }))).toBe(1);
670
+ expect(stderr()).toContain("--events-json and --progress-json both stream to stdout");
671
+ });
672
+ });
@@ -52,6 +52,7 @@ import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt } from "../..
52
52
  import { describeChangedWave, withWaveRecord, type WaveRecord } from "../../gated-waves";
53
53
  import { renderFanOutHuman, renderFanOutJson, renderFanOutPlan } from "../../components/fan-out-output";
54
54
  import { ndjsonProgressSink } from "../../components/run-progress";
55
+ import { openCliRunEvents, refusesBothStreams } from "../run-events";
55
56
  import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
56
57
  import { approveCommand, gateIsSealed } from "../../op/gate";
57
58
  import { getHeadCommit } from "../../lifecycle/git";
@@ -60,7 +61,7 @@ import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
60
61
  import { formatError, formatInfo, formatWarning } from "../format";
61
62
  import { FAN_OUT_GATE_OP } from "../../op/gate-name";
62
63
  import { resolveBuildRoot } from "./lifecycle";
63
- import { GATED_EXIT_CODE, recordAutoReleasesForRun } from "./run";
64
+ import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run";
64
65
  import type { CommandContext } from "../registry";
65
66
 
66
67
  /**
@@ -172,10 +173,11 @@ export async function resolveChangedUnits(
172
173
  * chant components fan-out --base <ref> | --from-affected <file>
173
174
  * [--head <ref>] [--include-dependents] [--env <env>] [--gate <name>]
174
175
  * [--dry-run] [--json] [--resume <file>] [--seed-outputs <file>]
175
- * [--dump-outputs <file>] [--progress-json]
176
+ * [--dump-outputs <file>] [--progress-json | --events-json]
176
177
  */
177
178
  export async function runComponentsFanOut(ctx: CommandContext): Promise<number> {
178
179
  const { args } = ctx;
180
+ if (refusesBothStreams(args)) return 1;
179
181
  const projectPath = resolve(".");
180
182
  const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
181
183
 
@@ -334,6 +336,11 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
334
336
  writeFanOutAttempt(resumePath, attempt);
335
337
  };
336
338
 
339
+ // The run id the release records carry, chosen before the run so that
340
+ // `--events-json` (#4041) names the run by it too.
341
+ const release = autoReleaseRun(`fan-out-${Date.now()}`);
342
+ const events = await openCliRunEvents(args, { kind: "component", name: `fan-out ${derived.plan.order.join(",")}`, runId: release.runId, cwd: projectPath });
343
+
337
344
  let result;
338
345
  try {
339
346
  result = await runFanOut(derived.plan, derived.components, registry, {
@@ -364,6 +371,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
364
371
  }
365
372
  : {}),
366
373
  ...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
374
+ ...(events ? { onRunEvent: events.emit } : {}),
367
375
  ...(resumePath
368
376
  ? {
369
377
  onComponentSettled: (settled, outputs) => {
@@ -379,6 +387,7 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
379
387
  : {}),
380
388
  });
381
389
  } catch (err) {
390
+ events?.settle("failed", err instanceof Error ? err.message : String(err));
382
391
  // `--wave <n>` reached before an earlier wave applied: nothing ran.
383
392
  if (err instanceof EarlierWaveNotAppliedError) {
384
393
  console.error(formatError({ message: err.message }));
@@ -386,6 +395,8 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
386
395
  }
387
396
  throw err;
388
397
  }
398
+ // The fan-out settles the stream; this only ends one it left open.
399
+ events?.settle(result.status === "fail" ? "failed" : result.status);
389
400
 
390
401
  if (args.dumpOutputs) {
391
402
  const dumpPath = resolve(args.dumpOutputs);
@@ -420,8 +431,9 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
420
431
  await recordAutoReleasesForRun(
421
432
  result.results,
422
433
  args.env ?? "local",
423
- `fan-out-${Date.now()}`,
434
+ release.runId,
424
435
  resolveAutoReleaseDisabled(config, args.noReleaseRecord),
436
+ release.runOrigin,
425
437
  );
426
438
  }
427
439
 
@@ -850,6 +850,43 @@ describe("runApprove — the resolution names the plan it approves (#2300)", ()
850
850
  errSpy.mockRestore();
851
851
  });
852
852
 
853
+ // #4020: the resolution names the commit and run the gate waited on, off the fact it answers.
854
+ test("it copies the standing fact's commit and run onto the resolution", async () => {
855
+ readGateLedgerMock.mockResolvedValue({
856
+ resolutions: [],
857
+ pending: [{
858
+ version: 1, kind: "pending", op: "fountain-apply", gate: "rollout-gate",
859
+ timestamp: "2026-01-01T00:00:00.000Z", expiresAt: "2099-01-01T00:00:00.000Z",
860
+ planDigest: PLAN_A, gitSha: "f".repeat(40), runId: "9001",
861
+ }],
862
+ malformed: 0,
863
+ });
864
+ appendGateResolutionMock.mockResolvedValue({
865
+ commit: "sha",
866
+ record: { version: 1, op: "fountain-apply", gate: "rollout-gate", resolvedBy: "alex", timestamp: "2026-01-01T00:00:00.000Z" },
867
+ });
868
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
869
+
870
+ expect(await runApprove(ctx({ path: "fountain-apply", extraPositional: "rollout-gate", actor: "alex" }))).toBe(0);
871
+ expect(appendGateResolutionMock).toHaveBeenCalledWith(expect.objectContaining({ gitSha: "f".repeat(40), runId: "9001" }));
872
+ errSpy.mockRestore();
873
+ });
874
+
875
+ test("a standing fact from before the commit was recorded gives a resolution without one", async () => {
876
+ seedPending("fountain-apply", "rollout-gate", PLAN_A);
877
+ appendGateResolutionMock.mockResolvedValue({
878
+ commit: "sha",
879
+ record: { version: 1, op: "fountain-apply", gate: "rollout-gate", resolvedBy: "alex", timestamp: "2026-01-01T00:00:00.000Z" },
880
+ });
881
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
882
+
883
+ expect(await runApprove(ctx({ path: "fountain-apply", extraPositional: "rollout-gate", actor: "alex" }))).toBe(0);
884
+ const written = appendGateResolutionMock.mock.calls.at(-1)![0] as Record<string, unknown>;
885
+ expect(written).not.toHaveProperty("gitSha");
886
+ expect(written).not.toHaveProperty("runId");
887
+ errSpy.mockRestore();
888
+ });
889
+
853
890
  test("--plan names one explicitly, and wins over the standing fact's", async () => {
854
891
  const other = `sha256:${"b".repeat(64)}`;
855
892
  seedPending("fountain-apply", "rollout-gate", PLAN_A);