@intentius/chant 0.124.25 → 0.124.27
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/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +4 -2
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +4 -2
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +39 -4
- package/dist/gate-decision.d.ts +1 -1
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +1 -1
- package/dist/lifecycle/gate-audit.d.ts +7 -1
- package/dist/lifecycle/gate-audit.d.ts.map +1 -1
- package/dist/lifecycle/gate-audit.js +40 -3
- package/dist/lifecycle/gate-checkpoint.d.ts +78 -0
- package/dist/lifecycle/gate-checkpoint.d.ts.map +1 -0
- package/dist/lifecycle/gate-checkpoint.js +103 -0
- package/dist/lifecycle/gate-ledger.d.ts +41 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +18 -0
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +2 -0
- package/dist/lint/rules/op/index.d.ts +6 -2
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +7 -3
- package/dist/lint/rules/op/ops016-gate-resume.d.ts +14 -0
- package/dist/lint/rules/op/ops016-gate-resume.d.ts.map +1 -0
- package/dist/lint/rules/op/ops016-gate-resume.js +31 -0
- package/dist/lint/rules/op/ops017-gate-alias.d.ts +30 -0
- package/dist/lint/rules/op/ops017-gate-alias.d.ts.map +1 -0
- package/dist/lint/rules/op/ops017-gate-alias.js +67 -0
- package/dist/op/activity-contract.d.ts +14 -0
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-contract.js +16 -0
- package/dist/op/activity-registry.d.ts +6 -3
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.js +7 -5
- package/dist/op/builders.d.ts +7 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +13 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +14 -7
- package/dist/op/gate-alias.d.ts +57 -0
- package/dist/op/gate-alias.d.ts.map +1 -0
- package/dist/op/gate-alias.js +127 -0
- package/dist/op/gate-checkpoint.d.ts +123 -0
- package/dist/op/gate-checkpoint.d.ts.map +1 -0
- package/dist/op/gate-checkpoint.js +280 -0
- package/dist/op/gate.d.ts +54 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +142 -1
- package/dist/op/index.d.ts +4 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +2 -1
- package/dist/op/local-executor.d.ts +57 -2
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +136 -6
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/local-output.js +20 -2
- package/dist/op/op-ir.d.ts +4 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-ir.js +4 -0
- package/dist/op/runtime.d.ts +15 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +9 -2
- package/dist/op/types.d.ts +31 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/wave-runtime.mjs +225 -245
- package/dist/workspace/trust/gate-seal.d.ts +32 -1
- package/dist/workspace/trust/gate-seal.d.ts.map +1 -1
- package/dist/workspace/trust/gate-seal.js +41 -2
- package/dist/workspace/trust/seal.d.ts +1 -1
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/seal.js +1 -1
- package/package.json +1 -1
- package/src/cli/commands/build.ts +9 -2
- package/src/cli/commands/lint.ts +4 -2
- package/src/cli/compiled-cli.e2e.test.ts +31 -6
- package/src/cli/handlers/approve.ts +46 -4
- package/src/cli/handlers/operator.test.ts +91 -0
- package/src/gate-decision.ts +2 -0
- package/src/lifecycle/gate-audit.ts +41 -4
- package/src/lifecycle/gate-checkpoint.test.ts +128 -0
- package/src/lifecycle/gate-checkpoint.ts +133 -0
- package/src/lifecycle/gate-ledger.ts +61 -0
- package/src/lifecycle/run-ledger.ts +4 -0
- package/src/lint/rules/op/index.ts +9 -3
- package/src/lint/rules/op/ops016-gate-resume.test.ts +53 -0
- package/src/lint/rules/op/ops016-gate-resume.ts +48 -0
- package/src/lint/rules/op/ops017-gate-alias.test.ts +85 -0
- package/src/lint/rules/op/ops017-gate-alias.ts +89 -0
- package/src/op/activity-contract.ts +22 -1
- package/src/op/activity-registry.ts +7 -5
- package/src/op/builders.ts +21 -1
- package/src/op/discover-root.test.ts +39 -0
- package/src/op/discover.ts +15 -8
- package/src/op/gate-alias.e2e.test.ts +78 -0
- package/src/op/gate-alias.test.ts +350 -0
- package/src/op/gate-alias.ts +137 -0
- package/src/op/gate-checkpoint.test.ts +284 -0
- package/src/op/gate-checkpoint.ts +322 -0
- package/src/op/gate.ts +198 -4
- package/src/op/index.ts +4 -1
- package/src/op/local-executor.ts +180 -7
- package/src/op/local-output.ts +18 -2
- package/src/op/op-ir.test.ts +25 -0
- package/src/op/op-ir.ts +8 -0
- package/src/op/runtime.ts +8 -0
- package/src/op/runtimes/local.ts +9 -2
- package/src/op/types.ts +32 -0
- package/src/workspace/trust/gate-seal.test.ts +29 -2
- package/src/workspace/trust/gate-seal.ts +46 -2
- package/src/workspace/trust/seal.ts +1 -1
package/src/op/gate.ts
CHANGED
|
@@ -55,6 +55,8 @@ import { pushLifecycleReport, readBlobBySha, readBlobFromPath, readPathSha, requ
|
|
|
55
55
|
import { parseDuration } from "./duration";
|
|
56
56
|
import { resolveGateRunLocator } from "./gate-run-locator";
|
|
57
57
|
import { refuseGateWithoutRecording } from "../lifecycle/no-record";
|
|
58
|
+
import { gateCheckpointDigest, gateCheckpointText, readGateCheckpointText, writeGateCheckpoint, type GateCheckpoint } from "../lifecycle/gate-checkpoint";
|
|
59
|
+
import type { ResolvedGateAlias } from "./gate-alias";
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
62
|
* What appending a pending fact learned about reaching the remote.
|
|
@@ -100,6 +102,15 @@ export interface GateLedgerPort {
|
|
|
100
102
|
readApplied?(op: string): Promise<AppliedGateRecord[]>;
|
|
101
103
|
/** Record that a wave is about to apply under an approval (#3784), which spends it. */
|
|
102
104
|
appendApplied?(record: AppliedGateRecord): Promise<{ pushed: boolean; pushWarning?: string }>;
|
|
105
|
+
/**
|
|
106
|
+
* Store the checkpoint of a run that stops at a gate that resumes (#3947),
|
|
107
|
+
* at `_gates/<op>/checkpoints/<runId>.json`. Written before the pending
|
|
108
|
+
* fact that names its digest, and pushed with it. Absent on a port that
|
|
109
|
+
* keeps none, and the gate then doesn't resume.
|
|
110
|
+
*/
|
|
111
|
+
writeCheckpoint?(checkpoint: GateCheckpoint): Promise<void>;
|
|
112
|
+
/** The text of a run's checkpoint, or null when there is none (#3947). */
|
|
113
|
+
readCheckpoint?(op: string, runId: string): Promise<string | null>;
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
/** A workspace's rule for which approvals of a gate count (#3163): a signed approval, from a class when it names one. */
|
|
@@ -148,6 +159,13 @@ export function gitGateLedgerPort(opts?: { cwd?: string }): GateLedgerPort {
|
|
|
148
159
|
const { record } = await appendPendingGate(input, opts);
|
|
149
160
|
return { record, ...(await pushLifecycleReport(opts)) };
|
|
150
161
|
},
|
|
162
|
+
async writeCheckpoint(checkpoint) {
|
|
163
|
+
await writeGateCheckpoint(checkpoint, opts);
|
|
164
|
+
},
|
|
165
|
+
async readCheckpoint(op, runId) {
|
|
166
|
+
await requireLifecycleLedger(opts);
|
|
167
|
+
return readGateCheckpointText(op, runId, opts);
|
|
168
|
+
},
|
|
151
169
|
async readApplied(op) {
|
|
152
170
|
await requireLifecycleLedger(opts);
|
|
153
171
|
const { dir, file } = appliedGatePath(op);
|
|
@@ -181,16 +199,30 @@ export function gitGateLedgerPort(opts?: { cwd?: string }): GateLedgerPort {
|
|
|
181
199
|
/** A gate ledger held in memory — what a test, or a caller that has already read the ledger, hands {@link evaluateGate}. */
|
|
182
200
|
export function memoryGateLedgerPort(
|
|
183
201
|
seed: { resolutions?: GateResolutionRecord[]; pending?: PendingGateRecord[]; applied?: AppliedGateRecord[] } = {},
|
|
184
|
-
): GateLedgerPort & {
|
|
202
|
+
): GateLedgerPort & {
|
|
203
|
+
appended: PendingGateRecord[];
|
|
204
|
+
resolved: GateResolutionRecord[];
|
|
205
|
+
applied: AppliedGateRecord[];
|
|
206
|
+
/** Checkpoint texts by `<op>/<runId>` (#3947). */
|
|
207
|
+
checkpoints: Map<string, string>;
|
|
208
|
+
} {
|
|
185
209
|
const resolutions = [...(seed.resolutions ?? [])];
|
|
186
210
|
const pending = [...(seed.pending ?? [])];
|
|
187
211
|
const appended: PendingGateRecord[] = [];
|
|
188
212
|
const resolved: GateResolutionRecord[] = [];
|
|
189
213
|
const applied: AppliedGateRecord[] = [...(seed.applied ?? [])];
|
|
214
|
+
const checkpoints = new Map<string, string>();
|
|
190
215
|
return {
|
|
191
216
|
appended,
|
|
192
217
|
resolved,
|
|
193
218
|
applied,
|
|
219
|
+
checkpoints,
|
|
220
|
+
async writeCheckpoint(checkpoint) {
|
|
221
|
+
checkpoints.set(`${checkpoint.op}/${checkpoint.runId}`, gateCheckpointText(checkpoint));
|
|
222
|
+
},
|
|
223
|
+
async readCheckpoint(op, runId) {
|
|
224
|
+
return checkpoints.get(`${op}/${runId}`) ?? null;
|
|
225
|
+
},
|
|
194
226
|
async readApplied(op) {
|
|
195
227
|
return applied.filter((a) => a.op === op);
|
|
196
228
|
},
|
|
@@ -271,6 +303,24 @@ export interface GateCheckInput {
|
|
|
271
303
|
fields?: Record<string, unknown>;
|
|
272
304
|
/** The pending fact it records carries `neverOverMcp` (#3821, chant#3485). */
|
|
273
305
|
neverOverMcp?: boolean;
|
|
306
|
+
/**
|
|
307
|
+
* The checkpoint of a run at a gate that resumes (#3947). Given, a pending
|
|
308
|
+
* fact this records first stores it through the port's `writeCheckpoint`
|
|
309
|
+
* and carries its digest as `checkpointDigest`. A standing fact with no
|
|
310
|
+
* checkpoint is recorded again, so the gate gets one.
|
|
311
|
+
*/
|
|
312
|
+
checkpoint?: GateCheckpoint;
|
|
313
|
+
/**
|
|
314
|
+
* The gate's old names (#3949), each with its Op filled in
|
|
315
|
+
* (`resolveGateAliases` in `./gate-alias.ts`). Read only on a gate bound
|
|
316
|
+
* to a plan. A person's approval recorded under one counts toward this
|
|
317
|
+
* gate when it answers that name's standing pending fact, the fact hasn't
|
|
318
|
+
* expired, it is for `planDigest` and `environment`, neither name has
|
|
319
|
+
* spent it, and it passes this gate's approval rule. An approval this
|
|
320
|
+
* counts is written again under this gate (`carriedFrom`), and a line
|
|
321
|
+
* carried earlier counts only while its alias is still declared.
|
|
322
|
+
*/
|
|
323
|
+
aliases?: readonly ResolvedGateAlias[];
|
|
274
324
|
}
|
|
275
325
|
|
|
276
326
|
/**
|
|
@@ -335,11 +385,14 @@ export type GateCheck =
|
|
|
335
385
|
/**
|
|
336
386
|
* Set on a gate that declares `approval` (#2508). `"quorum"` when enough
|
|
337
387
|
* human approvals were recorded, `"policy"` when an `enforce`-mode permit
|
|
338
|
-
* passed it on its own.
|
|
388
|
+
* passed it on its own. `"alias"` (#3949) when an approval recorded
|
|
389
|
+
* under one of the gate's aliases counted, on any gate.
|
|
339
390
|
*/
|
|
340
|
-
via?: "quorum" | "policy";
|
|
391
|
+
via?: "quorum" | "policy" | "alias";
|
|
341
392
|
/** Every approval that counted toward the quorum. Set with `via`. */
|
|
342
393
|
approvals?: GateResolutionRecord[];
|
|
394
|
+
/** The approvals this run carried from an alias and wrote under the gate's name (#3949). Set when `via` is `"alias"`. */
|
|
395
|
+
carried?: GateResolutionRecord[];
|
|
343
396
|
}
|
|
344
397
|
| {
|
|
345
398
|
satisfied: false;
|
|
@@ -500,6 +553,103 @@ export function humanQuorumSince(pending: readonly PendingGateRecord[], standing
|
|
|
500
553
|
return anchor.timestamp;
|
|
501
554
|
}
|
|
502
555
|
|
|
556
|
+
/**
|
|
557
|
+
* The approvals that answer `standing` (#3949): of its gate, environment and
|
|
558
|
+
* plan, recorded since it, or on a gate with an `approval` block since the
|
|
559
|
+
* human quorum's anchor ({@link humanQuorumSince}). The rule
|
|
560
|
+
* `resolutionAnswering` (`./gate-resume.ts`) decides by, without the tally:
|
|
561
|
+
* each one counts on its own toward a gate an alias names.
|
|
562
|
+
*/
|
|
563
|
+
export function approvalsAnswering(
|
|
564
|
+
resolutions: readonly GateResolutionRecord[],
|
|
565
|
+
standing: PendingGateRecord,
|
|
566
|
+
pending: readonly PendingGateRecord[] = [standing],
|
|
567
|
+
): GateResolutionRecord[] {
|
|
568
|
+
const since = new Date(standing.approval ? humanQuorumSince(pending, standing) : standing.timestamp).getTime();
|
|
569
|
+
return resolutions.filter(
|
|
570
|
+
(r) =>
|
|
571
|
+
r.gate === standing.gate && r.environment === standing.environment && samePlanDigest(r.planDigest, standing.planDigest) &&
|
|
572
|
+
new Date(r.timestamp).getTime() >= since,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The line a run writes for an approval it carries from an alias (#3949):
|
|
578
|
+
* the approval under `op`/`gate` at `now`, citing the line it came from.
|
|
579
|
+
* The approver, plan, environment, relay and seal are the original's. A
|
|
580
|
+
* policy decision is left behind, since it was evaluated against another
|
|
581
|
+
* gate, and so is a checkpoint digest, which moves onto `carriedFrom`: the
|
|
582
|
+
* approver saw that checkpoint, not one this gate stores, so a carried
|
|
583
|
+
* approval never lets a run resume.
|
|
584
|
+
*/
|
|
585
|
+
export function carriedApproval(r: GateResolutionRecord, op: string, gate: string, now: string): GateResolutionInput {
|
|
586
|
+
return {
|
|
587
|
+
op,
|
|
588
|
+
gate,
|
|
589
|
+
resolvedBy: r.resolvedBy,
|
|
590
|
+
...(r.relayedBy !== undefined ? { relayedBy: r.relayedBy } : {}),
|
|
591
|
+
timestamp: now,
|
|
592
|
+
...(r.note ? { note: r.note } : {}),
|
|
593
|
+
...(r.url ? { url: r.url } : {}),
|
|
594
|
+
...(r.planDigest !== undefined ? { planDigest: r.planDigest } : {}),
|
|
595
|
+
...(r.environment !== undefined ? { environment: r.environment } : {}),
|
|
596
|
+
...(r.origin ? { origin: r.origin } : {}),
|
|
597
|
+
...(r.sameOriginOverride ? { sameOriginOverride: true } : {}),
|
|
598
|
+
approver: approverOf(r),
|
|
599
|
+
...(r.seal ? { seal: r.seal } : {}),
|
|
600
|
+
carriedFrom: {
|
|
601
|
+
op: r.op,
|
|
602
|
+
gate: r.gate,
|
|
603
|
+
timestamp: r.timestamp,
|
|
604
|
+
...(r.checkpointDigest !== undefined ? { checkpointDigest: r.checkpointDigest } : {}),
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* The approvals recorded under `input.aliases` that may count toward this
|
|
611
|
+
* gate (#3949), oldest first. Each answers its alias's standing pending
|
|
612
|
+
* fact, which hasn't expired and was recorded for this plan and
|
|
613
|
+
* environment. Each is a person's, is spent under neither name, hasn't been
|
|
614
|
+
* carried already (`ledger` is this gate's own, so a carried line that was
|
|
615
|
+
* spent keeps its original from being carried again), and passes `rule`,
|
|
616
|
+
* whose seal check reads the names the approval was signed under. One the
|
|
617
|
+
* rule refuses is added to `refused`.
|
|
618
|
+
*/
|
|
619
|
+
async function aliasApprovals(
|
|
620
|
+
port: GateLedgerPort,
|
|
621
|
+
input: GateCheckInput & { aliases: readonly ResolvedGateAlias[] },
|
|
622
|
+
ledger: readonly GateResolutionRecord[],
|
|
623
|
+
rule: GateApprovalRule | null,
|
|
624
|
+
now: string,
|
|
625
|
+
refused: GateRefusedApproval[],
|
|
626
|
+
): Promise<GateResolutionRecord[]> {
|
|
627
|
+
const at = (iso: string) => new Date(iso).getTime();
|
|
628
|
+
const carried = ledger.filter((r) => r.gate === input.gate && r.carriedFrom !== undefined);
|
|
629
|
+
const wasCarried = (r: GateResolutionRecord) =>
|
|
630
|
+
carried.some((c) => c.carriedFrom!.op === r.op && c.carriedFrom!.gate === r.gate && at(c.carriedFrom!.timestamp) === at(r.timestamp) && c.resolvedBy === r.resolvedBy);
|
|
631
|
+
const ledgers = new Map<string, Awaited<ReturnType<GateLedgerPort["read"]>>>();
|
|
632
|
+
const out: GateResolutionRecord[] = [];
|
|
633
|
+
for (const alias of input.aliases) {
|
|
634
|
+
let other = ledgers.get(alias.op);
|
|
635
|
+
if (!other) ledgers.set(alias.op, (other = await port.read(alias.op)));
|
|
636
|
+
const pending = other.pending.filter((p) => p.environment === input.environment);
|
|
637
|
+
const standing = latestPendingGate(pending, alias.gate);
|
|
638
|
+
if (!standing || isPendingGateExpired(standing, now) || !samePlanDigest(standing.planDigest, input.planDigest)) continue;
|
|
639
|
+
const spent = alias.op === input.op && input.spent ? input.spent : ((await port.readApplied?.(alias.op)) ?? []);
|
|
640
|
+
for (const r of approvalsAnswering(other.resolutions, standing, pending)) {
|
|
641
|
+
if (r.carriedFrom !== undefined || approverOf(r).kind !== "human" || isApprovalSpent(spent, r) || wasCarried(r)) continue;
|
|
642
|
+
const reason = rule?.refuses(r) ?? null;
|
|
643
|
+
if (reason !== null) {
|
|
644
|
+
refused.push({ resolution: r, reason: `carried from ${alias.op}/${alias.gate}: ${reason}` });
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
out.push(r);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return out.sort((a, b) => at(a.timestamp) - at(b.timestamp));
|
|
651
|
+
}
|
|
652
|
+
|
|
503
653
|
/** Whether two resolved approval blocks are the same, so a standing pending fact still describes this gate. */
|
|
504
654
|
function sameApproval(a: ResolvedGateApproval | undefined, b: ResolvedGateApproval | undefined): boolean {
|
|
505
655
|
return JSON.stringify(a ?? null, sortedJsonReplacer) === JSON.stringify(b ?? null, sortedJsonReplacer);
|
|
@@ -546,6 +696,9 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
546
696
|
const rule = (await port.approvalRule?.(input.gate)) ?? null;
|
|
547
697
|
const standing = latestPendingGate(pending, input.gate);
|
|
548
698
|
const since = standing?.timestamp ?? EPOCH;
|
|
699
|
+
// #3949: aliases admit only approvals of this run's plan, and carrying one writes a line.
|
|
700
|
+
const aliases = input.planDigest !== undefined && port.appendResolution ? (input.aliases ?? []) : [];
|
|
701
|
+
const aliased = (from: { op: string; gate: string }) => aliases.some((a) => a.op === from.op && a.gate === from.gate);
|
|
549
702
|
// #3841: an approval of this plan the rule refuses is named, so a wait says why.
|
|
550
703
|
const refused: GateRefusedApproval[] = [];
|
|
551
704
|
const resolutions = (
|
|
@@ -553,6 +706,8 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
553
706
|
? ledger.resolutions
|
|
554
707
|
: ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env)
|
|
555
708
|
)
|
|
709
|
+
// #3949: a carried approval counts only while the gate still names its alias.
|
|
710
|
+
.filter((r) => r.gate !== input.gate || r.carriedFrom === undefined || aliased(r.carriedFrom))
|
|
556
711
|
.filter((r) => input.spent === undefined || r.gate !== input.gate || !isApprovalSpent(input.spent, r))
|
|
557
712
|
.filter((r) => {
|
|
558
713
|
if (rule === null || r.gate !== input.gate) return true;
|
|
@@ -561,8 +716,16 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
561
716
|
if (samePlanDigest(r.planDigest, input.planDigest) && new Date(r.timestamp).getTime() >= new Date(since).getTime()) refused.push({ resolution: r, reason });
|
|
562
717
|
return false;
|
|
563
718
|
});
|
|
719
|
+
const fromAliases = aliases.length > 0 ? await aliasApprovals(port, { ...input, aliases }, ledger.resolutions, rule, now, refused) : [];
|
|
564
720
|
refused.sort((a, b) => new Date(a.resolution.timestamp).getTime() - new Date(b.resolution.timestamp).getTime());
|
|
565
721
|
|
|
722
|
+
// #3949: write the approvals that counted from an alias under this gate.
|
|
723
|
+
const carry = async (records: readonly GateResolutionRecord[]): Promise<GateResolutionRecord[]> => {
|
|
724
|
+
const written: GateResolutionRecord[] = [];
|
|
725
|
+
for (const r of records) written.push((await port.appendResolution!(carriedApproval(r, input.op, input.gate, now))).record);
|
|
726
|
+
return written;
|
|
727
|
+
};
|
|
728
|
+
|
|
566
729
|
let mismatched: GateResolutionRecord | undefined;
|
|
567
730
|
let quorum: GateQuorumProgress | undefined;
|
|
568
731
|
if (input.approval) {
|
|
@@ -587,9 +750,28 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
587
750
|
}
|
|
588
751
|
mismatched = tally.mismatched;
|
|
589
752
|
quorum = { approvers: tally.counted.map((r) => r.resolvedBy), need: tally.need };
|
|
753
|
+
// #3949: old and new approvals add up toward one quorum, each approver once.
|
|
754
|
+
const counted = new Set(tally.counted.map((r) => r.resolvedBy));
|
|
755
|
+
const extra = fromAliases.filter((r) => !counted.has(r.resolvedBy));
|
|
756
|
+
if (extra.length > 0) {
|
|
757
|
+
const asCarried = new Map(extra.map((r) => [{ version: 1, ...carriedApproval(r, input.op, input.gate, now) } as GateResolutionRecord, r]));
|
|
758
|
+
const combined = tallyGateApprovals([...resolutions, ...asCarried.keys()], input.gate, since, input.planDigest, input.approval, humansSince);
|
|
759
|
+
if (combined.counted.length >= combined.need) {
|
|
760
|
+
const written = await carry(combined.counted.filter((r) => asCarried.has(r)).map((r) => asCarried.get(r)!));
|
|
761
|
+
const approvals = [...combined.counted.filter((r) => !asCarried.has(r)), ...written];
|
|
762
|
+
return { satisfied: true, resolution: written[written.length - 1]!, via: "alias", approvals, carried: written };
|
|
763
|
+
}
|
|
764
|
+
quorum = { approvers: combined.counted.map((r) => r.resolvedBy), need: combined.need };
|
|
765
|
+
}
|
|
590
766
|
} else {
|
|
591
767
|
const found = latestResolutionForPlan(resolutions, input.gate, since, input.planDigest);
|
|
592
768
|
if (found.resolution) return { satisfied: true, resolution: found.resolution };
|
|
769
|
+
// #3949: else the newest approval an alias admits passes it, written under this gate.
|
|
770
|
+
const newest = fromAliases[fromAliases.length - 1];
|
|
771
|
+
if (newest) {
|
|
772
|
+
const written = await carry([newest]);
|
|
773
|
+
return { satisfied: true, resolution: written[0]!, via: "alias", carried: written };
|
|
774
|
+
}
|
|
593
775
|
mismatched = found.mismatched;
|
|
594
776
|
}
|
|
595
777
|
|
|
@@ -614,13 +796,24 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
614
796
|
// The same holds for the approval block (#2508): a pending fact recorded
|
|
615
797
|
// under another policy version or another context would have `chant
|
|
616
798
|
// approve` evaluate the policy against something this run no longer has.
|
|
799
|
+
// #3947: and for the checkpoint, so a gate that resumes gets one. A fact
|
|
800
|
+
// that already carries a checkpoint stands: an approval copies that one.
|
|
617
801
|
if (
|
|
618
802
|
standing && !isPendingGateExpired(standing, now) && samePlanDigest(standing.planDigest, input.planDigest) &&
|
|
619
|
-
sameApproval(standing.approval, input.approval)
|
|
803
|
+
sameApproval(standing.approval, input.approval) &&
|
|
804
|
+
(input.checkpoint === undefined || port.writeCheckpoint === undefined || standing.checkpointDigest !== undefined)
|
|
620
805
|
) {
|
|
621
806
|
return { satisfied: false, pending: standing, recorded: false, ...asMismatch };
|
|
622
807
|
}
|
|
623
808
|
|
|
809
|
+
// The checkpoint is written before the fact that names it, so a fact never
|
|
810
|
+
// points at a file that isn't there.
|
|
811
|
+
let checkpointDigest: string | undefined;
|
|
812
|
+
if (input.checkpoint !== undefined && port.writeCheckpoint) {
|
|
813
|
+
checkpointDigest = gateCheckpointDigest(input.checkpoint);
|
|
814
|
+
await port.writeCheckpoint(input.checkpoint);
|
|
815
|
+
}
|
|
816
|
+
|
|
624
817
|
const url = resolveApprovalUrl();
|
|
625
818
|
// #3683: in CI, where this job runs, so an approval can start it again.
|
|
626
819
|
const resume = resolveGateRunLocator();
|
|
@@ -637,6 +830,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
637
830
|
...(url ? { url } : {}),
|
|
638
831
|
...(resume ? { resume } : {}),
|
|
639
832
|
...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
|
|
833
|
+
...(checkpointDigest !== undefined ? { checkpointDigest } : {}),
|
|
640
834
|
...(env !== undefined ? { environment: env } : {}),
|
|
641
835
|
...(input.approval ? { approval: input.approval } : {}),
|
|
642
836
|
...(input.neverOverMcp ? { neverOverMcp: true as const } : {}),
|
package/src/op/index.ts
CHANGED
|
@@ -156,9 +156,12 @@ export type {
|
|
|
156
156
|
export { renderHuman, renderJson } from "./local-output";
|
|
157
157
|
export {
|
|
158
158
|
activityContract, isActivityContract, collectActivityContracts, validateActivitySteps, KNOWN_ACTIVITY_PROFILES,
|
|
159
|
-
pathExistsInSchema,
|
|
159
|
+
pathExistsInSchema, readsSecretOutput,
|
|
160
160
|
} from "./activity-contract";
|
|
161
161
|
export type { ActivityContract, ActivityContractIssue } from "./activity-contract";
|
|
162
|
+
export { validateGateResume, resumeGatesOf, decideGateResume } from "./gate-checkpoint";
|
|
163
|
+
export type { ResumeGate, GateResumeDecision } from "./gate-checkpoint";
|
|
164
|
+
export type { GateCheckpoint } from "../lifecycle/gate-checkpoint";
|
|
162
165
|
export { stepOutput, isStepOutputRef, collectStepOutputRefs, validateStepOutputRefs, validateStepOutputRefScope, makeOutProxy } from "./step-output-ref";
|
|
163
166
|
export type { StepOutputRef, WithStepRefs } from "./step-output-ref";
|
|
164
167
|
export type { NamedActivityStep } from "./builders";
|
package/src/op/local-executor.ts
CHANGED
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* ledger, so `chant approve <op> <gate>` followed by `chant run <op>` is the
|
|
15
15
|
* whole loop.
|
|
16
16
|
*
|
|
17
|
+
* A gate with `resume: true` (#3947) also stores a checkpoint of the step
|
|
18
|
+
* outputs the steps after it read, and the next run after the approval starts
|
|
19
|
+
* at the gate instead of the first phase. It re-runs the step that produced
|
|
20
|
+
* the gate's plan first, and starts at the first phase after all when the
|
|
21
|
+
* plan moved. See `./gate-checkpoint.ts`.
|
|
22
|
+
*
|
|
17
23
|
* The executor is deliberately decoupled from any lexicon: activity
|
|
18
24
|
* implementations and profiles are passed in (loaded dynamically by the CLI),
|
|
19
25
|
* so core never statically depends on one.
|
|
@@ -31,6 +37,7 @@ import { stepLabel, withRunConsole, withStepConsole } from "./step-console";
|
|
|
31
37
|
import { stepTimeoutProblem } from "./activity-profiles";
|
|
32
38
|
import { describeGateMismatch, evaluateGate, gateIsSealed, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "./gate";
|
|
33
39
|
import { gateName } from "./gate-name";
|
|
40
|
+
import { resolveGateAliases } from "./gate-alias";
|
|
34
41
|
import { gatePointOf } from "./gate-point";
|
|
35
42
|
import { evaluatePointGate, workspaceGatePointAsker, type GatePointAsker } from "./gate-point-run";
|
|
36
43
|
import type { ResolvedGateApproval } from "./gate-approval";
|
|
@@ -47,6 +54,11 @@ import { RunWorkLease, LEASE_LOST, workLeaseProblems, workLeaseNeedsRunItem, typ
|
|
|
47
54
|
import { WORK_LEASE_STEP_ID } from "./types";
|
|
48
55
|
import { LiveRun, currentLiveRun, withLiveRun } from "./run-live";
|
|
49
56
|
import { runEnvOf } from "../lifecycle/run-ledger";
|
|
57
|
+
import { getHeadCommit } from "../lifecycle/git";
|
|
58
|
+
import { describePlanDigest, samePlanDigest } from "../lifecycle/plan-digest";
|
|
59
|
+
import type { GateCheckpoint } from "../lifecycle/gate-checkpoint";
|
|
60
|
+
import type { ActivityContract } from "./activity-contract";
|
|
61
|
+
import { buildGateCheckpoint, decideGateResume, resumeGatesOf, type GateResumeDecision, type ResumeGate } from "./gate-checkpoint";
|
|
50
62
|
|
|
51
63
|
export { parseDuration } from "./duration";
|
|
52
64
|
|
|
@@ -78,12 +90,17 @@ export interface StepRecord {
|
|
|
78
90
|
resolvedBy: string;
|
|
79
91
|
timestamp: string;
|
|
80
92
|
url?: string;
|
|
81
|
-
/**
|
|
82
|
-
|
|
93
|
+
/**
|
|
94
|
+
* On a gate with an `approval` block (#2508): whether a quorum or a policy permit passed it. `"point"` on a gate that asks a decision point (#3170).
|
|
95
|
+
* `"alias"` (#3949) when an approval recorded under one of the gate's aliases counted, on any gate.
|
|
96
|
+
*/
|
|
97
|
+
via?: "quorum" | "policy" | "point" | "alias";
|
|
83
98
|
/** On a gate with a quorum (#2508): every approver who counted toward it. */
|
|
84
99
|
approvers?: string[];
|
|
85
100
|
/** On a gate that asks a decision point (#3170): the answer record that passed it, which the gate ledger's resolution cites too. */
|
|
86
101
|
answer?: GateAnswerRef;
|
|
102
|
+
/** With `via: "alias"` (#3949): the approvals carried from an alias, each by the op, gate and time it was recorded under. */
|
|
103
|
+
carriedFrom?: Array<{ op: string; gate: string; resolvedBy: string; timestamp: string }>;
|
|
87
104
|
};
|
|
88
105
|
/**
|
|
89
106
|
* Why a step declined to proceed on something that is not a failure
|
|
@@ -105,6 +122,12 @@ export interface StepRecord {
|
|
|
105
122
|
* approve` takes. The step's status is `skipped` and the run's is `gated`.
|
|
106
123
|
*/
|
|
107
124
|
gate?: { op: string; gate: string };
|
|
125
|
+
/**
|
|
126
|
+
* Set on a step this run did not execute because it resumed at a gate
|
|
127
|
+
* (#3947): the run whose checkpoint supplied the step's output, and the
|
|
128
|
+
* gate. The step's status is `skipped`.
|
|
129
|
+
*/
|
|
130
|
+
resumed?: { run: string; gate: string };
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
export interface OpRunResult {
|
|
@@ -151,6 +174,18 @@ export interface OpRunResult {
|
|
|
151
174
|
* was released and with which outcome, or why it was lost.
|
|
152
175
|
*/
|
|
153
176
|
workLease?: WorkLeaseRunResult;
|
|
177
|
+
/**
|
|
178
|
+
* Set when this run resumed at an approved gate (#3947): the gate, the run
|
|
179
|
+
* whose checkpoint it carried on from, and that checkpoint's digest.
|
|
180
|
+
*/
|
|
181
|
+
resumed?: { gate: string; run: string; checkpoint: string };
|
|
182
|
+
/**
|
|
183
|
+
* Why a gate that resumes did not (#3947), one line each: an approved gate
|
|
184
|
+
* whose checkpoint failed a check, so the run started at the first phase,
|
|
185
|
+
* or a gate this run stopped at that stored no checkpoint. Absent when
|
|
186
|
+
* there is nothing to say.
|
|
187
|
+
*/
|
|
188
|
+
resumeNotes?: string[];
|
|
154
189
|
}
|
|
155
190
|
|
|
156
191
|
// ── Errors ────────────────────────────────────────────────────────────────��─
|
|
@@ -547,6 +582,14 @@ interface GateContext {
|
|
|
547
582
|
points?: GatePointAsker;
|
|
548
583
|
/** The environment the run was started for, a point gate's `gate.env` input. */
|
|
549
584
|
env?: string;
|
|
585
|
+
/** The Op's gates that resume (#3947), and what a checkpoint at one needs. Absent when it has none. */
|
|
586
|
+
resume?: {
|
|
587
|
+
config: OpConfig;
|
|
588
|
+
gates: Map<GateStep, ResumeGate>;
|
|
589
|
+
commit: string | undefined;
|
|
590
|
+
contracts?: ReadonlyMap<string, ActivityContract>;
|
|
591
|
+
note: (line: string) => void;
|
|
592
|
+
};
|
|
550
593
|
}
|
|
551
594
|
|
|
552
595
|
/**
|
|
@@ -679,6 +722,22 @@ async function runGateStep(
|
|
|
679
722
|
const resolvedPlan = resolveStepOutputRefs(step.plan, resultsById);
|
|
680
723
|
const planDigest = typeof resolvedPlan === "string" && resolvedPlan !== "" ? resolvedPlan : undefined;
|
|
681
724
|
const approval = resolveGateApproval(step, resultsById);
|
|
725
|
+
// #3947: a gate that resumes stores the outputs the steps after it read.
|
|
726
|
+
const resumeAt = gates.resume?.gates.get(step);
|
|
727
|
+
let checkpoint: GateCheckpoint | undefined;
|
|
728
|
+
let blocked: string | undefined;
|
|
729
|
+
if (resumeAt && gates.resume) {
|
|
730
|
+
const built = buildGateCheckpoint(gates.resume.config, resumeAt, {
|
|
731
|
+
runId: gates.runId ?? "",
|
|
732
|
+
commit: gates.resume.commit,
|
|
733
|
+
...(gates.env !== undefined ? { environment: gates.env } : {}),
|
|
734
|
+
...(planDigest !== undefined ? { planDigest } : {}),
|
|
735
|
+
outputs: resultsById,
|
|
736
|
+
...(gates.resume.contracts ? { contracts: gates.resume.contracts } : {}),
|
|
737
|
+
});
|
|
738
|
+
if ("blocked" in built) blocked = built.blocked;
|
|
739
|
+
else checkpoint = built.checkpoint;
|
|
740
|
+
}
|
|
682
741
|
let check: GateCheck;
|
|
683
742
|
try {
|
|
684
743
|
check = await evaluateGate(gates.port, {
|
|
@@ -690,6 +749,8 @@ async function runGateStep(
|
|
|
690
749
|
...(planDigest !== undefined ? { planDigest } : {}),
|
|
691
750
|
...(approval ? { approval } : {}),
|
|
692
751
|
...(gates.now ? { now: gates.now } : {}),
|
|
752
|
+
...(checkpoint ? { checkpoint } : {}),
|
|
753
|
+
...(step.aliases?.length ? { aliases: resolveGateAliases(step, gates.op) } : {}),
|
|
693
754
|
});
|
|
694
755
|
} catch (err) {
|
|
695
756
|
// Deciding a gate reads and writes the `chant/lifecycle` branch, so it
|
|
@@ -721,6 +782,9 @@ async function runGateStep(
|
|
|
721
782
|
...(resolution.url ? { url: resolution.url } : {}),
|
|
722
783
|
...(check.via ? { via: check.via } : {}),
|
|
723
784
|
...(check.approvals ? { approvers: check.approvals.map((r) => r.resolvedBy) } : {}),
|
|
785
|
+
...(check.carried?.length
|
|
786
|
+
? { carriedFrom: check.carried.map((r) => ({ op: r.carriedFrom!.op, gate: r.carriedFrom!.gate, resolvedBy: r.resolvedBy, timestamp: r.carriedFrom!.timestamp })) }
|
|
787
|
+
: {}),
|
|
724
788
|
};
|
|
725
789
|
// #2522: a later step reads this gate's approval from the run context
|
|
726
790
|
// rather than re-tallying the ledger, so it sees the decision this run made.
|
|
@@ -737,6 +801,7 @@ async function runGateStep(
|
|
|
737
801
|
};
|
|
738
802
|
}
|
|
739
803
|
|
|
804
|
+
if (blocked) gates.resume?.note(`gate "${gateName(step)}" stored no checkpoint, so it won't resume: ${blocked}`);
|
|
740
805
|
const refusal = check.mismatch
|
|
741
806
|
? { refusal: describeGateMismatch(gates.op, gateName(step), check.mismatch, await gateIsSealed(gateName(step))) }
|
|
742
807
|
: {};
|
|
@@ -952,6 +1017,8 @@ async function runEffectStep(
|
|
|
952
1017
|
/**
|
|
953
1018
|
* Run a phase. Throws `PhaseFailure` (with the records so far) if any step
|
|
954
1019
|
* fails, or `GateStop` if a `gate` step in it is still pending approval.
|
|
1020
|
+
* `from` starts a sequential phase at that step, for a run that resumes at a
|
|
1021
|
+
* gate in it (#3947); the steps before it are the caller's to record.
|
|
955
1022
|
*/
|
|
956
1023
|
async function runPhase(
|
|
957
1024
|
phase: PhaseDefinition,
|
|
@@ -960,6 +1027,7 @@ async function runPhase(
|
|
|
960
1027
|
resultsById: Map<string, unknown>,
|
|
961
1028
|
gates: GateContext,
|
|
962
1029
|
signal?: AbortSignal,
|
|
1030
|
+
from = 0,
|
|
963
1031
|
): Promise<StepRecord[]> {
|
|
964
1032
|
if (phase.parallel) {
|
|
965
1033
|
const eff = phase.steps.find(isEffect);
|
|
@@ -1039,7 +1107,7 @@ async function runPhase(
|
|
|
1039
1107
|
}
|
|
1040
1108
|
};
|
|
1041
1109
|
|
|
1042
|
-
for (let i =
|
|
1110
|
+
for (let i = from; i < steps.length; i++) {
|
|
1043
1111
|
const step = steps[i];
|
|
1044
1112
|
// The work lease (#2748): claimed here when it is due, and checked before
|
|
1045
1113
|
// every step, so no step starts once the lease is lost.
|
|
@@ -1184,6 +1252,19 @@ export interface RunOpOptions {
|
|
|
1184
1252
|
stepOutput?: (line: string) => void;
|
|
1185
1253
|
/** How long a step may be quiet before the still-running line. Default 60s; `0` turns it off. */
|
|
1186
1254
|
stepHeartbeatMs?: number;
|
|
1255
|
+
/**
|
|
1256
|
+
* The commit the checkout is at, for an Op with a gate that resumes
|
|
1257
|
+
* (#3947). A checkpoint records it and a resumed run must match it.
|
|
1258
|
+
* Default: `git rev-parse HEAD` in `cwd`, and none when that fails, so no
|
|
1259
|
+
* gate resumes.
|
|
1260
|
+
*/
|
|
1261
|
+
commit?: string;
|
|
1262
|
+
/**
|
|
1263
|
+
* The activity contracts the run knows (#3947). A gate that resumes stores
|
|
1264
|
+
* no output a contract marks secret, and doesn't resume when a later step
|
|
1265
|
+
* needs one. The local runtime passes the configured lexicons' contracts.
|
|
1266
|
+
*/
|
|
1267
|
+
contracts?: ReadonlyMap<string, ActivityContract>;
|
|
1187
1268
|
}
|
|
1188
1269
|
|
|
1189
1270
|
/**
|
|
@@ -1355,12 +1436,12 @@ async function runOpInTurn(
|
|
|
1355
1436
|
const start = Date.now();
|
|
1356
1437
|
// Each phase's wall-clock time, for the run record and the in-flight one.
|
|
1357
1438
|
const phaseDurations: Record<string, number> = {};
|
|
1358
|
-
const timedPhase = async (phase: PhaseDefinition, ctx: GateContext, phaseSignal: AbortSignal | undefined): Promise<StepRecord[]> => {
|
|
1439
|
+
const timedPhase = async (phase: PhaseDefinition, ctx: GateContext, phaseSignal: AbortSignal | undefined, from = 0): Promise<StepRecord[]> => {
|
|
1359
1440
|
const live = currentLiveRun();
|
|
1360
1441
|
const t0 = Date.now();
|
|
1361
1442
|
live?.phaseStarted(phase.name);
|
|
1362
1443
|
try {
|
|
1363
|
-
const ran = await runPhase(phase, activities, profiles, resultsById, ctx, phaseSignal);
|
|
1444
|
+
const ran = await runPhase(phase, activities, profiles, resultsById, ctx, phaseSignal, from);
|
|
1364
1445
|
live?.phaseEnded(phase.name, phaseVerdict(ran), Date.now() - t0);
|
|
1365
1446
|
return ran;
|
|
1366
1447
|
} finally {
|
|
@@ -1389,6 +1470,7 @@ async function runOpInTurn(
|
|
|
1389
1470
|
...(gate ? { gate: { name: gate.gate, since: gate.timestamp, ...(gate.op !== config.name ? { op: gate.op } : {}) } } : {}),
|
|
1390
1471
|
...(point ? { point: { ...point, since: ended } } : {}),
|
|
1391
1472
|
...(options.steward ? { steward: options.steward } : {}),
|
|
1473
|
+
...(resumed ? { resumed } : {}),
|
|
1392
1474
|
phaseDurations,
|
|
1393
1475
|
});
|
|
1394
1476
|
const record: OpRunRecord = { version: 1, ...input, id: runId };
|
|
@@ -1406,10 +1488,96 @@ async function runOpInTurn(
|
|
|
1406
1488
|
// values before reaching an activity function — see `runStep`.
|
|
1407
1489
|
const resultsById = new Map<string, unknown>();
|
|
1408
1490
|
|
|
1491
|
+
// #3947: the gates that resume. Each stores a checkpoint when the run stops
|
|
1492
|
+
// at it, and the run may start at one of them.
|
|
1493
|
+
let resumed: OpRunResult["resumed"];
|
|
1494
|
+
const resumeNotes: string[] = [];
|
|
1495
|
+
const resumeGates = resumeGatesOf(config);
|
|
1496
|
+
if (resumeGates.length > 0) {
|
|
1497
|
+
gates.resume = {
|
|
1498
|
+
config,
|
|
1499
|
+
gates: new Map(resumeGates.map((g) => [g.step, g])),
|
|
1500
|
+
commit: options.commit ?? (await getHeadCommit(options.cwd ? { cwd: options.cwd } : undefined).catch(() => undefined)),
|
|
1501
|
+
...(options.contracts ? { contracts: options.contracts } : {}),
|
|
1502
|
+
note: (line) => resumeNotes.push(line),
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
const resumeFields = (): Pick<OpRunResult, "resumed" | "resumeNotes"> => ({
|
|
1506
|
+
...(resumed ? { resumed } : {}),
|
|
1507
|
+
...(resumeNotes.length > 0 ? { resumeNotes: [...resumeNotes] } : {}),
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Resume at the gate `decision` names (#3947): seed the checkpoint's
|
|
1512
|
+
* outputs, record every step before the gate as resumed, and run the step
|
|
1513
|
+
* that produced the gate's plan again. Returns where the phases pick up, or
|
|
1514
|
+
* undefined when the plan moved and the run starts at the first phase.
|
|
1515
|
+
*/
|
|
1516
|
+
const resumeAt = async (
|
|
1517
|
+
decision: Extract<GateResumeDecision, { kind: "resume" }>,
|
|
1518
|
+
): Promise<{ phaseIndex: number; stepIndex: number } | undefined> => {
|
|
1519
|
+
const { gate, checkpoint } = decision;
|
|
1520
|
+
for (const [id, value] of Object.entries(checkpoint.outputs)) resultsById.set(id, value);
|
|
1521
|
+
const marker = { run: checkpoint.runId, gate: gate.name };
|
|
1522
|
+
const prefix: StepRecord[] = [];
|
|
1523
|
+
for (const [pi, phase] of config.phases.entries()) {
|
|
1524
|
+
if (pi > gate.phaseIndex) break;
|
|
1525
|
+
const steps = pi < gate.phaseIndex ? phase.steps : phase.parallel ? [] : phase.steps.slice(0, gate.stepIndex);
|
|
1526
|
+
for (const [si, step] of steps.entries()) {
|
|
1527
|
+
if (gate.planStep && pi === gate.planStep.phaseIndex && si === gate.planStep.stepIndex) {
|
|
1528
|
+
const ran = await runStep(gate.planStep.step, phase.name, activities, profiles, resultsById, stepSignal);
|
|
1529
|
+
prefix.push(ran.record);
|
|
1530
|
+
if (ran.record.status === "fail" || ran.pending || ran.record.point) {
|
|
1531
|
+
for (const r of prefix) gates.onRecord?.(r);
|
|
1532
|
+
if (ran.pending) throw new GateStop(prefix, ran.pending, phase.name);
|
|
1533
|
+
if (ran.record.point) throw new PointStop(prefix, ran.record.point, phase.name);
|
|
1534
|
+
throw new PhaseFailure(prefix);
|
|
1535
|
+
}
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const skipped = isEffect(step)
|
|
1539
|
+
? skippedRecord(phase.name, `effect:${step.receipt.name}`)
|
|
1540
|
+
: isGate(step)
|
|
1541
|
+
? skippedRecord(phase.name, gateFn(step))
|
|
1542
|
+
: skippedRecord(phase.name, step.fn, step.args);
|
|
1543
|
+
prefix.push({ ...skipped, resumed: marker });
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
// The plan the run would approve now against the one approved: a moved
|
|
1547
|
+
// plan is live drift since the approval, and starts the run over.
|
|
1548
|
+
const resolved = resolveStepOutputRefs(gate.step.plan, resultsById);
|
|
1549
|
+
const planned = typeof resolved === "string" && resolved !== "" ? resolved : undefined;
|
|
1550
|
+
if (!samePlanDigest(planned, decision.pending.planDigest)) {
|
|
1551
|
+
resultsById.clear();
|
|
1552
|
+
resumeNotes.push(
|
|
1553
|
+
`gate "${gate.name}" is approved, and the run starts at the first phase: the plan moved since the approval ` +
|
|
1554
|
+
`(approved ${describePlanDigest(decision.pending.planDigest)}, planned ${planned ?? "(none)"})`,
|
|
1555
|
+
);
|
|
1556
|
+
return undefined;
|
|
1557
|
+
}
|
|
1558
|
+
records.push(...prefix);
|
|
1559
|
+
for (const r of prefix) gates.onRecord?.(r);
|
|
1560
|
+
resumed = { gate: gate.name, run: checkpoint.runId, checkpoint: decision.digest };
|
|
1561
|
+
return { phaseIndex: gate.phaseIndex, stepIndex: config.phases[gate.phaseIndex]!.parallel ? 0 : gate.stepIndex };
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1409
1564
|
try {
|
|
1410
|
-
|
|
1565
|
+
let from: { phaseIndex: number; stepIndex: number } | undefined;
|
|
1566
|
+
if (gates.resume) {
|
|
1567
|
+
const decision = await decideGateResume(gates.port, config, {
|
|
1568
|
+
now: options.now ?? new Date().toISOString(),
|
|
1569
|
+
commit: gates.resume.commit,
|
|
1570
|
+
...(options.env !== undefined ? { environment: options.env } : {}),
|
|
1571
|
+
});
|
|
1572
|
+
if (decision.kind === "refused") {
|
|
1573
|
+
resumeNotes.push(`gate "${decision.gate}" is approved, and the run starts at the first phase: ${decision.reason}`);
|
|
1574
|
+
} else if (decision.kind === "resume") {
|
|
1575
|
+
from = await resumeAt(decision);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
for (let pi = from?.phaseIndex ?? 0; pi < config.phases.length; pi++) {
|
|
1411
1579
|
if (signal?.aborted) throw new PhaseFailure([]);
|
|
1412
|
-
records.push(...(await timedPhase(
|
|
1580
|
+
records.push(...(await timedPhase(config.phases[pi]!, gates, stepSignal, pi === from?.phaseIndex ? from.stepIndex : 0)));
|
|
1413
1581
|
}
|
|
1414
1582
|
// The last renewal before the run is recorded (#2748): a run whose lease
|
|
1415
1583
|
// was lost is never recorded as done.
|
|
@@ -1429,6 +1597,7 @@ async function runOpInTurn(
|
|
|
1429
1597
|
startedAt,
|
|
1430
1598
|
record,
|
|
1431
1599
|
...(await finishWork("ok")),
|
|
1600
|
+
...resumeFields(),
|
|
1432
1601
|
};
|
|
1433
1602
|
}
|
|
1434
1603
|
|
|
@@ -1447,6 +1616,7 @@ async function runOpInTurn(
|
|
|
1447
1616
|
point: err.question,
|
|
1448
1617
|
record,
|
|
1449
1618
|
...(await finishWork("waiting")),
|
|
1619
|
+
...resumeFields(),
|
|
1450
1620
|
};
|
|
1451
1621
|
}
|
|
1452
1622
|
|
|
@@ -1467,6 +1637,7 @@ async function runOpInTurn(
|
|
|
1467
1637
|
...(err.pushWarning ? { gatePushWarning: err.pushWarning } : {}),
|
|
1468
1638
|
record,
|
|
1469
1639
|
...(await finishWork("gated")),
|
|
1640
|
+
...resumeFields(),
|
|
1470
1641
|
};
|
|
1471
1642
|
}
|
|
1472
1643
|
|
|
@@ -1541,6 +1712,7 @@ async function runOpInTurn(
|
|
|
1541
1712
|
startedAt,
|
|
1542
1713
|
record: failed,
|
|
1543
1714
|
...(await finishWork("fail")),
|
|
1715
|
+
...resumeFields(),
|
|
1544
1716
|
},
|
|
1545
1717
|
{ cause: err },
|
|
1546
1718
|
);
|
|
@@ -1555,6 +1727,7 @@ async function runOpInTurn(
|
|
|
1555
1727
|
startedAt,
|
|
1556
1728
|
record,
|
|
1557
1729
|
...(await finishWork("ok")),
|
|
1730
|
+
...resumeFields(),
|
|
1558
1731
|
};
|
|
1559
1732
|
}
|
|
1560
1733
|
|