@intentius/chant 0.124.39 → 0.124.41

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 (337) hide show
  1. package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
  2. package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
  3. package/dist/__fixtures__/oidc-emulator.js +89 -0
  4. package/dist/ci-handover.d.ts +117 -0
  5. package/dist/ci-handover.d.ts.map +1 -0
  6. package/dist/ci-handover.js +217 -0
  7. package/dist/ci-identity.d.ts +190 -0
  8. package/dist/ci-identity.d.ts.map +1 -0
  9. package/dist/ci-identity.js +219 -0
  10. package/dist/cli/component-endpoint.d.ts +20 -0
  11. package/dist/cli/component-endpoint.d.ts.map +1 -0
  12. package/dist/cli/component-endpoint.js +36 -0
  13. package/dist/cli/handlers/approve.d.ts +2 -1
  14. package/dist/cli/handlers/approve.d.ts.map +1 -1
  15. package/dist/cli/handlers/approve.js +20 -6
  16. package/dist/cli/handlers/build.d.ts.map +1 -1
  17. package/dist/cli/handlers/build.js +15 -1
  18. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  19. package/dist/cli/handlers/fan-out.js +26 -4
  20. package/dist/cli/handlers/gate-resume.d.ts +3 -0
  21. package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
  22. package/dist/cli/handlers/gate-resume.js +15 -2
  23. package/dist/cli/handlers/lock.d.ts +8 -0
  24. package/dist/cli/handlers/lock.d.ts.map +1 -0
  25. package/dist/cli/handlers/lock.js +197 -0
  26. package/dist/cli/handlers/pr-env.d.ts +34 -0
  27. package/dist/cli/handlers/pr-env.d.ts.map +1 -0
  28. package/dist/cli/handlers/pr-env.js +436 -0
  29. package/dist/cli/handlers/pr.d.ts +74 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +514 -58
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +16 -2
  34. package/dist/cli/handlers/query.d.ts +6 -0
  35. package/dist/cli/handlers/query.d.ts.map +1 -0
  36. package/dist/cli/handlers/query.js +80 -0
  37. package/dist/cli/handlers/run.d.ts +4 -1
  38. package/dist/cli/handlers/run.d.ts.map +1 -1
  39. package/dist/cli/handlers/run.js +17 -6
  40. package/dist/cli/main.d.ts.map +1 -1
  41. package/dist/cli/main.js +41 -2
  42. package/dist/cli/parse-args.d.ts.map +1 -1
  43. package/dist/cli/parse-args.js +31 -2
  44. package/dist/cli/registry.d.ts +12 -2
  45. package/dist/cli/registry.d.ts.map +1 -1
  46. package/dist/components/auto-release.d.ts +35 -3
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +29 -2
  49. package/dist/components/cli-support.d.ts +2 -0
  50. package/dist/components/cli-support.d.ts.map +1 -1
  51. package/dist/components/cli-support.js +2 -0
  52. package/dist/components/deploy-units.d.ts.map +1 -1
  53. package/dist/components/deploy-units.js +5 -0
  54. package/dist/components/fan-out-run.d.ts +28 -0
  55. package/dist/components/fan-out-run.d.ts.map +1 -1
  56. package/dist/components/fan-out-run.js +27 -3
  57. package/dist/components/pr-environment.d.ts +298 -0
  58. package/dist/components/pr-environment.d.ts.map +1 -0
  59. package/dist/components/pr-environment.js +406 -0
  60. package/dist/components/pr-hooks.d.ts +66 -0
  61. package/dist/components/pr-hooks.d.ts.map +1 -0
  62. package/dist/components/pr-hooks.js +119 -0
  63. package/dist/components/pr-pipeline.d.ts +72 -2
  64. package/dist/components/pr-pipeline.d.ts.map +1 -1
  65. package/dist/components/pr-pipeline.js +96 -5
  66. package/dist/components/pr-policy.d.ts +109 -0
  67. package/dist/components/pr-policy.d.ts.map +1 -0
  68. package/dist/components/pr-policy.js +150 -0
  69. package/dist/components/pr-run.d.ts.map +1 -1
  70. package/dist/components/pr-run.js +5 -3
  71. package/dist/components/promote.d.ts.map +1 -1
  72. package/dist/components/promote.js +39 -16
  73. package/dist/gate-decision.d.ts +15 -2
  74. package/dist/gate-decision.d.ts.map +1 -1
  75. package/dist/gate-decision.js +17 -2
  76. package/dist/gate-policy.d.ts +140 -0
  77. package/dist/gate-policy.d.ts.map +1 -0
  78. package/dist/gate-policy.js +242 -0
  79. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
  80. package/dist/lifecycle/__fixtures__/publish-fakes.js +6 -1
  81. package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
  82. package/dist/lifecycle/gate-checkpoint.js +1 -1
  83. package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
  84. package/dist/lifecycle/gate-seal.d.ts.map +1 -0
  85. package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
  86. package/dist/lifecycle/publish-declaration.js +4 -3
  87. package/dist/lifecycle/publish-npm.d.ts +2 -2
  88. package/dist/lifecycle/publish-npm.d.ts.map +1 -1
  89. package/dist/lifecycle/publish-npm.js +31 -8
  90. package/dist/lifecycle/query.d.ts +68 -0
  91. package/dist/lifecycle/query.d.ts.map +1 -0
  92. package/dist/lifecycle/query.js +246 -0
  93. package/dist/lifecycle/release-ledger.d.ts +31 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +19 -1
  96. package/dist/lifecycle/run-ledger.d.ts +13 -0
  97. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  98. package/dist/lifecycle/run-ledger.js +16 -0
  99. package/dist/lifecycle/status.d.ts +5 -1
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/lifecycle/status.js +10 -5
  102. package/dist/lifecycle/target-lock.d.ts +97 -0
  103. package/dist/lifecycle/target-lock.d.ts.map +1 -0
  104. package/dist/lifecycle/target-lock.js +286 -0
  105. package/dist/live-endpoint.d.ts +18 -0
  106. package/dist/live-endpoint.d.ts.map +1 -1
  107. package/dist/live-endpoint.js +27 -0
  108. package/dist/op/activities/activity-contracts.d.ts +30 -0
  109. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  110. package/dist/op/activities/activity-contracts.js +26 -0
  111. package/dist/op/activities/index.d.ts +2 -0
  112. package/dist/op/activities/index.d.ts.map +1 -1
  113. package/dist/op/activities/index.js +4 -0
  114. package/dist/op/activities/review-agent.d.ts +137 -0
  115. package/dist/op/activities/review-agent.d.ts.map +1 -0
  116. package/dist/op/activities/review-agent.js +266 -0
  117. package/dist/op/builders.d.ts +9 -0
  118. package/dist/op/builders.d.ts.map +1 -1
  119. package/dist/op/builders.js +11 -0
  120. package/dist/op/cron.d.ts +8 -0
  121. package/dist/op/cron.d.ts.map +1 -1
  122. package/dist/op/cron.js +51 -0
  123. package/dist/op/gate-approval.d.ts +20 -13
  124. package/dist/op/gate-approval.d.ts.map +1 -1
  125. package/dist/op/gate-approval.js +13 -18
  126. package/dist/op/gate-quorum.d.ts +131 -0
  127. package/dist/op/gate-quorum.d.ts.map +1 -0
  128. package/dist/op/gate-quorum.js +183 -0
  129. package/dist/op/gate-resume.d.ts +4 -2
  130. package/dist/op/gate-resume.d.ts.map +1 -1
  131. package/dist/op/gate-resume.js +5 -4
  132. package/dist/op/gate-review.js +1 -1
  133. package/dist/op/gate.d.ts +32 -4
  134. package/dist/op/gate.d.ts.map +1 -1
  135. package/dist/op/gate.js +40 -24
  136. package/dist/op/index.d.ts +6 -4
  137. package/dist/op/index.d.ts.map +1 -1
  138. package/dist/op/index.js +4 -3
  139. package/dist/op/local-executor.d.ts.map +1 -1
  140. package/dist/op/local-executor.js +9 -3
  141. package/dist/op/op-waves-run.d.ts +62 -1
  142. package/dist/op/op-waves-run.d.ts.map +1 -1
  143. package/dist/op/op-waves-run.js +190 -11
  144. package/dist/op/op-waves.d.ts +55 -3
  145. package/dist/op/op-waves.d.ts.map +1 -1
  146. package/dist/op/op-waves.js +63 -8
  147. package/dist/op/schedule-overdue.d.ts +57 -0
  148. package/dist/op/schedule-overdue.d.ts.map +1 -0
  149. package/dist/op/schedule-overdue.js +138 -0
  150. package/dist/override-gate.d.ts +2 -2
  151. package/dist/override-gate.d.ts.map +1 -1
  152. package/dist/override-gate.js +1 -1
  153. package/dist/pr-forge.d.ts +36 -0
  154. package/dist/pr-forge.d.ts.map +1 -1
  155. package/dist/pr-forge.js +82 -0
  156. package/dist/pr-loop.d.ts +90 -1
  157. package/dist/pr-loop.d.ts.map +1 -1
  158. package/dist/pr-loop.js +77 -3
  159. package/dist/pr-post.d.ts +72 -0
  160. package/dist/pr-post.d.ts.map +1 -0
  161. package/dist/pr-post.js +134 -0
  162. package/dist/wave-blocked.d.ts +79 -0
  163. package/dist/wave-blocked.d.ts.map +1 -0
  164. package/dist/wave-blocked.js +245 -0
  165. package/dist/wave-engine.d.ts +6 -0
  166. package/dist/wave-engine.d.ts.map +1 -1
  167. package/dist/wave-engine.js +2 -0
  168. package/dist/wave-runtime.mjs +239 -239
  169. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
  170. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
  171. package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
  172. package/dist/workspace/ci-branches.d.ts +51 -0
  173. package/dist/workspace/ci-branches.d.ts.map +1 -0
  174. package/dist/workspace/ci-branches.js +69 -0
  175. package/dist/workspace/ci-cloud-identity.d.ts +11 -0
  176. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
  177. package/dist/workspace/ci-cloud-identity.js +24 -0
  178. package/dist/workspace/ci-locks.d.ts +12 -0
  179. package/dist/workspace/ci-locks.d.ts.map +1 -0
  180. package/dist/workspace/ci-locks.js +6 -0
  181. package/dist/workspace/ci-run.d.ts +24 -1
  182. package/dist/workspace/ci-run.d.ts.map +1 -1
  183. package/dist/workspace/ci-run.js +85 -6
  184. package/dist/workspace/declaration.d.ts +63 -0
  185. package/dist/workspace/declaration.d.ts.map +1 -1
  186. package/dist/workspace/declaration.js +72 -1
  187. package/dist/workspace/declaration.schema.json +147 -0
  188. package/dist/workspace/identity-gate-rule.d.ts +1 -1
  189. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  190. package/dist/workspace/identity-gate-rule.js +1 -1
  191. package/dist/workspace/identity.d.ts +1 -1
  192. package/dist/workspace/identity.d.ts.map +1 -1
  193. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  194. package/dist/workspace/lineage-upgrade.js +8 -5
  195. package/dist/workspace/member-pipeline.d.ts +6 -0
  196. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  197. package/dist/workspace/member-pipeline.js +23 -0
  198. package/dist/workspace/pr-report.schema.json +73 -2
  199. package/dist/workspace/security.d.ts.map +1 -1
  200. package/dist/workspace/security.js +4 -0
  201. package/dist/workspace/status-gates.d.ts +3 -2
  202. package/dist/workspace/status-gates.d.ts.map +1 -1
  203. package/dist/workspace/status-gates.js +8 -7
  204. package/dist/workspace/status.d.ts +3 -1
  205. package/dist/workspace/status.d.ts.map +1 -1
  206. package/dist/workspace/status.js +7 -3
  207. package/dist/workspace/status.schema.json +1 -0
  208. package/dist/workspace/trust/seal.d.ts +2 -2
  209. package/dist/workspace/trust/seal.d.ts.map +1 -1
  210. package/dist/workspace/trust/seal.js +2 -2
  211. package/package.json +1 -1
  212. package/src/__fixtures__/oidc-emulator.ts +118 -0
  213. package/src/ci-handover.test.ts +209 -0
  214. package/src/ci-handover.ts +237 -0
  215. package/src/ci-identity.test.ts +201 -0
  216. package/src/ci-identity.ts +310 -0
  217. package/src/cli/component-endpoint.ts +43 -0
  218. package/src/cli/handlers/approve.ts +18 -5
  219. package/src/cli/handlers/build.ts +15 -1
  220. package/src/cli/handlers/fan-out.test.ts +10 -0
  221. package/src/cli/handlers/fan-out.ts +29 -4
  222. package/src/cli/handlers/gate-resume.ts +16 -2
  223. package/src/cli/handlers/lock.test.ts +98 -0
  224. package/src/cli/handlers/lock.ts +202 -0
  225. package/src/cli/handlers/operator.test.ts +3 -1
  226. package/src/cli/handlers/pr-endpoint.test.ts +169 -0
  227. package/src/cli/handlers/pr-env.ts +462 -0
  228. package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
  229. package/src/cli/handlers/pr.test.ts +184 -0
  230. package/src/cli/handlers/pr.ts +508 -64
  231. package/src/cli/handlers/promote.ts +25 -2
  232. package/src/cli/handlers/query.ts +78 -0
  233. package/src/cli/handlers/run.test.ts +2 -1
  234. package/src/cli/handlers/run.ts +16 -5
  235. package/src/cli/main.ts +41 -2
  236. package/src/cli/parse-args.ts +23 -2
  237. package/src/cli/registry.ts +12 -2
  238. package/src/components/auto-release-infra.test.ts +131 -0
  239. package/src/components/auto-release.test.ts +40 -1
  240. package/src/components/auto-release.ts +50 -4
  241. package/src/components/cli-support.ts +4 -0
  242. package/src/components/deploy-units.ts +5 -0
  243. package/src/components/fan-out-run.ts +39 -2
  244. package/src/components/fan-out-waves.test.ts +45 -1
  245. package/src/components/pr-environment.test.ts +249 -0
  246. package/src/components/pr-environment.ts +538 -0
  247. package/src/components/pr-hooks.test.ts +118 -0
  248. package/src/components/pr-hooks.ts +157 -0
  249. package/src/components/pr-pipeline.ts +130 -5
  250. package/src/components/pr-policy.test.ts +145 -0
  251. package/src/components/pr-policy.ts +211 -0
  252. package/src/components/pr-run.ts +5 -2
  253. package/src/components/promote.test.ts +39 -0
  254. package/src/components/promote.ts +38 -14
  255. package/src/gate-decision-bundle.test.ts +4 -3
  256. package/src/gate-decision.ts +39 -2
  257. package/src/gate-policy-bundle.test.ts +28 -0
  258. package/src/gate-policy.test.ts +162 -0
  259. package/src/gate-policy.ts +298 -0
  260. package/src/lifecycle/__fixtures__/publish-fakes.ts +6 -1
  261. package/src/lifecycle/gate-checkpoint.ts +1 -1
  262. package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
  263. package/src/lifecycle/publish-declaration.ts +4 -3
  264. package/src/lifecycle/publish-npm.ts +33 -8
  265. package/src/lifecycle/publish-targets.test.ts +36 -3
  266. package/src/lifecycle/query.test.ts +91 -0
  267. package/src/lifecycle/query.ts +315 -0
  268. package/src/lifecycle/release-ledger.ts +42 -1
  269. package/src/lifecycle/run-ledger.ts +18 -0
  270. package/src/lifecycle/status.test.ts +19 -0
  271. package/src/lifecycle/status.ts +14 -6
  272. package/src/lifecycle/target-lock.test.ts +195 -0
  273. package/src/lifecycle/target-lock.ts +347 -0
  274. package/src/live-endpoint.ts +31 -0
  275. package/src/op/activities/activity-contracts.ts +31 -0
  276. package/src/op/activities/index.ts +6 -0
  277. package/src/op/activities/review-agent.test.ts +240 -0
  278. package/src/op/activities/review-agent.ts +355 -0
  279. package/src/op/builders.ts +13 -0
  280. package/src/op/cron.ts +44 -0
  281. package/src/op/gate-alias.test.ts +2 -2
  282. package/src/op/gate-approval.test.ts +30 -9
  283. package/src/op/gate-approval.ts +23 -26
  284. package/src/op/gate-quorum.test.ts +139 -0
  285. package/src/op/gate-quorum.ts +248 -0
  286. package/src/op/gate-resume.ts +6 -3
  287. package/src/op/gate-review.ts +1 -1
  288. package/src/op/gate.ts +64 -23
  289. package/src/op/index.ts +13 -1
  290. package/src/op/local-executor.ts +9 -2
  291. package/src/op/op-waves-hook-targets.test.ts +126 -0
  292. package/src/op/op-waves-policy-hold.test.ts +66 -0
  293. package/src/op/op-waves-quorum.test.ts +101 -0
  294. package/src/op/op-waves-run.ts +233 -11
  295. package/src/op/op-waves.ts +83 -10
  296. package/src/op/schedule-overdue.test.ts +220 -0
  297. package/src/op/schedule-overdue.ts +161 -0
  298. package/src/override-gate-bundle.test.ts +1 -1
  299. package/src/override-gate.ts +2 -2
  300. package/src/pr-forge-bundle.test.ts +2 -2
  301. package/src/pr-forge.test.ts +33 -0
  302. package/src/pr-forge.ts +114 -0
  303. package/src/pr-loop-bundle.test.ts +2 -2
  304. package/src/pr-loop.test.ts +58 -0
  305. package/src/pr-loop.ts +146 -4
  306. package/src/pr-post.test.ts +141 -0
  307. package/src/pr-post.ts +159 -0
  308. package/src/wave-blocked-bundle.test.ts +25 -0
  309. package/src/wave-blocked.test.ts +146 -0
  310. package/src/wave-blocked.ts +271 -0
  311. package/src/wave-engine.ts +8 -0
  312. package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
  313. package/src/workspace/ci-branches.test.ts +62 -0
  314. package/src/workspace/ci-branches.ts +79 -0
  315. package/src/workspace/ci-cloud-identity.test.ts +53 -0
  316. package/src/workspace/ci-cloud-identity.ts +23 -0
  317. package/src/workspace/ci-locks.ts +16 -0
  318. package/src/workspace/ci-run-index.test.ts +2 -1
  319. package/src/workspace/ci-run.test.ts +133 -8
  320. package/src/workspace/ci-run.ts +92 -7
  321. package/src/workspace/declaration.schema.json +147 -0
  322. package/src/workspace/declaration.test.ts +76 -0
  323. package/src/workspace/declaration.ts +134 -2
  324. package/src/workspace/identity-gate-rule.ts +1 -1
  325. package/src/workspace/identity.ts +1 -1
  326. package/src/workspace/lineage-upgrade.ts +11 -8
  327. package/src/workspace/member-pipeline.test.ts +29 -0
  328. package/src/workspace/member-pipeline.ts +30 -0
  329. package/src/workspace/pr-report.schema.json +73 -2
  330. package/src/workspace/security.ts +4 -0
  331. package/src/workspace/status-gates.ts +9 -6
  332. package/src/workspace/status.schema.json +1 -0
  333. package/src/workspace/status.ts +11 -4
  334. package/src/workspace/trust/gate-seal.test.ts +1 -1
  335. package/src/workspace/trust/seal.ts +2 -2
  336. package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
  337. /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
@@ -54,9 +54,11 @@ import { dirname, isAbsolute, join, posix } from "node:path";
54
54
  import { decideWaveGate, runWaveLoop, waveDigestMembers, waveSetDigest, writeWaveRecordFile, } from "../wave-engine.js";
55
55
  import { approveCommand, evaluateGate, gitGateLedgerPort } from "./gate.js";
56
56
  import { isApprovalSpent } from "../lifecycle/gate-applied.js";
57
+ import { samePlanDigest } from "../lifecycle/plan-digest.js";
57
58
  import { isLifecycleLocalNote } from "../lifecycle/local-note.js";
58
59
  import { ciRunIdFromEnv } from "./gate-run-locator.js";
59
- import { OP_WAVE_DEFAULT_APPLY, OP_WAVE_GATE_POLICIES, OP_WAVE_HOOK_MOMENTS, GATE_APPROVAL_SOURCES, assertOpWaveHooks, assertOpWavesSpec, opWaveApplyHooksRunBy, opWaveGateName, opWaveHooksAt, opWavePointAt, opWavePointProblems, opWaveRecordPath, opWaveGroupRuns, opWaveGroupsProblem, opWaveShare, } from "./op-waves.js";
60
+ import { quorumProblems, quorumRuleLine, quorumRulesFor, quorumWaiting, ruleNeedsProof, samePrincipal, trustQuorumJudge, } from "./gate-quorum.js";
61
+ import { OP_WAVE_DEFAULT_APPLY, OP_WAVE_GATE_POLICIES, OP_WAVE_HOOK_MOMENTS, GATE_APPROVAL_SOURCES, assertOpWaveHooks, assertOpWavesSpec, opWaveApplyHooksRunBy, opWaveGateName, opWaveHooksAt, opWaveHookRuns, opWavePointAt, opWavePointProblems, opWaveRecordPath, opWaveGroupRuns, opWaveGroupsProblem, opWaveShare, } from "./op-waves.js";
60
62
  /** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
61
63
  export const OP_WAVE_PENDING_RESERVED = [
62
64
  "version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume",
@@ -126,7 +128,7 @@ export function readOpWavePolicy(specFile, name, base, cwd, show = gitShowAtBase
126
128
  const spend = spec.approvals?.spend === true;
127
129
  const neverOverMcp = spec.neverOverMcp === true;
128
130
  // #3836: the gate's decision point, the spec's for a wave the base does not have yet.
129
- const asks = pointAtBase(spec, name, `${specFile} at ${sha.slice(0, 12)}`);
131
+ const asks = { ...pointAtBase(spec, name, `${specFile} at ${sha.slice(0, 12)}`), ...quorumAtBase(spec, `${specFile} at ${sha.slice(0, 12)}`) };
130
132
  const wave = spec.waves.find((w) => w.name === name);
131
133
  if (!wave)
132
134
  return { ...absent, spend, neverOverMcp, ...asks };
@@ -146,6 +148,16 @@ export function readOpWavePolicy(specFile, name, base, cwd, show = gitShowAtBase
146
148
  }
147
149
  return rules;
148
150
  }
151
+ /** The approval rules at base (#4290), or why they cannot be read. A spec at base with none has none. */
152
+ function quorumAtBase(spec, where) {
153
+ const raw = spec.approvals?.quorum;
154
+ if (raw === undefined)
155
+ return {};
156
+ const problems = Array.isArray(raw) ? quorumProblems(raw, "approvals.quorum", { notAuthor: true }) : ["`approvals.quorum` must be a list of rules"];
157
+ if (problems.length > 0)
158
+ return { quorumRefused: `the approval rules in ${where} cannot be read (${problems.join("; ")})` };
159
+ return raw.length > 0 ? { quorum: raw } : {};
160
+ }
149
161
  /** The point wave `name` asks at base (#3836), or why it cannot be asked. */
150
162
  function pointAtBase(spec, name, where) {
151
163
  const raw = spec.waves.find((w) => w.name === name)?.point ?? spec.point;
@@ -200,11 +212,13 @@ function planRun(opts, waveName, run) {
200
212
  return member(run.target, op, file);
201
213
  }
202
214
  function member(target, op, file) {
203
- const denied = file.denied;
204
- if (denied !== undefined) {
205
- const d = denied;
215
+ for (const key of ["denied", "held"]) {
216
+ const value = file[key];
217
+ if (value === undefined)
218
+ continue;
219
+ const d = value;
206
220
  if (!d || typeof d !== "object" || !Array.isArray(d.rules) || d.rules.length === 0 || !d.rules.every((r) => typeof r === "string" && r.trim() !== "") || (d.reason !== undefined && typeof d.reason !== "string")) {
207
- throw new Error(`the plan of ${target} has denied ${JSON.stringify(denied)}; a denial is { "rules": ["<rule id>", ...], "reason"?: "<text>" }`);
221
+ throw new Error(`the plan of ${target} has ${key} ${JSON.stringify(value)}; ${key === "denied" ? "a denial" : "a hold"} is { "rules": ["<rule id>", ...], "reason"?: "<text>" }`);
208
222
  }
209
223
  }
210
224
  return {
@@ -214,6 +228,7 @@ function member(target, op, file) {
214
228
  ...(file.destructive === true ? { destructive: true } : {}),
215
229
  ...(file.empty === true ? { empty: true } : {}),
216
230
  ...(file.denied ? { denied: { rules: file.denied.rules, ...(file.denied.reason ? { reason: file.denied.reason } : {}) } } : {}),
231
+ ...(file.held ? { held: { rules: [...new Set(file.held.rules)].sort(), ...(file.held.reason ? { reason: file.held.reason } : {}) } } : {}),
217
232
  };
218
233
  }
219
234
  /** Write the `{runs}` file: the runs, with their plans once planned. */
@@ -295,7 +310,7 @@ function planRuns(at, runs, discover = false) {
295
310
  const members = runs.map((run) => {
296
311
  const m = member(run.target, run.op ?? spec.op, byTarget.get(run.target));
297
312
  const path = join(cwd, opWavePlanPath(spec, k, run.target));
298
- writeFileSync(path, JSON.stringify({ planDigest: m.planDigest, ...(m.destructive ? { destructive: true } : {}), ...(m.empty ? { empty: true } : {}), ...(m.denied ? { denied: m.denied } : {}) }) + "\n");
313
+ writeFileSync(path, JSON.stringify({ planDigest: m.planDigest, ...(m.destructive ? { destructive: true } : {}), ...(m.empty ? { empty: true } : {}), ...(m.denied ? { denied: m.denied } : {}), ...(m.held ? { held: m.held } : {}) }) + "\n");
299
314
  return m;
300
315
  });
301
316
  const overriders = Array.isArray(parsed) ? undefined : parsed.overriders;
@@ -385,12 +400,18 @@ function runHookList(at, moment, hooks, runs, runsPath, extraEnv = {}) {
385
400
  return out;
386
401
  const { name: specName, op, waveName, k, cwd, exec, log } = at;
387
402
  // #3821: a hook that holds the wave on another gate names it in this file.
388
- const holdPath = `.chant/op-waves/${specName}/wave-${k}/hold.json`;
403
+ const holdPath = at.holdPath ?? `.chant/op-waves/${specName}/wave-${k}/hold.json`;
389
404
  const env = { CHANT_OP_WAVES: specName, CHANT_WAVE: waveName, CHANT_WAVE_NUMBER: String(k), CHANT_HOOK: moment, CHANT_RUNS: runsPath, CHANT_HOLD: holdPath, ...extraEnv };
390
405
  for (const hook of hooks) {
391
406
  const name = hook.name ?? (hook.command.join(" ").length > 60 ? `${hook.command.join(" ").slice(0, 57)}...` : hook.command.join(" "));
407
+ // #4288: a hook with `targets` is for the runs they match. Once for the wave, it runs when one matches or no run is known yet.
408
+ const mine = opWaveHookRuns(hook, runs);
409
+ if (hook.targets && (hook.perRun ? mine.length === 0 : mine.length === 0 && runs.length > 0)) {
410
+ log(`${moment} hook ${name}: no run matches its targets (${hook.targets.join(", ")}), so it did not run`);
411
+ continue;
412
+ }
392
413
  const calls = hook.perRun
393
- ? runs.map((run) => ({
414
+ ? mine.map((run) => ({
394
415
  target: run.target,
395
416
  argv: opWaveArgv(hook.command, { op: run.op, wave: waveName, target: run.target, runs: runsPath, ...(run.plan ? { plan: run.plan } : {}) }),
396
417
  env: { ...env, CHANT_TARGET: run.target, ...(run.plan ? { CHANT_PLAN: run.plan } : {}) },
@@ -508,6 +529,32 @@ async function runHooks(ctx, moment, hooks, runs, members, digest, extraEnv = {}
508
529
  }
509
530
  return back;
510
531
  }
532
+ /**
533
+ * Run one moment's hooks outside an Op waves job (#4288): the pull-request
534
+ * jobs, or a product's own stage, with the same `targets`, `perRun`,
535
+ * `onFailure` and argv filling a wave job uses. A hook that holds on another
536
+ * gate (`CHANT_HOLD`) is counted as a plain hold; settling that gate is the
537
+ * caller's.
538
+ */
539
+ export function runStageHooks(moment, hooks, options) {
540
+ const cwd = options.cwd ?? process.cwd();
541
+ const log = options.log ?? ((line) => console.error(line));
542
+ const place = { name: options.name, op: options.op, waveName: options.wave, k: options.k ?? 0, cwd, exec: options.exec ?? spawnExec, log, holdPath: options.holdFile };
543
+ const out = runHookList(place, moment, hooks, options.runs, options.runsFile, options.env);
544
+ return { outcomes: out.outcomes, ...(out.failed ? { failed: out.failed } : {}), holds: [...out.holds, ...out.otherGates.map((g) => `${g.label} (on ${g.request.op ?? options.name} / ${g.request.gate})`)] };
545
+ }
546
+ /**
547
+ * The hooks that run before a job plans (#4288): `before-observe`, then
548
+ * `before-plan`. A failure or a hold on another gate in the first ends it
549
+ * there; the holds of both add up.
550
+ */
551
+ async function runBeforePlanHooks(ctx, hooks, runs) {
552
+ const observe = await runHooks(ctx, "before-observe", hooks?.["before-observe"], runs);
553
+ if (observe.failed || observe.waitingOn)
554
+ return observe;
555
+ const plan = await runHooks(ctx, "before-plan", hooks?.["before-plan"], runs);
556
+ return { ...plan, holds: [...observe.holds, ...plan.holds] };
557
+ }
511
558
  /** How many gates one hook call holds a wave on in turn (#3847): the first hold and the holds its approved re-runs ask for. */
512
559
  export const OP_WAVE_MAX_CHAINED_HOLDS = 10;
513
560
  /**
@@ -827,7 +874,7 @@ async function planOpWave(options, { k, pass }) {
827
874
  log(`${part} of wave ${k} (${wave.name}) holds none of the wave's runs; nothing to apply`);
828
875
  return { end: result };
829
876
  }
830
- const before = await runHooks(ctx, "before-plan", decision.hooks?.["before-plan"], slice);
877
+ const before = await runBeforePlanHooks(ctx, decision.hooks, slice);
831
878
  if (before.failed)
832
879
  return { end: failed(ctx, `a hook before the plan failed, so nothing in ${part} of wave ${k} was planned or applied`) };
833
880
  if (before.waitingOn)
@@ -850,6 +897,14 @@ async function planOpWave(options, { k, pass }) {
850
897
  // #3836: a point at base that cannot be asked fails the wave; the ledger never stands in for it.
851
898
  if (rules.pointRefused)
852
899
  return { end: failed(ctx, `wave ${k} (${wave.name}): ${rules.pointRefused}, so nothing in it was planned or applied`) };
900
+ // #4290: rules at base that cannot be read fail the wave; fewer approvers never stand in for them.
901
+ if (rules.quorumRefused)
902
+ return { end: failed(ctx, `wave ${k} (${wave.name}): ${rules.quorumRefused}, so nothing in it was planned or applied`) };
903
+ if (rules.quorum && rules.point)
904
+ return { end: failed(ctx, `wave ${k} (${wave.name}): approvals.quorum and a decision point are both set at ${base.slice(0, 12)}; a gate that asks a point takes its deciders from the point, so nothing in it was planned or applied`) };
905
+ if (JSON.stringify(spec.approvals?.quorum ?? null) !== JSON.stringify(rules.quorum ?? null)) {
906
+ log(`wave ${k} (${wave.name}): its approval rules are read at ${base.slice(0, 12)}, and this change's differ; the ones at base hold`);
907
+ }
853
908
  const own = opWavePointAt(spec, wave.name);
854
909
  if (JSON.stringify(own) !== JSON.stringify(rules.point)) {
855
910
  log(`wave ${k} (${wave.name}): its gate's decision point is read at ${base.slice(0, 12)} (${rules.point?.name ?? "none"}), and this change's differs (${own?.name ?? "none"}); the one at base holds`);
@@ -880,7 +935,7 @@ async function planOpWave(options, { k, pass }) {
880
935
  ...(neverOverMcp ? { neverOverMcp: true } : {}),
881
936
  ...(rules.point ? { point: { ...rules.point } } : {}),
882
937
  };
883
- const before = await runHooks(ctx, "before-plan", rules.hooks["before-plan"], wave.runs);
938
+ const before = await runBeforePlanHooks(ctx, rules.hooks, wave.runs);
884
939
  if (before.failed)
885
940
  return { end: failed(ctx, `a hook before the plan failed, so nothing in wave ${k} was planned or applied`) };
886
941
  if (before.waitingOn)
@@ -948,6 +1003,10 @@ async function decideOpWave(options, { prior }, planned, digest) {
948
1003
  const { policy, source, base } = planned.rules;
949
1004
  decision.digest = digest;
950
1005
  decision.members = members;
1006
+ // #4290: the rules at base whose targets glob matches one of the wave's runs.
1007
+ const takes = planned.rules.quorum ? quorumRulesFor(planned.rules.quorum, members.map((m) => m.member)) : [];
1008
+ if (takes.length > 0)
1009
+ decision.quorum = takes;
951
1010
  if (extras)
952
1011
  decision.extras = extras;
953
1012
  if (pending)
@@ -956,6 +1015,10 @@ async function decideOpWave(options, { prior }, planned, digest) {
956
1015
  if (after.failed)
957
1016
  return { end: failed(ctx, `a hook after the plan failed, so nothing in wave ${k} was applied`) };
958
1017
  holds.push(...planned.before, ...after.holds);
1018
+ // #4294: a run the product's policy holds makes the wave wait for a person, as a held hook does.
1019
+ for (const m of members)
1020
+ if (m.held)
1021
+ holds.push(`policy on ${m.member} (${m.held.rules.join(", ")})${m.held.reason ? `: ${m.held.reason}` : ""}`);
959
1022
  if (holds.length > 0)
960
1023
  decision.heldBy = holds;
961
1024
  if (after.waitingOn)
@@ -1225,6 +1288,8 @@ function failed(ctx, line) {
1225
1288
  async function settleGate(options, ctx, decision) {
1226
1289
  if (decision.point)
1227
1290
  return settlePointGate(options, ctx, decision, decision.point);
1291
+ if (decision.quorum?.length)
1292
+ return settleQuorumGate(options, ctx, decision, decision.quorum);
1228
1293
  const { spec, k, wave, cwd } = ctx;
1229
1294
  const { gate, digest, approval, base, policy, members } = decision;
1230
1295
  const { ledger, port } = await gatePort(options, cwd, gate, approval, base);
@@ -1280,6 +1345,120 @@ async function settleGate(options, ctx, decision) {
1280
1345
  decision.status = "waiting";
1281
1346
  decision.approve = approveCommand(spec.name, gate, undefined, digest, approval === "sealed");
1282
1347
  }
1348
+ /**
1349
+ * Decide a gate under approval rules (#4290): the rules at base the wave's
1350
+ * targets take, each met by distinct human approvers of the digest. Roles
1351
+ * come from `.chant/trust.json` at base, for an approver a seal proves, or
1352
+ * under `pr-review` the merged pull request's review read from the forge in
1353
+ * this run. That review is written to the ledger once per approving
1354
+ * reviewer, so each counts on their own. `notAuthor` reads the author from
1355
+ * the same pull request; with none, no approver can show they are someone
1356
+ * else and the rule waits.
1357
+ */
1358
+ async function settleQuorumGate(options, ctx, decision, rules) {
1359
+ const { spec, k, wave, cwd, log } = ctx;
1360
+ const { gate, digest, approval, base, policy, members } = decision;
1361
+ const { ledger, port } = await gatePort(options, cwd, gate, approval, base);
1362
+ const now = options.now ?? new Date().toISOString();
1363
+ const spent = decision.spend === "base" ? ((await ledger.readApplied?.(spec.name)) ?? []).filter((a) => a.gate === gate) : undefined;
1364
+ const review = await import("./gate-review.js");
1365
+ const trust = (options.trust ?? ((b) => review.trustPolicyAt(cwd, b)))(base);
1366
+ const label = `wave ${k} (${wave.name})`;
1367
+ // The merged pull request: its author for notAuthor, and its review under pr-review.
1368
+ let merged = null;
1369
+ let unknown;
1370
+ const asks = approval === "pr-review" || rules.some((r) => r.notAuthor);
1371
+ const head = asks ? (options.head ?? execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim()) : "";
1372
+ if (asks) {
1373
+ try {
1374
+ merged = await (options.reviews ?? review.reviewSourceFromEnv()).mergedReview(head);
1375
+ if (!merged)
1376
+ unknown = `no merged pull request produced ${head.slice(0, 12)}`;
1377
+ }
1378
+ catch (err) {
1379
+ unknown = `the forge's review could not be read: ${err instanceof Error ? err.message : String(err)}`;
1380
+ }
1381
+ }
1382
+ if (unknown && rules.some((r) => r.notAuthor))
1383
+ log(`${label}: ${unknown}, so the change's author is not known`);
1384
+ if (approval === "pr-review" && unknown)
1385
+ decision.review = { reason: unknown };
1386
+ let reviewed;
1387
+ if (approval === "pr-review" && merged) {
1388
+ const plans = merged.approvers.length === 0 ? null : await (options.headPlans ?? ((name, h) => readHeadPlans(name, h, cwd)))(spec.name, merged.head);
1389
+ const { reviewApproval } = await import("../gate-decision.js");
1390
+ const outcome = reviewApproval(merged, plans?.waves.find((w) => w.wave === k)?.digest, digest);
1391
+ decision.review = outcome.review;
1392
+ const reviewSpent = spent !== undefined && isApprovalSpent(spent, { gate, planDigest: digest, review: { head: merged.head } });
1393
+ if (reviewSpent)
1394
+ decision.review = { ...outcome.review, reason: `a wave already applied ${digest} under the review of #${merged.pr} at ${merged.head.slice(0, 12)}, so the review is spent` };
1395
+ if (outcome.approved && !reviewSpent) {
1396
+ reviewed = { head: merged.head, approvers: merged.approvers };
1397
+ // Each approving reviewer once, so the rules count them one by one.
1398
+ const had = (await ledger.read(spec.name)).resolutions.filter((r) => r.gate === gate && r.review?.head === merged.head && samePlanDigest(r.planDigest, digest));
1399
+ const note = `approved by the review of #${merged.pr} at ${merged.head.slice(0, 12)}: ${merged.approvers.join(", ")}`;
1400
+ for (const who of merged.approvers) {
1401
+ if (had.some((r) => samePrincipal(r.resolvedBy, who)))
1402
+ continue;
1403
+ await ledger.appendResolution?.({
1404
+ op: spec.name,
1405
+ gate,
1406
+ resolvedBy: who,
1407
+ timestamp: now,
1408
+ planDigest: digest,
1409
+ approver: { kind: "human" },
1410
+ gitSha: head,
1411
+ ...(ciRunIdFromEnv() ? { runId: ciRunIdFromEnv() } : {}),
1412
+ note,
1413
+ ...(merged.url ? { url: merged.url } : {}),
1414
+ review: { pr: merged.pr, head: merged.head, approvers: merged.approvers },
1415
+ });
1416
+ }
1417
+ }
1418
+ }
1419
+ const judge = trustQuorumJudge({
1420
+ signers: trust,
1421
+ roles: trust.roles,
1422
+ ...(merged ? { authors: [merged.author] } : {}),
1423
+ ...(reviewed ? { review: reviewed } : {}),
1424
+ });
1425
+ const held = decision.heldBy?.length ? `, held by ${decision.heldBy.join(", ")}` : "";
1426
+ const check = await decideWaveGate(port, {
1427
+ op: spec.name,
1428
+ gate,
1429
+ digest,
1430
+ description: (typeof decision.pending?.description === "string" ? decision.pending.description : undefined) ?? `wave ${k} (${wave.name}): ${members.length} run${members.length === 1 ? "" : "s"}, gate ${policy}, approval ${approval}${held}`,
1431
+ now,
1432
+ ...(spent ? { spent } : {}),
1433
+ fields: { ...decision.pending, approvalMode: approval },
1434
+ ...(decision.neverOverMcp ? { neverOverMcp: true } : {}),
1435
+ approval: { quorum: rules, mode: "log-only" },
1436
+ judge,
1437
+ });
1438
+ if (check.satisfied) {
1439
+ const by = (check.approvals ?? [check.resolution]).map((r) => r.resolvedBy);
1440
+ decision.status = "approved";
1441
+ decision.via = reviewed && check.resolution.review?.head === reviewed.head ? "pr-review" : "ledger";
1442
+ decision.approvedBy = by.join(", ");
1443
+ decision.approvedAt = check.resolution.timestamp;
1444
+ log(`${label}: the approval rules at ${base.slice(0, 12)} are met by ${by.join(", ")}`);
1445
+ return;
1446
+ }
1447
+ logRefused(log, label, check);
1448
+ decision.pendingFact = check.pending;
1449
+ decision.recorded = check.recorded;
1450
+ decision.waitingSince = check.pending.timestamp;
1451
+ if (check.mismatch)
1452
+ decision.mismatch = check.mismatch;
1453
+ if (check.quorum?.rules) {
1454
+ decision.quorumProgress = check.quorum.rules;
1455
+ for (const p of check.quorum.rules)
1456
+ log(`${label}: approval rule ${quorumRuleLine(p)}`);
1457
+ log(`${label}: the approval rules at ${base.slice(0, 12)} are not met (${quorumWaiting(check.quorum.rules)}), so it waits for more approvals of ${digest}`);
1458
+ }
1459
+ decision.status = "waiting";
1460
+ decision.approve = approveCommand(spec.name, gate, undefined, digest, approval === "sealed" || rules.some(ruleNeedsProof));
1461
+ }
1283
1462
  /**
1284
1463
  * Decide a gate that asks a decision point (#3836), as an Op gate with a
1285
1464
  * `point` is decided (#3170): ask it with `gate.planDigest` set to the set
@@ -43,6 +43,14 @@
43
43
  * `chant/lifecycle`, as terragucci's wave gate does, and that approval is
44
44
  * spent: the same digest needs a new approval to apply again.
45
45
  *
46
+ * ## Approval rules (#4290)
47
+ *
48
+ * With `approvals: { quorum: [...] }` (read at base), a wave takes each rule
49
+ * whose `targets` glob matches one of its runs' targets and waits until each
50
+ * is met: `count` distinct people, from `roles` when it names them (granted
51
+ * in `.chant/trust.json` at base), one of them not the author with
52
+ * `notAuthor`. See `./gate-quorum.ts`.
53
+ *
46
54
  * ## Wave-level commands and hooks (#3762)
47
55
  *
48
56
  * A product whose tool plans a whole wave in one call (Terragrunt's `run
@@ -68,6 +76,7 @@
68
76
  import type { OpEnvironment, OpSetupStep } from "../lexicon.js";
69
77
  import type { CiRunner } from "../ci-jobs.js";
70
78
  import { type GatePoint } from "./gate-point.js";
79
+ import { type GateQuorumRule } from "./gate-quorum.js";
71
80
  import { type NotifyConfig } from "../notify.js";
72
81
  /**
73
82
  * The variables a wave job reads its notify secrets from, for the lexicons
@@ -128,13 +137,23 @@ export interface OpWaveHook {
128
137
  onFailure?: OpWaveHookFailure;
129
138
  /** Run once per run of the wave, rather than once for the wave. */
130
139
  perRun?: boolean;
140
+ /**
141
+ * The runs the hook is for (#4288): targets, or patterns where `*` matches
142
+ * within one `/` segment and `**` across segments, as a job group's `runs`.
143
+ * A `perRun` hook runs for the matching runs only. A hook that runs once
144
+ * for the wave runs when any run matches, or when no run is known yet (a
145
+ * `discover` wave before its plan). Absent: every run.
146
+ */
147
+ targets?: string[];
131
148
  }
132
149
  /** The stages a hook runs around. */
133
- export type OpWaveHookStage = "plan" | "apply" | "gate";
150
+ export type OpWaveHookStage = "observe" | "plan" | "apply" | "gate";
134
151
  /** When a hook runs. */
135
- export type OpWaveHookMoment = "before-plan" | "after-plan" | "after-gate" | "before-apply" | "after-apply";
152
+ export type OpWaveHookMoment = "before-observe" | "before-plan" | "after-plan" | "after-gate" | "before-apply" | "after-apply";
136
153
  /**
137
- * Every moment, in the order a wave reaches them. `after-gate` (#3821) runs
154
+ * Every moment, in the order a wave reaches them. `before-observe` (#4288)
155
+ * runs before anything reads live state: ahead of `before-plan` in every job
156
+ * that plans, the deciding job's and each share's. `after-gate` (#3821) runs
138
157
  * once the gate decided, whatever it decided: approved, let through,
139
158
  * waiting, or refused by a policy denial.
140
159
  */
@@ -149,7 +168,9 @@ export type OpWaveApplyHooksRunner = "chant" | "apply";
149
168
  export declare const OP_WAVE_APPLY_HOOKS_RUNNERS: readonly OpWaveApplyHooksRunner[];
150
169
  /** Stage hooks (#3762), read from the spec at the base commit like the gate policy. */
151
170
  export interface OpWaveHooks {
171
+ /** `before.observe` (#4288) runs before anything reads live state, ahead of `before.plan`. */
152
172
  before?: {
173
+ observe?: OpWaveHook[];
153
174
  plan?: OpWaveHook[];
154
175
  apply?: OpWaveHook[];
155
176
  };
@@ -418,6 +439,18 @@ export interface OpWavesApprovals {
418
439
  * approval of a digest passes that digest as often as it is planned.
419
440
  */
420
441
  spend?: boolean;
442
+ /**
443
+ * Approval rules (#4290): a wave takes each rule whose `targets` glob
444
+ * matches one of its runs' targets (a rule with no `targets` covers every
445
+ * wave), and applies once each is met by distinct human approvers of its
446
+ * set digest. Read at the base commit, with the roles from
447
+ * `.chant/trust.json` there, so a change cannot loosen the rules its own
448
+ * wave is judged by. A role, or `notAuthor`, counts only a proven
449
+ * approver: a seal that verifies against the signers at base, or under
450
+ * `pr-review` the merged pull request's review. `notAuthor` reads the
451
+ * author from that pull request. See `./gate-quorum.ts`.
452
+ */
453
+ quorum?: GateQuorumRule[];
421
454
  }
422
455
  /** The scheduled resume job of an Op waves pipeline (#3683). */
423
456
  export interface OpWavesResume {
@@ -462,6 +495,13 @@ export declare function assertOpWavesSpec(spec: OpWavesSpec): void;
462
495
  * across segments.
463
496
  */
464
497
  export declare function opWaveRunMatches(pattern: string, target: string): boolean;
498
+ /**
499
+ * The runs of `runs` hook `hook` is for (#4288): those its `targets` match,
500
+ * or every run when it names none.
501
+ */
502
+ export declare function opWaveHookRuns<T extends {
503
+ target: string;
504
+ }>(hook: Pick<OpWaveHook, "targets">, runs: readonly T[]): T[];
465
505
  /** The runs of `runs` that job group `group` holds (#3914). */
466
506
  export declare function opWaveGroupRuns<T extends {
467
507
  target: string;
@@ -473,6 +513,18 @@ export declare function opWaveGroupRuns<T extends {
473
513
  export declare function opWaveGroupsProblem(wave: Pick<OpWave, "name" | "jobs">, targets: readonly string[]): string | undefined;
474
514
  /** Refuse hooks no runner could run, naming what is wrong (#3762). */
475
515
  export declare function assertOpWaveHooks(where: string, hooks: OpWaveHooks | undefined): void;
516
+ /**
517
+ * The moments a pull-request job runs hooks at (#4288): `pr-plan` and
518
+ * `pr-record` run the observe and plan moments, and `pr-apply` the apply
519
+ * moments too. The pull request's gate has no `after-gate` moment.
520
+ */
521
+ export declare const PR_LOOP_HOOK_MOMENTS: readonly OpWaveHookMoment[];
522
+ /**
523
+ * Refuse `ci.hooks` a pull-request job could not run (#4288): what
524
+ * {@link assertOpWaveHooks} refuses, plus `after.gate` and `apply.runBy`,
525
+ * since chant itself runs a pull request's apply.
526
+ */
527
+ export declare function assertPrLoopHooks(where: string, hooks: OpWaveHooks | undefined): void;
476
528
  /** The hooks of each moment for wave `name`: the spec's, then the wave's own. */
477
529
  export declare function opWaveHooksAt(spec: Pick<OpWavesSpec, "hooks" | "waves">, name: string): Record<OpWaveHookMoment, OpWaveHook[]>;
478
530
  /** Who runs wave `name`'s apply-stage hooks (#3804): the wave's `hooks.apply.runBy`, else the spec's, else `chant`. */
@@ -1 +1 @@
1
- {"version":3,"file":"op-waves.d.ts","sourceRoot":"","sources":["../../src/op/op-waves.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"op-waves.d.ts","sourceRoot":"","sources":["../../src/op/op-waves.ts"],"names":[],"mappings":"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"}
@@ -43,6 +43,14 @@
43
43
  * `chant/lifecycle`, as terragucci's wave gate does, and that approval is
44
44
  * spent: the same digest needs a new approval to apply again.
45
45
  *
46
+ * ## Approval rules (#4290)
47
+ *
48
+ * With `approvals: { quorum: [...] }` (read at base), a wave takes each rule
49
+ * whose `targets` glob matches one of its runs' targets and waits until each
50
+ * is met: `count` distinct people, from `roles` when it names them (granted
51
+ * in `.chant/trust.json` at base), one of them not the author with
52
+ * `notAuthor`. See `./gate-quorum.ts`.
53
+ *
46
54
  * ## Wave-level commands and hooks (#3762)
47
55
  *
48
56
  * A product whose tool plans a whole wave in one call (Terragrunt's `run
@@ -66,6 +74,7 @@
66
74
  * the wave (exit 8). See `./op-waves-run.ts`.
67
75
  */
68
76
  import { gatePointProblems } from "./gate-point.js";
77
+ import { quorumProblems } from "./gate-quorum.js";
69
78
  import { notifyConfigProblem } from "../notify.js";
70
79
  import { WAVE_GATE_NAME_PATTERN, waveGateNameFrom, waveShare } from "../wave-layout.js";
71
80
  /**
@@ -102,11 +111,13 @@ export function opWavePointAt(spec, name) {
102
111
  }
103
112
  export const OP_WAVE_HOOK_FAILURES = ["fail", "approve"];
104
113
  /**
105
- * Every moment, in the order a wave reaches them. `after-gate` (#3821) runs
114
+ * Every moment, in the order a wave reaches them. `before-observe` (#4288)
115
+ * runs before anything reads live state: ahead of `before-plan` in every job
116
+ * that plans, the deciding job's and each share's. `after-gate` (#3821) runs
106
117
  * once the gate decided, whatever it decided: approved, let through,
107
118
  * waiting, or refused by a policy denial.
108
119
  */
109
- export const OP_WAVE_HOOK_MOMENTS = ["before-plan", "after-plan", "after-gate", "before-apply", "after-apply"];
120
+ export const OP_WAVE_HOOK_MOMENTS = ["before-observe", "before-plan", "after-plan", "after-gate", "before-apply", "after-apply"];
110
121
  export const OP_WAVE_APPLY_HOOKS_RUNNERS = ["chant", "apply"];
111
122
  export const OP_WAVES_DIGEST_OVER = ["all", "changing"];
112
123
  /** The default gate name pattern. */
@@ -171,13 +182,25 @@ export function assertOpWavesSpec(spec) {
171
182
  if (spec.approvals !== undefined) {
172
183
  const a = spec.approvals;
173
184
  if (a === null || typeof a !== "object" || Array.isArray(a))
174
- throw new Error(`${where}: \`approvals\` must be { "spend"?: true | false }.`);
185
+ throw new Error(`${where}: \`approvals\` must be { "spend"?: true | false, "quorum"?: [...] }.`);
175
186
  for (const [key, value] of Object.entries(a)) {
187
+ if (key === "quorum") {
188
+ // #4290: approval rules, read at base like the policy.
189
+ if (!Array.isArray(value))
190
+ throw new Error(`${where}: approvals.quorum must be a list of rules, each { "targets"?, "count", "roles"?, "notAuthor"? }.`);
191
+ const problems = quorumProblems(value, "approvals.quorum", { notAuthor: true });
192
+ if (problems.length > 0)
193
+ throw new Error(`${where}: ${problems.join("; ")}.`);
194
+ continue;
195
+ }
176
196
  if (key !== "spend")
177
- throw new Error(`${where}: approvals.${key} is not a setting; the one there is approvals.spend.`);
197
+ throw new Error(`${where}: approvals.${key} is not a setting; the ones there are approvals.spend and approvals.quorum.`);
178
198
  if (typeof value !== "boolean")
179
199
  throw new Error(`${where}: approvals.spend must be true or false.`);
180
200
  }
201
+ if (a.quorum?.length && (spec.point !== undefined || spec.waves.some((w) => w.point !== undefined))) {
202
+ throw new Error(`${where}: approvals.quorum and a decision point are both set; a gate that asks a point takes its deciders and quorum from the point, so drop one of them.`);
203
+ }
181
204
  }
182
205
  if (spec.gateName !== undefined) {
183
206
  if (typeof spec.gateName !== "string" || !spec.gateName.includes("{k}") || /\s/.test(spec.gateName)) {
@@ -394,6 +417,15 @@ export function opWaveRunMatches(pattern, target) {
394
417
  }
395
418
  return new RegExp(`^${re}$`).test(target);
396
419
  }
420
+ /**
421
+ * The runs of `runs` hook `hook` is for (#4288): those its `targets` match,
422
+ * or every run when it names none.
423
+ */
424
+ export function opWaveHookRuns(hook, runs) {
425
+ if (!hook.targets)
426
+ return [...runs];
427
+ return runs.filter((run) => hook.targets.some((pattern) => opWaveRunMatches(pattern, run.target)));
428
+ }
397
429
  /** The runs of `runs` that job group `group` holds (#3914). */
398
430
  export function opWaveGroupRuns(group, runs) {
399
431
  return runs.filter((run) => group.runs.some((pattern) => opWaveRunMatches(pattern, run.target)));
@@ -422,7 +454,7 @@ export function assertOpWaveHooks(where, hooks) {
422
454
  if (hooks === undefined)
423
455
  return;
424
456
  if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
425
- throw new Error(`${where}: \`hooks\` must be { "before"?: { "plan"?, "apply"? }, "after"?: { "plan"?, "apply"? } }.`);
457
+ throw new Error(`${where}: \`hooks\` must be { "before"?: { "observe"?, "plan"?, "apply"? }, "after"?: { "plan"?, "gate"?, "apply"? } }.`);
426
458
  }
427
459
  for (const [side, stages] of Object.entries(hooks)) {
428
460
  if (side === "apply") {
@@ -443,8 +475,10 @@ export function assertOpWaveHooks(where, hooks) {
443
475
  const at = `hooks.${side}.${stage}`;
444
476
  if (stage === "gate" && side === "before")
445
477
  throw new Error(`${where}: ${at} is not a moment; a hook runs after the gate (hooks.after.gate), or before the plan or the apply.`);
446
- if (stage !== "plan" && stage !== "apply" && stage !== "gate")
447
- throw new Error(`${where}: ${at} is not a stage; a hook runs before or after plan or apply, or after the gate.`);
478
+ if (stage === "observe" && side === "after")
479
+ throw new Error(`${where}: ${at} is not a moment; a hook runs before the observe (hooks.before.observe), or after the plan, which reads what the observe saw.`);
480
+ if (stage !== "observe" && stage !== "plan" && stage !== "apply" && stage !== "gate")
481
+ throw new Error(`${where}: ${at} is not a stage; a hook runs before the observe, before or after plan or apply, or after the gate.`);
448
482
  if (!Array.isArray(list))
449
483
  throw new Error(`${where}: ${at} must be a list of hooks.`);
450
484
  list.forEach((hook, i) => {
@@ -463,18 +497,39 @@ export function assertOpWaveHooks(where, hooks) {
463
497
  }
464
498
  if (hook.perRun !== undefined && typeof hook.perRun !== "boolean")
465
499
  throw new Error(`${where}: ${which}.perRun must be true or false.`);
500
+ if (hook.targets !== undefined && (!Array.isArray(hook.targets) || hook.targets.length === 0 || !hook.targets.every((t) => typeof t === "string" && t !== ""))) {
501
+ throw new Error(`${where}: ${which}.targets must be a non-empty list of targets or patterns, such as ["prod/*"].`);
502
+ }
466
503
  if (!hook.perRun) {
467
504
  const named = hook.command.find((part) => part.includes("{target}") || part.includes("{plan}"));
468
505
  if (named)
469
506
  throw new Error(`${where}: ${which} runs once for the wave, so it has no {target} or {plan} ("${named}"). Set "perRun": true, or read the runs from {runs}.`);
470
507
  }
471
- else if (side === "before" && stage === "plan" && hook.command.some((part) => part.includes("{plan}"))) {
508
+ else if (side === "before" && (stage === "plan" || stage === "observe") && hook.command.some((part) => part.includes("{plan}"))) {
472
509
  throw new Error(`${where}: ${which} runs before the plan, so it has no {plan} yet.`);
473
510
  }
474
511
  });
475
512
  }
476
513
  }
477
514
  }
515
+ /**
516
+ * The moments a pull-request job runs hooks at (#4288): `pr-plan` and
517
+ * `pr-record` run the observe and plan moments, and `pr-apply` the apply
518
+ * moments too. The pull request's gate has no `after-gate` moment.
519
+ */
520
+ export const PR_LOOP_HOOK_MOMENTS = ["before-observe", "before-plan", "after-plan", "before-apply", "after-apply"];
521
+ /**
522
+ * Refuse `ci.hooks` a pull-request job could not run (#4288): what
523
+ * {@link assertOpWaveHooks} refuses, plus `after.gate` and `apply.runBy`,
524
+ * since chant itself runs a pull request's apply.
525
+ */
526
+ export function assertPrLoopHooks(where, hooks) {
527
+ assertOpWaveHooks(where, hooks);
528
+ if (hooks?.after?.gate !== undefined)
529
+ throw new Error(`${where}: hooks.after.gate runs in an Op waves job only; a pull-request job runs hooks before the observe, before and after the plan, and before and after the apply.`);
530
+ if (hooks?.apply !== undefined)
531
+ throw new Error(`${where}: hooks.apply.runBy is for an Op waves spec; chant runs a pull request's apply, so it runs the apply-stage hooks itself.`);
532
+ }
478
533
  /** The hooks of each moment for wave `name`: the spec's, then the wave's own. */
479
534
  export function opWaveHooksAt(spec, name) {
480
535
  const wave = spec.waves.find((w) => w.name === name);