@relayflows/sdk 2.0.16 → 2.0.17
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/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +86 -0
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-lifecycle.d.ts +10 -0
- package/dist/authored-flow-lifecycle.d.ts.map +1 -1
- package/dist/authored-flow-lifecycle.js +7 -0
- package/dist/authored-flow-lifecycle.js.map +1 -1
- package/dist/authored-flow-operation.d.ts +11 -0
- package/dist/authored-flow-operation.d.ts.map +1 -1
- package/dist/authored-flow-operation.js +32 -7
- package/dist/authored-flow-operation.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +37 -14
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +14 -25
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +10 -0
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-schedule.d.ts +29 -0
- package/dist/cli/cloud-schedule.d.ts.map +1 -0
- package/dist/cli/cloud-schedule.js +131 -0
- package/dist/cli/cloud-schedule.js.map +1 -0
- package/dist/cli/direct-run.js +1 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +5 -3
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +12 -1
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-http.d.ts +16 -1
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +44 -8
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-run.d.ts +30 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +70 -27
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-schedule.d.ts +48 -0
- package/dist/cloud-schedule.d.ts.map +1 -0
- package/dist/cloud-schedule.js +125 -0
- package/dist/cloud-schedule.js.map +1 -0
- package/dist/failure-kinds.d.ts +3 -3
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +2 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/named-gate-lowering.js +9 -0
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/named-gates.d.ts +1 -1
- package/dist/named-gates.d.ts.map +1 -1
- package/dist/named-gates.js +11 -1
- package/dist/named-gates.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +155 -9
- package/dist/preflight.js.map +1 -1
- package/dist/schedule-trigger.d.ts +35 -0
- package/dist/schedule-trigger.d.ts.map +1 -0
- package/dist/schedule-trigger.js +53 -0
- package/dist/schedule-trigger.js.map +1 -0
- package/dist/spec.d.ts +11 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/trigger-executor.js +1 -1
- package/dist/trigger-executor.js.map +1 -1
- package/dist/validate.js +1 -1
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +10 -0
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +51 -22
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +9 -0
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +3 -1
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +6 -3
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +3 -3
- package/src/authored-flow-error.ts +1 -0
- package/src/authored-flow-executor.ts +87 -0
- package/src/authored-flow-lifecycle.ts +7 -0
- package/src/authored-flow-operation.ts +35 -10
- package/src/authored-node-runner.ts +35 -13
- package/src/authored-worker-step.ts +15 -26
- package/src/cli/check-triggers.ts +9 -0
- package/src/cli/check.ts +15 -0
- package/src/cli/cloud-schedule.ts +135 -0
- package/src/cli/direct-run.ts +1 -1
- package/src/cli/run.ts +5 -3
- package/src/cli.ts +32 -0
- package/src/cloud-deploy.ts +12 -1
- package/src/cloud-http.ts +51 -7
- package/src/cloud-run.ts +99 -26
- package/src/cloud-schedule.ts +158 -0
- package/src/failure-kinds.ts +2 -0
- package/src/index.ts +6 -0
- package/src/named-gate-lowering.ts +9 -0
- package/src/named-gates.ts +11 -1
- package/src/preflight.ts +132 -9
- package/src/schedule-trigger.ts +73 -0
- package/src/spec.ts +12 -1
- package/src/trigger-executor.ts +1 -1
- package/src/validate.ts +1 -1
- package/src/worker-cli.ts +41 -0
- package/src/worker.ts +9 -0
- package/src/wrapper-session.ts +5 -1
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
/** Slice-P kinds the surface `.gate(config)` accepts and the SDK lowers. */
|
|
10
10
|
const NAMED_GATE_KINDS = new Set([
|
|
11
|
-
'references_input', 'subprocess_gate', 'word_count_bounds', 'regex_match',
|
|
11
|
+
'references_input', 'subprocess_gate', 'word_count_bounds', 'regex_match', 'artifact_exists',
|
|
12
12
|
]);
|
|
13
13
|
|
|
14
14
|
function isNamedGateConfig(candidate: unknown): candidate is NamedGate {
|
|
@@ -31,6 +31,14 @@ export class AuthoredFlowOperation<T> {
|
|
|
31
31
|
* still throws `unsupported_gate` and never sets this field.
|
|
32
32
|
*/
|
|
33
33
|
namedGate: NamedGate | undefined = undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Predicate gate attached via `.gate(fn, because?)`. Author code: it runs
|
|
36
|
+
* in this process after the step completes, and its verdict is journaled as
|
|
37
|
+
* a lowered `<id>.gate` deterministic step (docs/SURFACE.md §6), so replay
|
|
38
|
+
* and resume see the recorded verdict and never re-run the closure. The
|
|
39
|
+
* function itself is never serialized. `flows check` cannot prove it.
|
|
40
|
+
*/
|
|
41
|
+
predicateGate: { predicate: (value: T) => boolean; because?: string } | undefined = undefined;
|
|
34
42
|
private state: OperationState = 'created';
|
|
35
43
|
private thenInvoked = false;
|
|
36
44
|
private rootFailureRecorded = false;
|
|
@@ -61,20 +69,29 @@ export class AuthoredFlowOperation<T> {
|
|
|
61
69
|
const operation = this;
|
|
62
70
|
const step: Step<T> = {
|
|
63
71
|
gate(configOrPredicate: NamedGate | ((value: T) => boolean), _because?: string): Step<T> {
|
|
72
|
+
if (operation.namedGate !== undefined || operation.predicateGate !== undefined) {
|
|
73
|
+
throw new AuthoredFlowExecutionError('unsupported_gate', 'a step takes one .gate().');
|
|
74
|
+
}
|
|
64
75
|
if (isNamedGateConfig(configOrPredicate)) {
|
|
65
76
|
// Config-object gate: lowers into the compiled StepSpec's
|
|
66
77
|
// `verification:` field via slice-P named-gate lowering.
|
|
67
78
|
operation.namedGate = configOrPredicate;
|
|
68
79
|
return step;
|
|
69
80
|
}
|
|
70
|
-
// Predicate gate:
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
// Predicate gate: runtime control flow. The closure cannot be
|
|
82
|
+
// journaled, but its VERDICT can — the executor runs it once after
|
|
83
|
+
// the step completes and records pass/fail as a `<id>.gate` step.
|
|
84
|
+
if (typeof configOrPredicate !== 'function') {
|
|
85
|
+
throw new AuthoredFlowExecutionError(
|
|
86
|
+
'unsupported_gate',
|
|
87
|
+
'.gate() takes a named gate config ({type: "…", …}) or a predicate function.',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (_because !== undefined && typeof _because !== 'string') {
|
|
91
|
+
throw new AuthoredFlowExecutionError('unsupported_gate', '.gate(predicate, because) takes a string reason.');
|
|
92
|
+
}
|
|
93
|
+
operation.predicateGate = { predicate: configOrPredicate, ...(_because === undefined ? {} : { because: _because }) };
|
|
94
|
+
return step;
|
|
78
95
|
},
|
|
79
96
|
then<TResult1 = T, TResult2 = never>(
|
|
80
97
|
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
|
@@ -144,7 +161,15 @@ export class AuthoredFlowOperation<T> {
|
|
|
144
161
|
try {
|
|
145
162
|
this.assertCanStart();
|
|
146
163
|
this.state = 'running';
|
|
147
|
-
const
|
|
164
|
+
const started = await this.start();
|
|
165
|
+
// The predicate gate, when present, is applied here for every kind of
|
|
166
|
+
// operation, so a helper or plugin step cannot carry a gate that never
|
|
167
|
+
// runs. The executor installs the applier; without one, a predicate
|
|
168
|
+
// gate is refused rather than skipped.
|
|
169
|
+
const value = this.predicateGate === undefined ? started
|
|
170
|
+
: this.scope.applyPredicateGate === undefined
|
|
171
|
+
? (() => { throw new AuthoredFlowExecutionError('unsupported_gate', 'this runtime cannot apply predicate gates'); })()
|
|
172
|
+
: await this.scope.applyPredicateGate(this as unknown as { id: string; predicateGate: unknown }, started);
|
|
148
173
|
this.state = 'fulfilled';
|
|
149
174
|
this.resolve(value);
|
|
150
175
|
} catch (error) {
|
|
@@ -161,30 +161,44 @@ export async function verifyAuthoredNodeResult(
|
|
|
161
161
|
result: AuthoredFlowExecutionResult, metadata: AuthoredRootMetadata,
|
|
162
162
|
rootRunId: string, socketPath: string,
|
|
163
163
|
): Promise<void> {
|
|
164
|
-
const invalid = (): never => { throw new Error(
|
|
164
|
+
const invalid = (why = ''): never => { throw new Error(`authored runtime result has no matching durable completion${process.env['FLOWS_VERIFIER_DEBUG'] && why ? ` (${why})` : ''}`); };
|
|
165
165
|
if (result.rootRunId !== rootRunId || result.name !== metadata.flowName
|
|
166
166
|
|| !isLoweredCompletion(result.completionReason)
|
|
167
|
-
|| !Array.isArray(result.journalSteps) || result.journalSteps.length === 0) invalid();
|
|
167
|
+
|| !Array.isArray(result.journalSteps) || result.journalSteps.length === 0) invalid('frame');
|
|
168
168
|
const terminal = result.journalSteps.at(-1)!;
|
|
169
|
-
if (!terminal || !/^complete-[1-9][0-9]*$/.test(terminal.id)) invalid();
|
|
169
|
+
if (!terminal || !/^complete-[1-9][0-9]*$/.test(terminal.id)) invalid('terminal');
|
|
170
170
|
const count = Number(terminal.id.slice('complete-'.length));
|
|
171
|
-
|
|
171
|
+
// A predicate gate is journaled as `<step>.gate`: a child run subordinate
|
|
172
|
+
// to the authored step it judges, in the same `<step>.gate` shape a named
|
|
173
|
+
// gate lowers to inside its step's own spec. Neither consumes an ordinal —
|
|
174
|
+
// `complete-N` counts the operations the author wrote (SURFACE.md §6) —
|
|
175
|
+
// so gates are set aside from the count and the contiguity check, and
|
|
176
|
+
// verified separately: every gate must name a claimed parent, and is then
|
|
177
|
+
// held to the same durable-completion evidence as any other child run.
|
|
178
|
+
const isGate = (id: string): boolean => /\.gate$/.test(id);
|
|
179
|
+
const authored = result.journalSteps.filter(step => typeof step?.id === 'string' && !isGate(step.id));
|
|
180
|
+
const gates = result.journalSteps.filter(step => typeof step?.id === 'string' && isGate(step.id));
|
|
181
|
+
if (!Number.isSafeInteger(count) || authored.length !== count || isGate(terminal.id)) invalid(`count ${authored.length}!=${count}`);
|
|
172
182
|
const ordinal = (id: string): number => Number(/-([1-9][0-9]*)$/.exec(id)?.[1]);
|
|
173
183
|
// Parallel awaits may finish in either order; validate a copy in declaration order.
|
|
174
|
-
const ordered = [...
|
|
184
|
+
const ordered = [...authored].sort((a,b)=>ordinal(a.id)-ordinal(b.id));
|
|
185
|
+
const authoredIds = new Set(ordered.map(step => step.id));
|
|
186
|
+
for (const gate of gates) {
|
|
187
|
+
if (!authoredIds.has(gate.id.slice(0, -'.gate'.length))) invalid(`orphan ${gate.id}`);
|
|
188
|
+
}
|
|
175
189
|
const runs = new Set<string>();
|
|
176
190
|
const journal = new JournalClient(socketPath);
|
|
177
191
|
await journal.connect();
|
|
178
192
|
try {
|
|
179
193
|
await journal.hello('flows-authored-result-verifier');
|
|
180
|
-
for (const [index, claimed] of ordered.entries()) {
|
|
194
|
+
for (const [index, claimed] of [...ordered, ...gates].entries()) {
|
|
181
195
|
if (!claimed || typeof claimed.id !== 'string' || typeof claimed.runId !== 'string'
|
|
182
|
-
|| ordinal(claimed.id) !== index+1 || claimed.completionReason !== 'success'
|
|
183
|
-
|| runs.has(claimed.runId)) invalid();
|
|
196
|
+
|| (index < ordered.length && ordinal(claimed.id) !== index+1) || claimed.completionReason !== 'success'
|
|
197
|
+
|| runs.has(claimed.runId)) invalid(`claim ${claimed?.id}`);
|
|
184
198
|
runs.add(claimed.runId);
|
|
185
199
|
const state = await journal.runGet(claimed.runId);
|
|
186
200
|
if (state.run_id !== claimed.runId || state.status !== 'completed'
|
|
187
|
-
|| state.steps[claimed.id]?.state !== 'done') invalid();
|
|
201
|
+
|| state.steps[claimed.id]?.state !== 'done') invalid(`state ${claimed.id} ${state.status} ${state.steps[claimed.id]?.state}`);
|
|
188
202
|
const entries: Array<{
|
|
189
203
|
seq: number; entry_type: string; step_id?: string; payload?: {
|
|
190
204
|
completionReason?: string; spec?: { name?: string; steps?: Array<{id?:string;type?:string;command?:string}> };
|
|
@@ -196,7 +210,7 @@ export async function verifyAuthoredNodeResult(
|
|
|
196
210
|
if (page.length === 0) break;
|
|
197
211
|
for (const raw of page) {
|
|
198
212
|
const entry = raw as typeof entries[number];
|
|
199
|
-
if (!Number.isSafeInteger(entry?.seq) || entry.seq < fromSeq) invalid();
|
|
213
|
+
if (!Number.isSafeInteger(entry?.seq) || entry.seq < fromSeq) invalid('seq');
|
|
200
214
|
fromSeq = entry.seq + 1;
|
|
201
215
|
// Keep only completion evidence; streaming logs can span many pages.
|
|
202
216
|
if (['run.spawned', 'step.completed', 'run.completed'].includes(entry.entry_type)) entries.push(entry);
|
|
@@ -204,12 +218,20 @@ export async function verifyAuthoredNodeResult(
|
|
|
204
218
|
}
|
|
205
219
|
const spec = entries.find(entry => entry.entry_type === 'run.spawned')?.payload?.spec;
|
|
206
220
|
const step = spec?.steps?.[0];
|
|
221
|
+
// A named gate lowers INTO the step's own spec as a second, dependent
|
|
222
|
+
// `<id>.gate` step (named-gate-lowering.ts); that is the only other
|
|
223
|
+
// step a child spec may carry, and it must have completed too.
|
|
224
|
+
const lowered = spec?.steps ?? [];
|
|
225
|
+
const specShape = lowered.length === 1 || (lowered.length === 2 && lowered[1]?.id === `${claimed.id}.gate`);
|
|
207
226
|
const completed = entries.filter(entry => entry.entry_type === 'step.completed' && entry.step_id === claimed.id);
|
|
227
|
+
const gateCompleted = lowered.length === 2
|
|
228
|
+
? entries.filter(entry => entry.entry_type === 'step.completed' && entry.step_id === `${claimed.id}.gate`) : [];
|
|
208
229
|
const terminalFacts = entries.filter(entry => entry.entry_type === 'run.completed');
|
|
209
|
-
if (spec?.name !== `${metadata.flowName}/${claimed.id}` ||
|
|
230
|
+
if (spec?.name !== `${metadata.flowName}/${claimed.id}` || !specShape
|
|
210
231
|
|| step?.id !== claimed.id || completed.length !== 1
|
|
211
232
|
|| completed[0]?.payload?.completionReason !== 'success'
|
|
212
|
-
||
|
|
233
|
+
|| (lowered.length === 2 && (gateCompleted.length !== 1 || gateCompleted[0]?.payload?.completionReason !== 'success'))
|
|
234
|
+
|| terminalFacts.length !== 1 || terminalFacts[0]?.payload?.completionReason !== 'success') invalid(`evidence ${claimed.id} spec=${spec?.name} step=${step?.id} completed=${completed.length}`);
|
|
213
235
|
if (claimed === terminal) {
|
|
214
236
|
// The claimed verdict must match the marker the journal actually
|
|
215
237
|
// recorded, so an IPC frame cannot claim `success` over a run whose
|
|
@@ -219,7 +241,7 @@ export async function verifyAuthoredNodeResult(
|
|
|
219
241
|
// the runtime validation of untrusted IPC, and it is why nothing has
|
|
220
242
|
// to be re-asserted here just to satisfy the type.
|
|
221
243
|
if (step?.type !== 'deterministic'
|
|
222
|
-
|| step.command !== completionMarker(result.completionReason)) invalid();
|
|
244
|
+
|| step.command !== completionMarker(result.completionReason)) invalid('marker');
|
|
223
245
|
}
|
|
224
246
|
}
|
|
225
247
|
} finally { journal.close(); }
|
|
@@ -7,12 +7,11 @@ import { classifyOutcome, type RunLifecycleOptions } from './cli/run.js';
|
|
|
7
7
|
import type { PreflightDiagnostic } from './preflight.js';
|
|
8
8
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
9
9
|
import type { JournalClient } from './journal-client.js';
|
|
10
|
-
import { SPEC_SCHEMA_VERSION, type FlowSpec, type StepSpec } from './spec.js';
|
|
10
|
+
import { SPEC_SCHEMA_VERSION, type FlowSpec, type PermissionsSpec, type StepSpec } from './spec.js';
|
|
11
11
|
import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput } from './authored-step-output.js';
|
|
12
12
|
import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
13
13
|
import { snapshotJsonValue } from './json-value.js';
|
|
14
14
|
import { authoredChildAdmissionKey } from './authored-admission.js';
|
|
15
|
-
import { diffWorkspaceFiles, snapshotWorkspaceFiles } from './agent-artifacts.js';
|
|
16
15
|
|
|
17
16
|
const WORKSPACE_PERMISSION_ANNOTATION = /:\s*(readonly|readwrite)\s*$/i;
|
|
18
17
|
|
|
@@ -97,13 +96,10 @@ export function authoredWorkerRunner(
|
|
|
97
96
|
if (options.workspace !== undefined && WORKSPACE_PERMISSION_ANNOTATION.test(options.workspace)) {
|
|
98
97
|
throw new AuthoredFlowExecutionError(
|
|
99
98
|
'unsupported_workspace_permission',
|
|
100
|
-
`flow "${definition.name}" step "${id}": workspace "${options.workspace}"
|
|
101
|
-
+
|
|
102
|
-
+
|
|
103
|
-
+ '
|
|
104
|
-
+ 'Silently accepting and ignoring it would let a flow believe a restriction is in effect '
|
|
105
|
-
+ "when it is not. Declare a bare surface name (no trailing \": readonly\"/\": readwrite\") "
|
|
106
|
-
+ 'if you do not need enforcement, or use the declarative spec\'s `permissions` field, which is real.',
|
|
99
|
+
`flow "${definition.name}" step "${id}": workspace "${options.workspace}": `
|
|
100
|
+
+ "Workspace permission suffixes are unsupported. Use a bare workspace name and f.agent's "
|
|
101
|
+
+ "permissions option, for example permissions: { fileGlobs: ['src/**'], accessPreset: 'readonly' }. "
|
|
102
|
+
+ 'This declaration is validated and recorded with the step spec; it is not currently enforced (gate 8 / #442).',
|
|
107
103
|
);
|
|
108
104
|
}
|
|
109
105
|
if (options.cli !== undefined && typeof options.cli !== 'string') {
|
|
@@ -130,22 +126,12 @@ export function authoredWorkerRunner(
|
|
|
130
126
|
`f.agent options.transport must be 'direct' or 'relay' (got ${JSON.stringify(options.transport)}).`,
|
|
131
127
|
);
|
|
132
128
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// where the CLI actually wrote — a workspace-scoped step never reaches
|
|
137
|
-
// here with a local agent attached (refused above). `transport: 'relay'`
|
|
138
|
-
// dispatches to agent-relay, which executes on a remote host even
|
|
139
|
-
// though a local agent stream is still attached, so it gets no local
|
|
140
|
-
// snapshot either. Any other worker attachment may execute on a
|
|
141
|
-
// different host entirely; snapshotting this process's filesystem for
|
|
142
|
-
// that case would be a guess, not a fact, so `artifacts` stays `[]`
|
|
143
|
-
// there, exactly as before this fix.
|
|
144
|
-
const artifactRoot = localAgentStream === undefined || options.transport === 'relay'
|
|
145
|
-
? undefined : options.cwd ?? process.cwd();
|
|
146
|
-
const before = artifactRoot === undefined ? undefined : await snapshotWorkspaceFiles(artifactRoot);
|
|
129
|
+
const permissions = options.permissions;
|
|
130
|
+
const permissionsSnapshot = permissions === undefined ? undefined
|
|
131
|
+
: snapshotJsonValue(permissions, 'f.agent options.permissions') as unknown as PermissionsSpec;
|
|
147
132
|
const output = await run({
|
|
148
133
|
id, type: 'agent', instruction: options.task,
|
|
134
|
+
...(permissionsSnapshot === undefined ? {} : { permissions: permissionsSnapshot }),
|
|
149
135
|
...(localAgentStream === undefined ? {} : { surfaces: { streams: [{ stream: localAgentStream }] } }),
|
|
150
136
|
...(options.workspace === undefined ? {} : { surfaces: { workspace: [{ surface: options.workspace }] } }),
|
|
151
137
|
...(options.cli === undefined ? {} : { cli: options.cli }),
|
|
@@ -158,9 +144,12 @@ export function authoredWorkerRunner(
|
|
|
158
144
|
throw new AuthoredFlowExecutionError('journal_protocol_violation', `step "${id}" produced a non-object output`);
|
|
159
145
|
}
|
|
160
146
|
const stdout = 'stdout_tail' in output ? output.stdout_tail : undefined;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
147
|
+
// The worker that ran the CLI measured the artifacts and journaled them
|
|
148
|
+
// in the step's output; read that fact back rather than re-scanning a
|
|
149
|
+
// directory this process may not even share with the agent.
|
|
150
|
+
const journaled = 'artifacts' in output ? output.artifacts : undefined;
|
|
151
|
+
const artifacts = Array.isArray(journaled) && journaled.every(entry => typeof entry === 'string')
|
|
152
|
+
? [...journaled] : [];
|
|
164
153
|
return { summary: typeof stdout === 'string' ? stdout : JSON.stringify(output), artifacts };
|
|
165
154
|
},
|
|
166
155
|
async llm(id: string, prompt: string, options?: LlmOptions, verification?: NamedGate): Promise<unknown> {
|
|
@@ -2,6 +2,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
2
2
|
import { loadAuthoredFlow, type LoadedAuthoredFlow } from '../authored-flow-loader.js';
|
|
3
3
|
import { preflightWebhookTriggers } from '../preflight.js';
|
|
4
4
|
import { preflightProviderTriggers } from '../provider-trigger-contract.js';
|
|
5
|
+
import { scheduleLowering } from '../schedule-trigger.js';
|
|
5
6
|
import { checkSlackHelpers } from '../slack-preflight.js';
|
|
6
7
|
import { inputFailureReport, readProjectConfig, type CheckReport } from './check.js';
|
|
7
8
|
|
|
@@ -31,12 +32,20 @@ export async function checkAuthoredTriggers(path: string): Promise<{
|
|
|
31
32
|
const diagnostics = [
|
|
32
33
|
...triggerDiagnostics, ...providerDiagnostics, ...helperReport.diagnostics,
|
|
33
34
|
];
|
|
35
|
+
// A schedule is inspectable data: print what it lowers to, and say plainly
|
|
36
|
+
// when the local runner cannot drive it. Neither is a refusal — Cloud can.
|
|
37
|
+
const schedules = triggers.flatMap((trigger, handler) => {
|
|
38
|
+
if (trigger.kind !== 'schedule') return [];
|
|
39
|
+
const lowering = scheduleLowering(definition.name, trigger);
|
|
40
|
+
return [{ handler, ...lowering }];
|
|
41
|
+
});
|
|
34
42
|
return {
|
|
35
43
|
loaded,
|
|
36
44
|
report: {
|
|
37
45
|
// Severity, not emptiness: a warning must never refuse a flow.
|
|
38
46
|
ok: !diagnostics.some(diagnostic => diagnostic.severity === 'refusal'),
|
|
39
47
|
path, gates: [], resolutions: [], diagnostics,
|
|
48
|
+
...(schedules.length === 0 ? {} : { schedules }),
|
|
40
49
|
...(config.path === undefined ? {} : { projectConfigPath: config.path }),
|
|
41
50
|
},
|
|
42
51
|
};
|
package/src/cli/check.ts
CHANGED
|
@@ -45,9 +45,24 @@ export interface CheckReport {
|
|
|
45
45
|
projectConfigPath?: string;
|
|
46
46
|
gates: StepGateInspection[];
|
|
47
47
|
resolutions: CliResolution[];
|
|
48
|
+
/** Authored `schedule.*` handlers and the `flows.tick` subscription each lowers to. */
|
|
49
|
+
schedules?: ScheduleInspection[];
|
|
48
50
|
diagnostics: Array<PreflightDiagnostic | CheckInputDiagnostic | CheckWarningDiagnostic>;
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
export interface ScheduleInspection {
|
|
54
|
+
/** Position among the flow's handlers, so two identical declarations stay distinct. */
|
|
55
|
+
handler: number;
|
|
56
|
+
cron?: string;
|
|
57
|
+
tz?: string;
|
|
58
|
+
intervalMs?: number;
|
|
59
|
+
epochMs?: number;
|
|
60
|
+
staleAfterMs?: number;
|
|
61
|
+
scheduleId: string;
|
|
62
|
+
/** Present when the local tick runner cannot drive it (only a cron-aware runner can). */
|
|
63
|
+
localUnsupported?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
export interface CheckWarningDiagnostic {
|
|
52
67
|
severity: 'warning';
|
|
53
68
|
kind: CheckWarningKind;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
+
import { listCloudSchedules, scheduleInCloud, unscheduleInCloud, type CloudSchedule } from '../cloud-schedule.js';
|
|
3
|
+
import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
|
|
4
|
+
import { snapshotJsonValue, type JsonValue } from '../json-value.js';
|
|
5
|
+
import type { CliIo } from '../cli.js';
|
|
6
|
+
|
|
7
|
+
export interface CloudScheduleArgs {
|
|
8
|
+
command: 'schedule';
|
|
9
|
+
value: string;
|
|
10
|
+
cron: string | undefined;
|
|
11
|
+
every: string | undefined;
|
|
12
|
+
tz: string | undefined;
|
|
13
|
+
input: string | undefined;
|
|
14
|
+
name: string | undefined;
|
|
15
|
+
json: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>]
|
|
20
|
+
* [--tz <IANA>] [--input <json|file>] [--name <n>] [--json]`
|
|
21
|
+
*
|
|
22
|
+
* With neither `--cron` nor `--every`, the flow's own `schedule.*` handler
|
|
23
|
+
* supplies the cron, so a declared schedule and its hosted registration
|
|
24
|
+
* cannot drift.
|
|
25
|
+
*/
|
|
26
|
+
export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleArgs | undefined {
|
|
27
|
+
let value: string | undefined;
|
|
28
|
+
const flags: Record<'cron' | 'every' | 'tz' | 'input' | 'name', string | undefined> = {
|
|
29
|
+
cron: undefined, every: undefined, tz: undefined, input: undefined, name: undefined,
|
|
30
|
+
};
|
|
31
|
+
let json = false;
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
const arg = args[i]!;
|
|
34
|
+
if (arg === '--json') {
|
|
35
|
+
if (json) return undefined;
|
|
36
|
+
json = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (arg === '--cron' || arg === '--every' || arg === '--tz' || arg === '--input' || arg === '--name') {
|
|
40
|
+
const key = arg.slice(2) as keyof typeof flags;
|
|
41
|
+
const next = args[i + 1];
|
|
42
|
+
// A cron expression legitimately starts with `*`; only a flag-shaped value is refused.
|
|
43
|
+
if (flags[key] !== undefined || next === undefined || next.startsWith('--')) return undefined;
|
|
44
|
+
flags[key] = next;
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg.startsWith('-') || value !== undefined) return undefined;
|
|
49
|
+
value = arg;
|
|
50
|
+
}
|
|
51
|
+
if (value === undefined) return undefined;
|
|
52
|
+
if (flags.cron !== undefined && flags.every !== undefined) return undefined;
|
|
53
|
+
if (flags.input !== undefined && !isAuthoredFlowPath(value)) return undefined;
|
|
54
|
+
return { command: 'schedule', value, ...flags, json };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function describe(schedule: CloudSchedule): string {
|
|
58
|
+
const last = schedule.lastTriggeredAt === undefined ? ''
|
|
59
|
+
: ` last ${schedule.lastTriggeredAt}${schedule.lastTriggerStatus === undefined ? '' : ` (${schedule.lastTriggerStatus})`}`;
|
|
60
|
+
return `${schedule.id} ${schedule.status} ${JSON.stringify(schedule.name)} cron "${schedule.cronExpression}" tz ${schedule.timezone}${last}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
64
|
+
try {
|
|
65
|
+
let input: JsonValue | undefined;
|
|
66
|
+
let inputPresent = false;
|
|
67
|
+
if (isAuthoredFlowPath(args.value)) {
|
|
68
|
+
try {
|
|
69
|
+
input = snapshotJsonValue(parseDirectInput(args.input), 'Cloud authored input');
|
|
70
|
+
inputPresent = true;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof DirectInputError) throw new CloudFlowError('invalid_input', error.message);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const schedule = await scheduleInCloud({
|
|
77
|
+
flow: { path: args.value },
|
|
78
|
+
...(args.cron === undefined ? {} : { cron: args.cron }),
|
|
79
|
+
...(args.every === undefined ? {} : { every: args.every }),
|
|
80
|
+
...(args.tz === undefined ? {} : { tz: args.tz }),
|
|
81
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
82
|
+
...(inputPresent ? { input } : {}),
|
|
83
|
+
});
|
|
84
|
+
if (args.json) {
|
|
85
|
+
io.stdout(JSON.stringify({ ok: true, ...schedule }));
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
io.stdout(`SCHEDULED ${describe(schedule)}`);
|
|
89
|
+
io.stdout('Each fire submits this exact source through the same admission as flows run --cloud; list with: flows schedules');
|
|
90
|
+
return 0;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return reportFailure(error, args.json, io);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runCloudSchedulesCli({ json }: { json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
|
|
97
|
+
try {
|
|
98
|
+
const schedules = await listCloudSchedules();
|
|
99
|
+
if (json) {
|
|
100
|
+
io.stdout(JSON.stringify({ ok: true, schedules }));
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
if (schedules.length === 0) io.stdout('No workflow schedules in this workspace.');
|
|
104
|
+
for (const schedule of schedules) io.stdout(describe(schedule));
|
|
105
|
+
return 0;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return reportFailure(error, json, io);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runCloudUnscheduleCli(
|
|
112
|
+
{ scheduleId, json }: { scheduleId: string; json: boolean }, io: CliIo,
|
|
113
|
+
): Promise<0 | 1 | 2> {
|
|
114
|
+
try {
|
|
115
|
+
await unscheduleInCloud(scheduleId);
|
|
116
|
+
io.stdout(json ? JSON.stringify({ ok: true, scheduleId, deleted: true }) : `UNSCHEDULED ${scheduleId}`);
|
|
117
|
+
return 0;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return reportFailure(error, json, io);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function reportFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
124
|
+
const code = error instanceof CloudFlowError ? error.code : 'cloud_schedule_failed';
|
|
125
|
+
let message = error instanceof Error ? error.message : 'Cloud schedule failed.';
|
|
126
|
+
// The schedule routes take a browser session or a `cli:auth` token.
|
|
127
|
+
if (error instanceof CloudFlowError && error.status === 403) {
|
|
128
|
+
message += ' Scheduling needs an interactive `cli:auth` credential: run `agent-relay cloud login`.';
|
|
129
|
+
}
|
|
130
|
+
if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
|
|
131
|
+
else io.stderr(`${code}: ${message}`);
|
|
132
|
+
return error instanceof CloudFlowError
|
|
133
|
+
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code) || error.status === 403 || error.status === 401)
|
|
134
|
+
? 2 : 1;
|
|
135
|
+
}
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -172,7 +172,7 @@ export async function runDirectFlow(
|
|
|
172
172
|
// diagnostic carried up from `classifyOutcome` already names the step, its
|
|
173
173
|
// exit code and its output tail; this branch is what lets it reach the
|
|
174
174
|
// terminal. `resumeFlow` takes the same branch, through the same helper.
|
|
175
|
-
if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
|
|
175
|
+
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
176
176
|
return authoredStepFailure('run', base, socketPath, error);
|
|
177
177
|
}
|
|
178
178
|
const runId = error instanceof AuthoredFlowExecutionError ? error.runId : undefined;
|
package/src/cli/run.ts
CHANGED
|
@@ -223,7 +223,7 @@ export async function resumeFlow(
|
|
|
223
223
|
// leaving it on `protocolFailure` meant `flows run` printed the evidence
|
|
224
224
|
// while `flows resume` still printed `protocol_error` and
|
|
225
225
|
// `RUN <id> unknown` for the identical failure.
|
|
226
|
-
if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
|
|
226
|
+
if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
|
|
227
227
|
return authoredStepFailure('resume', base, socketPath, error, runId);
|
|
228
228
|
}
|
|
229
229
|
if (!(error instanceof JournalProtocolError) || error.code !== 'run_not_found') {
|
|
@@ -287,10 +287,12 @@ export function authoredStepFailure(
|
|
|
287
287
|
completionReason: 'step_failed',
|
|
288
288
|
diagnostics: [...base.diagnostics, {
|
|
289
289
|
severity: 'failure',
|
|
290
|
-
|
|
290
|
+
// A predicate gate that judged false is a run failure with its own
|
|
291
|
+
// name, so the report says which kind of check the body did not pass.
|
|
292
|
+
kind: error.code === 'gate_failed' ? 'gate_failed' : 'step_failed',
|
|
291
293
|
// The `step_failed: ` prefix `AuthoredFlowExecutionError` adds is
|
|
292
294
|
// redundant once the diagnostic is labelled `[step_failed]`.
|
|
293
|
-
message: error.message.replace(/^step_failed: /, ''),
|
|
295
|
+
message: error.message.replace(/^(?:step_failed|gate_failed): /, ''),
|
|
294
296
|
}],
|
|
295
297
|
},
|
|
296
298
|
};
|
package/src/cli.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { checkTypeScriptFlow } from './cli/check-typescript.js';
|
|
|
26
26
|
import { runCloudCli } from './cli/cloud-run.js';
|
|
27
27
|
import { runCloudSyncCli } from './cli/cloud-sync.js';
|
|
28
28
|
import { parseCloudDeployArgs, runCloudDeployCli, runCloudDeploymentsCli, runCloudUndeployCli, type CloudDeployArgs } from './cli/cloud-deploy.js';
|
|
29
|
+
import { parseCloudScheduleArgs, runCloudScheduleCli, runCloudSchedulesCli, runCloudUnscheduleCli, type CloudScheduleArgs } from './cli/cloud-schedule.js';
|
|
29
30
|
import { isAuthoredFlowPath } from './direct-input.js';
|
|
30
31
|
import { parseDeployArgs, runDeploy, type DeployArgs } from './cli/deploy.js';
|
|
31
32
|
import { parseDigestReference } from './bundle-transport.js';
|
|
@@ -58,6 +59,9 @@ type ParsedArgs =
|
|
|
58
59
|
| CloudDeployArgs
|
|
59
60
|
| { command: 'deployments'; json: boolean }
|
|
60
61
|
| { command: 'undeploy'; agentId: string; json: boolean }
|
|
62
|
+
| CloudScheduleArgs
|
|
63
|
+
| { command: 'schedules'; json: boolean }
|
|
64
|
+
| { command: 'unschedule'; scheduleId: string; json: boolean }
|
|
61
65
|
| { command: 'check'; json: boolean; watch: boolean; value: string }
|
|
62
66
|
| { command: 'run'; bucket: string | undefined; reuseFromRunId: string | undefined; localAgent: boolean; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
63
67
|
| { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
@@ -75,6 +79,9 @@ const USAGE = [
|
|
|
75
79
|
'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--json]',
|
|
76
80
|
'flows deployments [--json]',
|
|
77
81
|
'flows undeploy [--json] <deployment-id>',
|
|
82
|
+
'flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>] [--tz <IANA>] [--input <inline-json-or-file>] [--name <name>] [--json]',
|
|
83
|
+
'flows schedules [--json]',
|
|
84
|
+
'flows unschedule [--json] <schedule-id>',
|
|
78
85
|
'flows deploy <flow>@sha256:<digest> --to <file-bucket-uri>',
|
|
79
86
|
'flows run <flow>@sha256:<digest> [--bucket <file-bucket-uri>] [--data-dir <dir>] [--json]',
|
|
80
87
|
'flows check [--watch] [--json] <flow.ts|flow.yaml|spec.json>',
|
|
@@ -131,6 +138,9 @@ export async function runCli(
|
|
|
131
138
|
if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
|
|
132
139
|
if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
|
|
133
140
|
if (parsed.command === 'undeploy') return runCloudUndeployCli(parsed, io);
|
|
141
|
+
if (parsed.command === 'schedule') return runCloudScheduleCli(parsed, io);
|
|
142
|
+
if (parsed.command === 'schedules') return runCloudSchedulesCli(parsed, io);
|
|
143
|
+
if (parsed.command === 'unschedule') return runCloudUnscheduleCli(parsed, io);
|
|
134
144
|
if (parsed.command === 'replay') return replayJournal(parsed, io);
|
|
135
145
|
if (parsed.command === 'build') return runBuild(parsed, io);
|
|
136
146
|
if (parsed.command === 'deploy') return runDeploy(parsed, io);
|
|
@@ -266,6 +276,7 @@ async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckR
|
|
|
266
276
|
return {
|
|
267
277
|
report: {
|
|
268
278
|
...mcp.report,
|
|
279
|
+
...(triggers?.report.schedules === undefined ? {} : { schedules: triggers.report.schedules }),
|
|
269
280
|
diagnostics: [...helper.report.diagnostics, ...mcp.report.diagnostics, ...triggerDiagnostics],
|
|
270
281
|
ok: helper.report.ok && mcp.report.ok && triggerOk,
|
|
271
282
|
},
|
|
@@ -440,6 +451,18 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
440
451
|
const source = args.slice(1).find(a => !a.startsWith('-') && isAuthoredFlowPath(a));
|
|
441
452
|
return source !== undefined ? parseCloudDeployArgs(args.slice(1)) : parseDeployArgs(args.slice(1));
|
|
442
453
|
}
|
|
454
|
+
if (command === 'schedule') return parseCloudScheduleArgs(args.slice(1));
|
|
455
|
+
if (command === 'schedules') {
|
|
456
|
+
const rest = args.slice(1);
|
|
457
|
+
if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--json')) return undefined;
|
|
458
|
+
return { command: 'schedules', json: rest.length === 1 };
|
|
459
|
+
}
|
|
460
|
+
if (command === 'unschedule') {
|
|
461
|
+
const rest = args.slice(1).filter(a => a !== '--json');
|
|
462
|
+
const json = args.length - 1 - rest.length;
|
|
463
|
+
if (json > 1 || rest.length !== 1 || rest[0]!.startsWith('-')) return undefined;
|
|
464
|
+
return { command: 'unschedule', scheduleId: rest[0]!, json: json === 1 };
|
|
465
|
+
}
|
|
443
466
|
if (command === 'undeploy') {
|
|
444
467
|
const rest = args.slice(1).filter(a => a !== '--json');
|
|
445
468
|
const json = args.length - 1 - rest.length;
|
|
@@ -751,6 +774,15 @@ function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
|
|
|
751
774
|
const vacuous = gate.acceptsAnyOutput === true ? ' [json_schema accepts any output]' : '';
|
|
752
775
|
io.stdout(`GATE step "${gate.stepId}" ${gate.checks.join('+')} from data (kernel, journal-replayable)${vacuous}`);
|
|
753
776
|
}
|
|
777
|
+
for (const schedule of report.schedules ?? []) {
|
|
778
|
+
const declared = schedule.cron !== undefined
|
|
779
|
+
? `cron "${schedule.cron}"${schedule.tz === undefined ? '' : ` tz ${schedule.tz}`}`
|
|
780
|
+
: `every ${schedule.intervalMs}ms`;
|
|
781
|
+
const local = schedule.localUnsupported !== undefined
|
|
782
|
+
? `Cloud only: ${schedule.localUnsupported}`
|
|
783
|
+
: `local: flows tick start --schedule-id ${schedule.scheduleId} --interval-ms ${schedule.intervalMs} --epoch-ms ${schedule.epochMs}`;
|
|
784
|
+
io.stdout(`SCHEDULE handler ${schedule.handler} ${declared} -> flows.tick schedule_id ${schedule.scheduleId} [${local}]`);
|
|
785
|
+
}
|
|
754
786
|
for (const resolution of report.resolutions) {
|
|
755
787
|
const config = resolution.source === 'project' && report.projectConfigPath !== undefined
|
|
756
788
|
? ` (${report.projectConfigPath})`
|
package/src/cloud-deploy.ts
CHANGED
|
@@ -21,7 +21,9 @@ export type FlowTriggerProvider = (typeof FLOW_TRIGGER_PROVIDERS)[number];
|
|
|
21
21
|
|
|
22
22
|
/** Settings Cloud's launcher prefilter reads per provider (`flow-trigger-sources.ts`). */
|
|
23
23
|
const PROVIDER_SETTINGS: Record<FlowTriggerProvider, readonly string[]> = {
|
|
24
|
-
|
|
24
|
+
// `events`: `issues` (default) or `pull_request` — which GitHub records
|
|
25
|
+
// wake the listener (AgentWorkforce/cloud#3772).
|
|
26
|
+
github: ['repository', 'labels', 'contains', 'events'],
|
|
25
27
|
slack: ['channel', 'contains'],
|
|
26
28
|
linear: ['team', 'contains'],
|
|
27
29
|
jira: ['project', 'contains'],
|
|
@@ -102,6 +104,15 @@ export function parseTriggerSource(value: string): FlowTriggerSource {
|
|
|
102
104
|
if (!setting || setting.length > MAX_SETTING_LENGTH || key in settings) {
|
|
103
105
|
throw new CloudFlowError('invalid_input', `Trigger setting "${key}" must be given once with a non-empty value.`);
|
|
104
106
|
}
|
|
107
|
+
if (key === 'events') {
|
|
108
|
+
// Cloud's enum is lowercase; send it that way whatever the shell typed.
|
|
109
|
+
const events = setting.toLowerCase();
|
|
110
|
+
if (!['issues', 'pull_request'].includes(events)) {
|
|
111
|
+
throw new CloudFlowError('invalid_input', `github events must be "issues" or "pull_request", got "${setting}".`);
|
|
112
|
+
}
|
|
113
|
+
settings[key] = events;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
105
116
|
settings[key] = setting;
|
|
106
117
|
}
|
|
107
118
|
}
|