@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
@@ -56,7 +56,9 @@ import type { PendingGateRecord } from "../lifecycle/gate-ledger";
56
56
  import type { CapabilityRegistry, DeployContext } from "./capability";
57
57
  import { isPromotedArtifact } from "./auto-release";
58
58
  import type { ReleaseIdentity } from "../telemetry-attribution";
59
- import type { RunProgressEvent, RunProgressStatus } from "./run-progress";
59
+ import type { RunProgressEvent } from "./run-progress";
60
+ import type { RunEventSettleStatus } from "../run-event";
61
+ import { gateEventsOf, joinRunEventSinks, progressSinkOf, type RunEventSink } from "../run-event-stream";
60
62
 
61
63
  export type { RunProgressEvent } from "./run-progress";
62
64
 
@@ -153,7 +155,16 @@ export interface DriverStepRecord {
153
155
  output?: unknown;
154
156
  error?: string;
155
157
  /** Set on a `gate` step that passed (#2119): who resolved it, when, and at what address. */
156
- approval?: { gate: string; resolvedBy: string; timestamp: string; url?: string };
158
+ approval?: {
159
+ gate: string;
160
+ resolvedBy: string;
161
+ timestamp: string;
162
+ url?: string;
163
+ /** The gate ledger the approval is in, when it isn't the component's own: a release gate's (#4045). */
164
+ op?: string;
165
+ /** Set when the gate passed over an empty change set (#4045) rather than on a person's approval. */
166
+ empty?: true;
167
+ };
157
168
  }
158
169
 
159
170
  export interface DriverComponentResult {
@@ -378,8 +389,8 @@ class GateStop extends Error {
378
389
  }
379
390
  }
380
391
 
381
- /** A result status as the progress stream spells it — the only difference is `fail` vs `failed`, which the two vocabularies have always disagreed on. */
382
- function progressStatus(status: DriverComponentResult["status"]): RunProgressStatus {
392
+ /** A result status as the event stream spells it — the only difference is `fail` vs `failed`, which the two vocabularies have always disagreed on. */
393
+ function progressStatus(status: DriverComponentResult["status"]): RunEventSettleStatus {
383
394
  return status === "fail" ? "failed" : status;
384
395
  }
385
396
 
@@ -395,6 +406,27 @@ export interface GateContext {
395
406
  releases?: ReadonlyMap<string, string>;
396
407
  /** This component's plan digest. Set by {@link runComponentDeploy}, never by a caller. */
397
408
  planDigest?: string;
409
+ /**
410
+ * The release gate binding (#4045): set by a promote under a release.
411
+ * Every gate the run reaches is then decided in `op`'s ledger against the
412
+ * change-set document's digest, the release and the environment, in place
413
+ * of the component's own ledger and composition digest.
414
+ */
415
+ releaseGate?: ReleaseGateBinding;
416
+ }
417
+
418
+ /**
419
+ * What a release gate binds (#4045, ws-104 question 2): the change-set
420
+ * document's digest, the release, and (from the run) the environment.
421
+ */
422
+ export interface ReleaseGateBinding {
423
+ /** The gate ledger the release gate is recorded in, for every component of the promote. */
424
+ op: string;
425
+ /** The digest of the change-set document planned for this release and environment. */
426
+ planDigest: string;
427
+ releaseDigest: string;
428
+ /** Reads the document stored under a digest and says whether it is empty (`evaluateGate`'s `verifyEmpty`). */
429
+ verifyEmpty?: (planDigest: string) => Promise<boolean>;
398
430
  }
399
431
 
400
432
  /** Run a single capability step. Never throws for a capability-run failure; returns a fail record instead. */
@@ -451,14 +483,16 @@ async function runCapabilityStep(
451
483
  * gate is a read of the ledger rather than work, and starting activities a
452
484
  * pending gate is about to strand defeats the point of stopping at it.
453
485
  *
454
- * `onProgress`, when supplied, is called with a `phase-start` event before any
455
- * step runs, a `step` event around each capability invocation (`"running"`
456
- * then `"ok"`/`"failed"`), and a `phase-done` event once the phase settles —
457
- * purely additive observation, never consulted for control flow. Recursion
458
- * into a nested fan-out phase passes the same callback through, so a nested
459
- * phase's events use its own name. Left `undefined` by every caller that
460
- * doesn't opt into `--progress-json` (see ./run-progress.ts), in which case
461
- * every `onProgress?.(...)` call below is a no-op and behavior is unchanged.
486
+ * `emit`, when supplied, gets the phase's run events (#4041): `phase-start`
487
+ * before any step runs, a `step-start` and a `step-settle` around each gate
488
+ * and each capability invocation, the gate's `gate-resolved` or
489
+ * `gate-waiting`, a `skipped` settle with `skip: "not-run"` for each step a
490
+ * failure or a gate stopped, and `phase-settle` once the phase settles. It
491
+ * is observation only, never consulted for control flow. Recursion into a
492
+ * nested fan-out phase passes the same sink through, so a nested phase's
493
+ * events use its own name. `--progress-json` reads the same events through
494
+ * `progressOfRunEvent` (../run-event-stream.ts), which keeps the older
495
+ * stream's shape.
462
496
  */
463
497
  async function runPhase(
464
498
  phaseDef: DriverPhase,
@@ -467,22 +501,34 @@ async function runPhase(
467
501
  phaseOutputs: Record<string, Record<string, unknown>>,
468
502
  componentOutputs: Record<string, Record<string, unknown>>,
469
503
  gates: GateContext,
470
- onProgress?: (event: RunProgressEvent) => void,
504
+ emit?: RunEventSink,
471
505
  ): Promise<{ records: DriverStepRecord[]; executed: ExecutedStep[] }> {
506
+ /** Where a step of this phase sits, for its events. */
507
+ const stepAt = (entry: DriverStep | DriverGate | DriverPhase, step: string) => ({
508
+ component: ctx.component,
509
+ phase: phaseDef.phase,
510
+ step,
511
+ index: phaseDef.steps.indexOf(entry),
512
+ });
472
513
  /** Decide this phase's gates against the ledger, in authored order, before any of its work starts. Throws `GateStop` at the first one nobody has approved. */
473
514
  const decideGates = async (): Promise<DriverStepRecord[]> => {
474
515
  const gateRecords: DriverStepRecord[] = [];
475
516
  for (const gate of phaseDef.steps.filter(isGateStep)) {
476
517
  const start = Date.now();
477
518
  const base = { component: ctx.component, phase: phaseDef.phase, kind: `gate:${gateName(gate)}` };
519
+ const at = stepAt(gate, base.kind);
520
+ emit?.({ type: "step-start", ...at });
478
521
  let check: Awaited<ReturnType<typeof evaluateGate>>;
479
522
  try {
480
523
  check = await evaluateGate(gates.port, {
481
- op: ctx.component,
524
+ op: gates.releaseGate?.op ?? ctx.component,
482
525
  gate: gateName(gate),
483
526
  // #2574: an approval counts only for this environment and this plan.
484
527
  environment: ctx.env,
485
528
  ...(gates.planDigest !== undefined ? { planDigest: gates.planDigest } : {}),
529
+ // #4045: and under a release, only for that release.
530
+ ...(gates.releaseGate ? { releaseDigest: gates.releaseGate.releaseDigest } : {}),
531
+ ...(gates.releaseGate?.verifyEmpty ? { verifyEmpty: gates.releaseGate.verifyEmpty } : {}),
486
532
  ...(gate.description ? { description: gate.description } : {}),
487
533
  ...(gate.timeout ? { timeout: gate.timeout } : {}),
488
534
  ...(gates.now ? { now: gates.now } : {}),
@@ -492,11 +538,22 @@ async function runPhase(
492
538
  // executor's gate step does, and the component unwinds like any failure.
493
539
  if (!(err instanceof GateNotRecordableError)) throw err;
494
540
  gateRecords.push({ ...base, status: "fail" as const, durationMs: Date.now() - start, error: err.message });
541
+ emit?.({ type: "step-settle", ...at, status: "failed", durationMs: Date.now() - start, error: err.message });
495
542
  for (const skipped of phaseDef.steps.filter((s): s is DriverStep | DriverPhase => !isGateStep(s))) {
496
543
  gateRecords.push(skippedRecord(skipped));
497
544
  }
498
545
  throw new StepFailure(gateRecords, []);
499
546
  }
547
+ for (const body of gateEventsOf(
548
+ check,
549
+ { ...at, op: gates.releaseGate?.op ?? ctx.component, gate: gateName(gate) },
550
+ {
551
+ durationMs: Date.now() - start,
552
+ ...(!check.satisfied && check.mismatch ? { message: "an approval stands for this gate, but for another environment or plan" } : {}),
553
+ },
554
+ )) {
555
+ emit?.(body);
556
+ }
500
557
  if (!check.satisfied) {
501
558
  gateRecords.push({ ...base, status: "skipped" as const, durationMs: Date.now() - start });
502
559
  for (const skipped of phaseDef.steps.filter((s): s is DriverStep | DriverPhase => !isGateStep(s))) {
@@ -513,6 +570,8 @@ async function runPhase(
513
570
  resolvedBy: check.resolution.resolvedBy,
514
571
  timestamp: check.resolution.timestamp,
515
572
  ...(check.resolution.url ? { url: check.resolution.url } : {}),
573
+ ...(gates.releaseGate ? { op: gates.releaseGate.op } : {}),
574
+ ...(check.via === "empty" ? { empty: true as const } : {}),
516
575
  },
517
576
  });
518
577
  }
@@ -534,7 +593,7 @@ async function runPhase(
534
593
  }> => {
535
594
  if (isPhaseStep(entry)) {
536
595
  try {
537
- const nested = await runPhase(entry, ctx, registry, phaseOutputs, componentOutputs, gates, onProgress);
596
+ const nested = await runPhase(entry, ctx, registry, phaseOutputs, componentOutputs, gates, emit);
538
597
  return { ...nested, failed: false };
539
598
  } catch (err) {
540
599
  if (err instanceof StepFailure) return { records: err.records, executed: err.executed, failed: true };
@@ -554,7 +613,8 @@ async function runPhase(
554
613
  throw err;
555
614
  }
556
615
  }
557
- onProgress?.({ type: "step", component: ctx.component, phase: phaseDef.phase, step: entry.kind, status: "running" });
616
+ const at = stepAt(entry, entry.kind);
617
+ emit?.({ type: "step-start", ...at });
558
618
  const { record, resolvedInput, output } = await runCapabilityStep(
559
619
  entry,
560
620
  phaseDef.phase,
@@ -563,12 +623,11 @@ async function runPhase(
563
623
  phaseOutputs,
564
624
  componentOutputs,
565
625
  );
566
- onProgress?.({
567
- type: "step",
568
- component: ctx.component,
569
- phase: phaseDef.phase,
570
- step: entry.kind,
626
+ emit?.({
627
+ type: "step-settle",
628
+ ...at,
571
629
  status: record.status === "ok" ? "ok" : "failed",
630
+ durationMs: record.durationMs,
572
631
  ...(record.error !== undefined ? { error: record.error } : {}),
573
632
  });
574
633
  if (record.status === "ok") {
@@ -586,13 +645,12 @@ async function runPhase(
586
645
  };
587
646
  };
588
647
 
589
- const skippedRecord = (entry: DriverStep | DriverPhase): DriverStepRecord => ({
590
- component: ctx.component,
591
- phase: phaseDef.phase,
592
- kind: isPhaseStep(entry) ? entry.phase : (entry as DriverStep).kind,
593
- status: "skipped",
594
- durationMs: 0,
595
- });
648
+ /** A step a failure or a gate stopped before it started: recorded, and settled `skipped` with `skip: "not-run"`. */
649
+ const skippedRecord = (entry: DriverStep | DriverPhase): DriverStepRecord => {
650
+ const kind = isPhaseStep(entry) ? entry.phase : (entry as DriverStep).kind;
651
+ emit?.({ type: "step-settle", ...stepAt(entry, kind), status: "skipped", skip: "not-run", durationMs: null });
652
+ return { component: ctx.component, phase: phaseDef.phase, kind, status: "skipped", durationMs: 0 };
653
+ };
596
654
 
597
655
  const runEntries = async (): Promise<{ records: DriverStepRecord[]; executed: ExecutedStep[] }> => {
598
656
  const gateRecords = await decideGates();
@@ -626,16 +684,16 @@ async function runPhase(
626
684
  return { records, executed };
627
685
  };
628
686
 
629
- onProgress?.({ type: "phase-start", component: ctx.component, phase: phaseDef.phase });
687
+ emit?.({ type: "phase-start", component: ctx.component, phase: phaseDef.phase });
630
688
  try {
631
689
  const result = await runEntries();
632
- onProgress?.({ type: "phase-done", component: ctx.component, phase: phaseDef.phase, status: "ok" });
690
+ emit?.({ type: "phase-settle", component: ctx.component, phase: phaseDef.phase, status: "ok" });
633
691
  return result;
634
692
  } catch (err) {
635
693
  if (err instanceof GateStop) {
636
- onProgress?.({ type: "phase-done", component: ctx.component, phase: phaseDef.phase, status: "gated" });
694
+ emit?.({ type: "phase-settle", component: ctx.component, phase: phaseDef.phase, status: "gated" });
637
695
  } else if (err instanceof StepFailure) {
638
- onProgress?.({ type: "phase-done", component: ctx.component, phase: phaseDef.phase, status: "failed" });
696
+ emit?.({ type: "phase-settle", component: ctx.component, phase: phaseDef.phase, status: "failed" });
639
697
  }
640
698
  throw err;
641
699
  }
@@ -710,14 +768,14 @@ async function rollbackExecuted(
710
768
  * any) are recorded into `componentOutputs` under its own name so downstream
711
769
  * components can reference `@<name>.publish.*`.
712
770
  *
713
- * `onProgress`, when supplied, is forwarded to every `runPhase` call (both the
714
- * forward `deploy` phases and, on failure, the component's own authored
715
- * `rollback` phases) so a `--progress-json` consumer sees `phase-start`/
716
- * `step`/`phase-done` events for whichever phases actually ran. The saga
717
- * unwind step-by-step compensation (`rollbackExecuted` below) is not part of
718
- * the `RunProgressEvent` contract and stays silent — it isn't a `deploy`
719
- * phase, and its record statuses (`rolled-back`/`rollback-opted-out`) don't
720
- * map onto the `step` event's `running`/`ok`/`failed` shape.
771
+ * `onProgress` (the older `RunProgressEvent` stream) and `onRunEvent` (run
772
+ * events, #4041), when supplied, are forwarded to every `runPhase` call (both
773
+ * the forward `deploy` phases and, on failure, the component's own authored
774
+ * `rollback` phases), so a consumer sees the phase and step events of
775
+ * whichever phases actually ran. The saga unwind step-by-step compensation
776
+ * (`rollbackExecuted` below) stays silent: it isn't a `deploy` phase, and
777
+ * its record statuses (`rolled-back`/`rollback-opted-out`) are not step
778
+ * statuses either stream has.
721
779
  */
722
780
  export async function runComponentDeploy(
723
781
  component: DriverComponent,
@@ -726,7 +784,9 @@ export async function runComponentDeploy(
726
784
  componentOutputs: Record<string, Record<string, unknown>>,
727
785
  onProgress?: (event: RunProgressEvent) => void,
728
786
  gateContext: GateContext = { port: gitGateLedgerPort() },
787
+ onRunEvent?: RunEventSink,
729
788
  ): Promise<DriverComponentResult> {
789
+ const emit = joinRunEventSinks(onRunEvent, progressSinkOf(onProgress));
730
790
  // Each component tracks its own release: a digest one component promoted never stamps another's deploy.
731
791
  if (ctx.release) ctx = { ...ctx, release: { ...ctx.release } };
732
792
  const phaseOutputs: Record<string, Record<string, unknown>> = {};
@@ -738,19 +798,22 @@ export async function runComponentDeploy(
738
798
  if (ctx.release && release !== undefined && ctx.release.version === undefined) ctx.release.version = release;
739
799
  const gates: GateContext = {
740
800
  ...gateContext,
741
- planDigest: componentPlanDigest({
742
- environment: ctx.env,
743
- component,
744
- ...(ctx.vars ? { vars: ctx.vars } : {}),
745
- ...(release !== undefined ? { release } : {}),
746
- }),
801
+ // #4045: a release gate binds the release's change set, not this composition.
802
+ planDigest:
803
+ gateContext.releaseGate?.planDigest ??
804
+ componentPlanDigest({
805
+ environment: ctx.env,
806
+ component,
807
+ ...(ctx.vars ? { vars: ctx.vars } : {}),
808
+ ...(release !== undefined ? { release } : {}),
809
+ }),
747
810
  };
748
811
  const records: DriverStepRecord[] = [];
749
812
  const allExecuted: ExecutedStep[] = [];
750
813
 
751
814
  try {
752
815
  for (const phaseDef of component.deploy) {
753
- const result = await runPhase(phaseDef, ctx, registry, phaseOutputs, componentOutputs, gates, onProgress);
816
+ const result = await runPhase(phaseDef, ctx, registry, phaseOutputs, componentOutputs, gates, emit);
754
817
  records.push(...result.records);
755
818
  allExecuted.push(...result.executed);
756
819
  }
@@ -784,7 +847,7 @@ export async function runComponentDeploy(
784
847
 
785
848
  for (const phaseDef of [...(component.rollback ?? [])].reverse()) {
786
849
  try {
787
- const result = await runPhase(phaseDef, ctx, registry, phaseOutputs, componentOutputs, gates, onProgress);
850
+ const result = await runPhase(phaseDef, ctx, registry, phaseOutputs, componentOutputs, gates, emit);
788
851
  records.push(...result.records);
789
852
  } catch (compErr) {
790
853
  if (compErr instanceof StepFailure || compErr instanceof GateStop) records.push(...compErr.records);
@@ -919,12 +982,20 @@ export interface InterpretRunOptions {
919
982
  * byte-for-byte the same as before this option existed.
920
983
  */
921
984
  onProgress?: (event: RunProgressEvent) => void;
985
+ /**
986
+ * Where the run's events go (#4041, `chant run --components --events-json`):
987
+ * the run-event bodies the stream in ../run-event-stream.ts stamps. Like
988
+ * `onProgress`, never consulted for control flow.
989
+ */
990
+ onRunEvent?: RunEventSink;
922
991
  /** Where a `gate` step's facts are read and written (#2119). Defaults to the `chant/lifecycle` orphan branch; a test passes `memoryGateLedgerPort()` from `../op/gate.ts`. */
923
992
  gates?: GateLedgerPort;
924
993
  /** ISO-8601 "now" for gate decisions, so a test is deterministic. */
925
994
  now?: string;
926
995
  /** The recorded digest a promote or rollback deploys, per component (#2574). Part of the plan a gate approval is bound to. */
927
996
  releases?: ReadonlyMap<string, string>;
997
+ /** A promote under a release (#4045): what every gate the run reaches binds. See {@link GateContext.releaseGate}. */
998
+ releaseGate?: ReleaseGateBinding;
928
999
  /**
929
1000
  * The release each component deploys (#3061, ws-081), threaded into its
930
1001
  * `DeployContext.release`. The driver fills in `version` when a publish
@@ -969,14 +1040,15 @@ export async function runInterpretDriver(
969
1040
  const { order, waves } = resolveComponentGraph(components);
970
1041
  const byName = new Map(components.map((c) => [c.name, c]));
971
1042
  const componentOutputs: Record<string, Record<string, unknown>> = { ...(options.componentOutputs ?? {}) };
972
- const { onProgress } = options;
1043
+ const emit = joinRunEventSinks(options.onRunEvent, progressSinkOf(options.onProgress));
973
1044
  const gates: GateContext = {
974
1045
  port: options.gates ?? gitGateLedgerPort(),
975
1046
  ...(options.now ? { now: options.now } : {}),
976
1047
  ...(options.releases ? { releases: options.releases } : {}),
1048
+ ...(options.releaseGate ? { releaseGate: options.releaseGate } : {}),
977
1049
  };
978
1050
 
979
- onProgress?.({ type: "run-start", waves });
1051
+ emit?.({ type: "run-start", waves });
980
1052
 
981
1053
  const results: DriverComponentResult[] = [];
982
1054
  let failedComponent: string | undefined;
@@ -984,26 +1056,27 @@ export async function runInterpretDriver(
984
1056
 
985
1057
  waveLoop: for (const [waveIndex, wave] of waves.entries()) {
986
1058
  const waveNum = waveIndex + 1;
987
- onProgress?.({ type: "wave-start", wave: waveNum, components: wave });
1059
+ emit?.({ type: "wave-start", wave: waveNum, components: wave });
988
1060
  const waveComponents = wave.map((name) => byName.get(name)!);
989
1061
  const waveResults = await Promise.all(
990
1062
  waveComponents.map(async (component) => {
991
- onProgress?.({ type: "component-start", wave: waveNum, component: component.name });
1063
+ emit?.({ type: "component-start", wave: waveNum, component: component.name });
992
1064
  const result = await runComponentDeploy(
993
1065
  component,
994
1066
  deployContext(options, component.name),
995
1067
  registry,
996
1068
  componentOutputs,
997
- onProgress,
1069
+ undefined,
998
1070
  gates,
1071
+ emit,
999
1072
  );
1000
- onProgress?.({ type: "component-done", wave: waveNum, component: component.name, status: progressStatus(result.status) });
1073
+ emit?.({ type: "component-settle", wave: waveNum, component: component.name, status: progressStatus(result.status) });
1001
1074
  return result;
1002
1075
  }),
1003
1076
  );
1004
1077
  results.push(...waveResults);
1005
1078
  const stopped = waveResults.find((r) => r.status !== "ok");
1006
- onProgress?.({ type: "wave-done", wave: waveNum, status: progressStatus(stopped?.status ?? "ok") });
1079
+ emit?.({ type: "wave-settle", wave: waveNum, status: progressStatus(stopped?.status ?? "ok") });
1007
1080
  if (stopped) {
1008
1081
  // A failure in the same wave outranks a gate: something is actually
1009
1082
  // broken, and reporting the run as "waiting on a human" would hide it.
@@ -1016,7 +1089,7 @@ export async function runInterpretDriver(
1016
1089
 
1017
1090
  const status: DriverRunResult["status"] = failedComponent ? "fail" : gated ? "gated" : "ok";
1018
1091
  const ok = status === "ok";
1019
- onProgress?.({ type: "run-done", status: progressStatus(status) });
1092
+ emit?.({ type: "run-settle", status: progressStatus(status), durationMs: null });
1020
1093
  const result: DriverRunResult = {
1021
1094
  order, waves, results, ok, status, failedComponent, componentOutputs,
1022
1095
  ...(gated
@@ -49,12 +49,18 @@ import type { ReleaseIdentity } from "../telemetry-attribution";
49
49
  import { downstreamWithin, remainingFanOut, type FanOutPlan, type FanOutProgress, type FanOutSkip } from "./fan-out";
50
50
  import { evaluateGate, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "../op/gate";
51
51
  import type { RunProgressEvent } from "./run-progress";
52
+ import { emitGateStep, joinRunEventSinks, progressSinkOf, type RunEventSink } from "../run-event-stream";
52
53
  import type { CapabilityRegistry } from "./capability";
53
54
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
54
55
  import type { DeployContext } from "./capability";
55
56
  import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
56
57
  import { decideWaveGate, runWaveLoop, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
57
58
 
59
+ /** The sink a fan-out emits to: run events, and the older stream through them. */
60
+ function fanOutSink(options: FanOutRunOptions): RunEventSink | undefined {
61
+ return joinRunEventSinks(options.onRunEvent, progressSinkOf(options.onProgress));
62
+ }
63
+
58
64
  /**
59
65
  * One gate per wave (#3049), each bound to its wave's set digest. The other
60
66
  * mode beside {@link FanOutGate}: that one approves the whole set once, this
@@ -115,6 +121,12 @@ export interface FanOutRunOptions {
115
121
  /** Outputs of components this run is not running: the ones the plan lists in `seeds`. */
116
122
  componentOutputs?: Record<string, Record<string, unknown>>;
117
123
  onProgress?: (event: RunProgressEvent) => void;
124
+ /**
125
+ * Where the run's events go (#4041, `--events-json`). A gate over the set
126
+ * or over a wave is a `gate:<name>` step of a `gate` phase with no
127
+ * component, settled before the components it lets through start.
128
+ */
129
+ onRunEvent?: RunEventSink;
118
130
  gates?: GateLedgerPort;
119
131
  now?: string;
120
132
  /** One approval over the ordered set, bound to `plan.digest`. */
@@ -206,18 +218,25 @@ export async function runFanOut(
206
218
  return runGatedWaves(plan, active, components, registry, options, options.waveGate, gates, componentOutputs);
207
219
  }
208
220
 
221
+ const emit = fanOutSink(options);
222
+ emit?.({ type: "run-start", waves: active.waves });
223
+
209
224
  if (options.gate) {
210
- const check = await evaluateGate(gates.port, {
211
- op: options.gate.op,
212
- gate: options.gate.gate,
213
- planDigest: active.digest,
214
- ...(options.gate.description ? { description: options.gate.description } : {}),
215
- ...(options.gate.timeout ? { timeout: options.gate.timeout } : {}),
216
- ...(options.now ? { now: options.now } : {}),
217
- });
225
+ const gate = options.gate;
226
+ const check = await emitGateStep(emit, gate, () =>
227
+ evaluateGate(gates.port, {
228
+ op: gate.op,
229
+ gate: gate.gate,
230
+ planDigest: active.digest,
231
+ ...(gate.description ? { description: gate.description } : {}),
232
+ ...(gate.timeout ? { timeout: gate.timeout } : {}),
233
+ ...(options.now ? { now: options.now } : {}),
234
+ }),
235
+ );
218
236
  if (!check.satisfied) {
219
237
  // Nothing ran, so nothing is half-applied and there is nothing to
220
238
  // compensate. A gate is a fact, not a failure.
239
+ emit?.({ type: "run-settle", status: "gated", durationMs: null });
221
240
  return {
222
241
  plan: active,
223
242
  status: "gated",
@@ -246,29 +265,28 @@ export async function runFanOut(
246
265
  }
247
266
  };
248
267
 
249
- options.onProgress?.({ type: "run-start", waves: active.waves });
250
-
251
268
  for (const [waveIndex, wave] of active.waves.entries()) {
252
269
  const waveNum = waveIndex + 1;
253
270
  // A component whose upstream failed in an earlier wave is not dispatched.
254
271
  const runnable = wave.filter((name) => inPlan.has(name) && !blockedBy.has(name));
255
272
  if (runnable.length === 0) continue;
256
273
 
257
- options.onProgress?.({ type: "wave-start", wave: waveNum, components: runnable });
274
+ emit?.({ type: "wave-start", wave: waveNum, components: runnable });
258
275
  const waveResults = await Promise.all(
259
276
  runnable.map(async (name) => {
260
- options.onProgress?.({ type: "component-start", wave: waveNum, component: name });
277
+ emit?.({ type: "component-start", wave: waveNum, component: name });
261
278
  const planned = options.plans?.[name];
262
279
  const result = await runComponentDeploy(
263
280
  byName.get(name)!,
264
281
  { ...fanOutContext(options, name), ...(planned && Object.keys(planned).length > 0 ? { plans: planned } : {}) },
265
282
  registry,
266
283
  componentOutputs,
267
- options.onProgress,
284
+ undefined,
268
285
  gates,
286
+ emit,
269
287
  );
270
- options.onProgress?.({
271
- type: "component-done",
288
+ emit?.({
289
+ type: "component-settle",
272
290
  wave: waveNum,
273
291
  component: name,
274
292
  status: result.status === "fail" ? "failed" : result.status,
@@ -297,8 +315,8 @@ export async function runFanOut(
297
315
  }
298
316
 
299
317
  const waveFailed = waveResults.some((r) => r.status === "fail");
300
- options.onProgress?.({
301
- type: "wave-done",
318
+ emit?.({
319
+ type: "wave-settle",
302
320
  wave: waveNum,
303
321
  status: waveFailed ? "failed" : waveResults.some((r) => r.status === "gated") ? "gated" : "ok",
304
322
  });
@@ -311,7 +329,7 @@ export async function runFanOut(
311
329
  // A failure outranks a gate: something is actually broken, and reporting the
312
330
  // run as "waiting on a human" would hide it. Same precedence the driver uses.
313
331
  const status: FanOutRunResult["status"] = failed.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
314
- options.onProgress?.({ type: "run-done", status: status === "fail" ? "failed" : status });
332
+ emit?.({ type: "run-settle", status: status === "fail" ? "failed" : status, durationMs: null });
315
333
 
316
334
  return {
317
335
  plan: active,
@@ -391,7 +409,8 @@ async function runGatedWaves(
391
409
  }
392
410
  };
393
411
 
394
- options.onProgress?.({ type: "run-start", waves: plan.waves });
412
+ const emit = fanOutSink(options);
413
+ emit?.({ type: "run-start", waves: plan.waves });
395
414
 
396
415
  /** What planning one wave gave. */
397
416
  interface Planned {
@@ -477,14 +496,16 @@ async function runGatedWaves(
477
496
  return { end: "next" };
478
497
  }
479
498
 
480
- const check = await decideWaveGate(gates.port, {
481
- op: waveGate.op,
482
- gate: gateName,
483
- digest,
484
- description: waveGate.description ?? `wave ${waveNum} of ${plan.waves.length}: ${toRun.join(", ")}`,
485
- ...(waveGate.timeout ? { timeout: waveGate.timeout } : {}),
486
- ...(options.now ? { now: options.now } : {}),
487
- });
499
+ const check = await emitGateStep(emit, { op: waveGate.op, gate: gateName }, () =>
500
+ decideWaveGate(gates.port, {
501
+ op: waveGate.op,
502
+ gate: gateName,
503
+ digest,
504
+ description: waveGate.description ?? `wave ${waveNum} of ${plan.waves.length}: ${toRun.join(", ")}`,
505
+ ...(waveGate.timeout ? { timeout: waveGate.timeout } : {}),
506
+ ...(options.now ? { now: options.now } : {}),
507
+ }),
508
+ );
488
509
  if (!check.satisfied) {
489
510
  // Nothing in this wave ran, and no later wave is planned: its plan
490
511
  // would read outputs this wave has not written.
@@ -502,15 +523,15 @@ async function runGatedWaves(
502
523
  },
503
524
 
504
525
  async apply({ k: waveNum }, { contexts, plansFor, waveFailures }, { check, record, toRun }) {
505
- options.onProgress?.({ type: "wave-start", wave: waveNum, components: toRun });
526
+ emit?.({ type: "wave-start", wave: waveNum, components: toRun });
506
527
  const waveResults = await Promise.all(
507
528
  toRun.map(async (name) => {
508
- options.onProgress?.({ type: "component-start", wave: waveNum, component: name });
529
+ emit?.({ type: "component-start", wave: waveNum, component: name });
509
530
  const plans = plansFor.get(name)!;
510
531
  const ctx = { ...contexts.get(name)!, ...(Object.keys(plans).length > 0 ? { plans } : {}) };
511
- const result = await runComponentDeploy(byName.get(name)!, ctx, registry, componentOutputs, options.onProgress, gates);
512
- options.onProgress?.({
513
- type: "component-done",
532
+ const result = await runComponentDeploy(byName.get(name)!, ctx, registry, componentOutputs, undefined, gates, emit);
533
+ emit?.({
534
+ type: "component-settle",
514
535
  wave: waveNum,
515
536
  component: name,
516
537
  status: result.status === "fail" ? "failed" : result.status,
@@ -543,8 +564,8 @@ async function runGatedWaves(
543
564
  };
544
565
  waves.push(settled);
545
566
  options.onWaveSettled?.(settled);
546
- options.onProgress?.({
547
- type: "wave-done",
567
+ emit?.({
568
+ type: "wave-settle",
548
569
  wave: waveNum,
549
570
  status: waveFailures.length > 0 ? "failed" : waveResults.some((r) => r.status === "gated") ? "gated" : "ok",
550
571
  });
@@ -557,7 +578,7 @@ async function runGatedWaves(
557
578
  .map(([component, by]) => ({ component, reason: "blocked" as const, blockedBy: by }));
558
579
 
559
580
  const status: FanOutRunResult["status"] = failed.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
560
- options.onProgress?.({ type: "run-done", status: status === "fail" ? "failed" : status });
581
+ emit?.({ type: "run-settle", status: status === "fail" ? "failed" : status, durationMs: null });
561
582
 
562
583
  return {
563
584
  // The derivation's waves, not the narrowed re-layering: their numbers name the gates.