@intentius/chant 0.115.0 → 0.117.0

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 (44) hide show
  1. package/dist/cli/handlers/gate-resume.d.ts +44 -0
  2. package/dist/cli/handlers/gate-resume.d.ts.map +1 -0
  3. package/dist/cli/handlers/operator.d.ts +1 -0
  4. package/dist/cli/handlers/operator.d.ts.map +1 -1
  5. package/dist/cli/handlers/run-generate.d.ts.map +1 -1
  6. package/dist/cli/handlers/run-wave.d.ts +7 -0
  7. package/dist/cli/handlers/run-wave.d.ts.map +1 -0
  8. package/dist/cli/main.d.ts.map +1 -1
  9. package/dist/cli/registry.d.ts +11 -1
  10. package/dist/cli/registry.d.ts.map +1 -1
  11. package/dist/lexicon.d.ts +22 -0
  12. package/dist/lexicon.d.ts.map +1 -1
  13. package/dist/lifecycle/gate-ledger.d.ts +7 -0
  14. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  15. package/dist/op/gate-resume.d.ts +104 -0
  16. package/dist/op/gate-resume.d.ts.map +1 -0
  17. package/dist/op/gate.d.ts.map +1 -1
  18. package/dist/op/generate-pipeline.d.ts +15 -1
  19. package/dist/op/generate-pipeline.d.ts.map +1 -1
  20. package/dist/op/index.d.ts +6 -2
  21. package/dist/op/index.d.ts.map +1 -1
  22. package/dist/op/op-waves-run.d.ts +115 -0
  23. package/dist/op/op-waves-run.d.ts.map +1 -0
  24. package/dist/op/op-waves.d.ts +158 -0
  25. package/dist/op/op-waves.d.ts.map +1 -0
  26. package/package.json +1 -1
  27. package/src/cli/handlers/gate-resume.test.ts +51 -0
  28. package/src/cli/handlers/gate-resume.ts +166 -0
  29. package/src/cli/handlers/operator.ts +8 -2
  30. package/src/cli/handlers/run-generate.ts +51 -1
  31. package/src/cli/handlers/run-wave.ts +43 -0
  32. package/src/cli/main.ts +28 -2
  33. package/src/cli/registry.ts +11 -1
  34. package/src/lexicon.ts +26 -0
  35. package/src/lifecycle/gate-ledger.ts +7 -0
  36. package/src/op/gate-resume.e2e.test.ts +135 -0
  37. package/src/op/gate-resume.test.ts +177 -0
  38. package/src/op/gate-resume.ts +267 -0
  39. package/src/op/gate.ts +4 -0
  40. package/src/op/generate-pipeline.ts +49 -0
  41. package/src/op/index.ts +13 -2
  42. package/src/op/op-waves-run.ts +325 -0
  43. package/src/op/op-waves.test.ts +95 -0
  44. package/src/op/op-waves.ts +250 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Resume the job that waits at a gate (#3683).
3
+ *
4
+ * A CI job that reaches a gate nobody approved records a pending fact on
5
+ * `chant/lifecycle` and exits 3. Nothing re-runs it when the approval lands,
6
+ * because a push to the ledger branch starts no pipeline. This module closes
7
+ * that loop through the forge's API.
8
+ *
9
+ * When the job runs in CI, the pending fact it records carries a
10
+ * {@link GateRunLocator}, read from the job's environment by
11
+ * {@link resolveGateRunLocator}: which forge, the API base, the repository,
12
+ * the run, and what the forge needs to start it again. After an approval,
13
+ * {@link resumeGateRun} starts it again:
14
+ *
15
+ * | Forge | Call | Token |
16
+ * |---|---|---|
17
+ * | GitHub | `POST /repos/{repo}/actions/runs/{run}/rerun-failed-jobs` | `actions: write` (the job's `GITHUB_TOKEN` with that permission, or a fine-grained token with Actions read and write) |
18
+ * | GitLab | `POST /projects/{id}/jobs/{job}/retry` | a project or personal access token with the `api` scope and the Developer role; `CI_JOB_TOKEN` cannot retry jobs |
19
+ * | Forgejo | `POST /repos/{repo}/actions/workflows/{file}/dispatches` on the run's branch | a token with the `write:repository` scope; Forgejo has no API to re-run a run, so it starts a new run of the workflow |
20
+ *
21
+ * GitHub re-runs the failed job and the jobs that need it, at the same
22
+ * commit. GitLab retries the job, and the jobs it skipped run after it. A
23
+ * Forgejo dispatch runs the whole workflow again at the branch's head, so the
24
+ * jobs before the gate run again too; they must be safe to repeat (a wave
25
+ * whose runs already applied plans nothing and applies nothing new).
26
+ *
27
+ * Resuming approves nothing. The resumed job decides the gate again from the
28
+ * ledger, so resuming a run whose approval does not count only makes it wait
29
+ * again. {@link resumeGateRun} skips a run that is still going, that ended
30
+ * well, or that was already resumed (a later GitHub attempt, a newer GitLab
31
+ * job of the same name, a newer Forgejo run of the workflow), so a scheduled
32
+ * resume job can call it every few minutes without starting a run twice.
33
+ */
34
+
35
+ import { ForgeApiError, type ForgeFetch, type ForgeKind } from "../pr-forge";
36
+ import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger";
37
+ import { latestResolutionForPlan } from "../lifecycle/gate-ledger";
38
+ import { tallyGateApprovals } from "./gate";
39
+
40
+ /** Where the job that recorded a pending fact runs, and what its forge needs to start it again. */
41
+ export interface GateRunLocator {
42
+ forge: ForgeKind;
43
+ /** The forge's API base: `$GITHUB_API_URL` (GitHub and Forgejo) or `$CI_API_V4_URL` (GitLab). */
44
+ api: string;
45
+ /** `owner/name` on GitHub and Forgejo, the project id on GitLab. */
46
+ repo: string;
47
+ /** The run: GitHub's and Forgejo's run id, GitLab's pipeline id. */
48
+ run: string;
49
+ /** GitHub: the run attempt that waited. A later attempt means it was already re-run. */
50
+ attempt?: number;
51
+ /** GitLab: the job that waited. */
52
+ job?: string;
53
+ /** GitLab: that job's name, to find a retry of it. */
54
+ jobName?: string;
55
+ /** Forgejo: the workflow file the run came from, which a dispatch names. */
56
+ workflow?: string;
57
+ /** Forgejo: the branch a dispatch runs on. */
58
+ ref?: string;
59
+ /** The run's page, for a person. */
60
+ url?: string;
61
+ }
62
+
63
+ const trimSlash = (s: string): string => s.replace(/\/+$/, "");
64
+
65
+ /**
66
+ * The locator of the CI job this process runs in, or undefined outside CI or
67
+ * when the environment lacks a part a resume needs. Forgejo Actions sets the
68
+ * `GITHUB_*` variables too, and is told apart by `FORGEJO_ACTIONS` or
69
+ * `GITEA_ACTIONS`.
70
+ */
71
+ export function resolveGateRunLocator(env: Record<string, string | undefined> = process.env): GateRunLocator | undefined {
72
+ if (env.GITLAB_CI === "true") {
73
+ if (!env.CI_API_V4_URL || !env.CI_PROJECT_ID || !env.CI_PIPELINE_ID || !env.CI_JOB_ID) return undefined;
74
+ return {
75
+ forge: "gitlab",
76
+ api: trimSlash(env.CI_API_V4_URL),
77
+ repo: env.CI_PROJECT_ID,
78
+ run: env.CI_PIPELINE_ID,
79
+ job: env.CI_JOB_ID,
80
+ ...(env.CI_JOB_NAME ? { jobName: env.CI_JOB_NAME } : {}),
81
+ ...(env.CI_JOB_URL ? { url: env.CI_JOB_URL } : {}),
82
+ };
83
+ }
84
+ if (env.GITHUB_ACTIONS !== "true" || !env.GITHUB_REPOSITORY || !env.GITHUB_RUN_ID) return undefined;
85
+ const forgejo = env.FORGEJO_ACTIONS === "true" || env.GITEA_ACTIONS === "true";
86
+ const server = env.GITHUB_SERVER_URL ? trimSlash(env.GITHUB_SERVER_URL) : undefined;
87
+ const url = server ? `${server}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : undefined;
88
+ if (forgejo) {
89
+ const workflow = forgejoWorkflowFile(env);
90
+ const api = env.GITHUB_API_URL ?? (server ? `${server}/api/v1` : undefined);
91
+ if (!api || !workflow || !env.GITHUB_REF_NAME) return undefined;
92
+ return {
93
+ forge: "forgejo",
94
+ api: trimSlash(api),
95
+ repo: env.GITHUB_REPOSITORY,
96
+ run: env.GITHUB_RUN_ID,
97
+ workflow,
98
+ ref: env.GITHUB_REF_NAME,
99
+ ...(url ? { url } : {}),
100
+ };
101
+ }
102
+ const attempt = Number(env.GITHUB_RUN_ATTEMPT);
103
+ return {
104
+ forge: "github",
105
+ api: trimSlash(env.GITHUB_API_URL ?? "https://api.github.com"),
106
+ repo: env.GITHUB_REPOSITORY,
107
+ run: env.GITHUB_RUN_ID,
108
+ ...(Number.isInteger(attempt) && attempt > 0 ? { attempt } : {}),
109
+ ...(url ? { url } : {}),
110
+ };
111
+ }
112
+
113
+ /**
114
+ * The workflow file a Forgejo run came from, by its base name. Forgejo names
115
+ * a workflow by its file (`GITHUB_WORKFLOW`, `deploy.yml`); `GITHUB_WORKFLOW_REF`
116
+ * (`owner/repo/.forgejo/workflows/deploy.yml@refs/heads/main`) says it too.
117
+ */
118
+ function forgejoWorkflowFile(env: Record<string, string | undefined>): string | undefined {
119
+ const fromRef = env.GITHUB_WORKFLOW_REF?.split("@")[0]?.split("/").pop();
120
+ if (fromRef && /\.ya?ml$/.test(fromRef)) return fromRef;
121
+ const name = env.GITHUB_WORKFLOW;
122
+ return name && /\.ya?ml$/.test(name) ? name.split("/").pop() : undefined;
123
+ }
124
+
125
+ /** Whether `value` is a {@link GateRunLocator} a resume can use. */
126
+ export function isGateRunLocator(value: unknown): value is GateRunLocator {
127
+ if (!value || typeof value !== "object") return false;
128
+ const v = value as Record<string, unknown>;
129
+ if (typeof v.api !== "string" || typeof v.repo !== "string" || typeof v.run !== "string") return false;
130
+ if (v.forge === "github") return true;
131
+ if (v.forge === "gitlab") return typeof v.job === "string";
132
+ if (v.forge === "forgejo") return typeof v.workflow === "string" && typeof v.ref === "string";
133
+ return false;
134
+ }
135
+
136
+ /** What {@link resumeGateRun} did. */
137
+ export type GateResumeOutcome =
138
+ | { status: "resumed"; forge: ForgeKind; how: string; url?: string }
139
+ | { status: "skipped"; forge: ForgeKind; reason: string; url?: string };
140
+
141
+ export interface ResumeGateRunOptions {
142
+ token: string;
143
+ /** The approval's time. A Forgejo run of the workflow created after it means the run was already resumed. */
144
+ since?: string;
145
+ fetch?: ForgeFetch;
146
+ }
147
+
148
+ const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
149
+
150
+ function client(base: string, headers: Record<string, string>, fetchImpl: ForgeFetch) {
151
+ return async (method: string, path: string, body?: unknown): Promise<unknown> => {
152
+ const url = `${trimSlash(base)}${path}`;
153
+ const res = await fetchImpl(url, {
154
+ method,
155
+ headers: { ...headers, ...(body !== undefined ? { "Content-Type": "application/json" } : {}) },
156
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
157
+ });
158
+ const text = await res.text();
159
+ if (!res.ok) throw new ForgeApiError(method, url, res.status, text);
160
+ if (!text) return null;
161
+ try {
162
+ return JSON.parse(text);
163
+ } catch {
164
+ return text;
165
+ }
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Start the job a pending fact's locator names again, or say why not. Throws
171
+ * {@link ForgeApiError} when the forge refuses a call, such as a token
172
+ * without the access the table in the module doc lists.
173
+ */
174
+ export async function resumeGateRun(loc: GateRunLocator, opts: ResumeGateRunOptions): Promise<GateResumeOutcome> {
175
+ const fetchImpl = opts.fetch ?? (globalThis.fetch as unknown as ForgeFetch);
176
+ const url = loc.url ? { url: loc.url } : {};
177
+ const skip = (reason: string): GateResumeOutcome => ({ status: "skipped", forge: loc.forge, reason, ...url });
178
+
179
+ if (loc.forge === "github") {
180
+ const call = client(loc.api, { Authorization: `Bearer ${opts.token}`, Accept: "application/vnd.github+json" }, fetchImpl);
181
+ const path = `/repos/${loc.repo}/actions/runs/${loc.run}`;
182
+ const run = await call("GET", path);
183
+ if (!isObject(run)) throw new Error(`GET ${path} returned no run`);
184
+ if (run.status !== "completed") return skip(`run ${loc.run} is ${String(run.status)}, not finished`);
185
+ const attempt = typeof run.run_attempt === "number" ? run.run_attempt : undefined;
186
+ if (loc.attempt !== undefined && attempt !== undefined && attempt > loc.attempt) {
187
+ return skip(`run ${loc.run} was already re-run (attempt ${attempt}; attempt ${loc.attempt} waited)`);
188
+ }
189
+ if (run.conclusion === "success") return skip(`run ${loc.run} succeeded`);
190
+ await call("POST", `${path}/rerun-failed-jobs`, {});
191
+ return { status: "resumed", forge: "github", how: `re-ran the failed jobs of run ${loc.run}`, ...url };
192
+ }
193
+
194
+ if (loc.forge === "gitlab") {
195
+ const call = client(loc.api, { "PRIVATE-TOKEN": opts.token }, fetchImpl);
196
+ const project = `/projects/${encodeURIComponent(decodeURIComponent(loc.repo))}`;
197
+ const job = await call("GET", `${project}/jobs/${loc.job}`);
198
+ if (!isObject(job)) throw new Error(`GET ${project}/jobs/${loc.job} returned no job`);
199
+ const status = String(job.status);
200
+ if (status === "success") return skip(`job ${loc.job} succeeded`);
201
+ if (!["failed", "canceled", "skipped"].includes(status)) return skip(`job ${loc.job} is ${status}, not finished`);
202
+ const name = loc.jobName ?? (typeof job.name === "string" ? job.name : undefined);
203
+ if (name) {
204
+ // The pipeline's jobs list leaves retried jobs out, so the job of this name is its newest try.
205
+ const jobs = await call("GET", `${project}/pipelines/${loc.run}/jobs?per_page=100`);
206
+ const newest = Array.isArray(jobs) ? jobs.find((j) => isObject(j) && j.name === name) : undefined;
207
+ if (isObject(newest) && String(newest.id) !== String(loc.job)) {
208
+ return skip(`job ${loc.job} (${name}) was already retried as job ${String(newest.id)}`);
209
+ }
210
+ }
211
+ const retried = await call("POST", `${project}/jobs/${loc.job}/retry`);
212
+ const id = isObject(retried) && retried.id !== undefined ? ` as job ${String(retried.id)}` : "";
213
+ return { status: "resumed", forge: "gitlab", how: `retried job ${loc.job}${id}`, ...url };
214
+ }
215
+
216
+ const call = client(loc.api, { Authorization: `token ${opts.token}`, Accept: "application/json" }, fetchImpl);
217
+ const repo = `/repos/${loc.repo}`;
218
+ const run = await call("GET", `${repo}/actions/runs/${loc.run}`);
219
+ if (!isObject(run)) throw new Error(`GET ${repo}/actions/runs/${loc.run} returned no run`);
220
+ const status = String(run.status);
221
+ if (status === "success") return skip(`run ${loc.run} succeeded`);
222
+ if (["waiting", "running", "blocked", "unknown"].includes(status)) return skip(`run ${loc.run} is ${status}, not finished`);
223
+ const listed = await call("GET", `${repo}/actions/runs?workflow_id=${encodeURIComponent(loc.workflow!)}&limit=50`);
224
+ const runs = isObject(listed) && Array.isArray(listed.workflow_runs) ? listed.workflow_runs : Array.isArray(listed) ? listed : [];
225
+ // Forgejo reports `created` in whole seconds.
226
+ const since = opts.since ? Math.floor(new Date(opts.since).getTime() / 1000) * 1000 : undefined;
227
+ const later = runs.find(
228
+ (r) =>
229
+ isObject(r) && Number(r.id) > Number(loc.run) &&
230
+ (r.prettyref === undefined || r.prettyref === loc.ref) &&
231
+ (since === undefined || (typeof r.created === "string" && new Date(r.created).getTime() >= since)),
232
+ );
233
+ if (isObject(later)) return skip(`run ${String(later.id)} of ${loc.workflow} started after the approval`);
234
+ await call("POST", `${repo}/actions/workflows/${encodeURIComponent(loc.workflow!)}/dispatches`, { ref: loc.ref });
235
+ return { status: "resumed", forge: "forgejo", how: `dispatched ${loc.workflow} on ${loc.ref}`, ...url };
236
+ }
237
+
238
+ /**
239
+ * The resolution that answers `standing`, read the way a run decides the gate
240
+ * (the plan, the environment, the quorum), or undefined. A workspace's
241
+ * `identity.gates` rule is not applied here: the resumed job applies it, and
242
+ * waits again when the approval does not count.
243
+ */
244
+ export function resolutionAnswering(
245
+ resolutions: readonly GateResolutionRecord[],
246
+ standing: PendingGateRecord,
247
+ ): GateResolutionRecord | undefined {
248
+ const env = standing.environment;
249
+ const mine = resolutions.filter((r) => env === undefined || r.environment === env);
250
+ if (standing.approval) {
251
+ const tally = tallyGateApprovals(mine, standing.gate, standing.timestamp, standing.planDigest, standing.approval);
252
+ if (tally.permit) return tally.permit;
253
+ return tally.counted.length >= tally.need ? tally.counted[tally.counted.length - 1] : undefined;
254
+ }
255
+ return latestResolutionForPlan(mine, standing.gate, standing.timestamp, standing.planDigest).resolution;
256
+ }
257
+
258
+ /**
259
+ * The token a resume uses: `CHANT_FORGE_TOKEN`, else the forge's own
260
+ * (`GITHUB_TOKEN` or `GH_TOKEN` on GitHub and Forgejo, `GITLAB_TOKEN` on
261
+ * GitLab), the same order the pull-request loop reads (`../pr-forge.ts`).
262
+ */
263
+ export function resumeTokenFromEnv(forge: ForgeKind, env: Record<string, string | undefined> = process.env): string | undefined {
264
+ if (env.CHANT_FORGE_TOKEN) return env.CHANT_FORGE_TOKEN;
265
+ return forge === "gitlab" ? env.GITLAB_TOKEN : env.GITHUB_TOKEN ?? env.GH_TOKEN;
266
+ }
267
+
package/src/op/gate.ts CHANGED
@@ -52,6 +52,7 @@ import { sortedJsonReplacer } from "../utils";
52
52
  import type { GateApprover, ResolvedGateApproval } from "./gate-approval";
53
53
  import { pushLifecycle, requireLifecycleLedger } from "../lifecycle/git";
54
54
  import { parseDuration } from "./duration";
55
+ import { resolveGateRunLocator } from "./gate-resume";
55
56
 
56
57
  /**
57
58
  * What appending a pending fact learned about reaching the remote.
@@ -505,6 +506,8 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
505
506
  }
506
507
 
507
508
  const url = resolveApprovalUrl();
509
+ // #3683: in CI, where this job runs, so an approval can start it again.
510
+ const resume = resolveGateRunLocator();
508
511
  const { record, pushed, pushWarning } = await port.appendPending({
509
512
  op: input.op,
510
513
  gate: input.gate,
@@ -515,6 +518,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
515
518
  ...(input.description ? { description: input.description } : {}),
516
519
  ...(input.runId ? { runId: input.runId } : {}),
517
520
  ...(url ? { url } : {}),
521
+ ...(resume ? { resume } : {}),
518
522
  ...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
519
523
  ...(env !== undefined ? { environment: env } : {}),
520
524
  ...(input.approval ? { approval: input.approval } : {}),
@@ -25,7 +25,9 @@ import {
25
25
  type ScheduledOpSpec,
26
26
  type OpPipelineFile,
27
27
  type OpPipelineJob,
28
+ type OpWavesPipelineOptions,
28
29
  } from "../lexicon";
30
+ import { assertOpWavesSpec, type OpWaveJob, type OpWavesSpec } from "./op-waves";
29
31
 
30
32
  /**
31
33
  * Load a lexicon's plugin (`@intentius/chant-lexicon-<name>`) to reach its
@@ -115,3 +117,50 @@ export async function generateOpsPipeline(
115
117
  const { files, jobs } = plugin.generateOpPipeline(withOpSchedules(ops, discovered.ops), options);
116
118
  return { success: true, files, jobs };
117
119
  }
120
+
121
+ /** Result of generating an Op waves pipeline (#3679). */
122
+ export interface GenerateOpWavesPipelineResult {
123
+ success: boolean;
124
+ files?: OpPipelineFile[];
125
+ jobs?: OpWaveJob[];
126
+ error?: string;
127
+ }
128
+
129
+ /**
130
+ * Render ordered waves of Op runs for `lexicon` (#3679, `./op-waves.ts`):
131
+ * check the spec, check that every Op it names exists, then hand it to the
132
+ * lexicon's `generateOpWavesPipeline`.
133
+ */
134
+ export async function generateOpWavesPipeline(
135
+ spec: OpWavesSpec,
136
+ lexicon: string,
137
+ options: OpWavesPipelineOptions,
138
+ cwd?: string,
139
+ ): Promise<GenerateOpWavesPipelineResult> {
140
+ const plugin = await loadLexiconPlugin(lexicon);
141
+ if (!plugin?.generateOpWavesPipeline) {
142
+ return {
143
+ success: false,
144
+ error: `Lexicon "${lexicon}" does not render Op waves (no generateOpWavesPipeline). GitLab, GitHub, and Forgejo do.`,
145
+ };
146
+ }
147
+ try {
148
+ assertOpWavesSpec(spec);
149
+ } catch (err) {
150
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
151
+ }
152
+ const discovered = await discoverOps({ cwd });
153
+ if (discovered.errors.length > 0) return { success: false, error: discovered.errors.join("\n") };
154
+ const named = [...new Set([spec.op, ...spec.waves.flatMap((w) => w.runs.map((r) => r.op ?? spec.op))])];
155
+ const unknown = named.filter((name) => !discovered.ops.has(name));
156
+ if (unknown.length > 0) {
157
+ const known = [...discovered.ops.keys()].sort().join(", ");
158
+ return { success: false, error: `Unknown Op(s): ${unknown.join(", ")}.${known ? ` Known Ops: ${known}` : " No Ops discovered."}` };
159
+ }
160
+ try {
161
+ const { files, jobs } = plugin.generateOpWavesPipeline(spec, options);
162
+ return { success: true, files, jobs };
163
+ } catch (err) {
164
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
165
+ }
166
+ }
package/src/op/index.ts CHANGED
@@ -49,8 +49,19 @@ export type {
49
49
  } from "./receipt-store";
50
50
  export { discoverOps } from "./discover";
51
51
  export type { DiscoveredOp, OpDiscoveryResult } from "./discover";
52
- export { generateOpsPipeline, withOpSchedules } from "./generate-pipeline";
53
- export type { GenerateOpsPipelineResult } from "./generate-pipeline";
52
+ export { generateOpsPipeline, generateOpWavesPipeline, withOpSchedules } from "./generate-pipeline";
53
+ export type { GenerateOpsPipelineResult, GenerateOpWavesPipelineResult } from "./generate-pipeline";
54
+ export {
55
+ OP_WAVE_DEFAULT_APPLY,
56
+ OP_WAVE_GATE_POLICIES,
57
+ assertOpWavesSpec,
58
+ opWaveJobs,
59
+ opWaveRecordPath,
60
+ opWaveShare,
61
+ } from "./op-waves";
62
+ export type { OpWave, OpWaveGatePolicy, OpWaveJob, OpWaveRun, OpWavesSpec } from "./op-waves";
63
+ export { opWaveNeedsApproval, parseOpWavesSpec, readOpWavePolicy, runOpWave } from "./op-waves-run";
64
+ export type { OpWaveDecision, OpWaveMember, OpWavePlanFile, RunOpWaveOptions, RunOpWaveResult } from "./op-waves-run";
54
65
  export { loadActivities, loadProfiles, resolveActivity } from "./activity-registry";
55
66
  export type { ActivityFn } from "./activity-registry";
56
67
  export { loadActivityContracts, mergeActivityContracts } from "./activity-contract-registry";