@relayflows/sdk 2.0.17 → 2.0.19
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 +21 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +14 -0
- 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 +47 -3
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-human.d.ts +49 -0
- package/dist/authored-human.d.ts.map +1 -0
- package/dist/authored-human.js +97 -0
- package/dist/authored-human.js.map +1 -0
- package/dist/authored-node-entry.js +3 -2
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +15 -4
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +29 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/cli/answer.d.ts +17 -0
- package/dist/cli/answer.d.ts.map +1 -0
- package/dist/cli/answer.js +89 -0
- package/dist/cli/answer.js.map +1 -0
- package/dist/cli/build.d.ts +9 -0
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +26 -7
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +2 -0
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check.d.ts +11 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +14 -2
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-connect-cli.d.ts +54 -0
- package/dist/cli/cloud-connect-cli.d.ts.map +1 -0
- package/dist/cli/cloud-connect-cli.js +109 -0
- package/dist/cli/cloud-connect-cli.js.map +1 -0
- package/dist/cli/cloud-deploy.d.ts +3 -1
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +44 -6
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-run.d.ts +9 -2
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +27 -15
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/cloud-schedule.d.ts +3 -1
- package/dist/cli/cloud-schedule.d.ts.map +1 -1
- package/dist/cli/cloud-schedule.js +31 -6
- package/dist/cli/cloud-schedule.js.map +1 -1
- package/dist/cli/cloud-sync.d.ts +8 -1
- package/dist/cli/cloud-sync.d.ts.map +1 -1
- package/dist/cli/cloud-sync.js +79 -8
- package/dist/cli/cloud-sync.js.map +1 -1
- package/dist/cli/deploy.d.ts +6 -0
- package/dist/cli/deploy.d.ts.map +1 -1
- package/dist/cli/deploy.js +29 -4
- package/dist/cli/deploy.js.map +1 -1
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +5 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +22 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +34 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/serve-webhook.d.ts +7 -1
- package/dist/cli/serve-webhook.d.ts.map +1 -1
- package/dist/cli/serve-webhook.js +19 -9
- package/dist/cli/serve-webhook.js.map +1 -1
- package/dist/cli-commands.d.ts +398 -0
- package/dist/cli-commands.d.ts.map +1 -0
- package/dist/cli-commands.js +254 -0
- package/dist/cli-commands.js.map +1 -0
- package/dist/cli-watch.d.ts +3 -1
- package/dist/cli-watch.d.ts.map +1 -1
- package/dist/cli-watch.js +4 -10
- package/dist/cli-watch.js.map +1 -1
- package/dist/cli.d.ts +127 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +195 -57
- package/dist/cli.js.map +1 -1
- package/dist/cloud-connect.d.ts +52 -0
- package/dist/cloud-connect.d.ts.map +1 -0
- package/dist/cloud-connect.js +138 -0
- package/dist/cloud-connect.js.map +1 -0
- package/dist/cloud-deploy.d.ts +19 -1
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +40 -1
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-http.d.ts +2 -2
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-schedule.d.ts +7 -0
- package/dist/cloud-schedule.d.ts.map +1 -1
- package/dist/cloud-schedule.js +22 -1
- package/dist/cloud-schedule.js.map +1 -1
- package/dist/cloud-sync.d.ts +85 -2
- package/dist/cloud-sync.d.ts.map +1 -1
- package/dist/cloud-sync.js +123 -10
- package/dist/cloud-sync.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- 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/flow-requirements.d.ts +68 -0
- package/dist/flow-requirements.d.ts.map +1 -0
- package/dist/flow-requirements.js +152 -0
- package/dist/flow-requirements.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +11 -0
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +13 -0
- package/dist/journal-client.js.map +1 -1
- package/dist/progress.d.ts +2 -1
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +9 -3
- package/dist/progress.js.map +1 -1
- package/dist/protocol.d.ts +25 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/relay-cli.d.ts +50 -0
- package/dist/relay-cli.d.ts.map +1 -0
- package/dist/relay-cli.js +64 -0
- package/dist/relay-cli.js.map +1 -0
- package/package.json +7 -2
- package/src/authored-flow-error.ts +29 -0
- package/src/authored-flow-executor.ts +54 -2
- package/src/authored-human.ts +150 -0
- package/src/authored-node-entry.ts +3 -2
- package/src/authored-node-runner.ts +20 -5
- package/src/authored-root.ts +28 -1
- package/src/cli/answer.ts +98 -0
- package/src/cli/build.ts +21 -7
- package/src/cli/check-triggers.ts +2 -0
- package/src/cli/check.ts +24 -2
- package/src/cli/cloud-connect-cli.ts +130 -0
- package/src/cli/cloud-deploy.ts +40 -6
- package/src/cli/cloud-run.ts +26 -15
- package/src/cli/cloud-schedule.ts +31 -6
- package/src/cli/cloud-sync.ts +85 -8
- package/src/cli/deploy.ts +27 -5
- package/src/cli/direct-run.ts +5 -0
- package/src/cli/run.ts +48 -2
- package/src/cli/serve-webhook.ts +15 -8
- package/src/cli-commands.ts +339 -0
- package/src/cli-watch.ts +8 -9
- package/src/cli.ts +206 -54
- package/src/cloud-connect.ts +187 -0
- package/src/cloud-deploy.ts +57 -2
- package/src/cloud-http.ts +1 -1
- package/src/cloud-schedule.ts +20 -3
- package/src/cloud-sync.ts +164 -11
- package/src/failure-kinds.ts +2 -0
- package/src/flow-requirements.ts +201 -0
- package/src/index.ts +12 -2
- package/src/journal-client.ts +20 -0
- package/src/progress.ts +11 -4
- package/src/protocol.ts +23 -0
- package/src/relay-cli.ts +117 -0
package/src/cli/cloud-deploy.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
deployToCloud, listCloudDeployments, parseAgentHarnesses, parseRepository, parseTriggerSource, undeployFromCloud,
|
|
4
4
|
type FlowTriggerSource,
|
|
5
5
|
} from '../cloud-deploy.js';
|
|
6
|
+
import { describeFlowRequirements } from '../flow-requirements.js';
|
|
7
|
+
import { cliConnectPrompt, flowRequirementsForPath, harnessRemedy } from './cloud-connect-cli.js';
|
|
6
8
|
import type { CliIo } from '../cli.js';
|
|
7
9
|
|
|
8
10
|
export interface CloudDeployArgs {
|
|
@@ -14,12 +16,14 @@ export interface CloudDeployArgs {
|
|
|
14
16
|
name: string | undefined;
|
|
15
17
|
agents: string | undefined;
|
|
16
18
|
draft: boolean;
|
|
19
|
+
/** Refuse a missing integration instead of offering to connect it. */
|
|
20
|
+
noConnect: boolean;
|
|
17
21
|
json: boolean;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* `flows deploy <flow.ts> --repo <owner/name> --on <provider>[:k=v,…] [--on …]
|
|
22
|
-
* --approver <handle> [--name <n>] [--json]`
|
|
26
|
+
* --approver <handle> [--name <n>] [--agents <list>] [--draft] [--no-connect] [--json]`
|
|
23
27
|
*
|
|
24
28
|
* Parsed here rather than in `parseDeployArgs` because the two `deploy` forms
|
|
25
29
|
* share nothing but the word: the digest form copies a sealed bundle into a
|
|
@@ -32,6 +36,7 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
32
36
|
let name: string | undefined;
|
|
33
37
|
let agents: string | undefined;
|
|
34
38
|
let draft = false;
|
|
39
|
+
let noConnect = false;
|
|
35
40
|
let json = false;
|
|
36
41
|
const on: string[] = [];
|
|
37
42
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -46,6 +51,11 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
46
51
|
draft = true;
|
|
47
52
|
continue;
|
|
48
53
|
}
|
|
54
|
+
if (arg === '--no-connect') {
|
|
55
|
+
if (noConnect) return undefined;
|
|
56
|
+
noConnect = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
49
59
|
if (arg === '--agents') {
|
|
50
60
|
const next = args[i + 1];
|
|
51
61
|
if (agents !== undefined || next === undefined || next.startsWith('-')) return undefined;
|
|
@@ -68,7 +78,7 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
|
|
|
68
78
|
value = arg;
|
|
69
79
|
}
|
|
70
80
|
if (value === undefined || repo === undefined || on.length === 0) return undefined;
|
|
71
|
-
return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, json };
|
|
81
|
+
return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, noConnect, json };
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
function describeSource(source: FlowTriggerSource): string {
|
|
@@ -77,11 +87,19 @@ function describeSource(source: FlowTriggerSource): string {
|
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
90
|
+
const agents = args.agents === undefined ? undefined : parseAgentHarnessesOr(args.agents);
|
|
91
|
+
// What the source declares, so a harness refusal names the right remedy
|
|
92
|
+
// even when `--agents` was not given; the loader's own failure is reported
|
|
93
|
+
// by deployToCloud, not here.
|
|
94
|
+
let harnesses: readonly string[] = agents instanceof CloudFlowError ? [] : agents ?? [];
|
|
80
95
|
try {
|
|
81
96
|
if (args.approver === undefined) {
|
|
82
97
|
throw new CloudFlowError('invalid_input',
|
|
83
98
|
'--approver <handle> is required: every launched run receives it as input.approver for f.human.');
|
|
84
99
|
}
|
|
100
|
+
if (agents instanceof CloudFlowError) throw agents;
|
|
101
|
+
const connect = cliConnectPrompt(io, { noConnect: args.noConnect, json: args.json });
|
|
102
|
+
if (agents === undefined) harnesses = (await flowRequirementsForPath(args.value))?.harnesses ?? [];
|
|
85
103
|
const deployment = await deployToCloud({
|
|
86
104
|
path: args.value,
|
|
87
105
|
repository: parseRepository(args.repo),
|
|
@@ -89,7 +107,8 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
89
107
|
approver: args.approver,
|
|
90
108
|
draft: args.draft,
|
|
91
109
|
...(args.name === undefined ? {} : { name: args.name }),
|
|
92
|
-
...(
|
|
110
|
+
...(agents === undefined ? {} : { agents }),
|
|
111
|
+
...(connect === undefined ? {} : { connect }),
|
|
93
112
|
});
|
|
94
113
|
if (args.json) {
|
|
95
114
|
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|
|
@@ -99,12 +118,24 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
|
|
|
99
118
|
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
100
119
|
io.stdout(` repository: ${deployment.repository.owner}/${deployment.repository.name}`);
|
|
101
120
|
for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
121
|
+
const requires = describeFlowRequirements(deployment.requirements);
|
|
122
|
+
if (requires) io.stdout(` requires: ${requires}`);
|
|
123
|
+
for (const provider of deployment.connected) io.stdout(` connected: ${provider}`);
|
|
102
124
|
io.stdout(deployment.status === 'draft'
|
|
103
125
|
? 'Saved without activating; activate it from the Cloud dashboard, or redeploy without --draft.'
|
|
104
126
|
: 'Each matching ticket launches a run of this source in a fresh branch; list with: flows deployments');
|
|
105
127
|
return 0;
|
|
106
128
|
} catch (error) {
|
|
107
|
-
return reportCloudFailure(error, args.json, io);
|
|
129
|
+
return reportCloudFailure(error, args.json, io, harnesses);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseAgentHarnessesOr(value: string): ReturnType<typeof parseAgentHarnesses> | CloudFlowError {
|
|
134
|
+
try {
|
|
135
|
+
return parseAgentHarnesses(value);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof CloudFlowError) return error;
|
|
138
|
+
throw error;
|
|
108
139
|
}
|
|
109
140
|
}
|
|
110
141
|
|
|
@@ -140,9 +171,11 @@ export async function runCloudUndeployCli({ agentId, json }: { agentId: string;
|
|
|
140
171
|
}
|
|
141
172
|
}
|
|
142
173
|
|
|
143
|
-
function reportCloudFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
174
|
+
function reportCloudFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
|
|
144
175
|
const code = error instanceof CloudFlowError ? error.code : 'cloud_deploy_failed';
|
|
145
176
|
let message = error instanceof Error ? error.message : 'Cloud deploy failed.';
|
|
177
|
+
// Cloud names the missing coding-agent credential on activation; say how it is connected.
|
|
178
|
+
message += harnessRemedy(error, harnesses);
|
|
146
179
|
// The deploy routes take a browser session or a `cli:auth` token. A
|
|
147
180
|
// deployment (CI) token gets 403 `session_required`; say what fixes it.
|
|
148
181
|
if (error instanceof CloudFlowError && error.status === 403) {
|
|
@@ -152,6 +185,7 @@ function reportCloudFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
|
152
185
|
if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
|
|
153
186
|
else io.stderr(`${code}: ${message}`);
|
|
154
187
|
return error instanceof CloudFlowError
|
|
155
|
-
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code)
|
|
188
|
+
&& (['configuration', 'unsupported_source', 'invalid_input', 'integration_not_connected'].includes(error.code)
|
|
189
|
+
|| error.status === 403 || error.status === 401)
|
|
156
190
|
? 2 : 1;
|
|
157
191
|
}
|
package/src/cli/cloud-run.ts
CHANGED
|
@@ -2,26 +2,30 @@ import { CloudFlowError } from '../cloud-http.js';
|
|
|
2
2
|
import { runInCloud, waitForCloudFlowRun, type RunInCloudOptions } from '../cloud-run.js';
|
|
3
3
|
import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
|
|
4
4
|
import { snapshotJsonValue } from '../json-value.js';
|
|
5
|
+
import { cliConnectPrompt, ensureFlowConnections, harnessRemedy } from './cloud-connect-cli.js';
|
|
5
6
|
import type { CliIo } from '../cli.js';
|
|
6
7
|
|
|
7
8
|
/** Presentation only: the central CLI parser owns argv; the SDK owns the lifecycle. */
|
|
8
9
|
export async function runCloudCli(
|
|
9
|
-
{ value: path, json, wait, input, syncCode }: {
|
|
10
|
-
value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean;
|
|
10
|
+
{ value: path, json, wait, input, syncCode, noConnect = false }: {
|
|
11
|
+
value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean; noConnect?: boolean;
|
|
11
12
|
},
|
|
12
13
|
io: CliIo,
|
|
14
|
+
/**
|
|
15
|
+
* Cancellation, owned by the caller. `runCli` supplies either the embedder's
|
|
16
|
+
* signal or one it drives from SIGINT/SIGTERM itself, so no process-wide
|
|
17
|
+
* handler is installed here.
|
|
18
|
+
*/
|
|
19
|
+
signal: AbortSignal,
|
|
13
20
|
): Promise<0 | 1 | 2> {
|
|
14
|
-
const controller = new AbortController();
|
|
15
|
-
const abort = (): void => controller.abort();
|
|
16
|
-
process.once('SIGINT', abort);
|
|
17
|
-
process.once('SIGTERM', abort);
|
|
18
21
|
let runId: string | undefined;
|
|
19
22
|
// Flips at the run submission. An interruption before it — during prepare,
|
|
20
23
|
// packing or upload — admitted nothing and is safe to retry; only an
|
|
21
24
|
// interrupted submission has unknown admission.
|
|
22
25
|
let submitting = false;
|
|
26
|
+
let harnesses: readonly string[] = [];
|
|
23
27
|
try {
|
|
24
|
-
const options: RunInCloudOptions = { signal
|
|
28
|
+
const options: RunInCloudOptions = { signal, onSubmit: () => { submitting = true; } };
|
|
25
29
|
if (isAuthoredFlowPath(path)) {
|
|
26
30
|
// Same parse as a local direct run, so a file-or-inline argument means
|
|
27
31
|
// the same thing on both sides of `--cloud`.
|
|
@@ -32,6 +36,15 @@ export async function runCloudCli(
|
|
|
32
36
|
throw error;
|
|
33
37
|
}
|
|
34
38
|
}
|
|
39
|
+
// After local validation, before anything is packed or uploaded: a helper
|
|
40
|
+
// the run would call against an unconnected integration is connected
|
|
41
|
+
// here, or refused here. A command that is going to be refused for its
|
|
42
|
+
// arguments must not first open a browser.
|
|
43
|
+
const connections = await ensureFlowConnections({
|
|
44
|
+
path, prompt: cliConnectPrompt(io, { noConnect, json }),
|
|
45
|
+
}, { signal });
|
|
46
|
+
harnesses = connections?.requirements.harnesses ?? [];
|
|
47
|
+
for (const provider of connections?.outcome.connected ?? []) if (!json) io.stdout(`CONNECTED ${provider}`);
|
|
35
48
|
// The tree is the invoking directory, as with v1: the flow path is where
|
|
36
49
|
// the body lives, not the boundary of what the run may read.
|
|
37
50
|
if (syncCode) options.syncCode = { root: process.cwd() };
|
|
@@ -51,28 +64,26 @@ export async function runCloudCli(
|
|
|
51
64
|
if (json) io.stdout(JSON.stringify({ ok: true, ...receipt }));
|
|
52
65
|
return 0;
|
|
53
66
|
}
|
|
54
|
-
const run = await waitForCloudFlowRun(receipt.runId, { signal
|
|
67
|
+
const run = await waitForCloudFlowRun(receipt.runId, { signal });
|
|
55
68
|
const ok = run.status === 'completed';
|
|
56
69
|
if (json) io.stdout(JSON.stringify({ ok, ...receipt, ...run }));
|
|
57
70
|
else io.stdout(`${run.status.toUpperCase()} ${run.runId} completionReason: ${'completionReason' in run ? run.completionReason : 'unavailable'}`);
|
|
58
71
|
return ok ? 0 : 1;
|
|
59
72
|
} catch (error) {
|
|
60
|
-
const code =
|
|
73
|
+
const code = signal.aborted
|
|
61
74
|
? runId ? 'observation_aborted' : submitting ? 'admission_unknown' : 'submission_aborted'
|
|
62
75
|
: error instanceof CloudFlowError ? error.code : 'cloud_run_failed';
|
|
63
|
-
const message =
|
|
76
|
+
const message = signal.aborted
|
|
64
77
|
? runId ? 'Stopped observing; the hosted run has not been cancelled.'
|
|
65
78
|
: submitting
|
|
66
79
|
? 'Submission interrupted before a receipt was received. Admission is unknown; Cloud may have started the run. Do not resubmit blindly.'
|
|
67
80
|
: 'Interrupted before the run was submitted; nothing was admitted. Safe to run again.'
|
|
68
|
-
: error instanceof Error ? error.message : 'Cloud run failed.';
|
|
81
|
+
: (error instanceof Error ? error.message : 'Cloud run failed.') + harnessRemedy(error, harnesses);
|
|
69
82
|
if (json) io.stdout(JSON.stringify({ ok: false, code, message, ...(runId ? { runId } : {}) }));
|
|
70
83
|
else io.stderr(`${code}: ${message}${runId ? ` (run ${runId})` : ''}`);
|
|
71
84
|
return error instanceof CloudFlowError
|
|
72
|
-
&& (['configuration', 'unsupported_source', 'invalid_input', 'unsupported_storage_backend', 'sync_too_large', 'sync_unsupported'
|
|
85
|
+
&& (['configuration', 'unsupported_source', 'invalid_input', 'unsupported_storage_backend', 'sync_too_large', 'sync_unsupported',
|
|
86
|
+
'integration_not_connected'].includes(error.code)
|
|
73
87
|
|| (runId === undefined && error.code === 'http_error' && [401, 403].includes(error.status ?? 0))) ? 2 : 1;
|
|
74
|
-
} finally {
|
|
75
|
-
process.off('SIGINT', abort);
|
|
76
|
-
process.off('SIGTERM', abort);
|
|
77
88
|
}
|
|
78
89
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
-
import { listCloudSchedules, scheduleInCloud, unscheduleInCloud, type CloudSchedule } from '../cloud-schedule.js';
|
|
2
|
+
import { listCloudSchedules, scheduleInCloud, unscheduleInCloud, validateScheduleArgs, type CloudSchedule } from '../cloud-schedule.js';
|
|
3
3
|
import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
|
|
4
4
|
import { snapshotJsonValue, type JsonValue } from '../json-value.js';
|
|
5
|
+
import { cliConnectPrompt, ensureFlowConnections, harnessRemedy } from './cloud-connect-cli.js';
|
|
5
6
|
import type { CliIo } from '../cli.js';
|
|
6
7
|
|
|
7
8
|
export interface CloudScheduleArgs {
|
|
@@ -12,12 +13,14 @@ export interface CloudScheduleArgs {
|
|
|
12
13
|
tz: string | undefined;
|
|
13
14
|
input: string | undefined;
|
|
14
15
|
name: string | undefined;
|
|
16
|
+
/** Refuse a missing integration instead of offering to connect it. */
|
|
17
|
+
noConnect: boolean;
|
|
15
18
|
json: boolean;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* `flows schedule <flow.yaml|flow.ts> [--cron "<expr>" | --every <n><s|m|h|d>]
|
|
20
|
-
* [--tz <IANA>] [--input <json|file>] [--name <n>] [--json]`
|
|
23
|
+
* [--tz <IANA>] [--input <json|file>] [--name <n>] [--no-connect] [--json]`
|
|
21
24
|
*
|
|
22
25
|
* With neither `--cron` nor `--every`, the flow's own `schedule.*` handler
|
|
23
26
|
* supplies the cron, so a declared schedule and its hosted registration
|
|
@@ -29,6 +32,7 @@ export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleAr
|
|
|
29
32
|
cron: undefined, every: undefined, tz: undefined, input: undefined, name: undefined,
|
|
30
33
|
};
|
|
31
34
|
let json = false;
|
|
35
|
+
let noConnect = false;
|
|
32
36
|
for (let i = 0; i < args.length; i++) {
|
|
33
37
|
const arg = args[i]!;
|
|
34
38
|
if (arg === '--json') {
|
|
@@ -36,6 +40,11 @@ export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleAr
|
|
|
36
40
|
json = true;
|
|
37
41
|
continue;
|
|
38
42
|
}
|
|
43
|
+
if (arg === '--no-connect') {
|
|
44
|
+
if (noConnect) return undefined;
|
|
45
|
+
noConnect = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
39
48
|
if (arg === '--cron' || arg === '--every' || arg === '--tz' || arg === '--input' || arg === '--name') {
|
|
40
49
|
const key = arg.slice(2) as keyof typeof flags;
|
|
41
50
|
const next = args[i + 1];
|
|
@@ -51,7 +60,7 @@ export function parseCloudScheduleArgs(args: readonly string[]): CloudScheduleAr
|
|
|
51
60
|
if (value === undefined) return undefined;
|
|
52
61
|
if (flags.cron !== undefined && flags.every !== undefined) return undefined;
|
|
53
62
|
if (flags.input !== undefined && !isAuthoredFlowPath(value)) return undefined;
|
|
54
|
-
return { command: 'schedule', value, ...flags, json };
|
|
63
|
+
return { command: 'schedule', value, ...flags, noConnect, json };
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
function describe(schedule: CloudSchedule): string {
|
|
@@ -61,6 +70,7 @@ function describe(schedule: CloudSchedule): string {
|
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
73
|
+
let harnesses: readonly string[] = [];
|
|
64
74
|
try {
|
|
65
75
|
let input: JsonValue | undefined;
|
|
66
76
|
let inputPresent = false;
|
|
@@ -73,6 +83,18 @@ export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): P
|
|
|
73
83
|
throw error;
|
|
74
84
|
}
|
|
75
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 ?? [];
|
|
76
98
|
const schedule = await scheduleInCloud({
|
|
77
99
|
flow: { path: args.value },
|
|
78
100
|
...(args.cron === undefined ? {} : { cron: args.cron }),
|
|
@@ -86,10 +108,11 @@ export async function runCloudScheduleCli(args: CloudScheduleArgs, io: CliIo): P
|
|
|
86
108
|
return 0;
|
|
87
109
|
}
|
|
88
110
|
io.stdout(`SCHEDULED ${describe(schedule)}`);
|
|
111
|
+
for (const provider of connections?.outcome.connected ?? []) io.stdout(` connected: ${provider}`);
|
|
89
112
|
io.stdout('Each fire submits this exact source through the same admission as flows run --cloud; list with: flows schedules');
|
|
90
113
|
return 0;
|
|
91
114
|
} catch (error) {
|
|
92
|
-
return reportFailure(error, args.json, io);
|
|
115
|
+
return reportFailure(error, args.json, io, harnesses);
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
118
|
|
|
@@ -120,9 +143,10 @@ export async function runCloudUnscheduleCli(
|
|
|
120
143
|
}
|
|
121
144
|
}
|
|
122
145
|
|
|
123
|
-
function reportFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
146
|
+
function reportFailure(error: unknown, json: boolean, io: CliIo, harnesses: readonly string[] = []): 1 | 2 {
|
|
124
147
|
const code = error instanceof CloudFlowError ? error.code : 'cloud_schedule_failed';
|
|
125
148
|
let message = error instanceof Error ? error.message : 'Cloud schedule failed.';
|
|
149
|
+
message += harnessRemedy(error, harnesses);
|
|
126
150
|
// The schedule routes take a browser session or a `cli:auth` token.
|
|
127
151
|
if (error instanceof CloudFlowError && error.status === 403) {
|
|
128
152
|
message += ' Scheduling needs an interactive `cli:auth` credential: run `agent-relay cloud login`.';
|
|
@@ -130,6 +154,7 @@ function reportFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
|
130
154
|
if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
|
|
131
155
|
else io.stderr(`${code}: ${message}`);
|
|
132
156
|
return error instanceof CloudFlowError
|
|
133
|
-
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code)
|
|
157
|
+
&& (['configuration', 'unsupported_source', 'invalid_input', 'integration_not_connected'].includes(error.code)
|
|
158
|
+
|| error.status === 403 || error.status === 401)
|
|
134
159
|
? 2 : 1;
|
|
135
160
|
}
|
package/src/cli/cloud-sync.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applyCloudPatch, downloadCloudPatchSet, excludedPatchPaths, patchedPaths, type CloudPathPatch,
|
|
4
|
+
} from '../cloud-sync.js';
|
|
3
5
|
import type { CliIo } from '../cli.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -7,23 +9,50 @@ import type { CliIo } from '../cli.js';
|
|
|
7
9
|
* and apply it here. The patch is the sandbox's own `git diff` against the
|
|
8
10
|
* uploaded baseline, so applying it reproduces exactly what the run's steps
|
|
9
11
|
* wrote — no re-execution, no re-upload.
|
|
12
|
+
*
|
|
13
|
+
* The agent runtime's own bookkeeping inside that tree is dropped on the way
|
|
14
|
+
* in (`CLOUD_SYNC_PATCH_EXCLUDES`); `--dry-run` prints the patch instead of
|
|
15
|
+
* applying it, and a run that produced one patch per mounted path is shown but
|
|
16
|
+
* never applied, because no single `--dir` is the right destination for all of
|
|
17
|
+
* them.
|
|
10
18
|
*/
|
|
11
19
|
export async function runCloudSyncCli(
|
|
12
|
-
{ runId, json, root }: { runId: string; json: boolean; root: string },
|
|
20
|
+
{ runId, json, root, dryRun }: { runId: string; json: boolean; root: string; dryRun: boolean },
|
|
13
21
|
io: CliIo,
|
|
14
22
|
): Promise<0 | 1 | 2> {
|
|
15
23
|
try {
|
|
16
|
-
const
|
|
17
|
-
|
|
24
|
+
const set = await downloadCloudPatchSet(runId, {});
|
|
25
|
+
// A single-tree run can answer `hasChanges: true` with an empty body. The
|
|
26
|
+
// multi-path branch already discounts those (`patch.trim() !== ''` in
|
|
27
|
+
// `downloadCloudPatchSet`), and so did `agent-relay cloud sync`, which this
|
|
28
|
+
// command replaces. Without the same guard an empty patch reaches `git
|
|
29
|
+
// apply`, which fails as `patch_conflict` instead of reporting NO CHANGES.
|
|
30
|
+
const hasChanges = set.kind === 'single' ? set.hasChanges && set.patch.trim() !== '' : set.hasChanges;
|
|
31
|
+
if (!hasChanges) {
|
|
18
32
|
if (json) io.stdout(JSON.stringify({ ok: true, runId, hasChanges: false, applied: false }));
|
|
19
33
|
else io.stdout(`NO CHANGES ${runId}`);
|
|
20
34
|
return 0;
|
|
21
35
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
if (set.kind === 'multi-path') {
|
|
37
|
+
const changed = set.patches.filter(entry => entry.hasChanges && entry.patch.trim() !== '');
|
|
38
|
+
if (!dryRun) {
|
|
39
|
+
throw new CloudFlowError('sync_unsupported',
|
|
40
|
+
`Run ${runId} produced ${changed.length} path-scoped patch${changed.length === 1 ? '' : 'es'} `
|
|
41
|
+
+ `(${changed.map(entry => entry.name).join(', ')}); flows sync applies single-tree runs only. `
|
|
42
|
+
+ 'Inspect them with --dry-run, then apply each in its own repository.');
|
|
43
|
+
}
|
|
44
|
+
reportMultiPathDryRun(runId, changed, json, io);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
if (dryRun) {
|
|
48
|
+
reportDryRun(runId, set.patch, json, io);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const { files, excluded } = applyCloudPatch(root, set.patch);
|
|
52
|
+
if (json) io.stdout(JSON.stringify({ ok: true, runId, hasChanges: true, applied: true, files, excluded }));
|
|
25
53
|
else {
|
|
26
|
-
io.stdout(`APPLIED ${runId}: ${files.length} file${files.length === 1 ? '' : 's'}${files
|
|
54
|
+
io.stdout(`APPLIED ${runId}: ${files.length} file${files.length === 1 ? '' : 's'}${indented(files)}`);
|
|
55
|
+
if (excluded.length) io.stdout(`SKIPPED agent runtime paths: ${excluded.length}${indented(excluded)}`);
|
|
27
56
|
io.stdout('Applied to the working tree, uncommitted: review with git diff before keeping it.');
|
|
28
57
|
}
|
|
29
58
|
return 0;
|
|
@@ -35,3 +64,51 @@ export async function runCloudSyncCli(
|
|
|
35
64
|
return error instanceof CloudFlowError && ['configuration', 'sync_unsupported', 'patch_conflict'].includes(error.code) ? 2 : 1;
|
|
36
65
|
}
|
|
37
66
|
}
|
|
67
|
+
|
|
68
|
+
/** A single-tree patch, shown rather than applied. */
|
|
69
|
+
function reportDryRun(runId: string, patch: string, json: boolean, io: CliIo): void {
|
|
70
|
+
const excluded = excludedPatchPaths(patch);
|
|
71
|
+
const files = patchedPaths(patch).filter(path => !excluded.includes(path));
|
|
72
|
+
if (json) {
|
|
73
|
+
// The patch travels in the payload, not on stdout beside it: a --json
|
|
74
|
+
// consumer parses one object, and a diff printed alongside it is not JSON.
|
|
75
|
+
io.stdout(JSON.stringify({
|
|
76
|
+
ok: true, runId, hasChanges: true, applied: false, dryRun: true, files, excluded, patch,
|
|
77
|
+
}));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
io.stdout(`DRY RUN ${runId}: ${files.length} file${files.length === 1 ? '' : 's'} would be written${indented(files)}`);
|
|
81
|
+
if (excluded.length) io.stdout(`Would skip agent runtime paths: ${excluded.length}${indented(excluded)}`);
|
|
82
|
+
emitPatch(patch, io);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every changed path-scoped patch, shown; applying them is not this command's call. */
|
|
86
|
+
function reportMultiPathDryRun(
|
|
87
|
+
runId: string, changed: readonly CloudPathPatch[], json: boolean, io: CliIo,
|
|
88
|
+
): void {
|
|
89
|
+
if (json) {
|
|
90
|
+
io.stdout(JSON.stringify({
|
|
91
|
+
ok: true, runId, hasChanges: true, applied: false, dryRun: true, multiPath: true,
|
|
92
|
+
patches: changed.map(({ name, patch }) => {
|
|
93
|
+
const excluded = excludedPatchPaths(patch);
|
|
94
|
+
return { name, files: patchedPaths(patch).filter(path => !excluded.includes(path)), excluded, patch };
|
|
95
|
+
}),
|
|
96
|
+
}));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
io.stdout(`DRY RUN ${runId}: ${changed.length} path-scoped patch${changed.length === 1 ? '' : 'es'}`);
|
|
100
|
+
io.stdout('Each targets its own repository; apply them there, not with --dir.');
|
|
101
|
+
for (const { name, patch } of changed) {
|
|
102
|
+
io.stdout(`--- patch for path "${name}" ---`);
|
|
103
|
+
emitPatch(patch, io);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Write a diff through the line-oriented io without gaining or losing a newline. */
|
|
108
|
+
function emitPatch(patch: string, io: CliIo): void {
|
|
109
|
+
for (const line of patch.replace(/\n$/u, '').split('\n')) io.stdout(line);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function indented(paths: readonly string[]): string {
|
|
113
|
+
return paths.length ? `\n ${paths.join('\n ')}` : '';
|
|
114
|
+
}
|
package/src/cli/deploy.ts
CHANGED
|
@@ -4,19 +4,35 @@ import { BundleFailure, bucketDirectory, copyBundle, exists, parseDigestReferenc
|
|
|
4
4
|
verifyDigest, writableBucket } from '../bundle-transport.js';
|
|
5
5
|
import { checkRunnableBundle } from './bundle-preflight.js';
|
|
6
6
|
|
|
7
|
-
export interface DeployArgs { command: 'deploy'; value: string; to: string }
|
|
7
|
+
export interface DeployArgs { command: 'deploy'; value: string; to: string; json: boolean }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `--json` is declared on the `deploy` verb, which both forms share, so the
|
|
11
|
+
* bundle form has to accept it too -- the hosted-listener parser already did,
|
|
12
|
+
* and help cannot tell a reader the flag belongs to only one of them.
|
|
13
|
+
*/
|
|
8
14
|
export function parseDeployArgs(args: readonly string[]): DeployArgs | undefined {
|
|
9
15
|
let value: string | undefined;
|
|
10
16
|
let to: string | undefined;
|
|
17
|
+
let json = false;
|
|
11
18
|
for (let i = 0; i < args.length; i++) {
|
|
12
19
|
const arg = args[i]!;
|
|
13
20
|
if (arg === '--to') {
|
|
14
21
|
if (to !== undefined || !args[i + 1] || args[i + 1]!.startsWith('-')) return undefined;
|
|
15
22
|
to = args[++i];
|
|
23
|
+
} else if (arg === '--json') {
|
|
24
|
+
if (json) return undefined;
|
|
25
|
+
json = true;
|
|
16
26
|
} else if (arg.startsWith('-') || value !== undefined) return undefined;
|
|
17
27
|
else value = arg;
|
|
18
28
|
}
|
|
19
|
-
return value && to && parseDigestReference(value) ? { command: 'deploy', value, to } : undefined;
|
|
29
|
+
return value && to && parseDigestReference(value) ? { command: 'deploy', value, to, json } : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One machine-readable object per outcome, or the text line it mirrors. */
|
|
33
|
+
function emitDeployOutcome(status: 'deployed' | 'already-present', args: DeployArgs, io: CliIo): void {
|
|
34
|
+
if (args.json) io.stdout(JSON.stringify({ ok: true, bundle: args.value, to: args.to, status }));
|
|
35
|
+
else io.stdout(`${status === 'deployed' ? 'DEPLOYED' : 'SKIPPED (already-present)'} ${args.value} ${args.to}`);
|
|
20
36
|
}
|
|
21
37
|
export async function runDeploy(args: DeployArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
22
38
|
let started = false;
|
|
@@ -29,6 +45,10 @@ export async function runDeploy(args: DeployArgs, io: CliIo): Promise<0 | 1 | 2>
|
|
|
29
45
|
const checked = await checkRunnableBundle(source, ref.name);
|
|
30
46
|
if (!checked.report.ok) {
|
|
31
47
|
for (const diagnostic of checked.report.diagnostics) io.stderr(`${diagnostic.severity.toUpperCase()} [${diagnostic.kind}] ${diagnostic.message}`);
|
|
48
|
+
if (args.json) {
|
|
49
|
+
io.stdout(JSON.stringify({ ok: false, code: 'bundle_unrunnable', bundle: args.value, to: args.to,
|
|
50
|
+
diagnostics: checked.report.diagnostics }));
|
|
51
|
+
}
|
|
32
52
|
return 2;
|
|
33
53
|
}
|
|
34
54
|
// `ok` may still carry warnings (e.g. `budget_unmetered`); report them.
|
|
@@ -39,18 +59,20 @@ export async function runDeploy(args: DeployArgs, io: CliIo): Promise<0 | 1 | 2>
|
|
|
39
59
|
if (await exists(target)) {
|
|
40
60
|
await verifyDigest(target, ref.digest);
|
|
41
61
|
io.stderr(`deploy_noop: ${args.value}`);
|
|
42
|
-
|
|
62
|
+
emitDeployOutcome('already-present', args, io);
|
|
43
63
|
return 0;
|
|
44
64
|
}
|
|
45
65
|
await writableBucket(target);
|
|
46
66
|
started = true;
|
|
47
67
|
const copied = await copyBundle(source, target, ref.digest);
|
|
48
68
|
if (!copied) io.stderr(`deploy_noop: ${args.value}`);
|
|
49
|
-
|
|
69
|
+
emitDeployOutcome(copied ? 'deployed' : 'already-present', args, io);
|
|
50
70
|
return 0;
|
|
51
71
|
} catch (error) {
|
|
52
72
|
const kind = started ? 'deploy_partial' : error instanceof BundleFailure ? error.kind : 'bucket_unreachable';
|
|
53
|
-
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
io.stderr(`${started ? 'FAILED' : 'REFUSED'} [${kind}] ${message}`);
|
|
75
|
+
if (args.json) io.stdout(JSON.stringify({ ok: false, code: kind, message, bundle: args.value, to: args.to }));
|
|
54
76
|
return started ? 1 : 2;
|
|
55
77
|
}
|
|
56
78
|
}
|
package/src/cli/direct-run.ts
CHANGED
|
@@ -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,
|
|
@@ -175,6 +177,9 @@ export async function runDirectFlow(
|
|
|
175
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>;
|
|
@@ -226,6 +233,9 @@ export async function resumeFlow(
|
|
|
226
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
|
}
|
|
@@ -298,6 +308,42 @@ export function authoredStepFailure(
|
|
|
298
308
|
};
|
|
299
309
|
}
|
|
300
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)}`,
|
|
342
|
+
}],
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
301
347
|
/**
|
|
302
348
|
* An authored body that returned its own terminal verdict, reported as that verdict.
|
|
303
349
|
*
|