@intentius/chant 0.124.40 → 0.124.42
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.
- package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
- package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
- package/dist/__fixtures__/oidc-emulator.js +89 -0
- package/dist/ci-handover.d.ts +117 -0
- package/dist/ci-handover.d.ts.map +1 -0
- package/dist/ci-handover.js +217 -0
- package/dist/ci-identity.d.ts +190 -0
- package/dist/ci-identity.d.ts.map +1 -0
- package/dist/ci-identity.js +219 -0
- package/dist/cli/handlers/approve.d.ts +2 -1
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +20 -6
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +42 -5
- package/dist/cli/handlers/gate-resume.d.ts +3 -0
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.js +15 -2
- package/dist/cli/handlers/lock.d.ts +8 -0
- package/dist/cli/handlers/lock.d.ts.map +1 -0
- package/dist/cli/handlers/lock.js +197 -0
- package/dist/cli/handlers/pr-env.d.ts +34 -0
- package/dist/cli/handlers/pr-env.d.ts.map +1 -0
- package/dist/cli/handlers/pr-env.js +436 -0
- package/dist/cli/handlers/pr.d.ts +76 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +615 -55
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +9 -0
- package/dist/cli/handlers/query.d.ts +6 -0
- package/dist/cli/handlers/query.d.ts.map +1 -0
- package/dist/cli/handlers/query.js +80 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +11 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +49 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +46 -2
- package/dist/cli/registry.d.ts +31 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability.d.ts +47 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +44 -0
- package/dist/components/cli-support.d.ts +6 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +8 -1
- package/dist/components/driver.d.ts +10 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +52 -2
- package/dist/components/fan-out-run.d.ts +37 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +44 -4
- package/dist/components/gate-plan.d.ts +2 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +1 -0
- package/dist/components/pr-environment.d.ts +298 -0
- package/dist/components/pr-environment.d.ts.map +1 -0
- package/dist/components/pr-environment.js +406 -0
- package/dist/components/pr-hooks.d.ts +66 -0
- package/dist/components/pr-hooks.d.ts.map +1 -0
- package/dist/components/pr-hooks.js +119 -0
- package/dist/components/pr-pipeline.d.ts +92 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +150 -21
- package/dist/components/pr-policy.d.ts +109 -0
- package/dist/components/pr-policy.d.ts.map +1 -0
- package/dist/components/pr-policy.js +150 -0
- package/dist/components/pr-run.d.ts +6 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +8 -3
- package/dist/components/wave-plan.d.ts +14 -1
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +34 -2
- package/dist/gate-decision.d.ts +15 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +17 -2
- package/dist/gate-policy.d.ts +140 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/gate-policy.js +242 -0
- package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
- package/dist/lifecycle/gate-checkpoint.js +1 -1
- package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
- package/dist/lifecycle/gate-seal.d.ts.map +1 -0
- package/dist/lifecycle/query.d.ts +68 -0
- package/dist/lifecycle/query.d.ts.map +1 -0
- package/dist/lifecycle/query.js +246 -0
- package/dist/lifecycle/run-ledger.d.ts +13 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +16 -0
- package/dist/lifecycle/target-lock.d.ts +97 -0
- package/dist/lifecycle/target-lock.d.ts.map +1 -0
- package/dist/lifecycle/target-lock.js +286 -0
- package/dist/op/activities/activity-contracts.d.ts +30 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +26 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +4 -0
- package/dist/op/activities/pr-refs.d.ts +10 -0
- package/dist/op/activities/pr-refs.d.ts.map +1 -0
- package/dist/op/activities/pr-refs.js +26 -0
- package/dist/op/activities/predict-behaviour.d.ts +2 -4
- package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
- package/dist/op/activities/predict-behaviour.js +2 -21
- package/dist/op/activities/review-agent.d.ts +137 -0
- package/dist/op/activities/review-agent.d.ts.map +1 -0
- package/dist/op/activities/review-agent.js +266 -0
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +11 -0
- package/dist/op/cron.d.ts +8 -0
- package/dist/op/cron.d.ts.map +1 -1
- package/dist/op/cron.js +51 -0
- package/dist/op/gate-approval.d.ts +20 -13
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -18
- package/dist/op/gate-quorum.d.ts +131 -0
- package/dist/op/gate-quorum.d.ts.map +1 -0
- package/dist/op/gate-quorum.js +183 -0
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-resume.js +5 -4
- package/dist/op/gate-review.js +1 -1
- package/dist/op/gate.d.ts +32 -4
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +40 -24
- package/dist/op/index.d.ts +6 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +4 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/op-waves-run.d.ts +28 -43
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +170 -120
- package/dist/op/op-waves.d.ts +55 -3
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +63 -8
- package/dist/op/schedule-overdue.d.ts +57 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -0
- package/dist/op/schedule-overdue.js +138 -0
- package/dist/op/stage-hooks.d.ts +132 -0
- package/dist/op/stage-hooks.d.ts.map +1 -0
- package/dist/op/stage-hooks.js +142 -0
- package/dist/override-gate.d.ts +2 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/override-gate.js +1 -1
- package/dist/pr-forge.d.ts +64 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +331 -44
- package/dist/pr-loop.d.ts +107 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +84 -3
- package/dist/pr-post.d.ts +72 -0
- package/dist/pr-post.d.ts.map +1 -0
- package/dist/pr-post.js +134 -0
- package/dist/pr-requires.d.ts +38 -0
- package/dist/pr-requires.d.ts.map +1 -0
- package/dist/pr-requires.js +48 -0
- package/dist/wave-blocked.d.ts +79 -0
- package/dist/wave-blocked.d.ts.map +1 -0
- package/dist/wave-blocked.js +245 -0
- package/dist/wave-engine.d.ts +6 -0
- package/dist/wave-engine.d.ts.map +1 -1
- package/dist/wave-engine.js +2 -0
- package/dist/wave-runtime.mjs +243 -243
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +19 -4
- package/dist/workspace/ci-branches.d.ts +51 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -0
- package/dist/workspace/ci-branches.js +69 -0
- package/dist/workspace/ci-cloud-identity.d.ts +11 -0
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
- package/dist/workspace/ci-cloud-identity.js +24 -0
- package/dist/workspace/ci-init.d.ts.map +1 -1
- package/dist/workspace/ci-init.js +5 -4
- package/dist/workspace/ci-locks.d.ts +12 -0
- package/dist/workspace/ci-locks.d.ts.map +1 -0
- package/dist/workspace/ci-locks.js +6 -0
- package/dist/workspace/ci-run.d.ts +17 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +66 -4
- package/dist/workspace/ci-workflow-at.d.ts +14 -0
- package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
- package/dist/workspace/ci-workflow-at.js +17 -0
- package/dist/workspace/ci-workflow.d.ts +11 -3
- package/dist/workspace/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-workflow.js +10 -2
- package/dist/workspace/declaration.d.ts +69 -3
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +72 -1
- package/dist/workspace/declaration.schema.json +149 -2
- package/dist/workspace/identity-gate-rule.d.ts +1 -1
- package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
- package/dist/workspace/identity-gate-rule.js +1 -1
- package/dist/workspace/identity.d.ts +1 -1
- package/dist/workspace/identity.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +8 -5
- package/dist/workspace/member-pipeline.d.ts +6 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +23 -0
- package/dist/workspace/pr-report.schema.json +94 -3
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +1 -1
- package/dist/workspace/security.d.ts.map +1 -1
- package/dist/workspace/security.js +4 -0
- package/dist/workspace/status-gates.d.ts +3 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-gates.js +8 -7
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +4 -1
- package/dist/workspace/trust/seal.d.ts +2 -2
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +2 -2
- package/package.json +1 -1
- package/src/__fixtures__/oidc-emulator.ts +118 -0
- package/src/ci-handover.test.ts +209 -0
- package/src/ci-handover.ts +237 -0
- package/src/ci-identity.test.ts +201 -0
- package/src/ci-identity.ts +310 -0
- package/src/cli/handlers/approve.ts +18 -5
- package/src/cli/handlers/build.ts +18 -1
- package/src/cli/handlers/fan-out.ts +43 -6
- package/src/cli/handlers/gate-resume.ts +16 -2
- package/src/cli/handlers/lock.test.ts +98 -0
- package/src/cli/handlers/lock.ts +202 -0
- package/src/cli/handlers/operator.test.ts +3 -1
- package/src/cli/handlers/pr-before-merge.test.ts +276 -0
- package/src/cli/handlers/pr-env.ts +462 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
- package/src/cli/handlers/pr.test.ts +184 -0
- package/src/cli/handlers/pr.ts +605 -61
- package/src/cli/handlers/promote.ts +9 -0
- package/src/cli/handlers/query.ts +78 -0
- package/src/cli/handlers/run.ts +10 -0
- package/src/cli/main.ts +49 -4
- package/src/cli/parse-args.ts +34 -2
- package/src/cli/registry.ts +31 -2
- package/src/components/capability-targets.test.ts +162 -0
- package/src/components/capability.ts +71 -0
- package/src/components/cli-support.ts +14 -1
- package/src/components/driver.ts +54 -3
- package/src/components/fan-out-run.ts +58 -3
- package/src/components/fan-out-waves.test.ts +45 -1
- package/src/components/gate-plan.ts +3 -0
- package/src/components/pr-environment.test.ts +249 -0
- package/src/components/pr-environment.ts +538 -0
- package/src/components/pr-hooks.test.ts +118 -0
- package/src/components/pr-hooks.ts +157 -0
- package/src/components/pr-pipeline.ts +194 -21
- package/src/components/pr-policy.test.ts +145 -0
- package/src/components/pr-policy.ts +211 -0
- package/src/components/pr-run.ts +14 -2
- package/src/components/wave-plan.ts +38 -3
- package/src/gate-decision-bundle.test.ts +4 -3
- package/src/gate-decision.ts +39 -2
- package/src/gate-policy-bundle.test.ts +28 -0
- package/src/gate-policy.test.ts +162 -0
- package/src/gate-policy.ts +298 -0
- package/src/lifecycle/gate-checkpoint.ts +1 -1
- package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
- package/src/lifecycle/query.test.ts +91 -0
- package/src/lifecycle/query.ts +315 -0
- package/src/lifecycle/run-ledger.ts +18 -0
- package/src/lifecycle/target-lock.test.ts +195 -0
- package/src/lifecycle/target-lock.ts +347 -0
- package/src/op/activities/activity-contracts.ts +31 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activities/pr-refs.ts +28 -0
- package/src/op/activities/predict-behaviour.ts +3 -23
- package/src/op/activities/review-agent.test.ts +240 -0
- package/src/op/activities/review-agent.ts +355 -0
- package/src/op/builders.ts +13 -0
- package/src/op/cron.ts +44 -0
- package/src/op/gate-alias.test.ts +2 -2
- package/src/op/gate-approval.test.ts +30 -9
- package/src/op/gate-approval.ts +23 -26
- package/src/op/gate-quorum.test.ts +139 -0
- package/src/op/gate-quorum.ts +248 -0
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +1 -1
- package/src/op/gate.ts +64 -23
- package/src/op/index.ts +13 -1
- package/src/op/local-executor.ts +9 -2
- package/src/op/op-waves-hook-targets.test.ts +126 -0
- package/src/op/op-waves-policy-hold.test.ts +66 -0
- package/src/op/op-waves-quorum.test.ts +101 -0
- package/src/op/op-waves-run.ts +197 -183
- package/src/op/op-waves.ts +83 -10
- package/src/op/schedule-overdue.test.ts +220 -0
- package/src/op/schedule-overdue.ts +161 -0
- package/src/op/stage-hooks.ts +251 -0
- package/src/override-gate-bundle.test.ts +1 -1
- package/src/override-gate.ts +2 -2
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +33 -0
- package/src/pr-forge.ts +347 -35
- package/src/pr-loop-bundle.test.ts +2 -2
- package/src/pr-loop.test.ts +58 -0
- package/src/pr-loop.ts +170 -4
- package/src/pr-post.test.ts +141 -0
- package/src/pr-post.ts +159 -0
- package/src/pr-requires.test.ts +184 -0
- package/src/pr-requires.ts +79 -0
- package/src/review-agent-bundle.test.ts +35 -0
- package/src/stage-hooks-bundle.test.ts +38 -0
- package/src/wave-blocked-bundle.test.ts +25 -0
- package/src/wave-blocked.test.ts +146 -0
- package/src/wave-blocked.ts +271 -0
- package/src/wave-engine.ts +8 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
- package/src/workspace/checks/ci-workflow.test.ts +9 -0
- package/src/workspace/checks/ci-workflow.ts +18 -4
- package/src/workspace/ci-branches.test.ts +62 -0
- package/src/workspace/ci-branches.ts +79 -0
- package/src/workspace/ci-cloud-identity.test.ts +53 -0
- package/src/workspace/ci-cloud-identity.ts +23 -0
- package/src/workspace/ci-init.ts +5 -4
- package/src/workspace/ci-locks.ts +16 -0
- package/src/workspace/ci-run-index.test.ts +2 -1
- package/src/workspace/ci-run.test.ts +107 -8
- package/src/workspace/ci-run.ts +74 -5
- package/src/workspace/ci-workflow-at.ts +23 -0
- package/src/workspace/ci-workflow.ts +13 -2
- package/src/workspace/declaration.schema.json +149 -2
- package/src/workspace/declaration.test.ts +76 -0
- package/src/workspace/declaration.ts +140 -5
- package/src/workspace/identity-gate-rule.ts +1 -1
- package/src/workspace/identity.ts +1 -1
- package/src/workspace/lineage-upgrade.ts +11 -8
- package/src/workspace/member-pipeline.test.ts +29 -0
- package/src/workspace/member-pipeline.ts +30 -0
- package/src/workspace/pr-report.schema.json +94 -3
- package/src/workspace/reason-codes.ts +1 -1
- package/src/workspace/security.ts +4 -0
- package/src/workspace/status-gates.ts +9 -6
- package/src/workspace/status.ts +4 -1
- package/src/workspace/trust/gate-seal.test.ts +1 -1
- package/src/workspace/trust/seal.ts +2 -2
- package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
- /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
package/src/op/op-waves-run.ts
CHANGED
|
@@ -68,11 +68,16 @@ import type { GateAnswerRef, PendingGateRecord } from "../lifecycle/gate-ledger"
|
|
|
68
68
|
import type { GatePointAsker } from "../point-decision";
|
|
69
69
|
import type { WaitingPoint } from "./steward-points";
|
|
70
70
|
import { isApprovalSpent, type AppliedGateRecord } from "../lifecycle/gate-applied";
|
|
71
|
+
import { samePlanDigest } from "../lifecycle/plan-digest";
|
|
71
72
|
import { isLifecycleLocalNote } from "../lifecycle/local-note";
|
|
72
73
|
import type { HeadPlans, MergedReview, ReviewSource } from "./gate-review";
|
|
73
74
|
import type { TrustPolicy } from "../workspace/trust/policy";
|
|
74
|
-
import type { GateSigners } from "../
|
|
75
|
+
import type { GateSigners } from "../lifecycle/gate-seal";
|
|
75
76
|
import { ciRunIdFromEnv } from "./gate-run-locator";
|
|
77
|
+
import {
|
|
78
|
+
quorumProblems, quorumRuleLine, quorumRulesFor, quorumWaiting, ruleNeedsProof, samePrincipal, trustQuorumJudge,
|
|
79
|
+
type GateQuorumRule, type GateQuorumRuleProgress,
|
|
80
|
+
} from "./gate-quorum";
|
|
76
81
|
import {
|
|
77
82
|
OP_WAVE_DEFAULT_APPLY,
|
|
78
83
|
OP_WAVE_GATE_POLICIES,
|
|
@@ -83,6 +88,7 @@ import {
|
|
|
83
88
|
opWaveApplyHooksRunBy,
|
|
84
89
|
opWaveGateName,
|
|
85
90
|
opWaveHooksAt,
|
|
91
|
+
opWaveHookRuns,
|
|
86
92
|
opWavePointAt,
|
|
87
93
|
opWavePointProblems,
|
|
88
94
|
opWaveRecordPath,
|
|
@@ -99,6 +105,26 @@ import {
|
|
|
99
105
|
type OpWaveRun,
|
|
100
106
|
type OpWavesSpec,
|
|
101
107
|
} from "./op-waves";
|
|
108
|
+
import {
|
|
109
|
+
OP_WAVE_PENDING_RESERVED,
|
|
110
|
+
opWaveArgv,
|
|
111
|
+
pendingFields,
|
|
112
|
+
readHoldRequest,
|
|
113
|
+
runHookList,
|
|
114
|
+
runStageHooks,
|
|
115
|
+
spawnExec,
|
|
116
|
+
type HookPlace,
|
|
117
|
+
type OpWaveExec,
|
|
118
|
+
type OpWaveHoldRequest,
|
|
119
|
+
type OpWaveHookOutcome,
|
|
120
|
+
type OpWaveHookRun,
|
|
121
|
+
type OtherGateHold,
|
|
122
|
+
type RunStageHooksOptions,
|
|
123
|
+
type StageHooksRun,
|
|
124
|
+
} from "./stage-hooks";
|
|
125
|
+
|
|
126
|
+
export { OP_WAVE_PENDING_RESERVED, opWaveArgv, runStageHooks };
|
|
127
|
+
export type { OpWaveExec, OpWaveHoldRequest, OpWaveHookOutcome, OpWaveHookRun, RunStageHooksOptions, StageHooksRun };
|
|
102
128
|
|
|
103
129
|
/** What a run's plan command writes to `{plan}`. */
|
|
104
130
|
export interface OpWavePlanFile {
|
|
@@ -113,6 +139,13 @@ export interface OpWavePlanFile {
|
|
|
113
139
|
* plan digest and these rules counts (`@intentius/chant/override-gate`).
|
|
114
140
|
*/
|
|
115
141
|
denied?: { rules: string[]; reason?: string };
|
|
142
|
+
/**
|
|
143
|
+
* The product's policy holds this plan for a person (#4294): the ids of
|
|
144
|
+
* the hold rules that matched. The wave waits for an approval of its set
|
|
145
|
+
* digest whatever its gate policy says, as a hook with `onFailure:
|
|
146
|
+
* approve` holds it. `@intentius/chant/gate-policy` gives the rules.
|
|
147
|
+
*/
|
|
148
|
+
held?: { rules: string[]; reason?: string };
|
|
116
149
|
}
|
|
117
150
|
|
|
118
151
|
/** What a wave-level plan command writes to `{plans}` (#3762): one plan per run, by target. The array alone is read too. */
|
|
@@ -141,12 +174,6 @@ export interface OpWavePlansFile {
|
|
|
141
174
|
waves?: number;
|
|
142
175
|
}
|
|
143
176
|
|
|
144
|
-
/** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
|
|
145
|
-
export const OP_WAVE_PENDING_RESERVED = [
|
|
146
|
-
"version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume",
|
|
147
|
-
"environment", "origin", "neverOverMcp", "approval", "approvalMode", "answer", "wave", "heldBy",
|
|
148
|
-
] as const;
|
|
149
|
-
|
|
150
177
|
/** What chant writes to `{runs}` for a wave-level command or a hook (#3762). */
|
|
151
178
|
export interface OpWaveRunsFile {
|
|
152
179
|
/** The wave's name. */
|
|
@@ -170,6 +197,8 @@ export interface OpWaveMember extends WaveMember {
|
|
|
170
197
|
denied?: { rules: string[]; reason?: string };
|
|
171
198
|
/** The override that lets the denied plan through (#3821). */
|
|
172
199
|
override?: { by: string; at: string; digest: string; reason: string };
|
|
200
|
+
/** The product's policy holds the plan for a person (#4294). */
|
|
201
|
+
held?: { rules: string[]; reason?: string };
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
/** Where a wave's policy came from. */
|
|
@@ -207,6 +236,10 @@ export interface OpWaveDecision {
|
|
|
207
236
|
point?: OpWavePoint & { question?: WaitingPoint; answer?: GateAnswerRef };
|
|
208
237
|
/** The review that approved the wave, or why the review did not, under `pr-review`. */
|
|
209
238
|
review?: { pr?: number; head?: string; approvers?: string[]; url?: string; reason?: string };
|
|
239
|
+
/** The approval rules at base the wave's targets take (#4290). Absent when it takes none. */
|
|
240
|
+
quorum?: GateQuorumRule[];
|
|
241
|
+
/** Where each of those rules stands (#4290), while the wave waits. */
|
|
242
|
+
quorumProgress?: GateQuorumRuleProgress[];
|
|
210
243
|
/** The command that approves this digest, when waiting. */
|
|
211
244
|
approve?: string;
|
|
212
245
|
/** The hooks read at base (#3762), which a wide wave's share jobs run too. Absent when there are none. */
|
|
@@ -286,18 +319,6 @@ export interface OpWaveApplyOutcome {
|
|
|
286
319
|
|
|
287
320
|
export const OP_WAVE_APPLY_OUTCOMES = ["stood-down", "held", "replan"] as const;
|
|
288
321
|
|
|
289
|
-
/** What a hook asks for when it holds a wave on another gate (#3821): it writes this to `CHANT_HOLD` and exits non-zero. */
|
|
290
|
-
export interface OpWaveHoldRequest {
|
|
291
|
-
/** The op the gate is recorded under. Default: the spec's name. */
|
|
292
|
-
op?: string;
|
|
293
|
-
gate: string;
|
|
294
|
-
/** The digest an approval binds. */
|
|
295
|
-
digest: string;
|
|
296
|
-
description?: string;
|
|
297
|
-
/** Fields for the pending fact, as a plans file's `pending`. */
|
|
298
|
-
pending?: Record<string, unknown>;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
322
|
/** A hold on another gate, as the decision records it (#3821). */
|
|
302
323
|
export interface OpWaveHeldOn {
|
|
303
324
|
op: string;
|
|
@@ -328,26 +349,6 @@ export interface OpWaveHeldOnApproval {
|
|
|
328
349
|
approvedAt: string;
|
|
329
350
|
}
|
|
330
351
|
|
|
331
|
-
/** One hook's run (#3762). */
|
|
332
|
-
export interface OpWaveHookOutcome {
|
|
333
|
-
moment: OpWaveHookMoment;
|
|
334
|
-
name: string;
|
|
335
|
-
/** The run, for a `perRun` hook. */
|
|
336
|
-
target?: string;
|
|
337
|
-
exit: number;
|
|
338
|
-
outcome: "passed" | "failed" | "held" | "skipped";
|
|
339
|
-
/** Why the moment was skipped, for `skipped` (#3811). */
|
|
340
|
-
reason?: string;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** One run as a hook sees it (#3804). */
|
|
344
|
-
export interface OpWaveHookRun {
|
|
345
|
-
target: string;
|
|
346
|
-
op: string;
|
|
347
|
-
/** The run's plan file, once planned. */
|
|
348
|
-
plan?: string;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
352
|
/**
|
|
352
353
|
* What chant hands an apply command in `CHANT_HOOKS` (#3804), and what
|
|
353
354
|
* {@link opWaveHooksAtBase} builds for an apply outside the waves. Paths are
|
|
@@ -423,15 +424,6 @@ export interface OpWaveApplyHooksRun {
|
|
|
423
424
|
proceed: boolean;
|
|
424
425
|
}
|
|
425
426
|
|
|
426
|
-
/** Runs one argv; returns its exit code. `env` is added to the job's environment. Injected so tests run no processes. */
|
|
427
|
-
export type OpWaveExec = (argv: string[], cwd: string, env?: Record<string, string>) => number;
|
|
428
|
-
|
|
429
|
-
const spawnExec: OpWaveExec = (argv, cwd, env) => {
|
|
430
|
-
const result = spawnSync(argv[0]!, argv.slice(1), { cwd, stdio: "inherit", ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
431
|
-
if (result.error) throw result.error;
|
|
432
|
-
return result.status ?? 1;
|
|
433
|
-
};
|
|
434
|
-
|
|
435
427
|
/** Reads one file at one commit, or null when the commit has no such file. Injected for tests. */
|
|
436
428
|
export type OpWaveShowAtBase = (base: string, path: string, cwd: string) => { sha: string; text: string | null };
|
|
437
429
|
|
|
@@ -489,6 +481,10 @@ export interface OpWaveBaseRules {
|
|
|
489
481
|
point?: OpWavePoint;
|
|
490
482
|
/** Set when the point at base cannot be asked: the wave fails rather than fall back to the ledger (#3836). */
|
|
491
483
|
pointRefused?: string;
|
|
484
|
+
/** The approval rules at base (#4290), `approvals.quorum`. */
|
|
485
|
+
quorum?: GateQuorumRule[];
|
|
486
|
+
/** Set when the rules at base cannot be read: the wave fails rather than wait on fewer approvers than they ask. */
|
|
487
|
+
quorumRefused?: string;
|
|
492
488
|
}
|
|
493
489
|
|
|
494
490
|
const noHooks = (): Record<OpWaveHookMoment, OpWaveHook[]> =>
|
|
@@ -521,7 +517,7 @@ export function readOpWavePolicy(
|
|
|
521
517
|
const spend = (spec as { approvals?: { spend?: unknown } }).approvals?.spend === true;
|
|
522
518
|
const neverOverMcp = (spec as { neverOverMcp?: unknown }).neverOverMcp === true;
|
|
523
519
|
// #3836: the gate's decision point, the spec's for a wave the base does not have yet.
|
|
524
|
-
const asks = pointAtBase(spec, name, `${specFile} at ${sha.slice(0, 12)}`);
|
|
520
|
+
const asks = { ...pointAtBase(spec, name, `${specFile} at ${sha.slice(0, 12)}`), ...quorumAtBase(spec, `${specFile} at ${sha.slice(0, 12)}`) };
|
|
525
521
|
const wave = spec.waves.find((w) => w.name === name);
|
|
526
522
|
if (!wave) return { ...absent, spend, neverOverMcp, ...asks };
|
|
527
523
|
const policy = wave.gate ?? "always";
|
|
@@ -540,6 +536,15 @@ export function readOpWavePolicy(
|
|
|
540
536
|
return rules;
|
|
541
537
|
}
|
|
542
538
|
|
|
539
|
+
/** The approval rules at base (#4290), or why they cannot be read. A spec at base with none has none. */
|
|
540
|
+
function quorumAtBase(spec: OpWavesSpec, where: string): Pick<OpWaveBaseRules, "quorum" | "quorumRefused"> {
|
|
541
|
+
const raw = (spec as { approvals?: { quorum?: unknown } }).approvals?.quorum;
|
|
542
|
+
if (raw === undefined) return {};
|
|
543
|
+
const problems = Array.isArray(raw) ? quorumProblems(raw, "approvals.quorum", { notAuthor: true }) : ["`approvals.quorum` must be a list of rules"];
|
|
544
|
+
if (problems.length > 0) return { quorumRefused: `the approval rules in ${where} cannot be read (${problems.join("; ")})` };
|
|
545
|
+
return (raw as GateQuorumRule[]).length > 0 ? { quorum: raw as GateQuorumRule[] } : {};
|
|
546
|
+
}
|
|
547
|
+
|
|
543
548
|
/** The point wave `name` asks at base (#3836), or why it cannot be asked. */
|
|
544
549
|
function pointAtBase(spec: OpWavesSpec, name: string, where: string): Pick<OpWaveBaseRules, "point" | "pointRefused"> {
|
|
545
550
|
const raw = spec.waves.find((w) => w.name === name)?.point ?? spec.point;
|
|
@@ -549,22 +554,6 @@ function pointAtBase(spec: OpWavesSpec, name: string, where: string): Pick<OpWav
|
|
|
549
554
|
return { point: opWavePointAt(spec, name)! };
|
|
550
555
|
}
|
|
551
556
|
|
|
552
|
-
/** Fill one argv template. A placeholder whose value is not given is left as it is. */
|
|
553
|
-
export function opWaveArgv(
|
|
554
|
-
template: readonly string[],
|
|
555
|
-
values: { op: string; wave: string; target?: string; plan?: string; runs?: string; plans?: string },
|
|
556
|
-
): string[] {
|
|
557
|
-
const pairs: Array<[string, string | undefined]> = [
|
|
558
|
-
["{op}", values.op],
|
|
559
|
-
["{target}", values.target],
|
|
560
|
-
["{wave}", values.wave],
|
|
561
|
-
["{plans}", values.plans],
|
|
562
|
-
["{plan}", values.plan],
|
|
563
|
-
["{runs}", values.runs],
|
|
564
|
-
];
|
|
565
|
-
return template.map((part) => pairs.reduce((out, [key, value]) => (value === undefined ? out : out.replaceAll(key, value)), part));
|
|
566
|
-
}
|
|
567
|
-
|
|
568
557
|
/** Where a run's plan file goes. */
|
|
569
558
|
export function opWavePlanPath(spec: Pick<OpWavesSpec, "name">, wave: number, target: string): string {
|
|
570
559
|
return `.chant/op-waves/${spec.name}/wave-${wave}/${target.replace(/[^A-Za-z0-9._-]/g, "_")}.plan.json`;
|
|
@@ -674,11 +663,12 @@ function planRun(opts: { spec: OpWavesSpec; wave: number; cwd: string; exec: OpW
|
|
|
674
663
|
}
|
|
675
664
|
|
|
676
665
|
function member(target: string, op: string, file: OpWavePlanFile): OpWaveMember {
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
666
|
+
for (const key of ["denied", "held"] as const) {
|
|
667
|
+
const value = file[key] as unknown;
|
|
668
|
+
if (value === undefined) continue;
|
|
669
|
+
const d = value as { rules?: unknown; reason?: unknown } | null;
|
|
680
670
|
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")) {
|
|
681
|
-
throw new Error(`the plan of ${target} has
|
|
671
|
+
throw new Error(`the plan of ${target} has ${key} ${JSON.stringify(value)}; ${key === "denied" ? "a denial" : "a hold"} is { "rules": ["<rule id>", ...], "reason"?: "<text>" }`);
|
|
682
672
|
}
|
|
683
673
|
}
|
|
684
674
|
return {
|
|
@@ -688,6 +678,7 @@ function member(target: string, op: string, file: OpWavePlanFile): OpWaveMember
|
|
|
688
678
|
...(file.destructive === true ? { destructive: true } : {}),
|
|
689
679
|
...(file.empty === true ? { empty: true } : {}),
|
|
690
680
|
...(file.denied ? { denied: { rules: file.denied.rules, ...(file.denied.reason ? { reason: file.denied.reason } : {}) } } : {}),
|
|
681
|
+
...(file.held ? { held: { rules: [...new Set(file.held.rules)].sort(), ...(file.held.reason ? { reason: file.held.reason } : {}) } } : {}),
|
|
691
682
|
};
|
|
692
683
|
}
|
|
693
684
|
|
|
@@ -768,7 +759,7 @@ function planRuns(at: { spec: OpWavesSpec; k: number; wave: OpWave; cwd: string;
|
|
|
768
759
|
const members = runs.map((run) => {
|
|
769
760
|
const m = member(run.target, run.op ?? spec.op, byTarget.get(run.target)!);
|
|
770
761
|
const path = join(cwd, opWavePlanPath(spec, k, run.target));
|
|
771
|
-
writeFileSync(path, JSON.stringify({ planDigest: m.planDigest, ...(m.destructive ? { destructive: true } : {}), ...(m.empty ? { empty: true } : {}), ...(m.denied ? { denied: m.denied } : {}) }) + "\n");
|
|
762
|
+
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");
|
|
772
763
|
return m;
|
|
773
764
|
});
|
|
774
765
|
const overriders = Array.isArray(parsed) ? undefined : (parsed as { overriders?: unknown }).overriders;
|
|
@@ -819,16 +810,6 @@ function extraMembers(value: unknown, targets: ReadonlySet<string>, where: strin
|
|
|
819
810
|
return out.length > 0 ? out : undefined;
|
|
820
811
|
}
|
|
821
812
|
|
|
822
|
-
/** A plans file's `pending` (#3821): an object whose keys chant does not write itself, `description` aside. Undefined when absent or empty. */
|
|
823
|
-
function pendingFields(value: unknown, where: string): Record<string, unknown> | undefined {
|
|
824
|
-
if (value === undefined) return undefined;
|
|
825
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${where}: \`pending\` must be an object of fields for the pending fact`);
|
|
826
|
-
const reserved = Object.keys(value).filter((key) => (OP_WAVE_PENDING_RESERVED as readonly string[]).includes(key));
|
|
827
|
-
if (reserved.length > 0) throw new Error(`${where}: \`pending\` sets ${reserved.join(", ")}, which chant writes itself`);
|
|
828
|
-
if ("description" in value && typeof (value as { description?: unknown }).description !== "string") throw new Error(`${where}: pending.description must be a string`);
|
|
829
|
-
return Object.keys(value).length > 0 ? (value as Record<string, unknown>) : undefined;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
813
|
/**
|
|
833
814
|
* A wave's set digest (#3821): {@link waveSetDigest} over its runs' plans,
|
|
834
815
|
* or under `digest.over: "changing"` over the runs whose plan is not empty,
|
|
@@ -849,107 +830,6 @@ interface HooksOutcome {
|
|
|
849
830
|
waitingOn?: OpWaveHeldOn;
|
|
850
831
|
}
|
|
851
832
|
|
|
852
|
-
/** A hook call that held the wave on another gate (#3821), kept so it can run again once that gate is approved. */
|
|
853
|
-
interface OtherGateHold {
|
|
854
|
-
label: string;
|
|
855
|
-
name: string;
|
|
856
|
-
moment: OpWaveHookMoment;
|
|
857
|
-
target?: string;
|
|
858
|
-
argv: string[];
|
|
859
|
-
env: Record<string, string>;
|
|
860
|
-
request: OpWaveHoldRequest;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
/** Read the hold file a hook wrote (#3821), or undefined when it wrote none or one that names no gate and digest. */
|
|
864
|
-
function readHoldRequest(path: string, log: (line: string) => void, label: string): OpWaveHoldRequest | undefined {
|
|
865
|
-
if (!existsSync(path)) return undefined;
|
|
866
|
-
try {
|
|
867
|
-
const r = JSON.parse(readFileSync(path, "utf-8")) as Partial<OpWaveHoldRequest>;
|
|
868
|
-
if (typeof r.gate !== "string" || r.gate === "" || typeof r.digest !== "string" || r.digest === "") throw new Error("it names no gate and digest");
|
|
869
|
-
if (r.op !== undefined && (typeof r.op !== "string" || r.op === "")) throw new Error("its op is not a name");
|
|
870
|
-
return { gate: r.gate, digest: r.digest, ...(r.op ? { op: r.op } : {}), ...(typeof r.description === "string" ? { description: r.description } : {}), ...(r.pending ? { pending: pendingFields(r.pending, `the hold ${label} wrote`) ?? {} } : {}) };
|
|
871
|
-
} catch (err) {
|
|
872
|
-
log(`${label} wrote a hold file that cannot be read (${err instanceof Error ? err.message : String(err)}), so it holds the wave on its set digest`);
|
|
873
|
-
return undefined;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/** Where hooks run: the wave they belong to and the job's tools. */
|
|
878
|
-
interface HookPlace {
|
|
879
|
-
/** The spec's name and `op`. */
|
|
880
|
-
name: string;
|
|
881
|
-
op: string;
|
|
882
|
-
waveName: string;
|
|
883
|
-
k: number;
|
|
884
|
-
cwd: string;
|
|
885
|
-
exec: OpWaveExec;
|
|
886
|
-
log: (line: string) => void;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Run one moment's hooks, in order: each once for the wave, or once per run
|
|
891
|
-
* for a `perRun` hook. A hook that fails stops the rest, unless its
|
|
892
|
-
* `onFailure` is `approve`: then it holds the wave and the hooks after it run.
|
|
893
|
-
*/
|
|
894
|
-
function runHookList(
|
|
895
|
-
at: HookPlace,
|
|
896
|
-
moment: OpWaveHookMoment,
|
|
897
|
-
hooks: readonly OpWaveHook[] | undefined,
|
|
898
|
-
runs: readonly OpWaveHookRun[],
|
|
899
|
-
runsPath: string,
|
|
900
|
-
extraEnv: Record<string, string> = {},
|
|
901
|
-
): HooksOutcome & { outcomes: OpWaveHookOutcome[]; otherGates: OtherGateHold[] } {
|
|
902
|
-
const out: HooksOutcome & { outcomes: OpWaveHookOutcome[]; otherGates: OtherGateHold[] } = { holds: [], outcomes: [], otherGates: [] };
|
|
903
|
-
if (!hooks || hooks.length === 0) return out;
|
|
904
|
-
const { name: specName, op, waveName, k, cwd, exec, log } = at;
|
|
905
|
-
// #3821: a hook that holds the wave on another gate names it in this file.
|
|
906
|
-
const holdPath = `.chant/op-waves/${specName}/wave-${k}/hold.json`;
|
|
907
|
-
const env = { CHANT_OP_WAVES: specName, CHANT_WAVE: waveName, CHANT_WAVE_NUMBER: String(k), CHANT_HOOK: moment, CHANT_RUNS: runsPath, CHANT_HOLD: holdPath, ...extraEnv };
|
|
908
|
-
for (const hook of hooks) {
|
|
909
|
-
const name = hook.name ?? (hook.command.join(" ").length > 60 ? `${hook.command.join(" ").slice(0, 57)}...` : hook.command.join(" "));
|
|
910
|
-
const calls = hook.perRun
|
|
911
|
-
? runs.map((run) => ({
|
|
912
|
-
target: run.target as string | undefined,
|
|
913
|
-
argv: opWaveArgv(hook.command, { op: run.op, wave: waveName, target: run.target, runs: runsPath, ...(run.plan ? { plan: run.plan } : {}) }),
|
|
914
|
-
env: { ...env, CHANT_TARGET: run.target, ...(run.plan ? { CHANT_PLAN: run.plan } : {}) } as Record<string, string>,
|
|
915
|
-
}))
|
|
916
|
-
: [{ target: undefined, argv: opWaveArgv(hook.command, { op, wave: waveName, runs: runsPath }), env: env as Record<string, string> }];
|
|
917
|
-
for (const call of calls) {
|
|
918
|
-
const label = `${moment} hook ${name}${call.target ? ` (${call.target})` : ""}`;
|
|
919
|
-
let code: number;
|
|
920
|
-
rmSync(join(cwd, holdPath), { force: true });
|
|
921
|
-
try {
|
|
922
|
-
code = exec(call.argv, cwd, call.env);
|
|
923
|
-
} catch (err) {
|
|
924
|
-
log(`${label} did not run: ${err instanceof Error ? err.message : String(err)}`);
|
|
925
|
-
code = 127;
|
|
926
|
-
}
|
|
927
|
-
const base = { moment, name, ...(call.target ? { target: call.target } : {}), exit: code };
|
|
928
|
-
if (code === 0) {
|
|
929
|
-
out.outcomes.push({ ...base, outcome: "passed" });
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
if (hook.onFailure === "approve") {
|
|
933
|
-
out.outcomes.push({ ...base, outcome: "held" });
|
|
934
|
-
const request = readHoldRequest(join(cwd, holdPath), log, label);
|
|
935
|
-
if (request) {
|
|
936
|
-
out.otherGates.push({ label, name, moment, ...(call.target ? { target: call.target } : {}), argv: call.argv, env: call.env, request });
|
|
937
|
-
log(`${label} exited ${code} and holds wave ${k} (${waveName}) on gate ${request.op ?? specName} / ${request.gate}, digest ${request.digest}`);
|
|
938
|
-
continue;
|
|
939
|
-
}
|
|
940
|
-
out.holds.push(label);
|
|
941
|
-
log(`${label} exited ${code}, so wave ${k} (${waveName}) waits for an approval of its set digest`);
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
out.outcomes.push({ ...base, outcome: "failed" });
|
|
945
|
-
out.failed = `${label} exited ${code}`;
|
|
946
|
-
log(out.failed);
|
|
947
|
-
return out;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return out;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
833
|
const placeOf = (ctx: WaveContext): HookPlace => ({ name: ctx.spec.name, op: ctx.spec.op, waveName: ctx.wave.name, k: ctx.k, cwd: ctx.cwd, exec: ctx.exec, log: ctx.log });
|
|
954
834
|
|
|
955
835
|
const hookRuns = (spec: OpWavesSpec, k: number, runs: readonly OpWaveRun[], members?: ReadonlyMap<string, OpWaveMember>): OpWaveHookRun[] =>
|
|
@@ -1034,6 +914,18 @@ async function runHooks(
|
|
|
1034
914
|
return back;
|
|
1035
915
|
}
|
|
1036
916
|
|
|
917
|
+
/**
|
|
918
|
+
* The hooks that run before a job plans (#4288): `before-observe`, then
|
|
919
|
+
* `before-plan`. A failure or a hold on another gate in the first ends it
|
|
920
|
+
* there; the holds of both add up.
|
|
921
|
+
*/
|
|
922
|
+
async function runBeforePlanHooks(ctx: WaveContext, hooks: Partial<Record<OpWaveHookMoment, OpWaveHook[]>> | undefined, runs: readonly OpWaveRun[]): Promise<HooksOutcome> {
|
|
923
|
+
const observe = await runHooks(ctx, "before-observe", hooks?.["before-observe"], runs);
|
|
924
|
+
if (observe.failed || observe.waitingOn) return observe;
|
|
925
|
+
const plan = await runHooks(ctx, "before-plan", hooks?.["before-plan"], runs);
|
|
926
|
+
return { ...plan, holds: [...observe.holds, ...plan.holds] };
|
|
927
|
+
}
|
|
928
|
+
|
|
1037
929
|
/** 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. */
|
|
1038
930
|
export const OP_WAVE_MAX_CHAINED_HOLDS = 10;
|
|
1039
931
|
|
|
@@ -1396,7 +1288,7 @@ async function planOpWave(options: RunOpWaveOptions, { k, pass }: WaveAttempt<Ru
|
|
|
1396
1288
|
log(`${part} of wave ${k} (${wave.name}) holds none of the wave's runs; nothing to apply`);
|
|
1397
1289
|
return { end: result };
|
|
1398
1290
|
}
|
|
1399
|
-
const before = await
|
|
1291
|
+
const before = await runBeforePlanHooks(ctx, decision.hooks, slice);
|
|
1400
1292
|
if (before.failed) return { end: failed(ctx, `a hook before the plan failed, so nothing in ${part} of wave ${k} was planned or applied`) };
|
|
1401
1293
|
if (before.waitingOn) return { end: await waitOnOther(ctx, decision, before.waitingOn) };
|
|
1402
1294
|
const { members } = planRuns(ctx, slice);
|
|
@@ -1415,6 +1307,12 @@ async function planOpWave(options: RunOpWaveOptions, { k, pass }: WaveAttempt<Ru
|
|
|
1415
1307
|
noteHooksAtBase(ctx, rules);
|
|
1416
1308
|
// #3836: a point at base that cannot be asked fails the wave; the ledger never stands in for it.
|
|
1417
1309
|
if (rules.pointRefused) return { end: failed(ctx, `wave ${k} (${wave.name}): ${rules.pointRefused}, so nothing in it was planned or applied`) };
|
|
1310
|
+
// #4290: rules at base that cannot be read fail the wave; fewer approvers never stand in for them.
|
|
1311
|
+
if (rules.quorumRefused) return { end: failed(ctx, `wave ${k} (${wave.name}): ${rules.quorumRefused}, so nothing in it was planned or applied`) };
|
|
1312
|
+
if (rules.quorum && rules.point) 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`) };
|
|
1313
|
+
if (JSON.stringify(spec.approvals?.quorum ?? null) !== JSON.stringify(rules.quorum ?? null)) {
|
|
1314
|
+
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`);
|
|
1315
|
+
}
|
|
1418
1316
|
const own = opWavePointAt(spec, wave.name);
|
|
1419
1317
|
if (JSON.stringify(own) !== JSON.stringify(rules.point)) {
|
|
1420
1318
|
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`);
|
|
@@ -1445,7 +1343,7 @@ async function planOpWave(options: RunOpWaveOptions, { k, pass }: WaveAttempt<Ru
|
|
|
1445
1343
|
...(neverOverMcp ? { neverOverMcp: true as const } : {}),
|
|
1446
1344
|
...(rules.point ? { point: { ...rules.point } } : {}),
|
|
1447
1345
|
};
|
|
1448
|
-
const before = await
|
|
1346
|
+
const before = await runBeforePlanHooks(ctx, rules.hooks, wave.runs);
|
|
1449
1347
|
if (before.failed) return { end: failed(ctx, `a hook before the plan failed, so nothing in wave ${k} was planned or applied`) };
|
|
1450
1348
|
if (before.waitingOn) return { end: await decided(ctx, decision, recordPath, before.waitingOn) };
|
|
1451
1349
|
const { members, pending, overriders, overridersSource, extras, waves: found } = planRuns(ctx, wave.runs, wave.discover === true);
|
|
@@ -1519,11 +1417,16 @@ async function decideOpWave(
|
|
|
1519
1417
|
const { policy, source, base } = planned.rules;
|
|
1520
1418
|
decision.digest = digest;
|
|
1521
1419
|
decision.members = members;
|
|
1420
|
+
// #4290: the rules at base whose targets glob matches one of the wave's runs.
|
|
1421
|
+
const takes = planned.rules.quorum ? quorumRulesFor(planned.rules.quorum, members.map((m) => m.member)) : [];
|
|
1422
|
+
if (takes.length > 0) decision.quorum = takes;
|
|
1522
1423
|
if (extras) decision.extras = extras;
|
|
1523
1424
|
if (pending) decision.pending = pending;
|
|
1524
1425
|
const after = await runHooks(ctx, "after-plan", planned.rules.hooks["after-plan"], wave.runs, planned.planned, digest);
|
|
1525
1426
|
if (after.failed) return { end: failed(ctx, `a hook after the plan failed, so nothing in wave ${k} was applied`) };
|
|
1526
1427
|
holds.push(...planned.before, ...after.holds);
|
|
1428
|
+
// #4294: a run the product's policy holds makes the wave wait for a person, as a held hook does.
|
|
1429
|
+
for (const m of members) if (m.held) holds.push(`policy on ${m.member} (${m.held.rules.join(", ")})${m.held.reason ? `: ${m.held.reason}` : ""}`);
|
|
1527
1430
|
if (holds.length > 0) decision.heldBy = holds;
|
|
1528
1431
|
if (after.waitingOn) return { end: await decided(ctx, decision, recordPath, after.waitingOn) };
|
|
1529
1432
|
// #3821: a run the product's policy denied applies only under an override; with none the wave is refused, and no approval is waited for.
|
|
@@ -1779,6 +1682,7 @@ function failed(ctx: WaveContext, line: string): RunOpWaveResult {
|
|
|
1779
1682
|
*/
|
|
1780
1683
|
async function settleGate(options: RunOpWaveOptions, ctx: WaveContext, decision: OpWaveDecision): Promise<void> {
|
|
1781
1684
|
if (decision.point) return settlePointGate(options, ctx, decision, decision.point);
|
|
1685
|
+
if (decision.quorum?.length) return settleQuorumGate(options, ctx, decision, decision.quorum);
|
|
1782
1686
|
const { spec, k, wave, cwd } = ctx;
|
|
1783
1687
|
const { gate, digest, approval, base, policy, members } = decision;
|
|
1784
1688
|
const { ledger, port } = await gatePort(options, cwd, gate, approval, base);
|
|
@@ -1833,6 +1737,116 @@ async function settleGate(options: RunOpWaveOptions, ctx: WaveContext, decision:
|
|
|
1833
1737
|
decision.approve = approveCommand(spec.name, gate, undefined, digest, approval === "sealed");
|
|
1834
1738
|
}
|
|
1835
1739
|
|
|
1740
|
+
/**
|
|
1741
|
+
* Decide a gate under approval rules (#4290): the rules at base the wave's
|
|
1742
|
+
* targets take, each met by distinct human approvers of the digest. Roles
|
|
1743
|
+
* come from `.chant/trust.json` at base, for an approver a seal proves, or
|
|
1744
|
+
* under `pr-review` the merged pull request's review read from the forge in
|
|
1745
|
+
* this run. That review is written to the ledger once per approving
|
|
1746
|
+
* reviewer, so each counts on their own. `notAuthor` reads the author from
|
|
1747
|
+
* the same pull request; with none, no approver can show they are someone
|
|
1748
|
+
* else and the rule waits.
|
|
1749
|
+
*/
|
|
1750
|
+
async function settleQuorumGate(options: RunOpWaveOptions, ctx: WaveContext, decision: OpWaveDecision, rules: GateQuorumRule[]): Promise<void> {
|
|
1751
|
+
const { spec, k, wave, cwd, log } = ctx;
|
|
1752
|
+
const { gate, digest, approval, base, policy, members } = decision;
|
|
1753
|
+
const { ledger, port } = await gatePort(options, cwd, gate, approval, base);
|
|
1754
|
+
const now = options.now ?? new Date().toISOString();
|
|
1755
|
+
const spent: AppliedGateRecord[] | undefined = decision.spend === "base" ? ((await ledger.readApplied?.(spec.name)) ?? []).filter((a) => a.gate === gate) : undefined;
|
|
1756
|
+
const review = await import("./gate-review");
|
|
1757
|
+
const trust = (options.trust ?? ((b) => review.trustPolicyAt(cwd, b)))(base);
|
|
1758
|
+
const label = `wave ${k} (${wave.name})`;
|
|
1759
|
+
|
|
1760
|
+
// The merged pull request: its author for notAuthor, and its review under pr-review.
|
|
1761
|
+
let merged: MergedReview | null = null;
|
|
1762
|
+
let unknown: string | undefined;
|
|
1763
|
+
const asks = approval === "pr-review" || rules.some((r) => r.notAuthor);
|
|
1764
|
+
const head = asks ? (options.head ?? execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim()) : "";
|
|
1765
|
+
if (asks) {
|
|
1766
|
+
try {
|
|
1767
|
+
merged = await (options.reviews ?? review.reviewSourceFromEnv()).mergedReview(head);
|
|
1768
|
+
if (!merged) unknown = `no merged pull request produced ${head.slice(0, 12)}`;
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
unknown = `the forge's review could not be read: ${err instanceof Error ? err.message : String(err)}`;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (unknown && rules.some((r) => r.notAuthor)) log(`${label}: ${unknown}, so the change's author is not known`);
|
|
1774
|
+
if (approval === "pr-review" && unknown) decision.review = { reason: unknown };
|
|
1775
|
+
|
|
1776
|
+
let reviewed: { head: string; approvers: string[] } | undefined;
|
|
1777
|
+
if (approval === "pr-review" && merged) {
|
|
1778
|
+
const plans = merged.approvers.length === 0 ? null : await (options.headPlans ?? ((name, h) => readHeadPlans(name, h, cwd)))(spec.name, merged.head);
|
|
1779
|
+
const { reviewApproval } = await import("../gate-decision");
|
|
1780
|
+
const outcome = reviewApproval(merged, plans?.waves.find((w) => w.wave === k)?.digest, digest);
|
|
1781
|
+
decision.review = outcome.review;
|
|
1782
|
+
const reviewSpent = spent !== undefined && isApprovalSpent(spent, { gate, planDigest: digest, review: { head: merged.head } });
|
|
1783
|
+
if (reviewSpent) 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` };
|
|
1784
|
+
if (outcome.approved && !reviewSpent) {
|
|
1785
|
+
reviewed = { head: merged.head, approvers: merged.approvers };
|
|
1786
|
+
// Each approving reviewer once, so the rules count them one by one.
|
|
1787
|
+
const had = (await ledger.read(spec.name)).resolutions.filter((r) => r.gate === gate && r.review?.head === merged!.head && samePlanDigest(r.planDigest, digest));
|
|
1788
|
+
const note = `approved by the review of #${merged.pr} at ${merged.head.slice(0, 12)}: ${merged.approvers.join(", ")}`;
|
|
1789
|
+
for (const who of merged.approvers) {
|
|
1790
|
+
if (had.some((r) => samePrincipal(r.resolvedBy, who))) continue;
|
|
1791
|
+
await ledger.appendResolution?.({
|
|
1792
|
+
op: spec.name,
|
|
1793
|
+
gate,
|
|
1794
|
+
resolvedBy: who,
|
|
1795
|
+
timestamp: now,
|
|
1796
|
+
planDigest: digest,
|
|
1797
|
+
approver: { kind: "human" },
|
|
1798
|
+
gitSha: head,
|
|
1799
|
+
...(ciRunIdFromEnv() ? { runId: ciRunIdFromEnv()! } : {}),
|
|
1800
|
+
note,
|
|
1801
|
+
...(merged.url ? { url: merged.url } : {}),
|
|
1802
|
+
review: { pr: merged.pr, head: merged.head, approvers: merged.approvers },
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const judge = trustQuorumJudge({
|
|
1809
|
+
signers: trust,
|
|
1810
|
+
roles: trust.roles,
|
|
1811
|
+
...(merged ? { authors: [merged.author] } : {}),
|
|
1812
|
+
...(reviewed ? { review: reviewed } : {}),
|
|
1813
|
+
});
|
|
1814
|
+
const held = decision.heldBy?.length ? `, held by ${decision.heldBy.join(", ")}` : "";
|
|
1815
|
+
const check = await decideWaveGate(port, {
|
|
1816
|
+
op: spec.name,
|
|
1817
|
+
gate,
|
|
1818
|
+
digest,
|
|
1819
|
+
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}`,
|
|
1820
|
+
now,
|
|
1821
|
+
...(spent ? { spent } : {}),
|
|
1822
|
+
fields: { ...decision.pending, approvalMode: approval },
|
|
1823
|
+
...(decision.neverOverMcp ? { neverOverMcp: true } : {}),
|
|
1824
|
+
approval: { quorum: rules, mode: "log-only" },
|
|
1825
|
+
judge,
|
|
1826
|
+
});
|
|
1827
|
+
if (check.satisfied) {
|
|
1828
|
+
const by = (check.approvals ?? [check.resolution]).map((r) => r.resolvedBy);
|
|
1829
|
+
decision.status = "approved";
|
|
1830
|
+
decision.via = reviewed && check.resolution.review?.head === reviewed.head ? "pr-review" : "ledger";
|
|
1831
|
+
decision.approvedBy = by.join(", ");
|
|
1832
|
+
decision.approvedAt = check.resolution.timestamp;
|
|
1833
|
+
log(`${label}: the approval rules at ${base.slice(0, 12)} are met by ${by.join(", ")}`);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
logRefused(log, label, check);
|
|
1837
|
+
decision.pendingFact = check.pending;
|
|
1838
|
+
decision.recorded = check.recorded;
|
|
1839
|
+
decision.waitingSince = check.pending.timestamp;
|
|
1840
|
+
if (check.mismatch) decision.mismatch = check.mismatch;
|
|
1841
|
+
if (check.quorum?.rules) {
|
|
1842
|
+
decision.quorumProgress = check.quorum.rules;
|
|
1843
|
+
for (const p of check.quorum.rules) log(`${label}: approval rule ${quorumRuleLine(p)}`);
|
|
1844
|
+
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}`);
|
|
1845
|
+
}
|
|
1846
|
+
decision.status = "waiting";
|
|
1847
|
+
decision.approve = approveCommand(spec.name, gate, undefined, digest, approval === "sealed" || rules.some(ruleNeedsProof));
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1836
1850
|
/**
|
|
1837
1851
|
* Decide a gate that asks a decision point (#3836), as an Op gate with a
|
|
1838
1852
|
* `point` is decided (#3170): ask it with `gate.planDigest` set to the set
|