@relayflows/sdk 2.0.16 → 2.0.18

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 (184) hide show
  1. package/dist/authored-flow-error.d.ts +21 -1
  2. package/dist/authored-flow-error.d.ts.map +1 -1
  3. package/dist/authored-flow-error.js +14 -0
  4. package/dist/authored-flow-error.js.map +1 -1
  5. package/dist/authored-flow-executor.d.ts.map +1 -1
  6. package/dist/authored-flow-executor.js +133 -3
  7. package/dist/authored-flow-executor.js.map +1 -1
  8. package/dist/authored-flow-lifecycle.d.ts +10 -0
  9. package/dist/authored-flow-lifecycle.d.ts.map +1 -1
  10. package/dist/authored-flow-lifecycle.js +7 -0
  11. package/dist/authored-flow-lifecycle.js.map +1 -1
  12. package/dist/authored-flow-operation.d.ts +11 -0
  13. package/dist/authored-flow-operation.d.ts.map +1 -1
  14. package/dist/authored-flow-operation.js +32 -7
  15. package/dist/authored-flow-operation.js.map +1 -1
  16. package/dist/authored-human.d.ts +49 -0
  17. package/dist/authored-human.d.ts.map +1 -0
  18. package/dist/authored-human.js +97 -0
  19. package/dist/authored-human.js.map +1 -0
  20. package/dist/authored-node-entry.js +3 -2
  21. package/dist/authored-node-entry.js.map +1 -1
  22. package/dist/authored-node-runner.d.ts.map +1 -1
  23. package/dist/authored-node-runner.js +52 -18
  24. package/dist/authored-node-runner.js.map +1 -1
  25. package/dist/authored-root.d.ts.map +1 -1
  26. package/dist/authored-root.js +29 -1
  27. package/dist/authored-root.js.map +1 -1
  28. package/dist/authored-worker-step.d.ts.map +1 -1
  29. package/dist/authored-worker-step.js +14 -25
  30. package/dist/authored-worker-step.js.map +1 -1
  31. package/dist/cli/answer.d.ts +17 -0
  32. package/dist/cli/answer.d.ts.map +1 -0
  33. package/dist/cli/answer.js +89 -0
  34. package/dist/cli/answer.js.map +1 -0
  35. package/dist/cli/build.js +1 -1
  36. package/dist/cli/build.js.map +1 -1
  37. package/dist/cli/check-triggers.d.ts.map +1 -1
  38. package/dist/cli/check-triggers.js +12 -0
  39. package/dist/cli/check-triggers.js.map +1 -1
  40. package/dist/cli/check.d.ts +25 -0
  41. package/dist/cli/check.d.ts.map +1 -1
  42. package/dist/cli/check.js +14 -2
  43. package/dist/cli/check.js.map +1 -1
  44. package/dist/cli/cloud-connect-cli.d.ts +54 -0
  45. package/dist/cli/cloud-connect-cli.d.ts.map +1 -0
  46. package/dist/cli/cloud-connect-cli.js +109 -0
  47. package/dist/cli/cloud-connect-cli.js.map +1 -0
  48. package/dist/cli/cloud-deploy.d.ts +3 -1
  49. package/dist/cli/cloud-deploy.d.ts.map +1 -1
  50. package/dist/cli/cloud-deploy.js +44 -6
  51. package/dist/cli/cloud-deploy.js.map +1 -1
  52. package/dist/cli/cloud-run.d.ts +2 -1
  53. package/dist/cli/cloud-run.d.ts.map +1 -1
  54. package/dist/cli/cloud-run.js +17 -3
  55. package/dist/cli/cloud-run.js.map +1 -1
  56. package/dist/cli/cloud-schedule.d.ts +31 -0
  57. package/dist/cli/cloud-schedule.d.ts.map +1 -0
  58. package/dist/cli/cloud-schedule.js +156 -0
  59. package/dist/cli/cloud-schedule.js.map +1 -0
  60. package/dist/cli/direct-run.d.ts.map +1 -1
  61. package/dist/cli/direct-run.js +6 -2
  62. package/dist/cli/direct-run.js.map +1 -1
  63. package/dist/cli/run.d.ts +22 -2
  64. package/dist/cli/run.d.ts.map +1 -1
  65. package/dist/cli/run.js +39 -4
  66. package/dist/cli/run.js.map +1 -1
  67. package/dist/cli.d.ts.map +1 -1
  68. package/dist/cli.js +148 -8
  69. package/dist/cli.js.map +1 -1
  70. package/dist/cloud-connect.d.ts +52 -0
  71. package/dist/cloud-connect.d.ts.map +1 -0
  72. package/dist/cloud-connect.js +138 -0
  73. package/dist/cloud-connect.js.map +1 -0
  74. package/dist/cloud-deploy.d.ts +19 -1
  75. package/dist/cloud-deploy.d.ts.map +1 -1
  76. package/dist/cloud-deploy.js +52 -2
  77. package/dist/cloud-deploy.js.map +1 -1
  78. package/dist/cloud-http.d.ts +17 -2
  79. package/dist/cloud-http.d.ts.map +1 -1
  80. package/dist/cloud-http.js +44 -8
  81. package/dist/cloud-http.js.map +1 -1
  82. package/dist/cloud-run.d.ts +30 -0
  83. package/dist/cloud-run.d.ts.map +1 -1
  84. package/dist/cloud-run.js +70 -27
  85. package/dist/cloud-run.js.map +1 -1
  86. package/dist/cloud-schedule.d.ts +55 -0
  87. package/dist/cloud-schedule.d.ts.map +1 -0
  88. package/dist/cloud-schedule.js +146 -0
  89. package/dist/cloud-schedule.js.map +1 -0
  90. package/dist/failure-kinds.d.ts +3 -3
  91. package/dist/failure-kinds.d.ts.map +1 -1
  92. package/dist/failure-kinds.js +4 -0
  93. package/dist/failure-kinds.js.map +1 -1
  94. package/dist/flow-requirements.d.ts +68 -0
  95. package/dist/flow-requirements.d.ts.map +1 -0
  96. package/dist/flow-requirements.js +152 -0
  97. package/dist/flow-requirements.js.map +1 -0
  98. package/dist/index.d.ts +5 -0
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +5 -0
  101. package/dist/index.js.map +1 -1
  102. package/dist/journal-client.d.ts +11 -0
  103. package/dist/journal-client.d.ts.map +1 -1
  104. package/dist/journal-client.js +13 -0
  105. package/dist/journal-client.js.map +1 -1
  106. package/dist/named-gate-lowering.js +9 -0
  107. package/dist/named-gate-lowering.js.map +1 -1
  108. package/dist/named-gates.d.ts +1 -1
  109. package/dist/named-gates.d.ts.map +1 -1
  110. package/dist/named-gates.js +11 -1
  111. package/dist/named-gates.js.map +1 -1
  112. package/dist/preflight.d.ts.map +1 -1
  113. package/dist/preflight.js +155 -9
  114. package/dist/preflight.js.map +1 -1
  115. package/dist/progress.d.ts +2 -1
  116. package/dist/progress.d.ts.map +1 -1
  117. package/dist/progress.js +9 -3
  118. package/dist/progress.js.map +1 -1
  119. package/dist/protocol.d.ts +25 -1
  120. package/dist/protocol.d.ts.map +1 -1
  121. package/dist/schedule-trigger.d.ts +35 -0
  122. package/dist/schedule-trigger.d.ts.map +1 -0
  123. package/dist/schedule-trigger.js +53 -0
  124. package/dist/schedule-trigger.js.map +1 -0
  125. package/dist/spec.d.ts +11 -1
  126. package/dist/spec.d.ts.map +1 -1
  127. package/dist/spec.js.map +1 -1
  128. package/dist/trigger-executor.js +1 -1
  129. package/dist/trigger-executor.js.map +1 -1
  130. package/dist/validate.js +1 -1
  131. package/dist/validate.js.map +1 -1
  132. package/dist/worker-cli.d.ts +10 -0
  133. package/dist/worker-cli.d.ts.map +1 -1
  134. package/dist/worker-cli.js +51 -22
  135. package/dist/worker-cli.js.map +1 -1
  136. package/dist/worker.d.ts.map +1 -1
  137. package/dist/worker.js +9 -0
  138. package/dist/worker.js.map +1 -1
  139. package/dist/wrapper-session.d.ts +3 -1
  140. package/dist/wrapper-session.d.ts.map +1 -1
  141. package/dist/wrapper-session.js +6 -3
  142. package/dist/wrapper-session.js.map +1 -1
  143. package/package.json +3 -3
  144. package/src/authored-flow-error.ts +30 -0
  145. package/src/authored-flow-executor.ts +141 -2
  146. package/src/authored-flow-lifecycle.ts +7 -0
  147. package/src/authored-flow-operation.ts +35 -10
  148. package/src/authored-human.ts +150 -0
  149. package/src/authored-node-entry.ts +3 -2
  150. package/src/authored-node-runner.ts +55 -18
  151. package/src/authored-root.ts +28 -1
  152. package/src/authored-worker-step.ts +15 -26
  153. package/src/cli/answer.ts +98 -0
  154. package/src/cli/build.ts +1 -1
  155. package/src/cli/check-triggers.ts +11 -0
  156. package/src/cli/check.ts +39 -2
  157. package/src/cli/cloud-connect-cli.ts +130 -0
  158. package/src/cli/cloud-deploy.ts +40 -6
  159. package/src/cli/cloud-run.ts +16 -4
  160. package/src/cli/cloud-schedule.ts +160 -0
  161. package/src/cli/direct-run.ts +6 -1
  162. package/src/cli/run.ts +53 -5
  163. package/src/cli.ts +134 -9
  164. package/src/cloud-connect.ts +187 -0
  165. package/src/cloud-deploy.ts +69 -3
  166. package/src/cloud-http.ts +52 -8
  167. package/src/cloud-run.ts +99 -26
  168. package/src/cloud-schedule.ts +175 -0
  169. package/src/failure-kinds.ts +4 -0
  170. package/src/flow-requirements.ts +201 -0
  171. package/src/index.ts +14 -0
  172. package/src/journal-client.ts +20 -0
  173. package/src/named-gate-lowering.ts +9 -0
  174. package/src/named-gates.ts +11 -1
  175. package/src/preflight.ts +132 -9
  176. package/src/progress.ts +11 -4
  177. package/src/protocol.ts +23 -0
  178. package/src/schedule-trigger.ts +73 -0
  179. package/src/spec.ts +12 -1
  180. package/src/trigger-executor.ts +1 -1
  181. package/src/validate.ts +1 -1
  182. package/src/worker-cli.ts +41 -0
  183. package/src/worker.ts +9 -0
  184. package/src/wrapper-session.ts +5 -1
@@ -0,0 +1,160 @@
1
+ import { CloudFlowError } from '../cloud-http.js';
2
+ import { listCloudSchedules, scheduleInCloud, unscheduleInCloud, validateScheduleArgs, 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 { cliConnectPrompt, ensureFlowConnections, harnessRemedy } from './cloud-connect-cli.js';
6
+ import type { CliIo } from '../cli.js';
7
+
8
+ export interface CloudScheduleArgs {
9
+ command: 'schedule';
10
+ value: string;
11
+ cron: string | undefined;
12
+ every: string | undefined;
13
+ tz: string | undefined;
14
+ input: string | undefined;
15
+ name: string | undefined;
16
+ /** Refuse a missing integration instead of offering to connect it. */
17
+ noConnect: boolean;
18
+ json: boolean;
19
+ }
20
+
21
+ /**
22
+ * `flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>]
23
+ * [--tz <IANA>] [--input <json|file>] [--name <n>] [--no-connect] [--json]`
24
+ *
25
+ * With neither `--cron` nor `--every`, the flow's own `schedule.*` handler
26
+ * supplies the cron, so a declared schedule and its hosted registration
27
+ * cannot drift.
28
+ */
29
+ export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleArgs | undefined {
30
+ let value: string | undefined;
31
+ const flags: Record<'cron' | 'every' | 'tz' | 'input' | 'name', string | undefined> = {
32
+ cron: undefined, every: undefined, tz: undefined, input: undefined, name: undefined,
33
+ };
34
+ let json = false;
35
+ let noConnect = false;
36
+ for (let i = 0; i < args.length; i++) {
37
+ const arg = args[i]!;
38
+ if (arg === '--json') {
39
+ if (json) return undefined;
40
+ json = true;
41
+ continue;
42
+ }
43
+ if (arg === '--no-connect') {
44
+ if (noConnect) return undefined;
45
+ noConnect = true;
46
+ continue;
47
+ }
48
+ if (arg === '--cron' || arg === '--every' || arg === '--tz' || arg === '--input' || arg === '--name') {
49
+ const key = arg.slice(2) as keyof typeof flags;
50
+ const next = args[i + 1];
51
+ // A cron expression legitimately starts with `*`; only a flag-shaped value is refused.
52
+ if (flags[key] !== undefined || next === undefined || next.startsWith('--')) return undefined;
53
+ flags[key] = next;
54
+ i += 1;
55
+ continue;
56
+ }
57
+ if (arg.startsWith('-') || value !== undefined) return undefined;
58
+ value = arg;
59
+ }
60
+ if (value === undefined) return undefined;
61
+ if (flags.cron !== undefined && flags.every !== undefined) return undefined;
62
+ if (flags.input !== undefined && !isAuthoredFlowPath(value)) return undefined;
63
+ return { command: 'schedule', value, ...flags, noConnect, json };
64
+ }
65
+
66
+ function describe(schedule: CloudSchedule): string {
67
+ const last = schedule.lastTriggeredAt === undefined ? ''
68
+ : ` last ${schedule.lastTriggeredAt}${schedule.lastTriggerStatus === undefined ? '' : ` (${schedule.lastTriggerStatus})`}`;
69
+ return `${schedule.id} ${schedule.status} ${JSON.stringify(schedule.name)} cron "${schedule.cronExpression}" tz ${schedule.timezone}${last}`;
70
+ }
71
+
72
+ export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): Promise<0 | 1 | 2> {
73
+ let harnesses: readonly string[] = [];
74
+ try {
75
+ let input: JsonValue | undefined;
76
+ let inputPresent = false;
77
+ if (isAuthoredFlowPath(args.value)) {
78
+ try {
79
+ input = snapshotJsonValue(parseDirectInput(args.input), 'Cloud authored input');
80
+ inputPresent = true;
81
+ } catch (error) {
82
+ if (error instanceof DirectInputError) throw new CloudFlowError('invalid_input', error.message);
83
+ throw error;
84
+ }
85
+ }
86
+ validateScheduleArgs({
87
+ ...(args.cron === undefined ? {} : { cron: args.cron }),
88
+ ...(args.every === undefined ? {} : { every: args.every }),
89
+ ...(args.tz === undefined ? {} : { tz: args.tz }),
90
+ });
91
+ // Each fire runs the flow as this workspace, so what it needs connected
92
+ // is checked once here — after the arguments are known to be good, so a
93
+ // refused command never opens a browser first — and before the schedule exists.
94
+ const connections = await ensureFlowConnections({
95
+ path: args.value, prompt: cliConnectPrompt(io, { noConnect: args.noConnect, json: args.json }),
96
+ });
97
+ harnesses = connections?.requirements.harnesses ?? [];
98
+ const schedule = await scheduleInCloud({
99
+ flow: { path: args.value },
100
+ ...(args.cron === undefined ? {} : { cron: args.cron }),
101
+ ...(args.every === undefined ? {} : { every: args.every }),
102
+ ...(args.tz === undefined ? {} : { tz: args.tz }),
103
+ ...(args.name === undefined ? {} : { name: args.name }),
104
+ ...(inputPresent ? { input } : {}),
105
+ });
106
+ if (args.json) {
107
+ io.stdout(JSON.stringify({ ok: true, ...schedule }));
108
+ return 0;
109
+ }
110
+ io.stdout(`SCHEDULED ${describe(schedule)}`);
111
+ for (const provider of connections?.outcome.connected ?? []) io.stdout(` connected: ${provider}`);
112
+ io.stdout('Each fire submits this exact source through the same admission as flows run --cloud; list with: flows schedules');
113
+ return 0;
114
+ } catch (error) {
115
+ return reportFailure(error, args.json, io, harnesses);
116
+ }
117
+ }
118
+
119
+ export async function runCloudSchedulesCli({ json }: { json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
120
+ try {
121
+ const schedules = await listCloudSchedules();
122
+ if (json) {
123
+ io.stdout(JSON.stringify({ ok: true, schedules }));
124
+ return 0;
125
+ }
126
+ if (schedules.length === 0) io.stdout('No workflow schedules in this workspace.');
127
+ for (const schedule of schedules) io.stdout(describe(schedule));
128
+ return 0;
129
+ } catch (error) {
130
+ return reportFailure(error, json, io);
131
+ }
132
+ }
133
+
134
+ export async function runCloudUnscheduleCli(
135
+ { scheduleId, json }: { scheduleId: string; json: boolean }, io: CliIo,
136
+ ): Promise<0 | 1 | 2> {
137
+ try {
138
+ await unscheduleInCloud(scheduleId);
139
+ io.stdout(json ? JSON.stringify({ ok: true, scheduleId, deleted: true }) : `UNSCHEDULED ${scheduleId}`);
140
+ return 0;
141
+ } catch (error) {
142
+ return reportFailure(error, json, io);
143
+ }
144
+ }
145
+
146
+ function reportFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
147
+ const code = error instanceof CloudFlowError ? error.code : 'cloud_schedule_failed';
148
+ let message = error instanceof Error ? error.message : 'Cloud schedule failed.';
149
+ message += harnessRemedy(error, harnesses);
150
+ // The schedule routes take a browser session or a `cli:auth` token.
151
+ if (error instanceof CloudFlowError && error.status === 403) {
152
+ message += ' Scheduling needs an interactive `cli:auth` credential: run `agent-relay cloud login`.';
153
+ }
154
+ if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
155
+ else io.stderr(`${code}: ${message}`);
156
+ return error instanceof CloudFlowError
157
+ && (['configuration', 'unsupported_source', 'invalid_input', 'integration_not_connected'].includes(error.code)
158
+ || error.status === 403 || error.status === 401)
159
+ ? 2 : 1;
160
+ }
@@ -8,6 +8,7 @@ import {
8
8
  AuthoredFlowExecutionError,
9
9
  } from '../authored-flow-executor.js';
10
10
  import { executeDurableAuthoredFlow } from '../authored-root.js';
11
+ import { AuthoredHumanParked } from '../authored-flow-error.js';
11
12
  import { AuthoredFlowLoadError } from '../authored-flow-loader.js';
12
13
  import { DirectInputError, parseDirectInput } from '../direct-input.js';
13
14
  import { JournalClient } from '../journal-client.js';
@@ -15,6 +16,7 @@ import { inputFailureReport } from './check.js';
15
16
  import { checkAuthoredTriggers } from './check-triggers.js';
16
17
  import {
17
18
  authoredCompletion,
19
+ authoredHumanParked,
18
20
  authoredStepFailure,
19
21
  connect,
20
22
  emptyReport,
@@ -172,9 +174,12 @@ export async function runDirectFlow(
172
174
  // diagnostic carried up from `classifyOutcome` already names the step, its
173
175
  // exit code and its output tail; this branch is what lets it reach the
174
176
  // terminal. `resumeFlow` takes the same branch, through the same helper.
175
- if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
177
+ if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
176
178
  return authoredStepFailure('run', base, socketPath, error);
177
179
  }
180
+ if (error instanceof AuthoredHumanParked) {
181
+ return authoredHumanParked('run', base, socketPath, error, { dataDir, localAgent: options.localAgent === true });
182
+ }
178
183
  const runId = error instanceof AuthoredFlowExecutionError ? error.runId : undefined;
179
184
  return protocolFailure('run', base, socketPath, error, runId);
180
185
  } finally {
package/src/cli/run.ts CHANGED
@@ -2,7 +2,8 @@ import { parseDigestReference } from '../bundle-transport.js';
2
2
  import { prepareDigestRun } from './run-digest.js';
3
3
  import { reuseSummary } from './reuse.js';
4
4
  import { resumeHelperEffect } from '../authored-helper-effect.js';
5
- import { AuthoredFlowExecutionError } from '../authored-flow-error.js';
5
+ import { AuthoredFlowExecutionError, AuthoredHumanParked, type AuthoredHumanWait } from '../authored-flow-error.js';
6
+ import { answerCommand, resumeCommand } from '../authored-human.js';
6
7
  import { join, resolve } from 'node:path';
7
8
  import type { ProgressEvent } from '../progress.js';
8
9
  import { toKernelSpec } from '../compile.js';
@@ -29,7 +30,7 @@ import {
29
30
  } from './check.js';
30
31
 
31
32
  export type RunExitCode = 0 | 1 | 2 | 3;
32
- export type RunCommand = 'run' | 'resume';
33
+ export type RunCommand = 'run' | 'resume' | 'answer';
33
34
 
34
35
  export interface ParkedStep {
35
36
  id: string;
@@ -53,6 +54,12 @@ export interface RunReport {
53
54
  completedSteps?: number;
54
55
  reuse?: { fromRunId: string; reusedSteps: number; executedSteps: number };
55
56
  parkedStep?: ParkedStep;
57
+ /** The open `f.human` question a parked authored run is waiting on. */
58
+ humanWait?: AuthoredHumanWait;
59
+ /** `flows answer` only: the answer it recorded. */
60
+ answer?: { waitId: string; answer: boolean; note?: string };
61
+ /** The invocation that continues this run, when one is known. */
62
+ next?: string;
56
63
  projectConfigPath?: string;
57
64
  resolutions: CheckReport['resolutions'];
58
65
  diagnostics: Array<CheckReport['diagnostics'][number] | RunDiagnostic>;
@@ -223,9 +230,12 @@ export async function resumeFlow(
223
230
  // leaving it on `protocolFailure` meant `flows run` printed the evidence
224
231
  // while `flows resume` still printed `protocol_error` and
225
232
  // `RUN <id> unknown` for the identical failure.
226
- if (error instanceof AuthoredFlowExecutionError && error.code === 'step_failed') {
233
+ if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
227
234
  return authoredStepFailure('resume', base, socketPath, error, runId);
228
235
  }
236
+ if (error instanceof AuthoredHumanParked) {
237
+ return authoredHumanParked('resume', base, socketPath, error, { dataDir, localAgent: options.localAgent === true });
238
+ }
229
239
  if (!(error instanceof JournalProtocolError) || error.code !== 'run_not_found') {
230
240
  return protocolFailure('resume', base, socketPath, error, runId);
231
241
  }
@@ -287,10 +297,48 @@ export function authoredStepFailure(
287
297
  completionReason: 'step_failed',
288
298
  diagnostics: [...base.diagnostics, {
289
299
  severity: 'failure',
290
- kind: 'step_failed',
300
+ // A predicate gate that judged false is a run failure with its own
301
+ // name, so the report says which kind of check the body did not pass.
302
+ kind: error.code === 'gate_failed' ? 'gate_failed' : 'step_failed',
291
303
  // The `step_failed: ` prefix `AuthoredFlowExecutionError` adds is
292
304
  // redundant once the diagnostic is labelled `[step_failed]`.
293
- message: error.message.replace(/^step_failed: /, ''),
305
+ message: error.message.replace(/^(?:step_failed|gate_failed): /, ''),
306
+ }],
307
+ },
308
+ };
309
+ }
310
+
311
+ /**
312
+ * An authored body parked on an unanswered `f.human`: exit 3, like every
313
+ * other park, but the diagnostic names the question, who it is for, and the
314
+ * exact `flows answer` invocation — the run is waiting on a person, not on a
315
+ * worker. Shared by `run` and `resume` so the two never drift.
316
+ */
317
+ export function authoredHumanParked(
318
+ command: RunCommand,
319
+ base: CheckReport | RunReport,
320
+ socketPath: string,
321
+ error: AuthoredHumanParked,
322
+ where: { dataDir?: string; localAgent: boolean },
323
+ ): RunExecution {
324
+ const runId = error.runId!;
325
+ return {
326
+ exitCode: 3,
327
+ report: {
328
+ ...fromBase(command, base),
329
+ ok: false,
330
+ runId,
331
+ socketPath,
332
+ status: 'parked',
333
+ parkedStep: { id: 'authored-root', type: 'agent' },
334
+ humanWait: error.wait,
335
+ diagnostics: [...base.diagnostics, {
336
+ severity: 'parked',
337
+ kind: 'run_parked',
338
+ message: `Run "${runId}" is waiting for ${error.wait.to} to answer ${error.wait.waitId}: `
339
+ + `${JSON.stringify(error.wait.question)}\n`
340
+ + `Answer with: ${answerCommand(runId, error.wait.waitId, where.dataDir)}\n`
341
+ + `Then continue with: ${resumeCommand(runId, where.dataDir, where.localAgent)}`,
294
342
  }],
295
343
  },
296
344
  };
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { addPlugin } from './cli/add.js';
3
3
  import { watchCheck } from './cli-watch.js';
4
4
  import { checkHelperBody } from './cli/check-helper-body.js';
5
+ import { describeFlowRequirements } from './flow-requirements.js';
5
6
 
6
7
  import { renderProgress, type ProgressEvent } from './progress.js';
7
8
  import { realpathSync } from 'node:fs';
@@ -18,6 +19,7 @@ import {
18
19
  type RunProgress,
19
20
  type RunReport,
20
21
  } from './cli/run.js';
22
+ import { answerFlow } from './cli/answer.js';
21
23
  import { checkAuthoredTriggers } from './cli/check-triggers.js';
22
24
  import { parseWebhookArgs, runServeWebhook } from './cli/serve-webhook.js';
23
25
  import { runDirectFlow } from './cli/direct-run.js';
@@ -26,6 +28,7 @@ import { checkTypeScriptFlow } from './cli/check-typescript.js';
26
28
  import { runCloudCli } from './cli/cloud-run.js';
27
29
  import { runCloudSyncCli } from './cli/cloud-sync.js';
28
30
  import { parseCloudDeployArgs, runCloudDeployCli, runCloudDeploymentsCli, runCloudUndeployCli, type CloudDeployArgs } from './cli/cloud-deploy.js';
31
+ import { parseCloudScheduleArgs, runCloudScheduleCli, runCloudSchedulesCli, runCloudUnscheduleCli, type CloudScheduleArgs } from './cli/cloud-schedule.js';
29
32
  import { isAuthoredFlowPath } from './direct-input.js';
30
33
  import { parseDeployArgs, runDeploy, type DeployArgs } from './cli/deploy.js';
31
34
  import { parseDigestReference } from './bundle-transport.js';
@@ -53,14 +56,18 @@ type ParsedArgs =
53
56
  | BuildArgs
54
57
  | DeployArgs
55
58
  | { command: 'serve-webhook'; dataDir: string; port: number; admitted?: readonly string[] }
56
- | { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean }
59
+ | { command: 'cloud-run'; value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean; noConnect: boolean }
57
60
  | { command: 'sync'; runId: string; json: boolean; root: string }
58
61
  | CloudDeployArgs
59
62
  | { command: 'deployments'; json: boolean }
60
63
  | { command: 'undeploy'; agentId: string; json: boolean }
64
+ | CloudScheduleArgs
65
+ | { command: 'schedules'; json: boolean }
66
+ | { command: 'unschedule'; scheduleId: string; json: boolean }
61
67
  | { command: 'check'; json: boolean; watch: boolean; value: string }
62
68
  | { 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
69
  | { command: 'resume'; localAgent: boolean; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
70
+ | { command: 'answer'; dataDir: string; json: boolean; spawn: boolean; note: string | undefined; by: string | undefined; runId: string; waitId: string; answer: boolean }
64
71
  | { command: 'observer'; dataDir: string }
65
72
  | { command: 'hn-monitor'; sub: 'start'; dataDir: string; specPath: string; pollIntervalMs: number | undefined }
66
73
  | { command: 'tick'; sub: 'start'; dataDir: string; specPath: string; scheduleId: string;
@@ -72,20 +79,24 @@ const USAGE = [
72
79
  'flows add <helper-name|@flows/helper-name>',
73
80
  'flows build [--out <dir>] <flow.yaml|flow.ts>',
74
81
  'flows build --verify <bundle-dir>',
75
- 'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--json]',
82
+ 'flows deploy <flow.ts> --repo <owner/name> --on <provider>[:key=value,...] [--on ...] --approver <handle> [--agents claude[,codex]] [--name <name>] [--draft] [--no-connect] [--json]',
76
83
  'flows deployments [--json]',
77
84
  'flows undeploy [--json] <deployment-id>',
85
+ 'flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>] [--tz <IANA>] [--input <inline-json-or-file>] [--name <name>] [--no-connect] [--json]',
86
+ 'flows schedules [--json]',
87
+ 'flows unschedule [--json] <schedule-id>',
78
88
  'flows deploy <flow>@sha256:<digest> --to <file-bucket-uri>',
79
89
  'flows run <flow>@sha256:<digest> [--bucket <file-bucket-uri>] [--data-dir <dir>] [--json]',
80
90
  'flows check [--watch] [--json] <flow.ts|flow.yaml|spec.json>',
81
91
  'flows serve-webhook --data-dir <dir> --port <p> [--allow <name>[,<name>]]',
82
92
  'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] [--reuse-from <run-id>] <flow.yaml|spec.json>',
83
- 'flows run --cloud [--json] [--wait] [--sync-code] <flow.yaml|spec.json>',
84
- 'flows run --cloud [--json] [--wait] [--sync-code] <flow.ts> --input <inline-json-or-file>',
93
+ 'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.yaml|spec.json>',
94
+ 'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.ts> --input <inline-json-or-file>',
85
95
  'flows sync [--json] [--dir <path>] <run-id>',
86
96
  'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <flow.ts> --input <inline-json-or-file>',
87
97
  'flows tick start --schedule-id <id> --interval-ms <ms> [--epoch-ms <ms>] [--max-catch-up <n>] [--poll-interval-ms <ms>] [--data-dir <dir>] <spec.json>',
88
98
  'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent] <run-id>',
99
+ 'flows answer [--json] [--no-spawn] [--data-dir <dir>] [--note <text>] [--by <identity>] <run-id> <wait-id> <yes|no>',
89
100
  'flows replay [--allow-human-influenced] [--json] [--data-dir <dir>] <run-id> [--at <step-id>]',
90
101
  'flows observer [--data-dir <dir>]',
91
102
  'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
@@ -131,7 +142,19 @@ export async function runCli(
131
142
  if (parsed.command === 'cloud-deploy') return runCloudDeployCli(parsed, io);
132
143
  if (parsed.command === 'deployments') return runCloudDeploymentsCli(parsed, io);
133
144
  if (parsed.command === 'undeploy') return runCloudUndeployCli(parsed, io);
145
+ if (parsed.command === 'schedule') return runCloudScheduleCli(parsed, io);
146
+ if (parsed.command === 'schedules') return runCloudSchedulesCli(parsed, io);
147
+ if (parsed.command === 'unschedule') return runCloudUnscheduleCli(parsed, io);
134
148
  if (parsed.command === 'replay') return replayJournal(parsed, io);
149
+ if (parsed.command === 'answer') {
150
+ const execution = await answerFlow(parsed.runId, parsed.waitId, parsed.answer, parsed.dataDir, {
151
+ ...(parsed.note === undefined ? {} : { note: parsed.note }),
152
+ ...(parsed.by === undefined ? {} : { answeredBy: parsed.by }),
153
+ daemon: { spawn: parsed.spawn && spawnAllowedByEnv() },
154
+ });
155
+ emitRunReport(execution, parsed.json, io);
156
+ return execution.exitCode;
157
+ }
135
158
  if (parsed.command === 'build') return runBuild(parsed, io);
136
159
  if (parsed.command === 'deploy') return runDeploy(parsed, io);
137
160
 
@@ -266,6 +289,10 @@ async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckR
266
289
  return {
267
290
  report: {
268
291
  ...mcp.report,
292
+ ...(triggers?.report.schedules === undefined ? {} : { schedules: triggers.report.schedules }),
293
+ // The authored definition sees helper flags, body use and `cli:`
294
+ // declarations; the compiled view underneath knows only its steps.
295
+ ...(triggers?.report.requirements === undefined ? {} : { requirements: triggers.report.requirements }),
269
296
  diagnostics: [...helper.report.diagnostics, ...mcp.report.diagnostics, ...triggerDiagnostics],
270
297
  ok: helper.report.ok && mcp.report.ok && triggerOk,
271
298
  },
@@ -440,6 +467,18 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
440
467
  const source = args.slice(1).find(a => !a.startsWith('-') && isAuthoredFlowPath(a));
441
468
  return source !== undefined ? parseCloudDeployArgs(args.slice(1)) : parseDeployArgs(args.slice(1));
442
469
  }
470
+ if (command === 'schedule') return parseCloudScheduleArgs(args.slice(1));
471
+ if (command === 'schedules') {
472
+ const rest = args.slice(1);
473
+ if (rest.length > 1 || (rest.length === 1 && rest[0] !== '--json')) return undefined;
474
+ return { command: 'schedules', json: rest.length === 1 };
475
+ }
476
+ if (command === 'unschedule') {
477
+ const rest = args.slice(1).filter(a => a !== '--json');
478
+ const json = args.length - 1 - rest.length;
479
+ if (json > 1 || rest.length !== 1 || rest[0]!.startsWith('-')) return undefined;
480
+ return { command: 'unschedule', scheduleId: rest[0]!, json: json === 1 };
481
+ }
443
482
  if (command === 'undeploy') {
444
483
  const rest = args.slice(1).filter(a => a !== '--json');
445
484
  const json = args.length - 1 - rest.length;
@@ -456,6 +495,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
456
495
  if (command === 'tick') return parseTickArgs(args.slice(1));
457
496
  if (command === 'observer') return parseObserverArgs(args.slice(1));
458
497
  if (command === 'sync') return parseSyncArgs(args.slice(1));
498
+ if (command === 'answer') return parseAnswerArgs(args.slice(1));
459
499
  if (command !== 'check' && command !== 'run' && command !== 'resume') return undefined;
460
500
 
461
501
  let json = false;
@@ -463,6 +503,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
463
503
  let cloud = false;
464
504
  let wait = false;
465
505
  let syncCode = false;
506
+ let noConnect = false;
466
507
  let localAgent = false;
467
508
  let allowHumanInfluenced = false;
468
509
  let dataDir = DEFAULT_DATA_DIR;
@@ -476,11 +517,13 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
476
517
  const positionals: string[] = [];
477
518
  for (let index = 1; index < args.length; index += 1) {
478
519
  const argument = args[index]!;
479
- if (argument === '--cloud' || argument === '--wait' || argument === '--sync-code') {
480
- if (command !== 'run' || (argument === '--cloud' ? cloud : argument === '--wait' ? wait : syncCode)) return undefined;
520
+ if (argument === '--cloud' || argument === '--wait' || argument === '--sync-code' || argument === '--no-connect') {
521
+ if (command !== 'run') return undefined;
522
+ if (argument === '--cloud' ? cloud : argument === '--wait' ? wait : argument === '--sync-code' ? syncCode : noConnect) return undefined;
481
523
  if (argument === '--cloud') cloud = true;
482
524
  else if (argument === '--wait') wait = true;
483
- else syncCode = true;
525
+ else if (argument === '--sync-code') syncCode = true;
526
+ else noConnect = true;
484
527
  continue;
485
528
  }
486
529
  if (argument === '--allow-human-influenced') {
@@ -560,9 +603,9 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
560
603
  // the source, so it is accepted exactly where a local run accepts it.
561
604
  if (allowHumanInfluenced || sawDataDir || !spawn || localAgent || noObserverLink || reuseFromRunId !== undefined) return undefined;
562
605
  if (sawInput && !isAuthoredFlowPath(positionals[0]!)) return undefined;
563
- return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode };
606
+ return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode, noConnect };
564
607
  }
565
- if (wait || syncCode) return undefined;
608
+ if (wait || syncCode || noConnect) return undefined;
566
609
  if (reuseFromRunId !== undefined && isAuthoredFlowPath(positionals[0]!)) return undefined;
567
610
 
568
611
  if (command === 'run' && input !== undefined && !isAuthoredFlowPath(positionals[0]!)) return undefined;
@@ -573,6 +616,66 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
573
616
  : { command, localAgent, dataDir, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! };
574
617
  }
575
618
 
619
+ /**
620
+ * `flows answer [--json] [--no-spawn] [--data-dir <dir>] [--note <text>] [--by <identity>] <run-id> <wait-id> <yes|no>`.
621
+ * The answer is a literal `yes`/`no` (also `true`/`false`) so a shell cannot
622
+ * hand the kernel an ambiguous word as a decision. `--by` records who answered
623
+ * when the invoker is relaying a person's decision (Cloud's answer route runs
624
+ * this inside the resumed sandbox with the caller's identity); it defaults to
625
+ * the OS user.
626
+ */
627
+ function parseAnswerArgs(rest: readonly string[]): ParsedArgs | undefined {
628
+ let json = false;
629
+ let spawn = true;
630
+ let dataDir = DEFAULT_DATA_DIR;
631
+ let sawDataDir = false;
632
+ let note: string | undefined;
633
+ let by: string | undefined;
634
+ const positionals: string[] = [];
635
+ for (let index = 0; index < rest.length; index += 1) {
636
+ const argument = rest[index]!;
637
+ if (argument === '--json') {
638
+ if (json) return undefined;
639
+ json = true;
640
+ continue;
641
+ }
642
+ if (argument === '--no-spawn') {
643
+ if (!spawn) return undefined;
644
+ spawn = false;
645
+ continue;
646
+ }
647
+ if (argument === '--by') {
648
+ const value = rest[index + 1];
649
+ if (by !== undefined || value === undefined || value.startsWith('-') || value.trim() === '') return undefined;
650
+ by = value;
651
+ index += 1;
652
+ continue;
653
+ }
654
+ if (argument === '--data-dir') {
655
+ const value = rest[index + 1];
656
+ if (sawDataDir || value === undefined || value.startsWith('-')) return undefined;
657
+ dataDir = value;
658
+ sawDataDir = true;
659
+ index += 1;
660
+ continue;
661
+ }
662
+ if (argument === '--note') {
663
+ const value = rest[index + 1];
664
+ if (note !== undefined || value === undefined || value.startsWith('--')) return undefined;
665
+ note = value;
666
+ index += 1;
667
+ continue;
668
+ }
669
+ if (argument.startsWith('-')) return undefined;
670
+ positionals.push(argument);
671
+ }
672
+ if (positionals.length !== 3) return undefined;
673
+ const [runId, waitId, word] = positionals as [string, string, string];
674
+ const answer = word === 'yes' || word === 'true' ? true : word === 'no' || word === 'false' ? false : undefined;
675
+ if (answer === undefined) return undefined;
676
+ return { command: 'answer', dataDir, json, spawn, note, by, runId, waitId, answer };
677
+ }
678
+
576
679
  function parseHnMonitorArgs(rest: readonly string[]): ParsedArgs | undefined {
577
680
  const sub = rest[0];
578
681
  if (sub !== 'start') return undefined;
@@ -751,6 +854,15 @@ function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
751
854
  const vacuous = gate.acceptsAnyOutput === true ? ' [json_schema accepts any output]' : '';
752
855
  io.stdout(`GATE step "${gate.stepId}" ${gate.checks.join('+')} from data (kernel, journal-replayable)${vacuous}`);
753
856
  }
857
+ for (const schedule of report.schedules ?? []) {
858
+ const declared = schedule.cron !== undefined
859
+ ? `cron "${schedule.cron}"${schedule.tz === undefined ? '' : ` tz ${schedule.tz}`}`
860
+ : `every ${schedule.intervalMs}ms`;
861
+ const local = schedule.localUnsupported !== undefined
862
+ ? `Cloud only: ${schedule.localUnsupported}`
863
+ : `local: flows tick start --schedule-id ${schedule.scheduleId} --interval-ms ${schedule.intervalMs} --epoch-ms ${schedule.epochMs}`;
864
+ io.stdout(`SCHEDULE handler ${schedule.handler} ${declared} -> flows.tick schedule_id ${schedule.scheduleId} [${local}]`);
865
+ }
754
866
  for (const resolution of report.resolutions) {
755
867
  const config = resolution.source === 'project' && report.projectConfigPath !== undefined
756
868
  ? ` (${report.projectConfigPath})`
@@ -758,6 +870,10 @@ function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
758
870
  const model = resolution.model === undefined ? '' : ` model "${resolution.model}"`;
759
871
  io.stdout(`RESOLVED step "${resolution.stepId}" cli "${resolution.cli}"${model} from ${resolution.source}${config}`);
760
872
  }
873
+ // What the workspace must have connected before this flow can run there;
874
+ // the hosted verbs check the same list against Cloud before submitting.
875
+ const requires = report.requirements === undefined ? '' : describeFlowRequirements(report.requirements);
876
+ if (requires) io.stdout(`REQUIRES ${requires}`);
761
877
  if (report.ok) io.stdout(`CHECK PASSED ${report.path ?? ''}`.trimEnd());
762
878
  }
763
879
 
@@ -778,6 +894,15 @@ function emitRunReport(
778
894
  return;
779
895
  }
780
896
  if (report.runId === undefined) return;
897
+ if (report.answer !== undefined) {
898
+ io.stdout(`ANSWERED ${report.runId} ${report.answer.waitId} ${report.answer.answer ? 'yes' : 'no'}`
899
+ + (report.answer.note === undefined ? '' : ` (${report.answer.note})`));
900
+ if (report.next !== undefined) io.stdout(`Continue with: ${report.next}`);
901
+ return;
902
+ }
903
+ // A refused answer changed nothing about the run, so there is no run
904
+ // outcome to summarize; the refusal above is the whole report.
905
+ if (report.command === 'answer') return;
781
906
  const completed = report.completedSteps === undefined ? '' : ` (${report.completedSteps} ${report.completedSteps === 1 ? 'step' : 'steps'})`;
782
907
  const reason = report.completionReason === undefined
783
908
  ? ''