@intentius/chant 0.124.15 → 0.124.16

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 (81) hide show
  1. package/dist/cli/flush.d.ts +16 -0
  2. package/dist/cli/flush.d.ts.map +1 -0
  3. package/dist/cli/flush.js +42 -0
  4. package/dist/cli/handlers/approve.d.ts +98 -0
  5. package/dist/cli/handlers/approve.d.ts.map +1 -0
  6. package/dist/cli/handlers/approve.js +495 -0
  7. package/dist/cli/handlers/operator.d.ts +1 -95
  8. package/dist/cli/handlers/operator.d.ts.map +1 -1
  9. package/dist/cli/handlers/operator.js +4 -488
  10. package/dist/cli/handlers/override-approve.js +1 -1
  11. package/dist/cli/main.d.ts +4 -15
  12. package/dist/cli/main.d.ts.map +1 -1
  13. package/dist/cli/main.js +4 -1062
  14. package/dist/cli/parse-args.d.ts +6 -0
  15. package/dist/cli/parse-args.d.ts.map +1 -0
  16. package/dist/cli/parse-args.js +1028 -0
  17. package/dist/components/fan-out-record.d.ts +1 -1
  18. package/dist/components/fan-out-record.d.ts.map +1 -1
  19. package/dist/components/fan-out-record.js +3 -7
  20. package/dist/components/fan-out-run.d.ts +5 -3
  21. package/dist/components/fan-out-run.d.ts.map +1 -1
  22. package/dist/components/fan-out-run.js +7 -5
  23. package/dist/gated-waves.d.ts +25 -6
  24. package/dist/gated-waves.d.ts.map +1 -1
  25. package/dist/gated-waves.js +21 -6
  26. package/dist/op/activities/activity-contracts.d.ts +4 -4
  27. package/dist/op/op-waves-run.d.ts +1 -1
  28. package/dist/op/op-waves-run.d.ts.map +1 -1
  29. package/dist/op/op-waves-run.js +5 -6
  30. package/dist/op/op-waves.d.ts.map +1 -1
  31. package/dist/op/op-waves.js +4 -8
  32. package/dist/package-version.d.ts +2 -0
  33. package/dist/package-version.d.ts.map +1 -0
  34. package/dist/package-version.js +10 -0
  35. package/dist/wave-engine.d.ts +62 -0
  36. package/dist/wave-engine.d.ts.map +1 -0
  37. package/dist/wave-engine.js +59 -0
  38. package/dist/wave-layout.d.ts +29 -0
  39. package/dist/wave-layout.d.ts.map +1 -0
  40. package/dist/wave-layout.js +41 -0
  41. package/dist/wave-runtime.d.ts +29 -0
  42. package/dist/wave-runtime.d.ts.map +1 -0
  43. package/dist/wave-runtime.js +103 -0
  44. package/dist/wave-runtime.mjs +331 -0
  45. package/dist/workspace/ajv2020.d.ts +5 -0
  46. package/dist/workspace/ajv2020.d.ts.map +1 -0
  47. package/dist/workspace/ajv2020.js +14 -0
  48. package/dist/workspace/declaration.d.ts.map +1 -1
  49. package/dist/workspace/declaration.js +4 -8
  50. package/dist/workspace/kinds.d.ts.map +1 -1
  51. package/dist/workspace/kinds.js +2 -5
  52. package/dist/workspace/points.d.ts.map +1 -1
  53. package/dist/workspace/points.js +2 -5
  54. package/dist/workspace/principal-classes.d.ts.map +1 -1
  55. package/dist/workspace/principal-classes.js +2 -5
  56. package/package.json +5 -3
  57. package/src/cli/flush.ts +44 -0
  58. package/src/cli/handlers/approve.ts +618 -0
  59. package/src/cli/handlers/operator.ts +10 -608
  60. package/src/cli/handlers/override-approve.test.ts +1 -1
  61. package/src/cli/handlers/override-approve.ts +1 -1
  62. package/src/cli/main.ts +5 -816
  63. package/src/cli/parse-args.ts +781 -0
  64. package/src/components/fan-out-record.ts +3 -7
  65. package/src/components/fan-out-run.ts +7 -5
  66. package/src/gate-decision-bundle.test.ts +1 -0
  67. package/src/gated-waves.ts +28 -6
  68. package/src/op/op-waves-run.ts +5 -6
  69. package/src/op/op-waves.ts +4 -8
  70. package/src/package-version.ts +11 -0
  71. package/src/wave-engine.test.ts +107 -0
  72. package/src/wave-engine.ts +98 -0
  73. package/src/wave-layout.ts +44 -0
  74. package/src/wave-runtime-bundle.test.ts +70 -0
  75. package/src/wave-runtime.e2e.test.ts +177 -0
  76. package/src/wave-runtime.ts +104 -0
  77. package/src/workspace/ajv2020.ts +17 -0
  78. package/src/workspace/declaration.ts +4 -10
  79. package/src/workspace/kinds.ts +2 -5
  80. package/src/workspace/points.ts +2 -5
  81. package/src/workspace/principal-classes.ts +2 -5
@@ -1,8 +1,6 @@
1
1
  import { type ConvergeTickRecord } from "../../lifecycle/converge-ledger.js";
2
2
  import type { OpRunRecord } from "../../op/runtime.js";
3
3
  import type { GateResolutionRecord, PendingGateRecord } from "../../lifecycle/gate-ledger.js";
4
- import { type GateOrigin } from "../../lifecycle/gate-origin.js";
5
- import { type GatePolicyEvaluator, type ResolvedGateApproval } from "../../op/gate-approval.js";
6
4
  import type { CommandContext } from "../registry.js";
7
5
  /**
8
6
  * `chant operator [--env <env>] [--interval <duration>] [--lease-ttl
@@ -134,97 +132,5 @@ export declare function collectOperatorLog(ops: {
134
132
  * `operator status` — one JSON document on stdout, diagnostics on stderr.
135
133
  */
136
134
  export declare function runOperatorLog(ctx: CommandContext): Promise<number>;
137
- export declare function runApprove(ctx: CommandContext): Promise<number>;
138
- /** What a caller supplies alongside the op and gate names. */
139
- export interface GateApprovalOptions {
140
- /**
141
- * The channel this resolution arrived on (chant#2384). Defaults to the
142
- * process's own — `"cli"` unless an entry point declared otherwise — and is
143
- * a parameter only so a caller that knows better can say so.
144
- */
145
- origin?: GateOrigin;
146
- /**
147
- * Record a resolution that the same-origin rule would refuse (chant#2384).
148
- *
149
- * There is a legitimate case: a single operator driving an agent, who wants
150
- * the model's run resolved from the same session and accepts what that
151
- * means. The override exists so that is a deliberate act with a trace on the
152
- * record, rather than the default.
153
- */
154
- allowSameOrigin?: boolean;
155
- /** `--actor`/`--approver`; falls back to the CI or shell identity. */
156
- actor?: string;
157
- /** `--note` — free-text prose. */
158
- note?: string;
159
- /**
160
- * `--relayed-by <principal>` (#3402): who carried this approval to chant for
161
- * the approver, such as a follower program relaying a person's approval.
162
- * Recorded as `relayedBy` on the resolution line and covered by `--sign`.
163
- * The approver is still `--actor`; the relay is not an approver.
164
- */
165
- relayedBy?: string;
166
- /** `--url` — the approval surface, validated as absolute http/https. */
167
- url?: string;
168
- /**
169
- * `--plan <digest>` (#2300) — the plan this approval is for. Omitted, the
170
- * digest is read off the gate's standing pending fact, so the common path
171
- * stays one command: run, read what it planned, approve it.
172
- */
173
- plan?: string;
174
- /**
175
- * `--env <env>` (#2574) — which environment's pending fact a component gate
176
- * approval answers. A component's gate ledger holds every environment's
177
- * facts, so without it the newest one is approved. Ignored for a gate whose
178
- * pending facts record no environment (every Op gate).
179
- */
180
- environment?: string;
181
- /** `--role` (#2508) — roles the approver claims. */
182
- roles?: string[];
183
- /**
184
- * `--agent` (#2508) — record this as an agent's approval. On a
185
- * model-authored channel it is one whether or not this is set.
186
- */
187
- agent?: boolean;
188
- /**
189
- * `--sign [<key file>]` (#3163, ws-080): seal the approval with an ssh key,
190
- * a key file or true for git's `user.signingkey`. The seal attests the
191
- * approver when the signers file at base lists the key for them, and a gate
192
- * the declaration's `identity.gates` names counts only sealed approvals.
193
- */
194
- sign?: string | true;
195
- /** Where the workspace and the key file are looked up from. Defaults to the process's directory. */
196
- cwd?: string;
197
- /** Replaces the lexicon evaluator a gate policy is loaded from. For tests. */
198
- evaluator?: GatePolicyEvaluator;
199
- }
200
- export type GateApprovalOutcome = {
201
- ok: true;
202
- record: GateResolutionRecord;
203
- pushed: boolean;
204
- } | {
205
- ok: false;
206
- };
207
- /**
208
- * The ledger write behind `chant approve` — extracted so `chant run approve`
209
- * (#2121) records the identical fact before waking the runtime that hosts the
210
- * gated run, instead of growing a second, drifting writer.
211
- *
212
- * Prints the same warning for an undiscovered op and the same success line as
213
- * `chant approve` always did; the caller adds whatever it does next.
214
- *
215
- * Since #2300 the resolution names the plan it approves. By default that is
216
- * the standing pending fact's plan — the one the gated run produced — so the
217
- * common path is still `chant run`, read, `chant approve`. With no pending
218
- * fact and no `--plan`, this refuses: there is nothing on the ledger saying
219
- * what would be approved, and writing a resolution anyway is what made an
220
- * approval mean "the next run" instead of "this plan".
221
- */
222
- export declare function recordGateApproval(opName: string, gate: string, opts: GateApprovalOptions): Promise<GateApprovalOutcome>;
223
- /**
224
- * What a just-recorded approval did to a gate with an `approval` block
225
- * (#2508): the policy's decision and whether it binds, then the quorum count
226
- * for this plan. The run decides the gate from the ledger; these lines only
227
- * say what it will find.
228
- */
229
- export declare function describeApprovalProgress(approval: ResolvedGateApproval, record: GateResolutionRecord, prior: GateResolutionRecord[], standing: PendingGateRecord | undefined, pending?: readonly PendingGateRecord[]): string[];
135
+ export { runApprove, recordGateApproval, describeApprovalProgress, type GateApprovalOptions, type GateApprovalOutcome, } from "./approve.js";
230
136
  //# sourceMappingURL=operator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/operator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"operator.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/operator.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAE9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAG3F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAsFlD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+EtE;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA0I7E;AAaD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GACxC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAW9B;AAgED,mNAAmN;AACnN,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkG5E;AAID,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE;AAC9E,yGAAyG;GACvG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B;;;;;;OAMG;IACH,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,EACpC,IAAI,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GACvE,OAAO,CAAC,iBAAiB,CAAC,CAwD5B;AAmCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDzE;AAGD,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC"}
@@ -19,16 +19,8 @@ import { discoverConvergeOps, runOperatorRound, runOperatorForever, formatRoundL
19
19
  import { readLease, releaseLease, currentHolderId, DEFAULT_LEASE_TTL_MS } from "../../lifecycle/lease.js";
20
20
  import { readConvergeLedger } from "../../lifecycle/converge-ledger.js";
21
21
  import { readRunLedger, runEnvOf } from "../../lifecycle/run-ledger.js";
22
- import { currentGateOrigin, isModelAuthored, sameOriginRefusal, UNATTESTED_APPROVER, } from "../../lifecycle/gate-origin.js";
23
- import { appendGateResolution, appendPendingGate, readGateResolutions, readGateLedger, latestResolutionSince, latestPendingGate, isPendingGateExpired, resolveApprovalUrl, isApprovalUrl, } from "../../lifecycle/gate-ledger.js";
24
- import { isPlanDigest, samePlanDigest } from "../../lifecycle/plan-digest.js";
25
- import { gateContextDigest, gatePolicyRequest, loadGatePolicyEvaluator, } from "../../op/gate-approval.js";
26
- import { approverOf, humanQuorumSince, tallyGateApprovals } from "../../op/gate.js";
27
- import { FAN_OUT_GATE_OP, OVERRIDE_GATE_OP, WORKSPACE_UPGRADE_GATE_OP } from "../../op/gate-name.js";
28
- import { PR_GATE_OP } from "../../pr-loop.js";
29
- import { pushLifecycleReport, requireLifecycleLedger } from "../../lifecycle/git.js";
30
- import { LIFECYCLE_LOCAL_NOTE, isLifecycleLocalNote } from "../../lifecycle/local-note.js";
31
- import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format.js";
22
+ import { readGateResolutions, readGateLedger, latestResolutionSince, latestPendingGate, isPendingGateExpired } from "../../lifecycle/gate-ledger.js";
23
+ import { formatError, formatWarning, formatBold, formatInfo } from "../format.js";
32
24
  /** Load the activities a local tick needs, the same way a local run does: core's base activities plus whatever the project's configured lexicons contribute. Best-effort on the lexicon list — an unreadable `chant.config.ts` falls back to base activities only. */
33
25
  async function loadOperatorActivities() {
34
26
  let lexicons = [];
@@ -682,481 +674,5 @@ export async function runOperatorLog(ctx) {
682
674
  }
683
675
  return 0;
684
676
  }
685
- // ── chant approve <op> <gate> ───────────────────────────────────────────────
686
- /**
687
- * `chant approve <op> <gate> [--actor <name>] [--note <text>] [--url <url>]
688
- * [--expire]` — record the out-of-band resolution fact for a gate a run
689
- * stopped at (issue #1485: "resolution is an out-of-band act that writes the
690
- * counterpart fact"). Per that issue's leaning on open question 3 ("local
691
- * trust in v1"), this performs no authorization check beyond "you can run
692
- * `chant approve` locally".
693
- *
694
- * Since #2119 this closes the loop rather than just narrating it: the next
695
- * `chant run <op>` reads the resolution, finds it newer than the gate's
696
- * pending fact, and walks through.
697
- *
698
- * `--expire` is the other half — clear a standing pending fact *without*
699
- * approving anything, for a gate that was recorded against a run nobody
700
- * intends to finish. It writes no resolution; it appends a pending fact that
701
- * is already expired, which supersedes the standing one on an append-only
702
- * ledger, so the next run decides the gate from scratch.
703
- */
704
- /** The ledger branch, named in the warnings below so a reader can go look at it. */
705
- const LIFECYCLE_BRANCH = "chant/lifecycle";
706
- /**
707
- * Push the ledger and say so when it does not land (#2309 review, refs #2310).
708
- *
709
- * Both write paths here used `pushLifecycle().catch(() => undefined)` and then
710
- * printed unconditional success and exited 0. A rejected push — a stale lease,
711
- * no credentials, no network — therefore read as a completed approval, which
712
- * is the one thing an approval must never do: the operator walks away
713
- * believing the gate is answered for everybody, while the resolution exists
714
- * only in their own checkout.
715
- *
716
- * The append is still a correct *local* fact, so this is a warning and not a
717
- * failure; the exit code is unchanged. With no remote at all there is nobody
718
- * elsewhere to tell, so that case is a quiet suffix and no warning (#3677).
719
- */
720
- async function reportedPush(consequence) {
721
- const { pushed, pushWarning } = await pushLifecycleReport();
722
- if (pushed)
723
- return { pushed, suffix: "" };
724
- if (isLifecycleLocalNote(pushWarning))
725
- return { pushed, suffix: ` (${LIFECYCLE_LOCAL_NOTE})` };
726
- console.error(formatWarning({
727
- message: `The push to the remote did not land: ${pushWarning ?? "no reason given"}`,
728
- hint: consequence,
729
- }));
730
- return { pushed, suffix: " (local only — the push did not land)" };
731
- }
732
- export async function runApprove(ctx) {
733
- const opName = ctx.args.path;
734
- const gate = ctx.args.extraPositional;
735
- if (!opName || opName === "." || !gate) {
736
- console.error(formatError({ message: "Usage: chant approve <op> <gate>" }));
737
- return 1;
738
- }
739
- if (ctx.args.expire) {
740
- // Same guard as the approve path below (#2303): `--expire` reads the
741
- // standing fact and appends beside it, so a clone that never fetched the
742
- // branch would both fail to see the fact it is expiring and replace the
743
- // branch with the expiry alone.
744
- try {
745
- await requireLifecycleLedger();
746
- }
747
- catch (err) {
748
- console.error(formatError({
749
- message: `Cannot expire the gate: ${err instanceof Error ? err.message : String(err)}`,
750
- }));
751
- return 1;
752
- }
753
- const now = new Date().toISOString();
754
- const standing = latestPendingGate((await readGateLedger(opName)).pending, gate);
755
- if (!standing || isPendingGateExpired(standing, now)) {
756
- console.error(formatWarning({
757
- message: `Gate "${gate}" on "${opName}" has no standing pending fact — nothing to expire`,
758
- }));
759
- return 0;
760
- }
761
- await appendPendingGate({
762
- op: opName,
763
- gate,
764
- timestamp: now,
765
- expiresAt: now,
766
- ...(standing.description ? { description: standing.description } : {}),
767
- });
768
- const push = await reportedPush(`The expiry is recorded locally on ${LIFECYCLE_BRANCH}. Until it reaches the remote, ` +
769
- `a run in another checkout still sees the old pending fact.`);
770
- console.error(formatSuccess(`Gate "${gate}" on "${opName}" expired at ${now} — not approved` + push.suffix));
771
- console.error(formatInfo(opName === FAN_OUT_GATE_OP
772
- ? `The next \`chant components fan-out\` decides this gate from scratch and records a fresh pending fact.`
773
- : PR_GATE_OP.test(opName)
774
- ? `The next \`chant components pr-apply\` decides this gate from scratch and records a fresh pending fact.`
775
- : opName === WORKSPACE_UPGRADE_GATE_OP
776
- ? `The next \`chant workspace upgrade ${gate}\` decides this gate from scratch and records a fresh pending fact.`
777
- : `The next \`chant run ${opName}\` decides this gate from scratch and records a fresh pending fact.`));
778
- return 0;
779
- }
780
- // #3760: an override of a denied plan names its rules and gives a reason.
781
- if (opName === OVERRIDE_GATE_OP) {
782
- const { runOverrideApprove } = await import("./override-approve.js");
783
- return runOverrideApprove(ctx);
784
- }
785
- if (ctx.args.rules !== undefined || ctx.args.reason !== undefined) {
786
- console.error(formatError({
787
- message: `--rule and --reason override a denied plan: chant approve ${OVERRIDE_GATE_OP} <subject> --rule <id> --reason <text>`,
788
- hint: `To approve gate "${gate}" on "${opName}", put any prose in --note.`,
789
- }));
790
- return 1;
791
- }
792
- // `--approver` wins over `--actor`, the same precedence `chant run approve`
793
- // has applied since #2126 (`../handlers/run.ts`'s `runOpApprove`). Reading
794
- // only `--actor` here meant every README that teaches `--approver you`
795
- // recorded the shell user instead (#2192).
796
- const outcome = await recordGateApproval(opName, gate, {
797
- actor: ctx.args.approver ?? ctx.args.actor,
798
- note: ctx.args.note,
799
- url: ctx.args.url,
800
- plan: ctx.args.plan,
801
- ...(ctx.args.env !== undefined ? { environment: ctx.args.env } : {}),
802
- allowSameOrigin: ctx.args.allowSameOrigin,
803
- ...(ctx.args.roles ? { roles: ctx.args.roles } : {}),
804
- ...(ctx.args.agent ? { agent: true } : {}),
805
- ...(ctx.args.sign !== undefined ? { sign: ctx.args.sign } : {}),
806
- ...(ctx.args.relayedBy !== undefined ? { relayedBy: ctx.args.relayedBy } : {}),
807
- });
808
- if (!outcome.ok)
809
- return 1;
810
- // #3683: start the CI job that waits at this gate again.
811
- if (ctx.args.resumeRun || ctx.args.resume !== undefined) {
812
- const { resumeAfterApproval } = await import("./gate-resume.js");
813
- return resumeAfterApproval(opName, gate, outcome.record.environment, outcome.pushed);
814
- }
815
- console.error(formatInfo(`This records the resolution as a fact; it does not itself re-run anything. ` +
816
- (opName === FAN_OUT_GATE_OP
817
- ? `Repeat the \`chant components fan-out\` command and it walks through gate "${gate}".`
818
- : PR_GATE_OP.test(opName)
819
- ? `The pull request's apply (\`chant components pr-apply\`) walks through gate "${gate}" when it plans this same digest.`
820
- : opName === WORKSPACE_UPGRADE_GATE_OP
821
- ? `Repeat \`chant workspace upgrade ${gate}\` with the same target, and it applies the patch this approval names.`
822
- : `Run \`chant run ${opName}\` and it walks through gate "${gate}".`)));
823
- return 0;
824
- }
825
- /**
826
- * The ledger write behind `chant approve` — extracted so `chant run approve`
827
- * (#2121) records the identical fact before waking the runtime that hosts the
828
- * gated run, instead of growing a second, drifting writer.
829
- *
830
- * Prints the same warning for an undiscovered op and the same success line as
831
- * `chant approve` always did; the caller adds whatever it does next.
832
- *
833
- * Since #2300 the resolution names the plan it approves. By default that is
834
- * the standing pending fact's plan — the one the gated run produced — so the
835
- * common path is still `chant run`, read, `chant approve`. With no pending
836
- * fact and no `--plan`, this refuses: there is nothing on the ledger saying
837
- * what would be approved, and writing a resolution anyway is what made an
838
- * approval mean "the next run" instead of "this plan".
839
- */
840
- export async function recordGateApproval(opName, gate, opts) {
841
- const { ops } = await discoverOps();
842
- // `fan-out` is the op name every `chant components fan-out` gate is recorded
843
- // under (../handlers/fan-out.ts). It is a command rather than a declaration,
844
- // so there is no `*.op.ts` to find and nothing is wrong when none is there.
845
- if (!ops.has(opName) && opName !== FAN_OUT_GATE_OP && opName !== WORKSPACE_UPGRADE_GATE_OP && opName !== OVERRIDE_GATE_OP) {
846
- console.error(formatWarning({
847
- message: `Op "${opName}" was not found among discovered *.op.ts declarations — recording the resolution anyway`,
848
- }));
849
- }
850
- // Read the branch before appending to it (#2303 finding 2). Without this,
851
- // an approve in a clone that never fetched `chant/lifecycle` builds its
852
- // commit from an empty tree with no parent and replaces the branch with a
853
- // single commit holding only this resolution — the pending fact it is
854
- // answering is discarded rather than appended to.
855
- try {
856
- await requireLifecycleLedger();
857
- }
858
- catch (err) {
859
- console.error(formatError({
860
- message: `Cannot record the approval: ${err instanceof Error ? err.message : String(err)}`,
861
- hint: "The pending fact this answers lives on that branch; appending without it would drop it.",
862
- }));
863
- return { ok: false };
864
- }
865
- // #2028: the resolution's link is typed. `--url` wins; otherwise, running
866
- // inside the PR/MR job that carries the change is itself the address, the
867
- // same env fallback `--actor` uses. `--note` stays free-text prose.
868
- const url = opts.url ?? resolveApprovalUrl();
869
- if (url && !isApprovalUrl(url)) {
870
- console.error(formatError({
871
- message: `--url must be an absolute http/https URL (got "${url}")`,
872
- hint: "Pass the PR/MR link, or omit --url and put prose in --note.",
873
- }));
874
- return { ok: false };
875
- }
876
- // #2300: an approval is for a plan, so this command has to know which one.
877
- // `--plan` names it outright; otherwise it comes off the gate's standing
878
- // pending fact — the plan the run that stopped at this gate produced, which
879
- // is the plan whoever is typing this just read. There is deliberately no
880
- // third fallback: approving with no plan in sight is the behaviour
881
- // INTENTIUS/choudoufu#1026 measured, where the resolution authorised the
882
- // next run rather than anything anyone had seen.
883
- // #2574: a component gate's pending facts carry the environment they were
884
- // recorded in, and an approval answers one environment. `--env` picks it;
885
- // without it, the newest standing fact decides, and the success line below
886
- // names the environment it chose.
887
- const ledgerBefore = await readGateLedger(opName);
888
- const pendingFor = (all) => {
889
- const bound = all.some((p) => p.gate === gate && p.environment !== undefined);
890
- return bound && opts.environment !== undefined ? all.filter((p) => p.environment === opts.environment) : all;
891
- };
892
- const standing = latestPendingGate(pendingFor(ledgerBefore.pending), gate);
893
- const environment = standing?.environment;
894
- let planDigest;
895
- if (opts.plan !== undefined) {
896
- if (!isPlanDigest(opts.plan)) {
897
- console.error(formatError({
898
- message: `--plan must be a plan digest ("jcs1-sha256:" or "sha256:", then 64 hex characters), got "${opts.plan}"`,
899
- hint: "Copy it from the gated run's `plan :` line, or from the pending-gate summary. It is not a plan file path.",
900
- }));
901
- return { ok: false };
902
- }
903
- planDigest = opts.plan;
904
- }
905
- else {
906
- if (!standing) {
907
- console.error(formatError({
908
- message: opts.environment !== undefined && ledgerBefore.pending.some((p) => p.gate === gate && p.environment !== undefined)
909
- ? `Gate "${gate}" on "${opName}" has no pending fact in environment "${opts.environment}", so there is no plan to approve`
910
- : `Gate "${gate}" on "${opName}" has no pending fact, so there is no plan to approve`,
911
- hint: `Run \`chant run ${opName}\` first — it plans, stops at the gate, and records the plan this ` +
912
- `approval would be for. To approve a plan you already have the digest for, pass ` +
913
- `\`--plan <digest>\`.`,
914
- }));
915
- return { ok: false };
916
- }
917
- planDigest = standing.planDigest;
918
- }
919
- // chant#2384 — the gate's whole value is that its two halves have different
920
- // authors. At a shell they do: a person runs, reads the plan, and approves.
921
- // On MCP and ACP the person's only act was launching the server, and every
922
- // call after is the model's, so a gate reached over one of those channels and
923
- // resolved over the same one has no second party in it. Refused by default,
924
- // the way #2300 refuses a plan nobody approved.
925
- const origin = opts.origin ?? currentGateOrigin();
926
- const ledger = await readGateLedger(opName);
927
- const standingForOrigin = latestPendingGate(pendingFor(ledger.pending), gate);
928
- // chant#3485: a gate that is never resolved over MCP or ACP says so on its
929
- // own record, or on its Op's config. No flag overrides this one.
930
- if (isModelAuthored(origin) && (standingForOrigin?.neverOverMcp || ops.get(opName)?.config.neverOverMcp)) {
931
- console.error(formatError({
932
- message: `Gate "${gate}" on "${opName}" is never resolved over ${origin === "acp" ? "ACP" : "MCP"}`,
933
- hint: `Approvals belong to people: \`chant approve ${opName} ${gate}\` at a shell.`,
934
- }));
935
- return { ok: false };
936
- }
937
- // #3170: a gate that asks a decision point passes on the point's answer,
938
- // which the run cites on the ledger. An approval here would count for nothing.
939
- if (standingForOrigin?.answer) {
940
- console.error(formatError({
941
- message: `Gate "${gate}" on "${opName}" asks decision point ${standingForOrigin.answer.point}, so its answer passes the gate, not an approval`,
942
- hint: `Answer the question: \`chant workspace points answer ${standingForOrigin.answer.id} --answer <value> --by <name>\`, then run \`chant run ${opName}\` again.`,
943
- }));
944
- return { ok: false };
945
- }
946
- const refusal = sameOriginRefusal(standingForOrigin?.origin, origin);
947
- if (refusal && !opts.allowSameOrigin) {
948
- console.error(formatError({
949
- message: `Gate "${gate}" on "${opName}" cannot be resolved from here: ${refusal}`,
950
- hint: `Approve it from a channel the run did not come from — \`chant approve ${opName} ${gate}\` ` +
951
- `at a shell is the usual one. If resolving from the same channel genuinely is the intent, ` +
952
- `pass --allow-same-origin, which records that it was deliberate.`,
953
- }));
954
- return { ok: false };
955
- }
956
- // #3402: the relay is a principal, on one line (the seal's payload is LF-joined).
957
- const relayedBy = opts.relayedBy?.trim();
958
- if (opts.relayedBy !== undefined && (!relayedBy || /[\r\n]/.test(relayedBy))) {
959
- console.error(formatError({
960
- message: `--relayed-by needs one principal on one line (got ${JSON.stringify(opts.relayedBy)})`,
961
- hint: "Name who relayed the approval, such as --relayed-by github:<login> or a principal the signers file lists.",
962
- }));
963
- return { ok: false };
964
- }
965
- const resolvedBy = opts.actor
966
- ?? (isModelAuthored(origin)
967
- // A channel that cannot attest to a name does not get to write one down.
968
- // "unattested" reads as what it is; a name the model chose would read in
969
- // the ledger exactly like a name a person gave.
970
- ? UNATTESTED_APPROVER
971
- : process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER ?? "unknown");
972
- // #2508: who this is, as the gate's quorum and policy read it. A model on
973
- // MCP or ACP is an agent whatever it claims, the same reasoning that gives
974
- // it `UNATTESTED_APPROVER` for a name.
975
- const approver = {
976
- kind: opts.agent || isModelAuthored(origin) ? "agent" : "human",
977
- ...(opts.roles && opts.roles.length > 0 ? { roles: [...new Set(opts.roles)] } : {}),
978
- };
979
- // #3163, ws-080: the declaration at base may require a checkable approver
980
- // (identity.attribution "identified") and a signed approval for this gate
981
- // (identity.gates). Both are judged before anything is written.
982
- const cwd = opts.cwd ?? process.cwd();
983
- const identity = await import("../../workspace/identity.js");
984
- const { scopeSource } = await import("../../workspace/write-scope.js");
985
- const scope = scopeSource(cwd);
986
- try {
987
- identity.refuseUnidentified(scope, [resolvedBy], opts.actor !== undefined ? "--actor" : "the approver");
988
- if (relayedBy)
989
- identity.refuseUnidentified(scope, [relayedBy], "--relayed-by");
990
- }
991
- catch (err) {
992
- if (!(err instanceof identity.IdentityError))
993
- throw err;
994
- console.error(formatError({
995
- message: `Gate "${gate}" on "${opName}" was not approved: ${err.message}`,
996
- hint: "Pass --actor <forge identity or signer>, as the surface that signed the person in maps them, for example --actor github:<login>.",
997
- }));
998
- return { ok: false };
999
- }
1000
- const rule = identity.gateAdmissionFrom(scope, gate);
1001
- if (rule && opts.sign === undefined) {
1002
- console.error(formatError({
1003
- message: `Gate "${gate}" needs a signed approval: chant.workspace.json at base names it in identity.gates` +
1004
- (rule.requirement.class !== null ? `, from the class ${rule.requirement.class}` : "") +
1005
- `, and this approval is not signed`,
1006
- hint: `chant approve ${opName} ${gate} --actor <principal> --sign [<key file>], with a key ${scope.policy.signersPath} at base lists for that principal.`,
1007
- }));
1008
- return { ok: false };
1009
- }
1010
- const timestamp = new Date().toISOString();
1011
- let seal;
1012
- if (opts.sign !== undefined) {
1013
- const { resolveSigningKey, sealGateApproval, SealError } = await import("../../workspace/trust/seal.js");
1014
- try {
1015
- const key = resolveSigningKey(opts.sign, cwd);
1016
- try {
1017
- seal = sealGateApproval(key.file, {
1018
- op: opName,
1019
- gate,
1020
- ...(environment !== undefined ? { environment } : {}),
1021
- ...(planDigest !== undefined ? { planDigest } : {}),
1022
- resolvedBy,
1023
- ...(relayedBy ? { relayedBy } : {}),
1024
- timestamp,
1025
- });
1026
- }
1027
- finally {
1028
- key.cleanup();
1029
- }
1030
- }
1031
- catch (err) {
1032
- if (!(err instanceof SealError))
1033
- throw err;
1034
- console.error(formatError({ message: `Gate "${gate}" on "${opName}" was not approved: ${err.message}` }));
1035
- return { ok: false };
1036
- }
1037
- }
1038
- if (rule) {
1039
- const why = rule.refuses({
1040
- op: opName,
1041
- gate,
1042
- ...(environment !== undefined ? { environment } : {}),
1043
- ...(planDigest !== undefined ? { planDigest } : {}),
1044
- resolvedBy,
1045
- ...(relayedBy ? { relayedBy } : {}),
1046
- timestamp,
1047
- seal,
1048
- });
1049
- if (why !== null) {
1050
- console.error(formatError({
1051
- message: `Gate "${gate}" on "${opName}" was not approved: ${why}`,
1052
- hint: `identity.gates.${gate} in chant.workspace.json at base counts only an approval signed by a key ${scope.policy.signersPath} lists for the approver` +
1053
- (rule.requirement.class !== null ? `, who holds the role of the class ${rule.requirement.class}` : "") + ".",
1054
- }));
1055
- return { ok: false };
1056
- }
1057
- }
1058
- // #2508: a gate with a policy has it evaluated for this approval now, while
1059
- // the approver is known, against the context the gated run recorded. The
1060
- // decision is written next to the approval, so a pass can be traced to the
1061
- // rule that allowed it.
1062
- const approval = standingForOrigin?.approval;
1063
- let policyDecision;
1064
- if (approval?.policy) {
1065
- try {
1066
- const evaluator = opts.evaluator ?? (await loadGatePolicyEvaluator(approval.policy.lexicon));
1067
- const answer = await evaluator.evaluateGatePolicy(approval.policy, gatePolicyRequest({
1068
- op: opName,
1069
- gate,
1070
- resolvedBy,
1071
- approver,
1072
- ...(planDigest !== undefined ? { planDigest } : {}),
1073
- ...(approval.context ? { context: approval.context } : {}),
1074
- }));
1075
- policyDecision = {
1076
- policy: approval.policy.name,
1077
- version: approval.policy.version,
1078
- mode: approval.mode,
1079
- contextDigest: gateContextDigest(approval.context),
1080
- decision: answer.decision,
1081
- determining: answer.determining,
1082
- errors: answer.errors,
1083
- };
1084
- }
1085
- catch (err) {
1086
- console.error(formatError({
1087
- message: `Gate "${gate}" on "${opName}" declares policy "${approval.policy.name}", which could not be evaluated: ${err instanceof Error ? err.message : String(err)}`,
1088
- hint: `Install the ${approval.policy.lexicon} lexicon in this project, or approve from a checkout that has it.`,
1089
- }));
1090
- return { ok: false };
1091
- }
1092
- }
1093
- const { record } = await appendGateResolution({
1094
- op: opName,
1095
- gate,
1096
- resolvedBy,
1097
- ...(relayedBy ? { relayedBy } : {}),
1098
- timestamp,
1099
- ...(seal ? { seal } : {}),
1100
- approver,
1101
- ...(policyDecision ? { policyDecision } : {}),
1102
- ...(opts.note ? { note: opts.note } : {}),
1103
- ...(url ? { url } : {}),
1104
- ...(planDigest !== undefined ? { planDigest } : {}),
1105
- ...(environment !== undefined ? { environment } : {}),
1106
- origin,
1107
- ...(refusal && opts.allowSameOrigin ? { sameOriginOverride: true } : {}),
1108
- });
1109
- const push = await reportedPush(`The resolution is recorded locally on ${LIFECYCLE_BRANCH}. Until it reaches the remote, ` +
1110
- `a run in another checkout will not see it.`);
1111
- console.error(formatSuccess(`Gate "${gate}" on "${opName}" resolved by ${record.resolvedBy}` +
1112
- (record.relayedBy ? `, relayed by ${record.relayedBy},` : "") +
1113
- ` at ${record.timestamp}` +
1114
- (record.url ? ` (${record.url})` : "") +
1115
- (record.seal ? `, signed with ${record.seal.key}` : "") +
1116
- push.suffix));
1117
- if (record.planDigest) {
1118
- console.error(formatInfo(`This approves the plan ${record.planDigest}` +
1119
- (record.environment !== undefined ? ` in environment "${record.environment}"` : "") +
1120
- ", and only that plan. A run whose fresh plan differs refuses rather than applying it."));
1121
- }
1122
- if (approval) {
1123
- for (const line of describeApprovalProgress(approval, record, ledger.resolutions, standingForOrigin, ledger.pending)) {
1124
- console.error(formatInfo(line));
1125
- }
1126
- }
1127
- return { ok: true, record, pushed: push.pushed };
1128
- }
1129
- /**
1130
- * What a just-recorded approval did to a gate with an `approval` block
1131
- * (#2508): the policy's decision and whether it binds, then the quorum count
1132
- * for this plan. The run decides the gate from the ledger; these lines only
1133
- * say what it will find.
1134
- */
1135
- export function describeApprovalProgress(approval, record, prior, standing, pending = standing ? [standing] : []) {
1136
- const lines = [];
1137
- const decision = record.policyDecision;
1138
- if (decision) {
1139
- const rules = decision.determining.length > 0 ? ` (${decision.determining.join(", ")})` : "";
1140
- const binds = approval.mode === "enforce"
1141
- ? decision.decision === "allow"
1142
- ? "In enforce mode this permit passes the gate on its own."
1143
- : "In enforce mode only a permit passes the gate on its own, so this approval counts only toward the quorum."
1144
- : "The gate is in log-only mode, so the decision is recorded and does not change the outcome.";
1145
- lines.push(`Policy "${decision.policy}" ${decision.version}: ${decision.decision}${rules}. ${binds}`);
1146
- for (const error of decision.errors)
1147
- lines.push(`Policy evaluation error: ${error}`);
1148
- }
1149
- const since = standing?.timestamp ?? new Date(0).toISOString();
1150
- // #3206: count the humans the way the run will, back past facts re-recorded for this plan.
1151
- const humansSince = standing && samePlanDigest(standing.planDigest, record.planDigest) ? humanQuorumSince(pending, standing) : since;
1152
- const tally = tallyGateApprovals([...prior, record], record.gate, since, record.planDigest, approval, humansSince);
1153
- const roles = approval.quorum?.roles ? ` with role ${approval.quorum.roles.join(" or ")}` : "";
1154
- const who = tally.counted.map((r) => r.resolvedBy).join(", ") || "none yet";
1155
- lines.push(`Quorum: ${tally.counted.length} of ${tally.need} human approval(s)${roles} for this plan (${who}).`);
1156
- if (!tally.counted.includes(record)) {
1157
- lines.push(approverOf(record).kind === "agent"
1158
- ? "An agent's approval does not count toward the quorum."
1159
- : `This approval does not count toward the quorum: the approver claims none of its roles (${approval.quorum?.roles?.join(", ")}).`);
1160
- }
1161
- return lines;
1162
- }
677
+ // `chant approve` lives in ./approve.ts (#3896); these re-exports keep its importers unchanged.
678
+ export { runApprove, recordGateApproval, describeApprovalProgress, } from "./approve.js";
@@ -20,7 +20,7 @@ import { readOverrideLedger } from "../../lifecycle/override-ledger.js";
20
20
  import { samePlanDigest } from "../../lifecycle/plan-digest.js";
21
21
  import { OVERRIDE_OP, OVERRIDERS_PATH, overrideCommand, overridersAt, recordedDenials, sortedRules } from "../../override-gate.js";
22
22
  import { formatError, formatInfo, formatWarning } from "../format.js";
23
- import { recordGateApproval } from "./operator.js";
23
+ import { recordGateApproval } from "./approve.js";
24
24
  const norm = (s) => s.normalize("NFKC").trim().toLowerCase();
25
25
  export async function runOverrideApprove(ctx) {
26
26
  const subject = ctx.args.extraPositional;