@intentius/chant 0.124.27 → 0.124.29

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 (681) 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/build-params.d.ts +34 -1
  20. package/dist/build-params.d.ts.map +1 -1
  21. package/dist/build-params.js +31 -1
  22. package/dist/change-set-graph.d.ts +63 -0
  23. package/dist/change-set-graph.d.ts.map +1 -0
  24. package/dist/change-set-graph.js +130 -0
  25. package/dist/change-set.d.ts +62 -3
  26. package/dist/change-set.d.ts.map +1 -1
  27. package/dist/change-set.js +36 -4
  28. package/dist/chant-version.d.ts +9 -0
  29. package/dist/chant-version.d.ts.map +1 -0
  30. package/dist/chant-version.js +16 -0
  31. package/dist/cli/build-params-cli.d.ts +7 -1
  32. package/dist/cli/build-params-cli.d.ts.map +1 -1
  33. package/dist/cli/build-params-cli.js +17 -8
  34. package/dist/cli/commands/build.d.ts +8 -0
  35. package/dist/cli/commands/build.d.ts.map +1 -1
  36. package/dist/cli/commands/build.js +15 -11
  37. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  38. package/dist/cli/commands/check-lexicon-docs.js +30 -2
  39. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  40. package/dist/cli/commands/check-lexicon.js +13 -2
  41. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
  42. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  43. package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
  44. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  45. package/dist/cli/commands/init-lexicon.js +17 -8
  46. package/dist/cli/handlers/approve.d.ts.map +1 -1
  47. package/dist/cli/handlers/approve.js +64 -4
  48. package/dist/cli/handlers/build.d.ts.map +1 -1
  49. package/dist/cli/handlers/build.js +43 -4
  50. package/dist/cli/handlers/change-set.d.ts +10 -0
  51. package/dist/cli/handlers/change-set.d.ts.map +1 -1
  52. package/dist/cli/handlers/change-set.js +51 -1
  53. package/dist/cli/handlers/fan-out.d.ts +10 -1
  54. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  55. package/dist/cli/handlers/fan-out.js +54 -6
  56. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  57. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  58. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  59. package/dist/cli/handlers/pr.d.ts +16 -2
  60. package/dist/cli/handlers/pr.d.ts.map +1 -1
  61. package/dist/cli/handlers/pr.js +269 -37
  62. package/dist/cli/handlers/promote.d.ts +7 -0
  63. package/dist/cli/handlers/promote.d.ts.map +1 -1
  64. package/dist/cli/handlers/promote.js +440 -28
  65. package/dist/cli/handlers/release.d.ts +91 -0
  66. package/dist/cli/handlers/release.d.ts.map +1 -0
  67. package/dist/cli/handlers/release.js +589 -0
  68. package/dist/cli/handlers/run.d.ts +38 -2
  69. package/dist/cli/handlers/run.d.ts.map +1 -1
  70. package/dist/cli/handlers/run.js +234 -16
  71. package/dist/cli/main.d.ts.map +1 -1
  72. package/dist/cli/main.js +143 -17
  73. package/dist/cli/mcp/workspace-tools.js +1 -1
  74. package/dist/cli/parse-args.d.ts.map +1 -1
  75. package/dist/cli/parse-args.js +73 -5
  76. package/dist/cli/registry.d.ts +43 -5
  77. package/dist/cli/registry.d.ts.map +1 -1
  78. package/dist/cli/run-events.d.ts +30 -0
  79. package/dist/cli/run-events.d.ts.map +1 -0
  80. package/dist/cli/run-events.js +49 -0
  81. package/dist/cli/version.d.ts +3 -5
  82. package/dist/cli/version.d.ts.map +1 -1
  83. package/dist/cli/version.js +3 -13
  84. package/dist/codegen/docs-in-site.d.ts +71 -0
  85. package/dist/codegen/docs-in-site.d.ts.map +1 -0
  86. package/dist/codegen/docs-in-site.js +177 -0
  87. package/dist/codegen/docs-types.d.ts +14 -1
  88. package/dist/codegen/docs-types.d.ts.map +1 -1
  89. package/dist/codegen/docs.d.ts +18 -1
  90. package/dist/codegen/docs.d.ts.map +1 -1
  91. package/dist/codegen/docs.js +40 -7
  92. package/dist/codegen/rehype-base-url.mjs +15 -9
  93. package/dist/components/auto-release.d.ts +6 -1
  94. package/dist/components/auto-release.d.ts.map +1 -1
  95. package/dist/components/auto-release.js +3 -0
  96. package/dist/components/capability.d.ts +9 -0
  97. package/dist/components/capability.d.ts.map +1 -1
  98. package/dist/components/cli-support.d.ts +6 -0
  99. package/dist/components/cli-support.d.ts.map +1 -1
  100. package/dist/components/cli-support.js +10 -9
  101. package/dist/components/driver.d.ts +50 -9
  102. package/dist/components/driver.d.ts.map +1 -1
  103. package/dist/components/driver.js +116 -54
  104. package/dist/components/fan-out-inputs.d.ts +69 -0
  105. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  106. package/dist/components/fan-out-inputs.js +169 -0
  107. package/dist/components/fan-out-record.d.ts +13 -0
  108. package/dist/components/fan-out-record.d.ts.map +1 -1
  109. package/dist/components/fan-out-record.js +16 -0
  110. package/dist/components/fan-out-run.d.ts +7 -0
  111. package/dist/components/fan-out-run.d.ts.map +1 -1
  112. package/dist/components/fan-out-run.js +85 -26
  113. package/dist/components/fan-out.d.ts +4 -0
  114. package/dist/components/fan-out.d.ts.map +1 -1
  115. package/dist/components/pr-pipeline.d.ts +25 -0
  116. package/dist/components/pr-pipeline.d.ts.map +1 -1
  117. package/dist/components/pr-pipeline.js +22 -4
  118. package/dist/components/pr-run.d.ts +11 -0
  119. package/dist/components/pr-run.d.ts.map +1 -1
  120. package/dist/components/pr-run.js +20 -1
  121. package/dist/components/promote-order.d.ts +38 -0
  122. package/dist/components/promote-order.d.ts.map +1 -0
  123. package/dist/components/promote-order.js +62 -0
  124. package/dist/components/promote.d.ts +38 -2
  125. package/dist/components/promote.d.ts.map +1 -1
  126. package/dist/components/promote.js +77 -2
  127. package/dist/components/release-compile.d.ts +142 -0
  128. package/dist/components/release-compile.d.ts.map +1 -0
  129. package/dist/components/release-compile.js +382 -0
  130. package/dist/components/release-gate.d.ts +135 -0
  131. package/dist/components/release-gate.d.ts.map +1 -0
  132. package/dist/components/release-gate.js +288 -0
  133. package/dist/components/release-promote.d.ts +96 -0
  134. package/dist/components/release-promote.d.ts.map +1 -0
  135. package/dist/components/release-promote.js +204 -0
  136. package/dist/components/run-progress.d.ts +7 -0
  137. package/dist/components/run-progress.d.ts.map +1 -1
  138. package/dist/components/wave-plan.d.ts +8 -1
  139. package/dist/components/wave-plan.d.ts.map +1 -1
  140. package/dist/components/wave-plan.js +11 -3
  141. package/dist/config.d.ts +45 -0
  142. package/dist/config.d.ts.map +1 -1
  143. package/dist/config.js +3 -0
  144. package/dist/flag-params.d.ts +54 -0
  145. package/dist/flag-params.d.ts.map +1 -0
  146. package/dist/flag-params.js +130 -0
  147. package/dist/index.d.ts +1 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +2 -0
  150. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  151. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  152. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  153. package/dist/lifecycle/change-set-store.d.ts +68 -0
  154. package/dist/lifecycle/change-set-store.d.ts.map +1 -0
  155. package/dist/lifecycle/change-set-store.js +97 -0
  156. package/dist/lifecycle/commit-sha.d.ts +13 -0
  157. package/dist/lifecycle/commit-sha.d.ts.map +1 -0
  158. package/dist/lifecycle/commit-sha.js +21 -0
  159. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  160. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  161. package/dist/lifecycle/delivery-metrics.js +145 -0
  162. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  163. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  164. package/dist/lifecycle/deploy-outcome.js +101 -0
  165. package/dist/lifecycle/gate-change-set.d.ts +78 -0
  166. package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
  167. package/dist/lifecycle/gate-change-set.js +138 -0
  168. package/dist/lifecycle/gate-ledger.d.ts +51 -4
  169. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  170. package/dist/lifecycle/gate-ledger.js +18 -0
  171. package/dist/lifecycle/git.d.ts +30 -1
  172. package/dist/lifecycle/git.d.ts.map +1 -1
  173. package/dist/lifecycle/git.js +31 -5
  174. package/dist/lifecycle/index.d.ts +4 -0
  175. package/dist/lifecycle/index.d.ts.map +1 -1
  176. package/dist/lifecycle/index.js +4 -0
  177. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  178. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  179. package/dist/lifecycle/mixed-environment.js +154 -0
  180. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  181. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  182. package/dist/lifecycle/publish-attachments.js +179 -0
  183. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  184. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  185. package/dist/lifecycle/publish-credentials.js +69 -0
  186. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  187. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  188. package/dist/lifecycle/publish-declaration.js +83 -0
  189. package/dist/lifecycle/publish-forge.d.ts +53 -0
  190. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  191. package/dist/lifecycle/publish-forge.js +298 -0
  192. package/dist/lifecycle/publish-npm.d.ts +61 -0
  193. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  194. package/dist/lifecycle/publish-npm.js +242 -0
  195. package/dist/lifecycle/publish-oci.d.ts +64 -0
  196. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  197. package/dist/lifecycle/publish-oci.js +383 -0
  198. package/dist/lifecycle/publish-record.d.ts +66 -0
  199. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  200. package/dist/lifecycle/publish-record.js +84 -0
  201. package/dist/lifecycle/publish-targets.d.ts +45 -0
  202. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  203. package/dist/lifecycle/publish-targets.js +85 -0
  204. package/dist/lifecycle/release-cut.d.ts +154 -0
  205. package/dist/lifecycle/release-cut.d.ts.map +1 -0
  206. package/dist/lifecycle/release-cut.js +366 -0
  207. package/dist/lifecycle/release-join.d.ts +95 -0
  208. package/dist/lifecycle/release-join.d.ts.map +1 -0
  209. package/dist/lifecycle/release-join.js +139 -0
  210. package/dist/lifecycle/release-ledger.d.ts +54 -0
  211. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  212. package/dist/lifecycle/release-ledger.js +53 -20
  213. package/dist/lifecycle/release-publish.d.ts +312 -0
  214. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  215. package/dist/lifecycle/release-publish.js +335 -0
  216. package/dist/lifecycle/release-revocation.d.ts +80 -0
  217. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  218. package/dist/lifecycle/release-revocation.js +123 -0
  219. package/dist/lifecycle/release-store.d.ts +200 -0
  220. package/dist/lifecycle/release-store.d.ts.map +1 -0
  221. package/dist/lifecycle/release-store.js +320 -0
  222. package/dist/lifecycle/release-verify.d.ts +61 -0
  223. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  224. package/dist/lifecycle/release-verify.js +92 -0
  225. package/dist/lifecycle/release-workstation.d.ts +91 -0
  226. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  227. package/dist/lifecycle/release-workstation.js +180 -0
  228. package/dist/lifecycle/retention-policy.d.ts +45 -0
  229. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  230. package/dist/lifecycle/retention-policy.js +43 -0
  231. package/dist/lifecycle/retention.d.ts +185 -0
  232. package/dist/lifecycle/retention.d.ts.map +1 -0
  233. package/dist/lifecycle/retention.js +400 -0
  234. package/dist/lifecycle/run-ledger.d.ts +4 -0
  235. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  236. package/dist/lifecycle/run-ledger.js +2 -0
  237. package/dist/op/activities/change-coverage.d.ts.map +1 -1
  238. package/dist/op/activities/change-coverage.js +4 -2
  239. package/dist/op/activities/source-release.d.ts.map +1 -1
  240. package/dist/op/activities/source-release.js +7 -3
  241. package/dist/op/activities/source-rollback.d.ts.map +1 -1
  242. package/dist/op/activities/source-rollback.js +7 -3
  243. package/dist/op/gate-name.d.ts +11 -2
  244. package/dist/op/gate-name.d.ts.map +1 -1
  245. package/dist/op/gate-name.js +12 -3
  246. package/dist/op/gate-run-locator.d.ts +22 -0
  247. package/dist/op/gate-run-locator.d.ts.map +1 -1
  248. package/dist/op/gate-run-locator.js +31 -0
  249. package/dist/op/gate.d.ts +27 -3
  250. package/dist/op/gate.d.ts.map +1 -1
  251. package/dist/op/gate.js +57 -3
  252. package/dist/op/local-executor.d.ts +36 -1
  253. package/dist/op/local-executor.d.ts.map +1 -1
  254. package/dist/op/local-executor.js +158 -52
  255. package/dist/op/op-waves-run.d.ts.map +1 -1
  256. package/dist/op/op-waves-run.js +4 -0
  257. package/dist/op/run-live.d.ts +2 -0
  258. package/dist/op/run-live.d.ts.map +1 -1
  259. package/dist/op/run-live.js +1 -1
  260. package/dist/op/runtime.d.ts +46 -0
  261. package/dist/op/runtime.d.ts.map +1 -1
  262. package/dist/op/runtimes/local.d.ts.map +1 -1
  263. package/dist/op/runtimes/local.js +8 -0
  264. package/dist/plan-summary.d.ts.map +1 -1
  265. package/dist/plan-summary.js +8 -2
  266. package/dist/point-decision.d.ts +2 -0
  267. package/dist/point-decision.d.ts.map +1 -1
  268. package/dist/point-decision.js +9 -2
  269. package/dist/pr-loop.d.ts +7 -0
  270. package/dist/pr-loop.d.ts.map +1 -1
  271. package/dist/pr-loop.js +23 -4
  272. package/dist/pr-report-url.d.ts +82 -0
  273. package/dist/pr-report-url.d.ts.map +1 -0
  274. package/dist/pr-report-url.js +174 -0
  275. package/dist/provenance.d.ts +2 -2
  276. package/dist/provenance.d.ts.map +1 -1
  277. package/dist/run-event-store.d.ts +104 -0
  278. package/dist/run-event-store.d.ts.map +1 -0
  279. package/dist/run-event-store.js +251 -0
  280. package/dist/run-event-stream.d.ts +133 -0
  281. package/dist/run-event-stream.d.ts.map +1 -0
  282. package/dist/run-event-stream.js +242 -0
  283. package/dist/run-event.d.ts +285 -0
  284. package/dist/run-event.d.ts.map +1 -0
  285. package/dist/run-event.js +209 -0
  286. package/dist/wave-runtime.mjs +262 -226
  287. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  288. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  289. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  290. package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
  291. package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
  292. package/dist/workspace/__fixtures__/run-event-check.js +59 -0
  293. package/dist/workspace/change-set-show.d.ts +56 -0
  294. package/dist/workspace/change-set-show.d.ts.map +1 -0
  295. package/dist/workspace/change-set-show.js +54 -0
  296. package/dist/workspace/change-set-show.schema.json +102 -0
  297. package/dist/workspace/change-set.schema.json +32 -1
  298. package/dist/workspace/changes-cli.d.ts.map +1 -1
  299. package/dist/workspace/changes-cli.js +10 -1
  300. package/dist/workspace/changes.d.ts +37 -0
  301. package/dist/workspace/changes.d.ts.map +1 -1
  302. package/dist/workspace/changes.js +49 -2
  303. package/dist/workspace/changes.schema.json +135 -0
  304. package/dist/workspace/check.schema.json +4 -2
  305. package/dist/workspace/checks/records.d.ts +9 -1
  306. package/dist/workspace/checks/records.d.ts.map +1 -1
  307. package/dist/workspace/checks/records.js +35 -1
  308. package/dist/workspace/checks.d.ts +1 -0
  309. package/dist/workspace/checks.d.ts.map +1 -1
  310. package/dist/workspace/checks.js +2 -1
  311. package/dist/workspace/ci-green-cli.d.ts +19 -4
  312. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  313. package/dist/workspace/ci-green-cli.js +75 -12
  314. package/dist/workspace/ci-green-forge.d.ts +20 -3
  315. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  316. package/dist/workspace/ci-green-forge.js +6 -4
  317. package/dist/workspace/ci-green-workflow.d.ts +11 -0
  318. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  319. package/dist/workspace/ci-green-workflow.js +6 -0
  320. package/dist/workspace/ci-green.d.ts +281 -13
  321. package/dist/workspace/ci-green.d.ts.map +1 -1
  322. package/dist/workspace/ci-green.js +456 -38
  323. package/dist/workspace/ci-last-green.schema.json +32 -2
  324. package/dist/workspace/ci-list.schema.json +525 -0
  325. package/dist/workspace/commit-links.d.ts +85 -0
  326. package/dist/workspace/commit-links.d.ts.map +1 -0
  327. package/dist/workspace/commit-links.js +163 -0
  328. package/dist/workspace/declaration.d.ts +35 -0
  329. package/dist/workspace/declaration.d.ts.map +1 -1
  330. package/dist/workspace/declaration.js +27 -1
  331. package/dist/workspace/declaration.schema.json +76 -0
  332. package/dist/workspace/declared-kinds.d.ts +16 -0
  333. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  334. package/dist/workspace/declared-kinds.js +25 -0
  335. package/dist/workspace/delivery-metrics.d.ts +91 -0
  336. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  337. package/dist/workspace/delivery-metrics.js +217 -0
  338. package/dist/workspace/delivery-metrics.schema.json +519 -0
  339. package/dist/workspace/flags-read.d.ts +37 -0
  340. package/dist/workspace/flags-read.d.ts.map +1 -0
  341. package/dist/workspace/flags-read.js +103 -0
  342. package/dist/workspace/flags.d.ts +100 -0
  343. package/dist/workspace/flags.d.ts.map +1 -0
  344. package/dist/workspace/flags.js +98 -0
  345. package/dist/workspace/graph-cli.js +1 -1
  346. package/dist/workspace/identity-gate-rule.d.ts +37 -0
  347. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  348. package/dist/workspace/identity-gate-rule.js +72 -7
  349. package/dist/workspace/identity.d.ts +12 -0
  350. package/dist/workspace/identity.d.ts.map +1 -1
  351. package/dist/workspace/identity.js +13 -1
  352. package/dist/workspace/intent-cli.d.ts +11 -2
  353. package/dist/workspace/intent-cli.d.ts.map +1 -1
  354. package/dist/workspace/intent-cli.js +54 -5
  355. package/dist/workspace/intent-trace.d.ts +219 -0
  356. package/dist/workspace/intent-trace.d.ts.map +1 -0
  357. package/dist/workspace/intent-trace.js +314 -0
  358. package/dist/workspace/intent.d.ts +36 -7
  359. package/dist/workspace/intent.d.ts.map +1 -1
  360. package/dist/workspace/intent.js +192 -22
  361. package/dist/workspace/intent.schema.json +702 -6
  362. package/dist/workspace/ls.d.ts +4 -0
  363. package/dist/workspace/ls.d.ts.map +1 -1
  364. package/dist/workspace/ls.js +2 -0
  365. package/dist/workspace/ls.schema.json +3 -1
  366. package/dist/workspace/member-pipeline.d.ts +6 -0
  367. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  368. package/dist/workspace/member-pipeline.js +6 -0
  369. package/dist/workspace/overlap.d.ts +146 -0
  370. package/dist/workspace/overlap.d.ts.map +1 -0
  371. package/dist/workspace/overlap.js +276 -0
  372. package/dist/workspace/promotion-order.d.ts +41 -0
  373. package/dist/workspace/promotion-order.d.ts.map +1 -0
  374. package/dist/workspace/promotion-order.js +95 -0
  375. package/dist/workspace/reason-codes.d.ts +4 -0
  376. package/dist/workspace/reason-codes.d.ts.map +1 -1
  377. package/dist/workspace/reason-codes.js +7 -1
  378. package/dist/workspace/records.d.ts +15 -2
  379. package/dist/workspace/records.d.ts.map +1 -1
  380. package/dist/workspace/records.js +32 -0
  381. package/dist/workspace/records.schema.json +21 -2
  382. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  383. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  384. package/dist/workspace/releases.d.ts +81 -0
  385. package/dist/workspace/releases.d.ts.map +1 -0
  386. package/dist/workspace/releases.js +103 -0
  387. package/dist/workspace/releases.schema.json +513 -0
  388. package/dist/workspace/run-event.schema.json +295 -0
  389. package/dist/workspace/status-gates.d.ts +34 -1
  390. package/dist/workspace/status-gates.d.ts.map +1 -1
  391. package/dist/workspace/status-gates.js +31 -2
  392. package/dist/workspace/status-stewards.d.ts +4 -0
  393. package/dist/workspace/status-stewards.d.ts.map +1 -1
  394. package/dist/workspace/status-stewards.js +2 -0
  395. package/dist/workspace/status.d.ts +117 -0
  396. package/dist/workspace/status.d.ts.map +1 -1
  397. package/dist/workspace/status.js +218 -4
  398. package/dist/workspace/status.schema.json +268 -2
  399. package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
  400. package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
  401. package/dist/workspace/trust/ci-tag-statement.js +123 -0
  402. package/dist/workspace/trust/op-run-statement.d.ts +91 -0
  403. package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
  404. package/dist/workspace/trust/op-run-statement.js +143 -0
  405. package/dist/workspace/trust/rotation.d.ts +18 -2
  406. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  407. package/dist/workspace/trust/rotation.js +65 -16
  408. package/dist/workspace/trust/seal.d.ts +11 -0
  409. package/dist/workspace/trust/seal.d.ts.map +1 -1
  410. package/dist/workspace/trust/seal.js +9 -0
  411. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  412. package/dist/workspace/trust/ssh-commit.js +10 -10
  413. package/dist/workspace/wip.d.ts.map +1 -1
  414. package/dist/workspace/wip.js +11 -3
  415. package/package.json +1 -1
  416. package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
  417. package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
  418. package/src/__golden__/wave-loop/fan-out-only.json +6 -0
  419. package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  420. package/src/__golden__/wave-loop/op-cost-member.json +3 -0
  421. package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  422. package/src/__golden__/wave-loop/op-held-on.json +2 -0
  423. package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
  424. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  425. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  426. package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
  427. package/src/__golden__/wave-loop/op-per-run.json +7 -0
  428. package/src/__golden__/wave-loop/op-pr-review.json +7 -0
  429. package/src/__golden__/wave-loop/op-sealed.json +3 -0
  430. package/src/__golden__/wave-loop/op-spend.json +3 -0
  431. package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
  432. package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
  433. package/src/build-params.test.ts +46 -0
  434. package/src/build-params.ts +55 -1
  435. package/src/change-set-graph.test.ts +72 -0
  436. package/src/change-set-graph.ts +141 -0
  437. package/src/change-set.ts +87 -6
  438. package/src/chant-version.ts +16 -0
  439. package/src/cli/build-params-cli.ts +22 -9
  440. package/src/cli/commands/build.ts +25 -11
  441. package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
  442. package/src/cli/commands/check-lexicon-docs.ts +39 -2
  443. package/src/cli/commands/check-lexicon.ts +14 -2
  444. package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
  445. package/src/cli/commands/init-lexicon.test.ts +25 -0
  446. package/src/cli/commands/init-lexicon.ts +18 -8
  447. package/src/cli/handlers/approve-release.test.ts +84 -0
  448. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  449. package/src/cli/handlers/approve.ts +65 -2
  450. package/src/cli/handlers/build.test.ts +67 -0
  451. package/src/cli/handlers/build.ts +46 -4
  452. package/src/cli/handlers/change-set.ts +49 -1
  453. package/src/cli/handlers/fan-out.test.ts +228 -1
  454. package/src/cli/handlers/fan-out.ts +53 -6
  455. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  456. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  457. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  458. package/src/cli/handlers/operator.test.ts +37 -0
  459. package/src/cli/handlers/pr.test.ts +341 -0
  460. package/src/cli/handlers/pr.ts +292 -47
  461. package/src/cli/handlers/promote.test.ts +669 -7
  462. package/src/cli/handlers/promote.ts +464 -29
  463. package/src/cli/handlers/release-implicit.e2e.test.ts +161 -0
  464. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  465. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  466. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  467. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  468. package/src/cli/handlers/release.e2e.test.ts +199 -0
  469. package/src/cli/handlers/release.ts +593 -0
  470. package/src/cli/handlers/run-auto-release.test.ts +80 -0
  471. package/src/cli/handlers/run-events-cmd.test.ts +92 -0
  472. package/src/cli/handlers/run-release.test.ts +55 -0
  473. package/src/cli/handlers/run.test.ts +30 -0
  474. package/src/cli/handlers/run.ts +243 -18
  475. package/src/cli/main.test.ts +8 -0
  476. package/src/cli/main.ts +143 -17
  477. package/src/cli/mcp/never-over-mcp.test.ts +2 -0
  478. package/src/cli/mcp/workspace-tools.ts +1 -1
  479. package/src/cli/parse-args.ts +56 -5
  480. package/src/cli/registry.ts +43 -5
  481. package/src/cli/run-events.ts +57 -0
  482. package/src/cli/version.ts +3 -13
  483. package/src/codegen/docs-in-site.test.ts +166 -0
  484. package/src/codegen/docs-in-site.ts +182 -0
  485. package/src/codegen/docs-site/AgentPrompt.astro +26 -1
  486. package/src/codegen/docs-types.ts +14 -1
  487. package/src/codegen/docs.ts +60 -7
  488. package/src/codegen/rehype-base-url.d.mts +3 -1
  489. package/src/codegen/rehype-base-url.mjs +15 -9
  490. package/src/codegen/rehype-base-url.test.ts +34 -0
  491. package/src/components/auto-release.test.ts +30 -0
  492. package/src/components/auto-release.ts +9 -1
  493. package/src/components/capability.ts +9 -0
  494. package/src/components/cli-support.test.ts +82 -0
  495. package/src/components/cli-support.ts +16 -9
  496. package/src/components/driver.test.ts +24 -0
  497. package/src/components/driver.ts +165 -56
  498. package/src/components/fan-out-inputs.test.ts +85 -0
  499. package/src/components/fan-out-inputs.ts +192 -0
  500. package/src/components/fan-out-record.test.ts +10 -0
  501. package/src/components/fan-out-record.ts +19 -0
  502. package/src/components/fan-out-run.ts +104 -35
  503. package/src/components/fan-out.ts +4 -0
  504. package/src/components/pr-pipeline.ts +35 -4
  505. package/src/components/pr-run.ts +30 -1
  506. package/src/components/promote-order.test.ts +90 -0
  507. package/src/components/promote-order.ts +75 -0
  508. package/src/components/promote.test.ts +10 -1
  509. package/src/components/promote.ts +94 -3
  510. package/src/components/release-compile.test.ts +147 -0
  511. package/src/components/release-compile.ts +459 -0
  512. package/src/components/release-gate.test.ts +215 -0
  513. package/src/components/release-gate.ts +355 -0
  514. package/src/components/release-promote.test.ts +159 -0
  515. package/src/components/release-promote.ts +232 -0
  516. package/src/components/run-progress.ts +7 -0
  517. package/src/components/wave-plan.ts +16 -3
  518. package/src/config.ts +15 -1
  519. package/src/flag-params.test.ts +144 -0
  520. package/src/flag-params.ts +134 -0
  521. package/src/index.ts +2 -0
  522. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  523. package/src/lifecycle/change-set-store.test.ts +217 -0
  524. package/src/lifecycle/change-set-store.ts +113 -0
  525. package/src/lifecycle/commit-sha.ts +23 -0
  526. package/src/lifecycle/delivery-metrics.ts +263 -0
  527. package/src/lifecycle/deploy-outcome.ts +147 -0
  528. package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
  529. package/src/lifecycle/gate-change-set.ts +164 -0
  530. package/src/lifecycle/gate-ledger.test.ts +33 -0
  531. package/src/lifecycle/gate-ledger.ts +68 -3
  532. package/src/lifecycle/git.ts +61 -8
  533. package/src/lifecycle/index.ts +4 -0
  534. package/src/lifecycle/mixed-environment.test.ts +97 -0
  535. package/src/lifecycle/mixed-environment.ts +204 -0
  536. package/src/lifecycle/publish-attachments.ts +196 -0
  537. package/src/lifecycle/publish-credentials.ts +84 -0
  538. package/src/lifecycle/publish-declaration.ts +95 -0
  539. package/src/lifecycle/publish-forge.ts +325 -0
  540. package/src/lifecycle/publish-npm.ts +259 -0
  541. package/src/lifecycle/publish-oci.ts +396 -0
  542. package/src/lifecycle/publish-record.ts +113 -0
  543. package/src/lifecycle/publish-resume.test.ts +168 -0
  544. package/src/lifecycle/publish-targets.test.ts +494 -0
  545. package/src/lifecycle/publish-targets.ts +100 -0
  546. package/src/lifecycle/release-cut-signed.test.ts +180 -0
  547. package/src/lifecycle/release-cut.test.ts +483 -0
  548. package/src/lifecycle/release-cut.ts +503 -0
  549. package/src/lifecycle/release-flag.test.ts +112 -0
  550. package/src/lifecycle/release-join.test.ts +68 -0
  551. package/src/lifecycle/release-join.ts +182 -0
  552. package/src/lifecycle/release-ledger.test.ts +14 -0
  553. package/src/lifecycle/release-ledger.ts +85 -17
  554. package/src/lifecycle/release-publish.test.ts +225 -0
  555. package/src/lifecycle/release-publish.ts +568 -0
  556. package/src/lifecycle/release-revocation.ts +147 -0
  557. package/src/lifecycle/release-store.ts +434 -0
  558. package/src/lifecycle/release-verify.test.ts +110 -0
  559. package/src/lifecycle/release-verify.ts +120 -0
  560. package/src/lifecycle/release-workstation.test.ts +204 -0
  561. package/src/lifecycle/release-workstation.ts +216 -0
  562. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  563. package/src/lifecycle/retention-policy.ts +63 -0
  564. package/src/lifecycle/retention.test.ts +363 -0
  565. package/src/lifecycle/retention.ts +508 -0
  566. package/src/lifecycle/run-ledger.test.ts +10 -0
  567. package/src/lifecycle/run-ledger.ts +6 -0
  568. package/src/op/activities/change-coverage.ts +4 -1
  569. package/src/op/activities/source-release.ts +8 -3
  570. package/src/op/activities/source-rollback.ts +8 -4
  571. package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
  572. package/src/op/gate-name.ts +13 -3
  573. package/src/op/gate-release.test.ts +126 -0
  574. package/src/op/gate-run-locator.test.ts +110 -0
  575. package/src/op/gate-run-locator.ts +35 -0
  576. package/src/op/gate.ts +88 -6
  577. package/src/op/local-executor.ts +207 -52
  578. package/src/op/op-waves-run.ts +4 -0
  579. package/src/op/run-events.test.ts +166 -0
  580. package/src/op/run-live.ts +1 -1
  581. package/src/op/runtime.ts +39 -0
  582. package/src/op/runtimes/local.test.ts +16 -0
  583. package/src/op/runtimes/local.ts +9 -0
  584. package/src/plan-summary.test.ts +20 -0
  585. package/src/plan-summary.ts +8 -1
  586. package/src/point-decision.ts +12 -2
  587. package/src/pr-loop.ts +25 -4
  588. package/src/pr-report-url.test.ts +131 -0
  589. package/src/pr-report-url.ts +209 -0
  590. package/src/provenance.ts +2 -2
  591. package/src/run-event-store.test.ts +144 -0
  592. package/src/run-event-store.ts +273 -0
  593. package/src/run-event-stream.test.ts +175 -0
  594. package/src/run-event-stream.ts +309 -0
  595. package/src/run-event.test.ts +277 -0
  596. package/src/run-event.ts +471 -0
  597. package/src/wave-loop-golden.test.ts +9 -0
  598. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  599. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  600. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  601. package/src/workspace/__fixtures__/run-event-check.ts +58 -0
  602. package/src/workspace/ancestry-index.test.ts +93 -0
  603. package/src/workspace/change-set-show.schema.json +102 -0
  604. package/src/workspace/change-set-show.ts +72 -0
  605. package/src/workspace/change-set.schema.json +32 -1
  606. package/src/workspace/changes-cli.ts +10 -1
  607. package/src/workspace/changes.schema.json +135 -0
  608. package/src/workspace/changes.test.ts +1 -1
  609. package/src/workspace/changes.ts +84 -2
  610. package/src/workspace/check.schema.json +4 -2
  611. package/src/workspace/checks/records.ts +35 -1
  612. package/src/workspace/checks.ts +2 -1
  613. package/src/workspace/ci-green-cli.ts +75 -13
  614. package/src/workspace/ci-green-forge.ts +24 -4
  615. package/src/workspace/ci-green-runs.test.ts +242 -0
  616. package/src/workspace/ci-green-signed.test.ts +302 -0
  617. package/src/workspace/ci-green-workflow.test.ts +16 -0
  618. package/src/workspace/ci-green-workflow.ts +12 -0
  619. package/src/workspace/ci-green.test.ts +131 -2
  620. package/src/workspace/ci-green.ts +635 -42
  621. package/src/workspace/ci-last-green.schema.json +32 -2
  622. package/src/workspace/ci-list.schema.json +525 -0
  623. package/src/workspace/commit-links.test.ts +279 -0
  624. package/src/workspace/commit-links.ts +183 -0
  625. package/src/workspace/declaration.schema.json +76 -0
  626. package/src/workspace/declaration.test.ts +28 -0
  627. package/src/workspace/declaration.ts +60 -2
  628. package/src/workspace/declared-kinds.ts +40 -1
  629. package/src/workspace/delivery-metrics.schema.json +519 -0
  630. package/src/workspace/delivery-metrics.test.ts +236 -0
  631. package/src/workspace/delivery-metrics.ts +286 -0
  632. package/src/workspace/flags-read.ts +121 -0
  633. package/src/workspace/flags.test.ts +141 -0
  634. package/src/workspace/flags.ts +157 -0
  635. package/src/workspace/graph-cli.ts +1 -1
  636. package/src/workspace/identity-gate-rule.ts +91 -7
  637. package/src/workspace/identity.ts +14 -1
  638. package/src/workspace/intent-cli.ts +46 -5
  639. package/src/workspace/intent-trace.test.ts +173 -0
  640. package/src/workspace/intent-trace.ts +484 -0
  641. package/src/workspace/intent.schema.json +702 -6
  642. package/src/workspace/intent.ts +196 -26
  643. package/src/workspace/ls.schema.json +3 -1
  644. package/src/workspace/ls.ts +6 -0
  645. package/src/workspace/member-pipeline.test.ts +17 -0
  646. package/src/workspace/member-pipeline.ts +9 -0
  647. package/src/workspace/older-records.test.ts +412 -0
  648. package/src/workspace/overlap.test.ts +165 -0
  649. package/src/workspace/overlap.ts +340 -0
  650. package/src/workspace/promotion-order.ts +97 -0
  651. package/src/workspace/read-contract.e2e.test.ts +5 -1
  652. package/src/workspace/reason-codes.test.ts +7 -1
  653. package/src/workspace/reason-codes.ts +7 -1
  654. package/src/workspace/record-assets.test.ts +2 -1
  655. package/src/workspace/records-contract.e2e.test.ts +2 -1
  656. package/src/workspace/records.schema.json +21 -2
  657. package/src/workspace/records.ts +36 -2
  658. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  659. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  660. package/src/workspace/releases.schema.json +513 -0
  661. package/src/workspace/releases.test.ts +108 -0
  662. package/src/workspace/releases.ts +133 -0
  663. package/src/workspace/run-event.schema.json +295 -0
  664. package/src/workspace/status-contract.test.ts +81 -1
  665. package/src/workspace/status-gates.ts +64 -1
  666. package/src/workspace/status-mixed.test.ts +152 -0
  667. package/src/workspace/status-revoked.test.ts +153 -0
  668. package/src/workspace/status-stewards.ts +6 -0
  669. package/src/workspace/status.schema.json +268 -2
  670. package/src/workspace/status.ts +334 -4
  671. package/src/workspace/trust/ci-tag-statement.ts +151 -0
  672. package/src/workspace/trust/op-run-statement.ts +148 -0
  673. package/src/workspace/trust/rotation.ts +61 -14
  674. package/src/workspace/trust/seal.ts +10 -0
  675. package/src/workspace/trust/ssh-commit.ts +10 -9
  676. package/src/workspace/wip.ts +11 -2
  677. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
  678. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
  679. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
  680. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
  681. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
@@ -0,0 +1,117 @@
1
+ /**
2
+ * A gated deploy leaves four records that name each other (#4020): the run
3
+ * record, the pending gate fact, the resolution and the release record. From
4
+ * any one of them, the other three are one lookup away, by the commit sha, the
5
+ * run id or the gate.
6
+ */
7
+ import { describe, test, expect, afterEach, vi } from "vitest";
8
+ import type { ActivityFn, ActivityProfile } from "./activity-registry";
9
+ import type { OpConfig } from "./types";
10
+ import { runOpLocally } from "./local-executor";
11
+ import { memoryGateLedgerPort } from "./gate";
12
+ import { resolveGateRunLocator, runOriginOfLocator } from "./gate-run-locator";
13
+ import { admissionOf, resolveRunId, type ReleaseRecord } from "../lifecycle/release-ledger";
14
+ import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
15
+
16
+ const SHA = "c0ffee".repeat(6) + "c0ff";
17
+ const PROFILES: Record<string, ActivityProfile> = { single: { timeout: "5m", retry: { maximumAttempts: 1 } } };
18
+ const NOW = "2026-10-16T10:00:00.000Z";
19
+ const CI = {
20
+ GITHUB_ACTIONS: "true",
21
+ GITHUB_REPOSITORY: "acme/app",
22
+ GITHUB_RUN_ID: "9001",
23
+ GITHUB_SERVER_URL: "https://github.example",
24
+ GITHUB_API_URL: "https://github.example/api/v3",
25
+ };
26
+
27
+ afterEach(() => vi.unstubAllEnvs());
28
+
29
+ const config: OpConfig = {
30
+ name: "ship",
31
+ overview: "",
32
+ labels: { Env: "prod" },
33
+ phases: [
34
+ { name: "Build", steps: [{ kind: "activity", fn: "build" }] },
35
+ { name: "Release", steps: [{ kind: "gate", gate: "approve-release" }, { kind: "activity", fn: "release" }] },
36
+ ],
37
+ };
38
+ const activities = new Map<string, ActivityFn>([
39
+ ["build", async () => undefined],
40
+ ["release", async () => undefined],
41
+ ]);
42
+
43
+ describe("a gated deploy: commit, run, gate and deploy record join (#4020)", () => {
44
+ test("each record names the commit, and the run, gate and deploy name one another", async () => {
45
+ for (const [k, v] of Object.entries(CI)) vi.stubEnv(k, v);
46
+ const port = memoryGateLedgerPort();
47
+
48
+ // 1. The run reaches the gate and stops. Its record and the pending fact carry the commit.
49
+ const first = await runOpLocally(config, activities, PROFILES, undefined, { gates: port, now: NOW, commit: SHA });
50
+ expect(first.status).toBe("gated");
51
+ const run = first.record;
52
+ const pending = port.appended[0]!;
53
+ expect(run.gitSha).toBe(SHA);
54
+ expect(run.runOrigin).toEqual({ forge: "github", repo: "acme/app", url: "https://github.example/acme/app/actions/runs/9001" });
55
+ expect(pending.gitSha).toBe(SHA);
56
+ expect(pending.runId).toBe(run.id);
57
+
58
+ // 2. Someone approves. `chant approve` copies the commit and run off the pending fact it answers.
59
+ const resolution: GateResolutionRecord = {
60
+ version: 1, op: pending.op, gate: pending.gate, resolvedBy: "alice", timestamp: "2026-10-16T10:05:00.000Z",
61
+ gitSha: pending.gitSha, runId: pending.runId,
62
+ };
63
+ const second = await runOpLocally(config, activities, PROFILES, undefined, {
64
+ gates: memoryGateLedgerPort({ pending: [pending], resolutions: [resolution] }),
65
+ now: "2026-10-16T10:06:00.000Z",
66
+ commit: SHA,
67
+ });
68
+ expect(second.status).toBe("ok");
69
+
70
+ // 3. The release activity records the deploy, naming the gate that admitted it.
71
+ const admitted = admissionOf([resolution], { op: "ship", gate: "approve-release" })!;
72
+ const ci = resolveRunId();
73
+ const release: ReleaseRecord = {
74
+ version: 1, component: "site", env: "prod", digest: `sha256:${"d".repeat(64)}`, gitSha: SHA,
75
+ runId: ci.runId, runOrigin: ci.runOrigin, timestamp: "2026-10-16T10:07:00.000Z", actor: "ci",
76
+ approver: admitted.approver, admission: admitted.admission,
77
+ };
78
+
79
+ // From the commit: the run, the gate and the deploy.
80
+ const byCommit = {
81
+ runs: [run, second.record].filter((r) => r.gitSha === SHA),
82
+ pending: port.appended.filter((p) => p.gitSha === SHA),
83
+ resolutions: [resolution].filter((r) => r.gitSha === SHA),
84
+ releases: [release].filter((r) => r.gitSha === SHA),
85
+ };
86
+ expect(byCommit.runs.map((r) => r.id)).toContain(run.id);
87
+ expect(byCommit.pending).toEqual([pending]);
88
+ expect(byCommit.resolutions).toEqual([resolution]);
89
+ expect(byCommit.releases).toEqual([release]);
90
+
91
+ // From the run: its gate, by run id, and the CI run it ran in.
92
+ expect(run.gate).toMatchObject({ name: pending.gate });
93
+ expect(resolution.runId).toBe(run.id);
94
+ expect(pending.runId).toBe(run.id);
95
+
96
+ // From the deploy: the gate resolution that admitted it, the approver, and the CI run the run was in.
97
+ expect(release.admission).toEqual({ op: pending.op, gate: pending.gate, resolvedAt: resolution.timestamp });
98
+ expect(release.approver).toBe(resolution.resolvedBy);
99
+ expect(release.runOrigin).toEqual(run.runOrigin);
100
+ expect(release.runId).toBe(runOriginOfLocator(resolveGateRunLocator()!).runId);
101
+ expect(release.gitSha).toBe(run.gitSha);
102
+ });
103
+
104
+ test("a run outside CI records its commit and no run origin", async () => {
105
+ for (const k of ["GITHUB_RUN_ID", "CI_PIPELINE_ID"]) vi.stubEnv(k, "");
106
+ const result = await runOpLocally(config, activities, PROFILES, undefined, { gates: memoryGateLedgerPort(), now: NOW, commit: SHA });
107
+ expect(result.record.gitSha).toBe(SHA);
108
+ expect(result.record).not.toHaveProperty("runOrigin");
109
+ });
110
+
111
+ test("admissionOf names the newest of several approvals and joins their approvers", () => {
112
+ const at = (resolvedBy: string, timestamp: string): GateResolutionRecord => ({ version: 1, op: "ship", gate: "g", resolvedBy, timestamp, planDigest: "p" });
113
+ expect(admissionOf([at("a", "2026-01-01T00:00:00Z"), at("b", "2026-01-02T00:00:00Z"), at("a", "2026-01-03T00:00:00Z")], { op: "ship", gate: "g", planDigest: "p" }))
114
+ .toEqual({ approver: "a, b", admission: { op: "ship", gate: "g", resolvedAt: "2026-01-03T00:00:00Z" } });
115
+ expect(admissionOf([at("a", "2026-01-01T00:00:00Z")], { op: "ship", gate: "g", planDigest: "other" })).toBeUndefined();
116
+ });
117
+ });
@@ -44,6 +44,16 @@ export const WORKSPACE_UPGRADE_GATE_OP = "workspace-upgrade";
44
44
  */
45
45
  export const OVERRIDE_GATE_OP = "policy-override";
46
46
 
47
+ /**
48
+ * The op name every release gate is recorded under (#4045): the gates
49
+ * `chant components promote --release` reaches, `_gates/release.jsonl`, each
50
+ * named as its composition names it and bound to a release, an environment
51
+ * and the release's change set. Like a fan-out, it has no `*.op.ts`, and like
52
+ * the pull-request gate it approves a change to an environment, so it is
53
+ * never resolved over MCP or ACP.
54
+ */
55
+ export const RELEASE_GATE_OP = "release";
56
+
47
57
  /**
48
58
  * The pull-request loop records its gate under `pr-<number>`, or
49
59
  * `pr-<number>-<member>` in a workspace member (`prOp` in `pr-loop.ts`), and
@@ -54,12 +64,12 @@ const PR_GATE_OP = /^pr-\d+(?:-[a-z0-9][a-z0-9-]*)?$/;
54
64
  /**
55
65
  * Whether gates recorded under this op name are never resolved over MCP or ACP
56
66
  * (chant#3485, chant#3513). These are the gate-only names MCP cannot discover:
57
- * `fan-out`, `workspace-upgrade`, `policy-override` (#3760) and the
58
- * pull-request gate `pr-<number>`. With
67
+ * `fan-out`, `workspace-upgrade`, `policy-override` (#3760), `release`
68
+ * (#4045) and the pull-request gate `pr-<number>`. With
59
69
  * no Op config to read `neverOverMcp` from, the pending record carries it.
60
70
  */
61
71
  export function isNeverOverMcpGateOp(op: string): boolean {
62
- return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || op === OVERRIDE_GATE_OP || PR_GATE_OP.test(op);
72
+ return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || op === OVERRIDE_GATE_OP || op === RELEASE_GATE_OP || PR_GATE_OP.test(op);
63
73
  }
64
74
 
65
75
  /** Either spelling of a gate step's name. `gate` since #2202; `signalName` through 0.59.0. */
@@ -0,0 +1,126 @@
1
+ /**
2
+ * A release gate (#4045, ws-104 question 2) binds the release, the
3
+ * environment and the change-set document digest; an approval counts only
4
+ * when all three match. An `empty` pass (question 6) counts only once the
5
+ * stored document reads back empty. Decided against an in-memory ledger.
6
+ */
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import { evaluateGate, memoryGateLedgerPort } from "./gate";
10
+ import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
11
+
12
+ const sha = (c: string) => `jcs1-sha256:${c.repeat(64)}`;
13
+ const RELEASE_A = sha("a");
14
+ const RELEASE_B = sha("b");
15
+ const DOC = sha("d");
16
+ const NOW = "2026-10-16T12:00:00.000Z";
17
+ const LATER = "2026-10-16T13:00:00.000Z";
18
+
19
+ const gateInput = (over: Partial<Parameters<typeof evaluateGate>[1]> = {}) => ({
20
+ op: "release",
21
+ gate: "prod-release",
22
+ environment: "prod",
23
+ planDigest: DOC,
24
+ releaseDigest: RELEASE_A,
25
+ now: NOW,
26
+ ...over,
27
+ });
28
+
29
+ const approval = (over: Partial<GateResolutionRecord> = {}): GateResolutionRecord => ({
30
+ version: 1,
31
+ op: "release",
32
+ gate: "prod-release",
33
+ resolvedBy: "alice",
34
+ timestamp: "2026-10-16T12:30:00.000Z",
35
+ planDigest: DOC,
36
+ environment: "prod",
37
+ releaseDigest: RELEASE_A,
38
+ ...over,
39
+ });
40
+
41
+ describe("a release gate binds the release, the environment and the change set", () => {
42
+ test("the pending fact names the release it waits on", async () => {
43
+ const port = memoryGateLedgerPort();
44
+ const check = await evaluateGate(port, gateInput());
45
+ expect(check.satisfied).toBe(false);
46
+ expect(port.appended[0]).toMatchObject({ op: "release", gate: "prod-release", environment: "prod", planDigest: DOC, releaseDigest: RELEASE_A });
47
+ });
48
+
49
+ test("an approval for the release, environment and change set passes, and names the release it admitted", async () => {
50
+ const port = memoryGateLedgerPort({ resolutions: [approval()] });
51
+ const check = await evaluateGate(port, gateInput({ now: LATER }));
52
+ expect(check.satisfied).toBe(true);
53
+ if (check.satisfied) expect(check.resolution.releaseDigest).toBe(RELEASE_A);
54
+ });
55
+
56
+ test("an approval for one release can't be used by another, even for the same change-set digest", async () => {
57
+ const port = memoryGateLedgerPort({ resolutions: [approval()] });
58
+ const check = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, now: LATER }));
59
+ expect(check.satisfied).toBe(false);
60
+ if (!check.satisfied) {
61
+ expect(check.mismatch).toMatchObject({ resolvedBy: "alice", approved: DOC, planned: DOC, environment: "prod" });
62
+ expect(check.pending.releaseDigest).toBe(RELEASE_B);
63
+ }
64
+ });
65
+
66
+ test("an approval for one environment can't be used for another", async () => {
67
+ const port = memoryGateLedgerPort({ resolutions: [approval({ environment: "staging" })] });
68
+ const check = await evaluateGate(port, gateInput({ now: LATER }));
69
+ expect(check.satisfied).toBe(false);
70
+ if (!check.satisfied) expect(check.pending.environment).toBe("prod");
71
+ });
72
+
73
+ test("an approval for another change set of the same release doesn't pass", async () => {
74
+ const port = memoryGateLedgerPort({ resolutions: [approval({ planDigest: sha("e") })] });
75
+ const check = await evaluateGate(port, gateInput({ now: LATER }));
76
+ expect(check.satisfied).toBe(false);
77
+ if (!check.satisfied) expect(check.mismatch?.approved).toBe(sha("e"));
78
+ });
79
+
80
+ test("a gate that binds no release counts no approval recorded for one", async () => {
81
+ const port = memoryGateLedgerPort({ resolutions: [approval()] });
82
+ const check = await evaluateGate(port, gateInput({ releaseDigest: undefined, now: LATER }));
83
+ expect(check.satisfied).toBe(false);
84
+ });
85
+
86
+ test("a standing fact for another release is recorded again for this one", async () => {
87
+ const port = memoryGateLedgerPort();
88
+ await evaluateGate(port, gateInput());
89
+ const again = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, now: "2026-10-16T12:01:00.000Z" }));
90
+ expect(again.satisfied).toBe(false);
91
+ if (!again.satisfied) expect(again.recorded).toBe(true);
92
+ expect(port.appended.map((p) => p.releaseDigest)).toEqual([RELEASE_A, RELEASE_B]);
93
+ });
94
+ });
95
+
96
+ describe("an empty pass", () => {
97
+ const pass = approval({ resolvedBy: "bob", empty: true, timestamp: "2026-10-16T11:59:00.000Z" });
98
+
99
+ test("passes the gate once the stored change set reads back empty", async () => {
100
+ const asked: string[] = [];
101
+ const port = memoryGateLedgerPort({ resolutions: [pass] });
102
+ const check = await evaluateGate(port, gateInput({ verifyEmpty: async (d) => (asked.push(d), true) }));
103
+ expect(check).toMatchObject({ satisfied: true, via: "empty", resolution: { resolvedBy: "bob", empty: true, releaseDigest: RELEASE_A } });
104
+ expect(asked).toEqual([DOC]);
105
+ expect(port.appended).toEqual([]);
106
+ });
107
+
108
+ test("doesn't pass when the stored change set is not empty", async () => {
109
+ const port = memoryGateLedgerPort({ resolutions: [pass] });
110
+ const check = await evaluateGate(port, gateInput({ verifyEmpty: async () => false }));
111
+ expect(check.satisfied).toBe(false);
112
+ });
113
+
114
+ test("is never an approval: without a verifier, or on a gate without a plan, it counts for nothing", async () => {
115
+ const port = memoryGateLedgerPort({ resolutions: [pass] });
116
+ expect((await evaluateGate(port, gateInput())).satisfied).toBe(false);
117
+ const plain = memoryGateLedgerPort({ resolutions: [{ ...pass, planDigest: undefined, releaseDigest: undefined }] });
118
+ expect((await evaluateGate(plain, { op: "release", gate: "prod-release", now: NOW })).satisfied).toBe(false);
119
+ });
120
+
121
+ test("for another release it doesn't pass", async () => {
122
+ const port = memoryGateLedgerPort({ resolutions: [pass] });
123
+ const check = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, verifyEmpty: async () => true }));
124
+ expect(check.satisfied).toBe(false);
125
+ });
126
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { ciCommitFromEnv, ciRunIdFromEnv, resolveGateRunLocator, runOriginOfLocator } from "./gate-run-locator";
3
+ import { resolveCiRun, resolveRunId } from "../lifecycle/release-ledger";
4
+
5
+ const GITHUB = {
6
+ GITHUB_ACTIONS: "true",
7
+ GITHUB_REPOSITORY: "acme/app",
8
+ GITHUB_RUN_ID: "9001",
9
+ GITHUB_RUN_ATTEMPT: "2",
10
+ GITHUB_SERVER_URL: "https://github.example",
11
+ GITHUB_API_URL: "https://github.example/api/v3",
12
+ GITHUB_SHA: "a".repeat(40),
13
+ };
14
+ const GITLAB = {
15
+ GITLAB_CI: "true",
16
+ CI_API_V4_URL: "https://gitlab.example/api/v4",
17
+ CI_PROJECT_ID: "77",
18
+ CI_PROJECT_PATH: "acme/app",
19
+ CI_PIPELINE_ID: "5150",
20
+ CI_PIPELINE_URL: "https://gitlab.example/acme/app/-/pipelines/5150",
21
+ CI_JOB_ID: "31",
22
+ CI_JOB_NAME: "deploy",
23
+ CI_JOB_URL: "https://gitlab.example/acme/app/-/jobs/31",
24
+ CI_COMMIT_SHA: "b".repeat(40),
25
+ };
26
+ const FORGEJO = {
27
+ ...GITHUB,
28
+ FORGEJO_ACTIONS: "true",
29
+ GITHUB_WORKFLOW_REF: "acme/app/.forgejo/workflows/deploy.yml@refs/heads/main",
30
+ GITHUB_REF_NAME: "main",
31
+ GITHUB_SERVER_URL: "https://forgejo.example",
32
+ GITHUB_API_URL: "https://forgejo.example/api/v1",
33
+ };
34
+
35
+ describe("runOriginOfLocator (#4020)", () => {
36
+ test("a GitHub locator maps to forge github, with its repo and run page, and the run as the run id", () => {
37
+ const locator = resolveGateRunLocator(GITHUB)!;
38
+ expect(runOriginOfLocator(locator)).toEqual({
39
+ runId: "9001",
40
+ runOrigin: { forge: "github", repo: "acme/app", url: "https://github.example/acme/app/actions/runs/9001" },
41
+ });
42
+ });
43
+
44
+ test("a Forgejo locator maps to forge github, since the url names the host", () => {
45
+ const locator = resolveGateRunLocator(FORGEJO)!;
46
+ expect(locator.forge).toBe("forgejo");
47
+ expect(runOriginOfLocator(locator)).toEqual({
48
+ runId: "9001",
49
+ runOrigin: { forge: "github", repo: "acme/app", url: "https://forgejo.example/acme/app/actions/runs/9001" },
50
+ });
51
+ });
52
+
53
+ test("a GitLab locator stays gitlab, with the project id as repo and the pipeline id as the run id", () => {
54
+ const locator = resolveGateRunLocator(GITLAB)!;
55
+ expect(runOriginOfLocator(locator)).toEqual({
56
+ runId: "5150",
57
+ runOrigin: { forge: "gitlab", repo: "77", url: "https://gitlab.example/acme/app/-/jobs/31" },
58
+ });
59
+ });
60
+
61
+ test("the API base, attempt, job and workflow fields do not reach the origin", () => {
62
+ const { runOrigin } = runOriginOfLocator(resolveGateRunLocator(GITHUB)!);
63
+ expect(Object.keys(runOrigin).sort()).toEqual(["forge", "repo", "url"]);
64
+ });
65
+
66
+ test("a locator without a url maps to an origin without one", () => {
67
+ const { api, repo, run } = resolveGateRunLocator(GITHUB)!;
68
+ expect(runOriginOfLocator({ forge: "github", api, repo, run })).toEqual({ runId: "9001", runOrigin: { forge: "github", repo: "acme/app" } });
69
+ });
70
+ });
71
+
72
+ describe("a GateRunLocator and the RunOrigin written for the same CI job name the same run (#4020)", () => {
73
+ test.each([
74
+ ["GitHub", GITHUB, "github"],
75
+ ["Forgejo", FORGEJO, "github"],
76
+ ["GitLab", GITLAB, "gitlab"],
77
+ ] as const)("%s", (_name, env, forge) => {
78
+ const locator = resolveGateRunLocator(env)!;
79
+ const fromLocator = runOriginOfLocator(locator);
80
+ const written = resolveCiRun(env);
81
+ // A deploy record's run id is the one resolveRunId gives, and the gate's is the CI run id.
82
+ expect(resolveRunId(undefined, env).runId).toBe(fromLocator.runId);
83
+ expect(ciRunIdFromEnv(env)).toBe(fromLocator.runId);
84
+ expect(written?.runId).toBe(fromLocator.runId);
85
+ expect(written?.runOrigin.forge).toBe(forge);
86
+ expect(fromLocator.runOrigin.forge).toBe(written?.runOrigin.forge);
87
+ });
88
+
89
+ test("on GitHub and Forgejo the two origins are the same object, repo and url included", () => {
90
+ for (const env of [GITHUB, FORGEJO]) {
91
+ expect(runOriginOfLocator(resolveGateRunLocator(env)!).runOrigin).toEqual(resolveCiRun(env)!.runOrigin);
92
+ }
93
+ });
94
+
95
+ test("on GitLab the locator keeps the project id and the job page where the origin keeps the path and the pipeline page", () => {
96
+ expect(runOriginOfLocator(resolveGateRunLocator(GITLAB)!).runOrigin).toEqual({ forge: "gitlab", repo: "77", url: GITLAB.CI_JOB_URL });
97
+ expect(resolveCiRun(GITLAB)!.runOrigin).toEqual({ forge: "gitlab", repo: "acme/app", url: GITLAB.CI_PIPELINE_URL });
98
+ });
99
+ });
100
+
101
+ describe("the CI environment helpers", () => {
102
+ test("the commit is GITHUB_SHA or CI_COMMIT_SHA, and none outside CI", () => {
103
+ expect(ciCommitFromEnv(GITHUB)).toBe("a".repeat(40));
104
+ expect(ciCommitFromEnv(GITLAB)).toBe("b".repeat(40));
105
+ expect(ciCommitFromEnv({})).toBeUndefined();
106
+ });
107
+ test("the run id is none outside CI", () => {
108
+ expect(ciRunIdFromEnv({})).toBeUndefined();
109
+ });
110
+ });
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { ForgeKind } from "../pr-forge";
10
+ import type { RunOrigin } from "../lifecycle/release-ledger";
10
11
 
11
12
  /** Where the job that recorded a pending fact runs, and what its forge needs to start it again. */
12
13
  export interface GateRunLocator {
@@ -103,3 +104,37 @@ export function isGateRunLocator(value: unknown): value is GateRunLocator {
103
104
  if (v.forge === "forgejo") return typeof v.workflow === "string" && typeof v.ref === "string";
104
105
  return false;
105
106
  }
107
+
108
+ /**
109
+ * The CI run a {@link GateRunLocator} names, in the shape a run and a deploy
110
+ * record carry (#4020). `runId` is the locator's `run`. Forge `github` and
111
+ * `forgejo` both become `github`, as `RunOrigin.forge` documents (the `url`
112
+ * names the host), and `gitlab` stays `gitlab`. `repo` and `url` carry over.
113
+ * The API base, attempt, job and workflow fields stay on the locator, which
114
+ * exists to start the job again.
115
+ */
116
+ export function runOriginOfLocator(locator: GateRunLocator): { runId: string; runOrigin: RunOrigin } {
117
+ return {
118
+ runId: locator.run,
119
+ runOrigin: {
120
+ forge: locator.forge === "gitlab" ? "gitlab" : "github",
121
+ repo: locator.repo,
122
+ ...(locator.url ? { url: locator.url } : {}),
123
+ },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * The id of the CI run this process is in, or undefined outside CI: the
129
+ * GitHub (and Forgejo) run id, else the GitLab pipeline id. The same
130
+ * precedence `resolveRunId` (../lifecycle/release-ledger.ts) uses for a
131
+ * deploy record, so a gate and a deploy name the run alike.
132
+ */
133
+ export function ciRunIdFromEnv(env: Record<string, string | undefined> = process.env): string | undefined {
134
+ return env.GITHUB_RUN_ID || env.CI_PIPELINE_ID || undefined;
135
+ }
136
+
137
+ /** The commit the CI job was started for: `GITHUB_SHA` (GitHub and Forgejo) or `CI_COMMIT_SHA` (GitLab). Undefined outside CI. */
138
+ export function ciCommitFromEnv(env: Record<string, string | undefined> = process.env): string | undefined {
139
+ return env.GITHUB_SHA || env.CI_COMMIT_SHA || undefined;
140
+ }
package/src/op/gate.ts CHANGED
@@ -53,7 +53,8 @@ import { sortedJsonReplacer } from "../utils";
53
53
  import { gateContextDigest, type GateApprover, type ResolvedGateApproval } from "./gate-approval";
54
54
  import { pushLifecycleReport, readBlobBySha, readBlobFromPath, readPathSha, requireLifecycleLedger, writeBlobToPath, RefCASConflictError } from "../lifecycle/git";
55
55
  import { parseDuration } from "./duration";
56
- import { resolveGateRunLocator } from "./gate-run-locator";
56
+ import { ciRunIdFromEnv, resolveGateRunLocator } from "./gate-run-locator";
57
+ import { resolveCommitSha } from "../lifecycle/commit-sha";
57
58
  import { refuseGateWithoutRecording } from "../lifecycle/no-record";
58
59
  import { gateCheckpointDigest, gateCheckpointText, readGateCheckpointText, writeGateCheckpoint, type GateCheckpoint } from "../lifecycle/gate-checkpoint";
59
60
  import type { ResolvedGateAlias } from "./gate-alias";
@@ -258,8 +259,10 @@ export interface GateCheckInput {
258
259
  description?: string;
259
260
  /** The gate's authored `timeout`, which becomes the pending fact's expiry. Default {@link DEFAULT_GATE_EXPIRY}. */
260
261
  timeout?: string;
261
- /** Identifies the run that reached the gate, when the caller has one. */
262
+ /** Identifies the run that reached the gate, when the caller has one. Absent, a run in CI records the CI run id (#4020). */
262
263
  runId?: string;
264
+ /** The commit the run is at (#4020). Absent, the checkout's `HEAD`, else the commit the CI job started for. */
265
+ gitSha?: string;
263
266
  /**
264
267
  * The plan this run reached the gate with (#2300) — `computePlanDigest`'s
265
268
  * output (`../lifecycle/plan-digest.ts`), from the Plan phase that ran a
@@ -321,6 +324,25 @@ export interface GateCheckInput {
321
324
  * carried earlier counts only while its alias is still declared.
322
325
  */
323
326
  aliases?: readonly ResolvedGateAlias[];
327
+ /**
328
+ * The release this gate binds (#4045, ws-104 question 2): set on a release
329
+ * gate, the gate `chant components promote --release` passes for an
330
+ * environment, with `planDigest` the change-set document's digest and
331
+ * `environment` the environment. Set, a resolution counts only when it was
332
+ * recorded for this release as well as this plan and environment, and the
333
+ * pending fact it records carries it. Absent, a resolution recorded for a
334
+ * release never counts.
335
+ */
336
+ releaseDigest?: string;
337
+ /**
338
+ * Whether the change set stored under a digest is empty (#4045, ws-104
339
+ * question 6): read back from `chant/lifecycle` and recomputed, never
340
+ * taken from the resolution. Given, an `empty: true` resolution for this
341
+ * gate, environment, release and plan passes the gate when this says its
342
+ * document is empty; quorum and approval rules don't apply to it. Absent,
343
+ * no `empty` resolution counts.
344
+ */
345
+ verifyEmpty?: (planDigest: string) => Promise<boolean>;
324
346
  }
325
347
 
326
348
  /**
@@ -386,14 +408,18 @@ export type GateCheck =
386
408
  * Set on a gate that declares `approval` (#2508). `"quorum"` when enough
387
409
  * human approvals were recorded, `"policy"` when an `enforce`-mode permit
388
410
  * passed it on its own. `"alias"` (#3949) when an approval recorded
389
- * under one of the gate's aliases counted, on any gate.
411
+ * under one of the gate's aliases counted, on any gate. `"empty"`
412
+ * (#4045) when a release gate passed over an empty change set, on the
413
+ * `empty: true` resolution the promote wrote, after the stored
414
+ * document was read back and found empty.
390
415
  */
391
- via?: "quorum" | "policy" | "alias";
416
+ via?: "quorum" | "policy" | "alias" | "empty";
392
417
  /** Every approval that counted toward the quorum. Set with `via`. */
393
418
  approvals?: GateResolutionRecord[];
394
419
  /** The approvals this run carried from an alias and wrote under the gate's name (#3949). Set when `via` is `"alias"`. */
395
420
  carried?: GateResolutionRecord[];
396
421
  }
422
+
397
423
  | {
398
424
  satisfied: false;
399
425
  pending: PendingGateRecord;
@@ -593,6 +619,7 @@ export function carriedApproval(r: GateResolutionRecord, op: string, gate: strin
593
619
  ...(r.url ? { url: r.url } : {}),
594
620
  ...(r.planDigest !== undefined ? { planDigest: r.planDigest } : {}),
595
621
  ...(r.environment !== undefined ? { environment: r.environment } : {}),
622
+ ...(r.releaseDigest !== undefined ? { releaseDigest: r.releaseDigest } : {}),
596
623
  ...(r.origin ? { origin: r.origin } : {}),
597
624
  ...(r.sameOriginOverride ? { sameOriginOverride: true } : {}),
598
625
  approver: approverOf(r),
@@ -650,6 +677,16 @@ async function aliasApprovals(
650
677
  return out.sort((a, b) => at(a.timestamp) - at(b.timestamp));
651
678
  }
652
679
 
680
+ /**
681
+ * The commit and run a gate record names (#4020): the input's own, else the
682
+ * checkout's commit and, in CI, the CI run id. Fields nobody knows are left out.
683
+ */
684
+ async function runStamp(input: { runId?: string; gitSha?: string }): Promise<{ runId?: string; gitSha?: string }> {
685
+ const runId = input.runId ?? ciRunIdFromEnv();
686
+ const gitSha = input.gitSha ?? (await resolveCommitSha());
687
+ return { ...(runId ? { runId } : {}), ...(gitSha ? { gitSha } : {}) };
688
+ }
689
+
653
690
  /** Whether two resolved approval blocks are the same, so a standing pending fact still describes this gate. */
654
691
  function sameApproval(a: ResolvedGateApproval | undefined, b: ResolvedGateApproval | undefined): boolean {
655
692
  return JSON.stringify(a ?? null, sortedJsonReplacer) === JSON.stringify(b ?? null, sortedJsonReplacer);
@@ -692,20 +729,48 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
692
729
  // a mismatch with a message instead of silently not counting.
693
730
  const env = input.environment;
694
731
  const pending = env === undefined ? ledger.pending : ledger.pending.filter((p) => p.environment === env);
732
+ // #4045: an approval is for one release. On this gate, a resolution for
733
+ // another release (or for one, on a gate that binds none) never counts; the
734
+ // newest one since the standing fact comes back as a mismatch instead.
735
+ const forOtherRelease = (r: GateResolutionRecord) => r.gate === input.gate && r.releaseDigest !== input.releaseDigest;
695
736
  // #3163: a gate the workspace's identity.gates names counts only signed approvals from its class.
696
737
  const rule = (await port.approvalRule?.(input.gate)) ?? null;
697
738
  const standing = latestPendingGate(pending, input.gate);
698
739
  const since = standing?.timestamp ?? EPOCH;
740
+
741
+ // #4045: a pass over an empty change set. The promote wrote it, and it
742
+ // counts only once the document stored under its plan reads back empty.
743
+ const envResolutions = env === undefined ? ledger.resolutions : ledger.resolutions.filter((r) => r.environment === env);
744
+ if (input.verifyEmpty && input.planDigest !== undefined) {
745
+ const passes = envResolutions
746
+ .filter(
747
+ (r) =>
748
+ r.empty === true && r.gate === input.gate && !forOtherRelease(r) && samePlanDigest(r.planDigest, input.planDigest) &&
749
+ new Date(r.timestamp).getTime() >= new Date(since).getTime() &&
750
+ (input.spent === undefined || !isApprovalSpent(input.spent, r)),
751
+ )
752
+ .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
753
+ const pass = passes[passes.length - 1];
754
+ if (pass && (await input.verifyEmpty(input.planDigest))) return { satisfied: true, resolution: pass, via: "empty" };
755
+ }
699
756
  // #3949: aliases admit only approvals of this run's plan, and carrying one writes a line.
700
757
  const aliases = input.planDigest !== undefined && port.appendResolution ? (input.aliases ?? []) : [];
701
758
  const aliased = (from: { op: string; gate: string }) => aliases.some((a) => a.op === from.op && a.gate === from.gate);
702
759
  // #3841: an approval of this plan the rule refuses is named, so a wait says why.
703
760
  const refused: GateRefusedApproval[] = [];
761
+ const otherRelease: GateResolutionRecord[] = [];
704
762
  const resolutions = (
705
763
  env === undefined
706
764
  ? ledger.resolutions
707
765
  : ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env)
708
766
  )
767
+ // #4045: an `empty` pass is never an approval; it counts only above.
768
+ .filter((r) => r.empty !== true)
769
+ .filter((r) => {
770
+ if (!forOtherRelease(r)) return true;
771
+ otherRelease.push(r);
772
+ return false;
773
+ })
709
774
  // #3949: a carried approval counts only while the gate still names its alias.
710
775
  .filter((r) => r.gate !== input.gate || r.carriedFrom === undefined || aliased(r.carriedFrom))
711
776
  .filter((r) => input.spent === undefined || r.gate !== input.gate || !isApprovalSpent(input.spent, r))
@@ -722,7 +787,10 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
722
787
  // #3949: write the approvals that counted from an alias under this gate.
723
788
  const carry = async (records: readonly GateResolutionRecord[]): Promise<GateResolutionRecord[]> => {
724
789
  const written: GateResolutionRecord[] = [];
725
- for (const r of records) written.push((await port.appendResolution!(carriedApproval(r, input.op, input.gate, now))).record);
790
+ if (records.length === 0) return written;
791
+ // #4020: a carried approval is for this run's gate, so it names this run's commit and run.
792
+ const at = await runStamp(input);
793
+ for (const r of records) written.push((await port.appendResolution!({ ...carriedApproval(r, input.op, input.gate, now), ...at })).record);
726
794
  return written;
727
795
  };
728
796
 
@@ -775,6 +843,16 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
775
843
  mismatched = found.mismatched;
776
844
  }
777
845
 
846
+ // #4045: an approval of this gate for another release stands as a mismatch
847
+ // when nothing closer does, so the refusal says an approval exists.
848
+ if (!mismatched && input.planDigest !== undefined) {
849
+ const sinceMs = new Date(since).getTime();
850
+ mismatched = otherRelease
851
+ .filter((r) => new Date(r.timestamp).getTime() >= sinceMs)
852
+ .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
853
+ .at(-1);
854
+ }
855
+
778
856
  // `input.planDigest` is defined whenever `mismatched` is — `latestResolutionForPlan`
779
857
  // returns a mismatch only on the plan-bound path.
780
858
  const mismatch: GateDigestMismatch | undefined = mismatched
@@ -800,6 +878,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
800
878
  // that already carries a checkpoint stands: an approval copies that one.
801
879
  if (
802
880
  standing && !isPendingGateExpired(standing, now) && samePlanDigest(standing.planDigest, input.planDigest) &&
881
+ standing.releaseDigest === input.releaseDigest &&
803
882
  sameApproval(standing.approval, input.approval) &&
804
883
  (input.checkpoint === undefined || port.writeCheckpoint === undefined || standing.checkpointDigest !== undefined)
805
884
  ) {
@@ -817,6 +896,8 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
817
896
  const url = resolveApprovalUrl();
818
897
  // #3683: in CI, where this job runs, so an approval can start it again.
819
898
  const resume = resolveGateRunLocator();
899
+ // #4020: the commit and run the gate waits on, so a reader can join it to both.
900
+ const stamp = await runStamp(input);
820
901
  const { record, pushed, pushWarning } = await port.appendPending({
821
902
  ...(input.fields as Partial<PendingGateInput> | undefined),
822
903
  op: input.op,
@@ -826,12 +907,13 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
826
907
  new Date(now).getTime() + parseDuration(input.timeout ?? DEFAULT_GATE_EXPIRY),
827
908
  ).toISOString(),
828
909
  ...(input.description ? { description: input.description } : {}),
829
- ...(input.runId ? { runId: input.runId } : {}),
910
+ ...stamp,
830
911
  ...(url ? { url } : {}),
831
912
  ...(resume ? { resume } : {}),
832
913
  ...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
833
914
  ...(checkpointDigest !== undefined ? { checkpointDigest } : {}),
834
915
  ...(env !== undefined ? { environment: env } : {}),
916
+ ...(input.releaseDigest !== undefined ? { releaseDigest: input.releaseDigest } : {}),
835
917
  ...(input.approval ? { approval: input.approval } : {}),
836
918
  ...(input.neverOverMcp ? { neverOverMcp: true as const } : {}),
837
919
  });