@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
@@ -27,11 +27,41 @@
27
27
  *
28
28
  * The forge is asked only for check runs (`./ci-green-forge.ts`). The tags
29
29
  * are fetched from and pushed to the remote with git.
30
+ *
31
+ * Signed tags (#4031): when `ci.green.signedSince` names a commit, a tag on
32
+ * any commit that is not a strict first-parent ancestor of it counts only
33
+ * when its annotation carries a statement signed by a runner key (`./trust/ci-tag-statement.ts`). Every
34
+ * reader here, and the release cut, ignores a tag without a valid one and
35
+ * reports it. A tick given a runner key signs the tags it makes. Without
36
+ * `signedSince` every tag counts, as before.
37
+ *
38
+ * Chant runs as the source (#4044): with `ci.green.source: "runs"` (or
39
+ * `--source runs`) the tick reads chant's own Op runs from the run ledger on
40
+ * `chant/lifecycle` in place of the forge's check runs, so a project whose
41
+ * pipelines run as chant Ops, such as one on GitLab or Forgejo, gets green
42
+ * and revoked tags with no forge check runs. A phase's patterns then match
43
+ * Op names, a run counts for the commit its record names as `gitSha`, and
44
+ * the latest run of each Op decides, by its end time. A run counts only when
45
+ * its record carries a statement signed by a runner key whose principal
46
+ * holds the `runner` role at base (`./trust/op-run-statement.ts`); the tick
47
+ * ignores and reports any other, so it neither greens nor revokes a commit.
48
+ * A tick from runs signs every tag it makes, and each run in the tag's
49
+ * record names the Op run's id and carries its statement.
30
50
  */
51
+ import { judgeCommits, mergeCommits, readWorkIndex } from "./commit-links.js";
31
52
  import { execFileSync } from "node:child_process";
32
53
  import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError } from "./declaration.js";
54
+ import { readRunLedger } from "../lifecycle/run-ledger.js";
55
+ import { fetchLifecycleStatus, listFilesInDir, listLedgerTrees } from "../lifecycle/git.js";
56
+ import { signOpRunRecord, verifyOpRunStatement } from "./trust/op-run-statement.js";
33
57
  import { gitTop } from "./tree.js";
34
58
  import { locateWorkspace } from "./which-chant.js";
59
+ import { gitRevisionSource } from "./record-source.js";
60
+ import { emptyPolicy, readTrustPolicy } from "./trust/policy.js";
61
+ import { loadRunnerKey } from "./trust/dsse.js";
62
+ import { signCiTagRecord, verifyCiTagStatement, CI_TAG_SIGNER_ROLE } from "./trust/ci-tag-statement.js";
63
+ /** The environment variable a tick (#4031) and `chant run` (#4044) read a runner key from, as PEM, when `--key` is not given. */
64
+ export const RUNNER_KEY_ENV = "CHANT_RUNNER_KEY";
35
65
  export const CI_GREEN_TAG_PREFIX = "ci/green/";
36
66
  export const CI_REVOKED_TAG_PREFIX = "ci/revoked/";
37
67
  export const CI_LAST_GREEN_CONTRACT_VERSION = 1;
@@ -80,7 +110,7 @@ export function evaluatePhase(phase, latest) {
80
110
  for (const r of matched) {
81
111
  if (runs.some((x) => x.name === r.name))
82
112
  continue;
83
- runs.push({ name: r.name, id: r.id, status: r.status, conclusion: r.conclusion, url: r.url, verdict: runVerdict(phase, r) });
113
+ runs.push({ name: r.name, id: r.id, status: r.status, conclusion: r.conclusion, url: r.url, verdict: runVerdict(phase, r), ...(r.opRun ? { opRun: r.opRun } : {}) });
84
114
  }
85
115
  }
86
116
  const verdict = runs.some((r) => r.verdict === "fail")
@@ -108,23 +138,36 @@ export function windowMs(window) {
108
138
  throw new Error(`ci.green.window must be a number of minutes, hours or days, such as 24h, not ${JSON.stringify(window)}`);
109
139
  return Number(m[1]) * { m: 60000, h: 3600000, d: 86400000 }[m[2]];
110
140
  }
111
- const runRecord = (r) => ({ name: r.name, id: r.id, conclusion: r.conclusion, url: r.url });
112
- /** The annotation of a green tag: a subject line, then the record as JSON. */
113
- export function greenAnnotation(green, sha, verdict, at) {
141
+ /**
142
+ * A run as a tag's record holds it. An Op run (#4044) has no forge id; it
143
+ * names the Op run's id, env and signer, and carries the run's statement.
144
+ */
145
+ const runRecord = (r) => r.opRun
146
+ ? { name: r.name, id: null, conclusion: r.conclusion, url: r.url, opRun: { id: r.opRun.id, env: r.opRun.env, signer: r.opRun.signer, statement: r.opRun.statement } }
147
+ : { name: r.name, id: r.id, conclusion: r.conclusion, url: r.url };
148
+ /** `source: "runs"` on a record made from chant runs (#4044); nothing for check runs, so their records read as before. */
149
+ const sourceField = (green) => (green.source === "runs" ? { source: "runs" } : {});
150
+ /** The record as JSON, with the statement that signs it when a signer is given (#4031). */
151
+ function recordJson(record, signer) {
152
+ return JSON.stringify(signer ? { ...record, statement: signCiTagRecord(record, signer) } : record, null, 2);
153
+ }
154
+ /** The annotation of a green tag: a subject line, then the record as JSON, signed when `signer` is given (#4031). */
155
+ export function greenAnnotation(green, sha, verdict, at, signer) {
114
156
  const record = {
115
157
  schema: 1,
116
158
  tag: "green",
117
159
  commit: sha,
118
160
  branch: green.branch,
119
161
  at: iso(at),
162
+ ...sourceField(green),
120
163
  phases: Object.fromEntries(verdict.phases.map((p) => [p.phase, p.runs.map(runRecord)])),
121
164
  };
122
- return `ci: every required phase passed (${green.require.join(", ")})\n\n${JSON.stringify(record, null, 2)}\n`;
165
+ return `ci: every required phase passed (${green.require.join(", ")})\n\n${recordJson(record, signer)}\n`;
123
166
  }
124
- /** The annotation of a revoked tag: the phase and the run that failed, and the time. */
125
- export function revokedAnnotation(green, sha, failure, at) {
126
- const record = { schema: 1, tag: "revoked", commit: sha, branch: green.branch, at: iso(at), phase: failure.phase, run: runRecord(failure.run) };
127
- return `ci: phase ${failure.phase} failed in ${failure.run.name} (${failure.run.conclusion ?? failure.run.status})\n\n${JSON.stringify(record, null, 2)}\n`;
167
+ /** The annotation of a revoked tag: the phase and the run that failed, and the time, signed when `signer` is given (#4031). */
168
+ export function revokedAnnotation(green, sha, failure, at, signer) {
169
+ const record = { schema: 1, tag: "revoked", commit: sha, branch: green.branch, at: iso(at), ...sourceField(green), phase: failure.phase, run: runRecord(failure.run) };
170
+ return `ci: phase ${failure.phase} failed in ${failure.run.name} (${failure.run.conclusion ?? failure.run.status})\n\n${recordJson(record, signer)}\n`;
128
171
  }
129
172
  /**
130
173
  * Decide what a tick does, without touching git: a commit with no green tag
@@ -137,18 +180,22 @@ export async function planTick(input) {
137
180
  for (const c of input.commits) {
138
181
  const before = input.revokedTags.has(c.sha) ? "revoked" : input.greenTags.has(c.sha) ? "green" : "none";
139
182
  if (before === "revoked") {
140
- out.push({ ...c, before, made: null });
183
+ const ignored = input.ignored?.get(c.sha);
184
+ out.push({ ...c, before, made: null, ...(ignored && ignored.length > 0 ? { ignored: [...ignored] } : {}) });
141
185
  continue;
142
186
  }
143
187
  const verdict = evaluateCommit(input.green, await input.checkRuns(c.sha));
144
188
  let made = null;
145
189
  if (before === "none" && verdict.verdict === "pass") {
146
- made = { tag: `${CI_GREEN_TAG_PREFIX}${c.sha}`, message: greenAnnotation(input.green, c.sha, verdict, input.now) };
190
+ made = { tag: `${CI_GREEN_TAG_PREFIX}${c.sha}`, message: greenAnnotation(input.green, c.sha, verdict, input.now, input.signer) };
147
191
  }
148
192
  else if (before === "green" && verdict.verdict === "fail") {
149
- made = { tag: `${CI_REVOKED_TAG_PREFIX}${c.sha}`, message: revokedAnnotation(input.green, c.sha, verdict.failure, input.now) };
193
+ made = { tag: `${CI_REVOKED_TAG_PREFIX}${c.sha}`, message: revokedAnnotation(input.green, c.sha, verdict.failure, input.now, input.signer) };
150
194
  }
151
- out.push({ ...c, before, verdict, made });
195
+ const ignored = input.ignored?.get(c.sha);
196
+ // A tag is never moved or deleted by chant, so an ignored tag holding the name stops the tick making it.
197
+ const blocked = made !== null && ignored?.some((t) => t.tag === made.tag) ? made.tag : undefined;
198
+ out.push({ ...c, before, verdict, made: blocked ? null : made, ...(ignored && ignored.length > 0 ? { ignored: [...ignored] } : {}), ...(blocked ? { blocked } : {}) });
152
199
  }
153
200
  return out;
154
201
  }
@@ -169,19 +216,91 @@ function tryGit(top, args) {
169
216
  return null;
170
217
  }
171
218
  }
172
- /** The ci tags in the repository: the commits with a green tag, and those with a revoked one. */
173
- export function readCiTags(top) {
174
- const green = new Map();
175
- const revoked = new Set();
176
- const lines = git(top, ["for-each-ref", "--format=%(refname:lstrip=2)%09%(taggerdate:iso-strict)", `refs/tags/${CI_GREEN_TAG_PREFIX}`, `refs/tags/${CI_REVOKED_TAG_PREFIX}`]);
177
- for (const line of lines.split("\n").filter(Boolean)) {
178
- const [tag, date] = line.split("\t");
179
- if (tag.startsWith(CI_GREEN_TAG_PREFIX))
180
- green.set(tag.slice(CI_GREEN_TAG_PREFIX.length), date ?? "");
219
+ /** Every ci tag in the repository, read from local refs. */
220
+ function readCiTagRefs(top) {
221
+ const out = [];
222
+ const format = "%(refname:lstrip=2)%00%(objecttype)%00%(objectname)%00%(*objectname)%00%(taggerdate:iso-strict)%00%(contents)%01";
223
+ const raw = git(top, ["for-each-ref", `--format=${format}`, `refs/tags/${CI_GREEN_TAG_PREFIX}`, `refs/tags/${CI_REVOKED_TAG_PREFIX}`]);
224
+ for (const entry of raw.split("\u0001")) {
225
+ const [tag, type, object, peeled, date, contents] = entry.replace(/^\n/, "").split("\u0000");
226
+ if (!tag)
227
+ continue;
228
+ const annotated = type === "tag";
229
+ let record = null;
230
+ // A lightweight tag has no tagger date, and its "contents" are the commit's message.
231
+ if (annotated && date && contents && contents.includes("{")) {
232
+ try {
233
+ const parsed = JSON.parse(contents.slice(contents.indexOf("{")));
234
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
235
+ record = parsed;
236
+ }
237
+ catch {
238
+ record = null;
239
+ }
240
+ }
241
+ const green = tag.startsWith(CI_GREEN_TAG_PREFIX);
242
+ out.push({
243
+ kind: green ? "green" : "revoked",
244
+ sha: tag.slice((green ? CI_GREEN_TAG_PREFIX : CI_REVOKED_TAG_PREFIX).length),
245
+ tag,
246
+ tagged: date || null,
247
+ record,
248
+ target: (annotated ? peeled : object) ?? "",
249
+ });
250
+ }
251
+ return out;
252
+ }
253
+ /**
254
+ * The commits whose tags count unsigned under `signedSince` (#4031): its
255
+ * strict first-parent ancestors, the history the green branch had before the
256
+ * first commit whose tags must be signed. Position in history is something a
257
+ * committer can't set, unlike a commit's or a tag's date (ws-069 d). Throws
258
+ * `declaration-invalid` when `signedSince` names no commit here.
259
+ */
260
+ export function unsignedHistory(top, signedSince) {
261
+ if (tryGit(top, ["rev-parse", "--verify", "--quiet", `${signedSince}^{commit}`]) === null) {
262
+ throw new WorkspaceReadError("declaration-invalid", `ci.green.signedSince names ${signedSince}, which is not a commit in this repository; it must be the full sha of the first commit on the green branch whose tags must be signed (fetch the branch if it is newer than this clone)`);
263
+ }
264
+ return new Set(git(top, ["rev-list", "--first-parent", `${signedSince}^@`]).split("\n").filter(Boolean));
265
+ }
266
+ /**
267
+ * Read the ci tags and judge each one (#4031). Without `signedSince` in
268
+ * `green` every tag counts, as before. With it, a tag on a strict
269
+ * first-parent ancestor of `signedSince` counts unsigned, and any other tag
270
+ * counts only when it points at the commit its name gives and its annotation
271
+ * carries a statement a runner key at `base` signed for that commit, tag and
272
+ * branch. The runner keys and roles are read from `.chant/trust.json` at
273
+ * `base`, the commit the caller reads the branch at. Throws
274
+ * `declaration-invalid` when `signedSince` names no commit here.
275
+ */
276
+ export function readCiTags(top, green = null, base = null) {
277
+ const out = { green: new Map(), revoked: new Map(), ignored: new Map() };
278
+ const before = green?.signedSince ? unsignedHistory(top, green.signedSince) : null;
279
+ let policy;
280
+ const policyAtBase = () => (policy ?? (policy = base ? readTrustPolicy(gitRevisionSource(top, base), base) : emptyPolicy(null)));
281
+ for (const t of readCiTagRefs(top)) {
282
+ const counted = (signer) => (t.kind === "green" ? out.green : out.revoked).set(t.sha, { tagged: t.tagged, record: t.record, signer });
283
+ if (before === null) {
284
+ counted(null);
285
+ continue;
286
+ }
287
+ const ignore = (status, reason) => out.ignored.set(t.sha, [...(out.ignored.get(t.sha) ?? []), { tag: t.tag, status, reason }]);
288
+ if (t.target !== t.sha) {
289
+ ignore("mismatch", `${t.tag} points at ${t.target.slice(0, 12) || "nothing"}, not at the commit its name gives`);
290
+ continue;
291
+ }
292
+ if (before.has(t.sha)) {
293
+ counted(null);
294
+ continue;
295
+ }
296
+ const p = policyAtBase();
297
+ const v = verifyCiTagStatement(t.record, { sha: t.sha, kind: t.kind, branch: green.branch }, p.runners, p.roles);
298
+ if (v.status === "signed")
299
+ counted(v.signer);
181
300
  else
182
- revoked.add(tag.slice(CI_REVOKED_TAG_PREFIX.length));
301
+ ignore(v.status, `${v.reason}; ci.green.signedSince is ${green.signedSince}, so a tag on a commit that is not a strict first-parent ancestor of it needs one`);
183
302
  }
184
- return { green, revoked };
303
+ return out;
185
304
  }
186
305
  /** The declaration's `ci.green`, read where `cwd` is, and the git top. */
187
306
  function readGreen(cwd) {
@@ -195,13 +314,44 @@ function readGreen(cwd) {
195
314
  throw new WorkspaceReadError("not-a-git-repository", "the ci tags are git tags, and this directory is not in a git repository");
196
315
  return { green, top, workspace: { name: declaration.name, root: located.root, file: declaration.file } };
197
316
  }
317
+ /**
318
+ * The signer a runner key makes (#4031): the principal `.chant/trust.json`
319
+ * at `base` lists the key for, who must hold the `runner` role. Throws when
320
+ * the key is not one, so a tick never makes a tag its readers would ignore.
321
+ */
322
+ export function ciTagSigner(top, base, pem, what = "the tags it signed") {
323
+ let loaded;
324
+ try {
325
+ loaded = loadRunnerKey(pem);
326
+ }
327
+ catch (err) {
328
+ throw new Error(`the runner key is not an Ed25519 private key in PEM: ${err instanceof Error ? err.message : String(err)}`);
329
+ }
330
+ const policy = readTrustPolicy(gitRevisionSource(top, base), base);
331
+ const at = `.chant/trust.json at ${base.slice(0, 12)}`;
332
+ if (policy.signers.some((s) => s.key === loaded.publicKey)) {
333
+ throw new Error(`the runner key is a signer's key in ${policy.signersPath}; ci tags and runs are signed by a CI identity's key, never by a person's`);
334
+ }
335
+ const listed = policy.runners.find((r) => r.key === loaded.publicKey);
336
+ if (!listed)
337
+ throw new Error(`${at} lists no runner with this key, so ${what} would be ignored. Add it under "runners" first:\n ${loaded.publicKey}`);
338
+ const holds = (policy.roles[CI_TAG_SIGNER_ROLE] ?? []).some((p) => p.normalize("NFKC").trim().toLowerCase() === listed.principal.normalize("NFKC").trim().toLowerCase());
339
+ if (!holds)
340
+ throw new Error(`${listed.principal} holds no ${CI_TAG_SIGNER_ROLE} role in ${at}, so ${what} would be ignored. Grant it: "roles": { "${CI_TAG_SIGNER_ROLE}": ["${listed.principal}"] }`);
341
+ return { principal: listed.principal, key: loaded.key, publicKey: loaded.publicKey };
342
+ }
198
343
  /**
199
344
  * One tick: fetch the branch and the ci tags from the remote (pruning a ci
200
345
  * tag deleted there by hand), judge the first-parent commits within the
201
346
  * window, make the tags that are due and push them, never forced.
202
347
  */
203
348
  export async function ciTick(req) {
204
- const { green, top } = readGreen(req.cwd);
349
+ const read = readGreen(req.cwd);
350
+ const top = read.top;
351
+ const green = { ...read.green, source: req.source ?? read.green.source };
352
+ const forge = green.source === "checks" ? (typeof req.forge === "function" ? req.forge() : req.forge) : undefined;
353
+ if (green.source === "checks" && !forge)
354
+ throw new Error("ci.green.source is checks, so the tick reads check runs and needs a forge");
205
355
  const remote = req.remote ?? "origin";
206
356
  const now = req.now ?? new Date();
207
357
  const tracking = `refs/remotes/${remote}/${green.branch}`;
@@ -221,15 +371,45 @@ export async function ciTick(req) {
221
371
  .map((line) => line.split(" "))
222
372
  .filter(([, ct]) => Number(ct) >= cutoff)
223
373
  .map(([sha, ct]) => ({ sha, committed: iso(new Date(Number(ct) * 1000)) }));
224
- const tags = readCiTags(top);
374
+ const base = git(top, ["rev-parse", `${tracking}^{commit}`]);
375
+ if (green.signedSince && req.runnerKey === undefined && !req.dryRun) {
376
+ throw new Error(`ci.green.signedSince is ${green.signedSince}, so every tag the tick makes needs a statement signed by a runner key, and none was given: pass --key <runner key.pem> or set CHANT_RUNNER_KEY`);
377
+ }
378
+ if (green.source === "runs" && req.runnerKey === undefined && !req.dryRun) {
379
+ throw new Error("the tick reads chant runs (source runs), and every tag it makes from them is signed by a runner key, and none was given: pass --key <runner key.pem> or set CHANT_RUNNER_KEY");
380
+ }
381
+ const signer = req.runnerKey !== undefined ? ciTagSigner(top, base, req.runnerKey) : undefined;
382
+ const links = await commitLinks(req.cwd, top, tracking, commits.map((c) => c.sha));
383
+ const tags = readCiTags(top, green, base);
384
+ let runs;
385
+ if (green.source === "runs") {
386
+ // The runs live on chant/lifecycle, which a CI checkout may not have yet.
387
+ const fetched = await fetchLifecycleStatus({ cwd: top });
388
+ if (fetched.status === "diverged" || fetched.status === "unreachable") {
389
+ throw new Error(`could not fetch chant/lifecycle, where the Op runs are recorded, from ${fetched.remote}: ${fetched.status === "diverged" ? "the local branch and the remote's have diverged" : fetched.stderr}`);
390
+ }
391
+ runs = await readOpRunSource(req.cwd, top, base);
392
+ }
225
393
  const planned = await planTick({
226
394
  green,
227
395
  commits,
228
396
  greenTags: new Set(tags.green.keys()),
229
- revokedTags: tags.revoked,
230
- checkRuns: (sha) => req.forge.checkRuns(sha),
397
+ revokedTags: new Set(tags.revoked.keys()),
398
+ ignored: tags.ignored,
399
+ checkRuns: async (sha) => (runs ? runs.checkRuns(sha) : forge.checkRuns(sha)),
231
400
  now,
401
+ signer,
232
402
  });
403
+ if (links)
404
+ for (const c of planned)
405
+ c.work = links.get(c.sha);
406
+ if (runs) {
407
+ for (const c of planned) {
408
+ const ignored = runs.ignored.get(c.sha);
409
+ if (ignored && ignored.length > 0)
410
+ c.ignoredRuns = ignored;
411
+ }
412
+ }
233
413
  const due = planned.filter((c) => c.made !== null);
234
414
  if (!req.dryRun && due.length > 0) {
235
415
  const ident = taggerConfig(top);
@@ -245,7 +425,72 @@ export async function ciTick(req) {
245
425
  throw new Error(`could not push ${due.map((c) => c.made.tag).join(", ")} to ${remote}: ${cause ? `${cause}\n` : ""}${stderr}`);
246
426
  }
247
427
  }
248
- return { branch: green.branch, window: green.window, ref: tracking, commits: planned, made: due.map((c) => c.made.tag) };
428
+ return { branch: green.branch, window: green.window, ref: tracking, commits: planned, made: due.map((c) => c.made.tag), signer: signer?.principal ?? null, source: green.source };
429
+ }
430
+ const RUN_LEDGER_FILE = /^runs__(.+)\.jsonl$/;
431
+ /**
432
+ * An Op run as a check run (#4044): `ok` passes and `fail` fails once the
433
+ * run has settled; a run stopped at a gate or a decision point is still
434
+ * going. The id is the run's end time in milliseconds, so the latest run of
435
+ * an Op is its latest attempt.
436
+ */
437
+ export function opRunCheckRun(record, signer) {
438
+ const settled = record.status === "ok" || record.status === "fail";
439
+ const ended = Date.parse(record.ended);
440
+ return {
441
+ id: Number.isFinite(ended) ? ended : 0,
442
+ name: record.op,
443
+ status: settled ? "completed" : record.status,
444
+ conclusion: settled ? (record.status === "ok" ? "success" : "failure") : null,
445
+ completedAt: settled ? record.ended : null,
446
+ url: record.runOrigin?.url ?? null,
447
+ opRun: { id: record.id, env: record.env, signer, statement: record.statement },
448
+ };
449
+ }
450
+ /**
451
+ * Judge every Op run record that names a commit against `.chant/trust.json`
452
+ * at `base` (#4044), from the run ledger the project at `cwd` writes to. A
453
+ * record with a valid statement becomes a check run on its `gitSha`; any
454
+ * other is kept as an ignored run. Reads local refs only.
455
+ */
456
+ export async function readOpRunSource(cwd, top, base) {
457
+ const policy = readTrustPolicy(gitRevisionSource(top, base), base);
458
+ const byCommit = new Map();
459
+ const ignored = new Map();
460
+ for (const env of await listLedgerTrees({ cwd })) {
461
+ for (const file of await listFilesInDir(env, { cwd })) {
462
+ const op = RUN_LEDGER_FILE.exec(file)?.[1];
463
+ if (!op)
464
+ continue;
465
+ const { records } = await readRunLedger(env, op, { cwd });
466
+ for (const record of records) {
467
+ if (!record.gitSha)
468
+ continue;
469
+ const v = verifyOpRunStatement(record, policy.runners, policy.roles);
470
+ if (v.status === "signed")
471
+ byCommit.set(record.gitSha, [...(byCommit.get(record.gitSha) ?? []), opRunCheckRun(record, v.signer)]);
472
+ else
473
+ ignored.set(record.gitSha, [...(ignored.get(record.gitSha) ?? []), { run: record.id, op: record.op, env: record.env, status: v.status, reason: v.reason }]);
474
+ }
475
+ }
476
+ }
477
+ return { checkRuns: (sha) => byCommit.get(sha) ?? [], ignored };
478
+ }
479
+ /**
480
+ * What `chant run <op> --key` signs a run's record with (#4044): the runner
481
+ * key `.chant/trust.json` at the checkout's `HEAD` lists for a principal
482
+ * holding the `runner` role. Throws when the key is not one, or `cwd` is in
483
+ * no git checkout.
484
+ */
485
+ export function opRunSigner(cwd, pem) {
486
+ const top = gitTop(cwd);
487
+ if (!top)
488
+ throw new Error("a run is signed against .chant/trust.json at HEAD, and this directory is not in a git repository");
489
+ const head = tryGit(top, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
490
+ if (head === null)
491
+ throw new Error("a run is signed against .chant/trust.json at HEAD, and this repository has no commit yet");
492
+ const signer = ciTagSigner(top, head, pem, "the runs it signed");
493
+ return (record) => signOpRunRecord(record, signer);
249
494
  }
250
495
  /**
251
496
  * Why the remote refused a tag push, when chant can tell. GitHub refuses a
@@ -272,11 +517,41 @@ function stderrOf(err) {
272
517
  const e = err;
273
518
  return String(e.stderr ?? e.message ?? err).trim();
274
519
  }
520
+ /**
521
+ * Whether each commit names its work item (#4029), judged against the work
522
+ * records at `ref`. Undefined when the workspace declares no work kind or
523
+ * they can't be read: then no commit has a link state to report.
524
+ */
525
+ async function commitLinks(cwd, top, ref, shas) {
526
+ if (shas.length === 0)
527
+ return undefined;
528
+ try {
529
+ const index = await readWorkIndex(cwd, top, ref);
530
+ if (!index)
531
+ return undefined;
532
+ return judgeCommits(top, shas, index, mergeCommits(top, shas));
533
+ }
534
+ catch {
535
+ // A tag and a green list stand without the link, which only explains a commit.
536
+ return undefined;
537
+ }
538
+ }
539
+ /** Why a commit's work item is unknown, for the text of `ci tick` and `ci list`; undefined for a commit that names one. */
540
+ export function whyWorkUnknown(work) {
541
+ if (!work)
542
+ return undefined;
543
+ if (work.state === "unlinked")
544
+ return "no Chant-Record or Chant-Lease trailer names a work item";
545
+ if (work.state === "wrong")
546
+ return `the work item named is not there: ${work.problems.map((p) => `${p.trailer} (${p.message})`).join("; ")}`;
547
+ return undefined;
548
+ }
275
549
  /** The text `chant ci tick` prints. */
276
550
  export function formatTick(r, dryRun = false) {
277
- const lines = [`${r.branch}, commits within ${r.window}: ${r.commits.length}`];
551
+ const lines = [`${r.branch}, commits within ${r.window}${r.source === "runs" ? ", judged from chant runs" : ""}: ${r.commits.length}`];
278
552
  for (const c of r.commits) {
279
553
  const short = c.sha.slice(0, 12);
554
+ const before = lines.length;
280
555
  if (c.made) {
281
556
  lines.push(` ${short} ${dryRun ? "would make" : "made"} ${c.made.tag.slice(0, c.made.tag.lastIndexOf("/"))}${c.verdict?.failure ? `: phase ${c.verdict.failure.phase}, ${c.verdict.failure.run.name} ${c.verdict.failure.run.conclusion}` : ""}`);
282
557
  }
@@ -289,9 +564,29 @@ export function formatTick(r, dryRun = false) {
289
564
  const what = c.before === "green" ? "green" : v.verdict === "fail" ? `failed: phase ${v.failure.phase}, ${v.failure.run.name} ${v.failure.run.conclusion}` : `undecided: ${waiting.join(", ")}`;
290
565
  lines.push(` ${short} ${what}`);
291
566
  }
567
+ const why = whyWorkUnknown(c.work);
568
+ if (why)
569
+ lines[before] += `; why unknown: ${why}`;
570
+ for (const t of c.ignored ?? [])
571
+ lines.push(` ignored ${t.tag} (${t.status}): ${t.reason}`);
572
+ for (const r of c.ignoredRuns ?? [])
573
+ lines.push(` ignored run ${r.run} of ${r.op} in ${r.env} (${r.status}): ${r.reason}`);
574
+ if (c.blocked)
575
+ lines.push(` did not make ${c.blocked}: an ignored tag holds the name. Delete it to let the tick tag the commit: git push origin :refs/tags/${c.blocked}`);
292
576
  }
577
+ if (r.signer && r.made.length > 0)
578
+ lines.push(`signed as ${r.signer}`);
293
579
  return lines.join("\n");
294
580
  }
581
+ // ── last-green ───────────────────────────────────────────────────────────────
582
+ /** The ref a reader walks: the remote-tracking branch when there is one, else the local branch. */
583
+ function branchRefOf(top, green, remote = "origin") {
584
+ const candidates = [`refs/remotes/${remote}/${green.branch}`, `refs/heads/${green.branch}`];
585
+ const ref = candidates.find((r) => tryGit(top, ["rev-parse", "--verify", "--quiet", `${r}^{commit}`]) !== null);
586
+ if (!ref)
587
+ throw new CiGreenError("ci-branch-unknown", `this repository has neither ${candidates.join(" nor ")}`);
588
+ return ref;
589
+ }
295
590
  /**
296
591
  * `chant ci last-green`: the newest first-parent commit of the branch with a
297
592
  * green tag and no revoked tag. It reads local refs and tags and never
@@ -301,21 +596,28 @@ export function lastGreen(req) {
301
596
  const head = { $schema: CI_LAST_GREEN_SCHEMA_ID, contract: CI_LAST_GREEN_CONTRACT_VERSION, chant: readerVersion() };
302
597
  try {
303
598
  const { green, top, workspace } = readGreen(req.cwd);
304
- const candidates = [`refs/remotes/${req.remote ?? "origin"}/${green.branch}`, `refs/heads/${green.branch}`];
305
- const ref = candidates.find((r) => tryGit(top, ["rev-parse", "--verify", "--quiet", `${r}^{commit}`]) !== null);
306
- if (!ref)
307
- throw new CiGreenError("ci-branch-unknown", `this repository has neither ${candidates.join(" nor ")}`);
308
- const tags = readCiTags(top);
599
+ const branchRef = branchRefOf(top, green, req.remote);
600
+ let ref = branchRef;
601
+ if (req.from !== undefined) {
602
+ if (req.from.startsWith("-") || tryGit(top, ["rev-parse", "--verify", "--quiet", `${req.from}^{commit}`]) === null) {
603
+ throw new WorkspaceReadError("revision-unknown", `--from ${req.from} names no commit in this repository`);
604
+ }
605
+ ref = req.from;
606
+ }
607
+ // The runner keys are read where the branch is, whichever commit the walk starts from.
608
+ const tags = readCiTags(top, green, git(top, ["rev-parse", `${branchRef}^{commit}`]));
309
609
  let commit = null;
310
- if (tags.green.size > 0) {
610
+ const ignored = [];
611
+ if (tags.green.size > 0 || tags.ignored.size > 0) {
311
612
  for (const sha of git(top, ["rev-list", "--first-parent", ref]).split("\n")) {
613
+ ignored.push(...(tags.ignored.get(sha) ?? []));
312
614
  if (!tags.green.has(sha) || tags.revoked.has(sha))
313
615
  continue;
314
- commit = { sha, tag: `${CI_GREEN_TAG_PREFIX}${sha}`, tagged: tags.green.get(sha) || null, committed: git(top, ["show", "-s", "--format=%cI", sha]) };
616
+ commit = { sha, tag: `${CI_GREEN_TAG_PREFIX}${sha}`, tagged: tags.green.get(sha).tagged, committed: git(top, ["show", "-s", "--format=%cI", sha]) };
315
617
  break;
316
618
  }
317
619
  }
318
- return { ...head, workspace, branch: green.branch, ref, commit };
620
+ return { ...head, workspace, branch: green.branch, ref, commit, ignored };
319
621
  }
320
622
  catch (err) {
321
623
  if (err instanceof CiGreenError || err instanceof WorkspaceReadError)
@@ -323,3 +625,112 @@ export function lastGreen(req) {
323
625
  throw err;
324
626
  }
325
627
  }
628
+ // ── list ─────────────────────────────────────────────────────────────────────
629
+ export const CI_LIST_CONTRACT_VERSION = 1;
630
+ export const CI_LIST_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/ci-list/v1/ci-list.schema.json";
631
+ const asRun = (r) => {
632
+ if (!r || typeof r !== "object")
633
+ return null;
634
+ const o = r;
635
+ if (typeof o.name !== "string")
636
+ return null;
637
+ const op = o.opRun;
638
+ const opRun = op && typeof op === "object" && typeof op.id === "string" && typeof op.env === "string" && typeof op.signer === "string" ? { opRun: { id: op.id, env: op.env, signer: op.signer } } : {};
639
+ return { name: o.name, id: typeof o.id === "number" ? o.id : null, conclusion: typeof o.conclusion === "string" ? o.conclusion : null, url: typeof o.url === "string" ? o.url : null, ...opRun };
640
+ };
641
+ /** A judged run as `ci list` shows it. */
642
+ const listRun = (r) => ({ name: r.name, id: r.opRun ? null : r.id, conclusion: r.conclusion, url: r.url, ...(r.opRun ? { opRun: { id: r.opRun.id, env: r.opRun.env, signer: r.opRun.signer } } : {}) });
643
+ /** What the green tag's annotation recorded for a phase: its runs, all of which passed when the tag was made. */
644
+ function recordedPhase(green, phase) {
645
+ const runs = green?.record?.phases?.[phase];
646
+ if (!Array.isArray(runs))
647
+ return { phase, result: "unrecorded", runs: [] };
648
+ return { phase, result: "pass", runs: runs.map(asRun).filter((r) => r !== null) };
649
+ }
650
+ /**
651
+ * `chant ci list`: the branch's first-parent commits within the window, each
652
+ * with its state from the ci tags and each required phase's result. Tags
653
+ * give the green and revoked results offline; only undecided commits need
654
+ * the forge, and only when a `forge` is given. It never fetches.
655
+ */
656
+ export async function ciList(req) {
657
+ const head = { $schema: CI_LIST_SCHEMA_ID, contract: CI_LIST_CONTRACT_VERSION, chant: readerVersion() };
658
+ try {
659
+ const { green, top, workspace } = readGreen(req.cwd);
660
+ const ref = branchRefOf(top, green, req.remote);
661
+ const logArgs = ["log", "--first-parent", "--format=%H %ct"];
662
+ let cutoff = 0;
663
+ if (req.since !== undefined) {
664
+ if (tryGit(top, ["rev-parse", "--verify", "--quiet", `${req.since}^{commit}`]) === null)
665
+ throw new WorkspaceReadError("revision-unknown", `--since ${req.since} names no commit in this repository`);
666
+ logArgs.push(`${req.since}..${ref}`);
667
+ }
668
+ else {
669
+ cutoff = Math.floor(((req.now ?? new Date()).getTime() - windowMs(green.window)) / 1000);
670
+ logArgs.push(`--max-age=${cutoff}`, ref);
671
+ }
672
+ let commits = git(top, logArgs)
673
+ .split("\n")
674
+ .filter(Boolean)
675
+ .map((line) => line.split(" "))
676
+ .filter(([, ct]) => Number(ct) >= cutoff)
677
+ .map(([sha, ct]) => ({ sha, committed: iso(new Date(Number(ct) * 1000)) }));
678
+ if (req.limit !== undefined)
679
+ commits = commits.slice(0, req.limit);
680
+ const links = await commitLinks(req.cwd, top, ref, commits.map((c) => c.sha));
681
+ const base = git(top, ["rev-parse", `${ref}^{commit}`]);
682
+ const tags = readCiTags(top, green, base);
683
+ const source = req.source ?? green.source;
684
+ if (source === "runs" && req.forge)
685
+ throw new Error("--forge reads check runs, and the source is chant runs (ci.green.source or --source runs); drop one");
686
+ const runs = source === "runs" ? await readOpRunSource(req.cwd, top, base) : undefined;
687
+ const out = [];
688
+ for (const c of commits) {
689
+ const g = tags.green.get(c.sha);
690
+ const r = tags.revoked.get(c.sha);
691
+ const state = r ? "revoked" : g ? "green" : "undecided";
692
+ let phases = green.require.map((name) => recordedPhase(g, name));
693
+ let revoked = null;
694
+ if (r) {
695
+ const phase = typeof r.record?.phase === "string" ? r.record.phase : null;
696
+ const run = asRun(r.record?.run);
697
+ revoked = { tag: `${CI_REVOKED_TAG_PREFIX}${c.sha}`, tagged: r.tagged, phase, run, signer: r.signer };
698
+ phases = phases.map((p) => (p.phase === phase ? { phase: p.phase, result: "fail", runs: run ? [run] : [] } : p));
699
+ }
700
+ else if (!g && (runs || req.forge)) {
701
+ const verdict = evaluateCommit(green, runs ? runs.checkRuns(c.sha) : await req.forge.checkRuns(c.sha));
702
+ phases = verdict.phases.map((p) => ({ phase: p.phase, result: p.verdict, runs: p.runs.map(listRun) }));
703
+ }
704
+ out.push({
705
+ ...c,
706
+ state,
707
+ green: g ? { tag: `${CI_GREEN_TAG_PREFIX}${c.sha}`, tagged: g.tagged, signer: g.signer } : null,
708
+ revoked,
709
+ phases,
710
+ work: links?.get(c.sha) ?? null,
711
+ ignored: tags.ignored.get(c.sha) ?? [],
712
+ ...(runs ? { ignoredRuns: runs.ignored.get(c.sha) ?? [] } : {}),
713
+ });
714
+ }
715
+ return { ...head, workspace, branch: green.branch, ref, window: req.since === undefined ? green.window : null, since: req.since ?? null, require: [...green.require], commits: out };
716
+ }
717
+ catch (err) {
718
+ if (err instanceof CiGreenError || err instanceof WorkspaceReadError)
719
+ return { ...head, error: { code: err.code, message: err instanceof WorkspaceReadError ? err.describe() : err.message } };
720
+ throw err;
721
+ }
722
+ }
723
+ /** The text `chant ci list` prints: one line per commit. */
724
+ export function formatList(doc) {
725
+ const lines = [`${doc.branch}, ${doc.since ? `commits after ${doc.since}` : `commits within ${doc.window}`}: ${doc.commits.length}`];
726
+ for (const c of doc.commits) {
727
+ const phases = c.phases.map((p) => `${p.phase}=${p.result}`).join(" ");
728
+ const why = whyWorkUnknown(c.work);
729
+ lines.push(` ${c.sha.slice(0, 12)} ${c.state.padEnd(9)} ${phases}${why ? `; why unknown: ${why}` : ""}`);
730
+ for (const t of c.ignored)
731
+ lines.push(` ignored ${t.tag} (${t.status}): ${t.reason}`);
732
+ for (const r of c.ignoredRuns ?? [])
733
+ lines.push(` ignored run ${r.run} of ${r.op} in ${r.env} (${r.status}): ${r.reason}`);
734
+ }
735
+ return lines.join("\n");
736
+ }