@relayflows/sdk 2.0.30 → 2.0.32
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/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +2 -9
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/agent-cwd.d.ts +69 -0
- package/dist/agent-cwd.d.ts.map +1 -0
- package/dist/agent-cwd.js +140 -0
- package/dist/agent-cwd.js.map +1 -0
- package/dist/artifact-scan-policy.d.ts +35 -0
- package/dist/artifact-scan-policy.d.ts.map +1 -0
- package/dist/artifact-scan-policy.js +49 -0
- package/dist/artifact-scan-policy.js.map +1 -0
- package/dist/authored-completion-record.d.ts +35 -0
- package/dist/authored-completion-record.d.ts.map +1 -0
- package/dist/authored-completion-record.js +92 -0
- package/dist/authored-completion-record.js.map +1 -0
- package/dist/authored-completion.d.ts +99 -0
- package/dist/authored-completion.d.ts.map +1 -0
- package/dist/authored-completion.js +184 -0
- package/dist/authored-completion.js.map +1 -0
- package/dist/authored-flow-error.d.ts +13 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +12 -0
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +7 -30
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +54 -47
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +1 -3
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-node-entry.js +4 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +9 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +92 -17
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-preflight.d.ts +4 -0
- package/dist/authored-preflight.d.ts.map +1 -0
- package/dist/authored-preflight.js +50 -0
- package/dist/authored-preflight.js.map +1 -0
- package/dist/authored-root.d.ts +5 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +20 -2
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +35 -10
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +108 -40
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-verdict.d.ts +28 -0
- package/dist/authored-verdict.d.ts.map +1 -0
- package/dist/authored-verdict.js +79 -0
- package/dist/authored-verdict.js.map +1 -0
- package/dist/authored-worker-step.d.ts +5 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +51 -13
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-worker-surface.d.ts +27 -0
- package/dist/cli/check-worker-surface.d.ts.map +1 -0
- package/dist/cli/check-worker-surface.js +63 -0
- package/dist/cli/check-worker-surface.js.map +1 -0
- package/dist/cli/check.d.ts +28 -3
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +27 -140
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +20 -0
- package/dist/cli/cli-probe.d.ts.map +1 -0
- package/dist/cli/cli-probe.js +166 -0
- package/dist/cli/cli-probe.js.map +1 -0
- package/dist/cli/cloud-format.d.ts +9 -0
- package/dist/cli/cloud-format.d.ts.map +1 -0
- package/dist/cli/cloud-format.js +77 -0
- package/dist/cli/cloud-format.js.map +1 -0
- package/dist/cli/cloud-live.d.ts +39 -0
- package/dist/cli/cloud-live.d.ts.map +1 -0
- package/dist/cli/cloud-live.js +325 -0
- package/dist/cli/cloud-live.js.map +1 -0
- package/dist/cli/cloud-mirror-session.d.ts +97 -0
- package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
- package/dist/cli/cloud-mirror-session.js +264 -0
- package/dist/cli/cloud-mirror-session.js.map +1 -0
- package/dist/cli/cloud-read.d.ts +3 -7
- package/dist/cli/cloud-read.d.ts.map +1 -1
- package/dist/cli/cloud-read.js +14 -266
- package/dist/cli/cloud-read.js.map +1 -1
- package/dist/cli/cloud-refusal.d.ts +39 -0
- package/dist/cli/cloud-refusal.d.ts.map +1 -0
- package/dist/cli/cloud-refusal.js +95 -0
- package/dist/cli/cloud-refusal.js.map +1 -0
- package/dist/cli/cloud-status-view.d.ts +7 -0
- package/dist/cli/cloud-status-view.d.ts.map +1 -0
- package/dist/cli/cloud-status-view.js +214 -0
- package/dist/cli/cloud-status-view.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +16 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/hn-monitor.d.ts +0 -13
- package/dist/cli/hn-monitor.d.ts.map +1 -1
- package/dist/cli/hn-monitor.js +2 -1
- package/dist/cli/hn-monitor.js.map +1 -1
- package/dist/cli/local-agent-remedy.d.ts +80 -0
- package/dist/cli/local-agent-remedy.d.ts.map +1 -0
- package/dist/cli/local-agent-remedy.js +116 -0
- package/dist/cli/local-agent-remedy.js.map +1 -0
- package/dist/cli/observer-session.d.ts +41 -0
- package/dist/cli/observer-session.d.ts.map +1 -0
- package/dist/cli/observer-session.js +78 -0
- package/dist/cli/observer-session.js.map +1 -0
- package/dist/cli/run.d.ts +30 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +186 -24
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +9 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +31 -2
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/step-evidence.d.ts +90 -0
- package/dist/cli/step-evidence.d.ts.map +1 -0
- package/dist/cli/step-evidence.js +259 -0
- package/dist/cli/step-evidence.js.map +1 -0
- package/dist/cli/step-failure.d.ts +14 -4
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +41 -76
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +12 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +3 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +134 -57
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +1 -1
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +24 -8
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-mirror-ledger.d.ts +40 -0
- package/dist/cloud-mirror-ledger.d.ts.map +1 -0
- package/dist/cloud-mirror-ledger.js +142 -0
- package/dist/cloud-mirror-ledger.js.map +1 -0
- package/dist/cloud-mirror-step.d.ts +168 -0
- package/dist/cloud-mirror-step.d.ts.map +1 -0
- package/dist/cloud-mirror-step.js +504 -0
- package/dist/cloud-mirror-step.js.map +1 -0
- package/dist/cloud-mirror-transport.d.ts +94 -0
- package/dist/cloud-mirror-transport.d.ts.map +1 -0
- package/dist/cloud-mirror-transport.js +163 -0
- package/dist/cloud-mirror-transport.js.map +1 -0
- package/dist/cloud-mirror.d.ts +132 -0
- package/dist/cloud-mirror.d.ts.map +1 -0
- package/dist/cloud-mirror.js +556 -0
- package/dist/cloud-mirror.js.map +1 -0
- package/dist/cloud-read.d.ts +15 -0
- package/dist/cloud-read.d.ts.map +1 -1
- package/dist/cloud-read.js +20 -1
- package/dist/cloud-read.js.map +1 -1
- package/dist/cloud-run-record.d.ts +27 -0
- package/dist/cloud-run-record.d.ts.map +1 -0
- package/dist/cloud-run-record.js +56 -0
- package/dist/cloud-run-record.js.map +1 -0
- package/dist/cloud-run.d.ts +2 -9
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +3 -20
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +2 -1
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +13 -5
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +28 -6
- package/dist/compile.js.map +1 -1
- package/dist/direct-input.d.ts.map +1 -1
- package/dist/direct-input.js +21 -1
- package/dist/direct-input.js.map +1 -1
- package/dist/failure-kinds.d.ts +96 -6
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +29 -2
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +24 -9
- package/dist/flow-requirements.js.map +1 -1
- package/dist/gate-contract.d.ts +7 -0
- package/dist/gate-contract.d.ts.map +1 -1
- package/dist/gate-contract.js +6 -1
- package/dist/gate-contract.js.map +1 -1
- package/dist/helper-preflight.d.ts.map +1 -1
- package/dist/helper-preflight.js +10 -3
- package/dist/helper-preflight.js.map +1 -1
- package/dist/helper-reference.d.ts +25 -0
- package/dist/helper-reference.d.ts.map +1 -0
- package/dist/helper-reference.js +129 -0
- package/dist/helper-reference.js.map +1 -0
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +3 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +17 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-projection.d.ts +16 -0
- package/dist/journal-projection.d.ts.map +1 -0
- package/dist/journal-projection.js +108 -0
- package/dist/journal-projection.js.map +1 -0
- package/dist/llm-worker.js +1 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +4 -3
- package/dist/local-agent.js.map +1 -1
- package/dist/named-gate-lowering.js +35 -5
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/named-gate-preflight.d.ts +27 -0
- package/dist/named-gate-preflight.d.ts.map +1 -0
- package/dist/named-gate-preflight.js +47 -0
- package/dist/named-gate-preflight.js.map +1 -0
- package/dist/named-gates.d.ts +1 -1
- package/dist/named-gates.d.ts.map +1 -1
- package/dist/named-gates.js +3 -0
- package/dist/named-gates.js.map +1 -1
- package/dist/observer-link.d.ts +7 -0
- package/dist/observer-link.d.ts.map +1 -1
- package/dist/observer-link.js +3 -1
- package/dist/observer-link.js.map +1 -1
- package/dist/package-version.d.ts +10 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +22 -0
- package/dist/package-version.js.map +1 -0
- package/dist/preflight.d.ts +14 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +25 -3
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/redact.d.ts +42 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +128 -0
- package/dist/redact.js.map +1 -1
- package/dist/relay-cli.d.ts.map +1 -1
- package/dist/relay-cli.js +1 -21
- package/dist/relay-cli.js.map +1 -1
- package/dist/run-projection.d.ts +83 -0
- package/dist/run-projection.d.ts.map +1 -0
- package/dist/run-projection.js +199 -0
- package/dist/run-projection.js.map +1 -0
- package/dist/shell-word.d.ts +19 -0
- package/dist/shell-word.d.ts.map +1 -0
- package/dist/shell-word.js +23 -0
- package/dist/shell-word.js.map +1 -0
- package/dist/spec.d.ts +52 -3
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.js +1 -1
- package/dist/step-fields.js.map +1 -1
- package/dist/steps-green.d.ts +27 -0
- package/dist/steps-green.d.ts.map +1 -0
- package/dist/steps-green.js +164 -0
- package/dist/steps-green.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +39 -8
- package/dist/validate.js.map +1 -1
- package/dist/worker-lease.d.ts +8 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +34 -4
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts +8 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +17 -9
- package/dist/worker.js.map +1 -1
- package/package.json +3 -7
- package/src/agent-artifacts.ts +2 -10
- package/src/agent-cwd.ts +139 -0
- package/src/artifact-scan-policy.ts +51 -0
- package/src/authored-completion-record.ts +119 -0
- package/src/authored-completion.ts +203 -0
- package/src/authored-flow-error.ts +13 -1
- package/src/authored-flow-executor.ts +91 -58
- package/src/authored-human.ts +1 -4
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +96 -22
- package/src/authored-preflight.ts +46 -0
- package/src/authored-root.ts +20 -2
- package/src/authored-step-output.ts +139 -42
- package/src/authored-verdict.ts +87 -0
- package/src/authored-worker-step.ts +63 -14
- package/src/cli/check-worker-surface.ts +66 -0
- package/src/cli/check.ts +59 -166
- package/src/cli/cli-probe.ts +194 -0
- package/src/cli/cloud-format.ts +82 -0
- package/src/cli/cloud-live.ts +366 -0
- package/src/cli/cloud-mirror-session.ts +329 -0
- package/src/cli/cloud-read.ts +13 -282
- package/src/cli/cloud-refusal.ts +113 -0
- package/src/cli/cloud-status-view.ts +213 -0
- package/src/cli/direct-run.ts +19 -2
- package/src/cli/hn-monitor.ts +2 -1
- package/src/cli/local-agent-remedy.ts +160 -0
- package/src/cli/observer-session.ts +107 -0
- package/src/cli/run.ts +233 -28
- package/src/cli/status.ts +59 -3
- package/src/cli/step-evidence.ts +310 -0
- package/src/cli/step-failure.ts +57 -81
- package/src/cli-commands.ts +3 -0
- package/src/cli.ts +155 -60
- package/src/cloud-deploy.ts +26 -8
- package/src/cloud-mirror-ledger.ts +161 -0
- package/src/cloud-mirror-step.ts +618 -0
- package/src/cloud-mirror-transport.ts +261 -0
- package/src/cloud-mirror.ts +633 -0
- package/src/cloud-read.ts +25 -1
- package/src/cloud-run-record.ts +64 -0
- package/src/cloud-run.ts +5 -24
- package/src/communication/local.ts +2 -1
- package/src/communication/worker.ts +13 -5
- package/src/compile.ts +28 -6
- package/src/direct-input.ts +22 -1
- package/src/failure-kinds.ts +104 -2
- package/src/flow-requirements.ts +24 -9
- package/src/gate-contract.ts +13 -1
- package/src/helper-preflight.ts +10 -3
- package/src/helper-reference.ts +130 -0
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/index.ts +2 -0
- package/src/journal-client.ts +19 -2
- package/src/journal-projection.ts +106 -0
- package/src/llm-worker.ts +1 -1
- package/src/local-agent.ts +4 -2
- package/src/named-gate-lowering.ts +35 -5
- package/src/named-gate-preflight.ts +47 -0
- package/src/named-gates.ts +3 -0
- package/src/observer-link.ts +10 -1
- package/src/package-version.ts +23 -0
- package/src/preflight.ts +38 -7
- package/src/protocol.ts +2 -0
- package/src/redact.ts +111 -0
- package/src/relay-cli.ts +1 -24
- package/src/run-projection.ts +247 -0
- package/src/shell-word.ts +24 -0
- package/src/spec.ts +54 -3
- package/src/step-fields.ts +1 -1
- package/src/steps-green.ts +168 -0
- package/src/validate.ts +35 -8
- package/src/worker-lease.ts +35 -5
- package/src/worker.ts +27 -11
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,12 @@ import { describeFlowRequirements } from './flow-requirements.js';
|
|
|
7
7
|
import type { CliModelSource } from './cli-adapter.js';
|
|
8
8
|
|
|
9
9
|
import { renderProgress, type ProgressEvent } from './progress.js';
|
|
10
|
+
import type { JournalEvent } from './journal-reader.js';
|
|
11
|
+
import { createObserverSession } from './cli/observer-session.js';
|
|
12
|
+
import {
|
|
13
|
+
cloudMirrorRequested, createCloudMirrorSession, mirrorSourceFromJournal, mirrorSourceFromPath,
|
|
14
|
+
type CloudMirrorReceipt,
|
|
15
|
+
} from './cli/cloud-mirror-session.js';
|
|
10
16
|
import { realpathSync } from 'node:fs';
|
|
11
17
|
import { pathToFileURL } from 'node:url';
|
|
12
18
|
import {
|
|
@@ -31,6 +37,7 @@ import {
|
|
|
31
37
|
parseLogsArgs, parseRunsArgs, runCloudLogsCli, runCloudRunsCli, runCloudStatusCli,
|
|
32
38
|
type LogsArgs, type RunsArgs,
|
|
33
39
|
} from './cli/cloud-read.js';
|
|
40
|
+
import { runCloudLogsFollow, runCloudStatusWatch } from './cli/cloud-live.js';
|
|
34
41
|
import { transcriptTailSource } from './transcript-tail.js';
|
|
35
42
|
import { checkTypeScriptFlow } from './cli/check-typescript.js';
|
|
36
43
|
import { runCloudCli } from './cli/cloud-run.js';
|
|
@@ -51,12 +58,23 @@ import {
|
|
|
51
58
|
resolveObserverLinkEnv,
|
|
52
59
|
type MintObserverOptions,
|
|
53
60
|
} from './observer-link.js';
|
|
61
|
+
import { packageVersion } from './package-version.js';
|
|
54
62
|
|
|
55
63
|
export type { CheckInputDiagnostic, CheckReport } from './cli/check.js';
|
|
56
64
|
|
|
57
65
|
export interface CliIo {
|
|
58
66
|
stdout(line: string): void;
|
|
59
67
|
stderr(line: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* True when stdout is an interactive terminal.
|
|
70
|
+
*
|
|
71
|
+
* Only the live views read it, and only to decide whether a redraw may use
|
|
72
|
+
* ANSI control sequences: `flows status --cloud --watch` clears the screen
|
|
73
|
+
* for a terminal and appends whole pages when its output is redirected or
|
|
74
|
+
* mounted in a host that renders text. Absent means "not a terminal", so an
|
|
75
|
+
* embedder that says nothing gets the safe form.
|
|
76
|
+
*/
|
|
77
|
+
tty?: boolean;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
type CliExitCode = 0 | 1 | 2 | 3;
|
|
@@ -82,8 +100,8 @@ export type ParsedArgs =
|
|
|
82
100
|
| { command: 'schedules'; json: boolean }
|
|
83
101
|
| { command: 'unschedule'; scheduleId: string; json: boolean }
|
|
84
102
|
| { command: 'check'; json: boolean; watch: boolean; value: string }
|
|
85
|
-
| { command: 'run'; bucket: string | undefined; reuseFromRunId: string | undefined; localAgent: boolean; agentCapacity: number | undefined; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
86
|
-
| { command: 'resume'; localAgent: boolean; agentCapacity: number | undefined; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; allowHumanInfluenced: boolean; value: string }
|
|
103
|
+
| { command: 'run'; bucket: string | undefined; reuseFromRunId: string | undefined; localAgent: boolean; agentCapacity: number | undefined; dataDir: string; input: string | undefined; json: boolean; spawn: boolean; noObserverLink: boolean; cloudMirror: boolean; allowHumanInfluenced: boolean; value: string }
|
|
104
|
+
| { command: 'resume'; localAgent: boolean; agentCapacity: number | undefined; dataDir: string; json: boolean; spawn: boolean; noObserverLink: boolean; cloudMirror: boolean; allowHumanInfluenced: boolean; value: string }
|
|
87
105
|
| { command: 'answer'; dataDir: string; json: boolean; spawn: boolean; note: string | undefined; by: string | undefined; runId: string; waitId: string; answer: boolean }
|
|
88
106
|
| RunsArgs
|
|
89
107
|
| LogsArgs
|
|
@@ -113,19 +131,19 @@ const USAGE = [
|
|
|
113
131
|
'flows run <flow>@sha256:<digest> [--bucket <file-bucket-uri>] [--data-dir <dir>] [--json]',
|
|
114
132
|
'flows check [--watch] [--json] <flow.ts|flow.yaml|spec.json>',
|
|
115
133
|
'flows serve-webhook --data-dir <dir> --port <p> [--allow <name>[,<name>]]',
|
|
116
|
-
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] [--reuse-from <run-id>] <flow.yaml|spec.json>',
|
|
134
|
+
'flows run [--json] [--no-spawn] [--no-observer-link] [--cloud-mirror] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] [--reuse-from <run-id>] <flow.yaml|spec.json>',
|
|
117
135
|
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.yaml|spec.json>',
|
|
118
136
|
'flows run --cloud [--json] [--wait] [--sync-code] [--no-connect] <flow.ts> --input <inline-json-or-file>',
|
|
119
137
|
'flows sync [--json] [--dry-run] [--dir <path>] <run-id>',
|
|
120
|
-
'flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] <flow.ts> --input <inline-json-or-file>',
|
|
138
|
+
'flows run [--json] [--no-spawn] [--no-observer-link] [--cloud-mirror] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] <flow.ts> --input <inline-json-or-file>',
|
|
121
139
|
'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>',
|
|
122
|
-
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] <run-id>',
|
|
140
|
+
'flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--cloud-mirror] [--data-dir <dir>] [--local-agent [--agent-capacity <n>]] <run-id>',
|
|
123
141
|
'flows answer [--json] [--no-spawn] [--data-dir <dir>] [--note <text>] [--by <identity>] <run-id> <wait-id> <yes|no>',
|
|
124
142
|
'flows replay [--allow-human-influenced] [--json] [--data-dir <dir>] <run-id> [--at <step-id>]',
|
|
125
143
|
'flows status [--json] [--data-dir <dir>] [--tail <n>] [<run-id>]',
|
|
126
|
-
'flows status --cloud [--json] <run-id>',
|
|
144
|
+
'flows status --cloud [--json] [--watch] <run-id>',
|
|
127
145
|
'flows runs [--limit <n>] [--json]',
|
|
128
|
-
'flows logs [--step <name>] [--raw] [--json] <run-id>',
|
|
146
|
+
'flows logs [--step <name>] [--raw] [--json] [--follow] <run-id>',
|
|
129
147
|
'flows observer [--data-dir <dir>]',
|
|
130
148
|
'flows hn-monitor start [--data-dir <dir>] [--poll-interval-ms <n>] <spec.json>',
|
|
131
149
|
].join('\n');
|
|
@@ -143,10 +161,17 @@ function spawnAllowedByEnv(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
143
161
|
const PROCESS_IO: CliIo = {
|
|
144
162
|
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
145
163
|
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
164
|
+
tty: process.stdout.isTTY === true,
|
|
146
165
|
};
|
|
147
166
|
|
|
148
167
|
/** Optional knobs for an embedded caller. `bin/flows.js` passes none. */
|
|
149
168
|
export interface RunCliOptions {
|
|
169
|
+
/**
|
|
170
|
+
* Version to print for a self-contained executable that cannot read the
|
|
171
|
+
* installed package manifest. Normal package entrypoints leave this unset.
|
|
172
|
+
*/
|
|
173
|
+
version?: string;
|
|
174
|
+
|
|
150
175
|
/**
|
|
151
176
|
* Cancellation for the long-running verbs (`run --cloud`, `check --watch`,
|
|
152
177
|
* `serve-webhook`, `hn-monitor start`, `tick start`).
|
|
@@ -188,6 +213,10 @@ export async function runCli(
|
|
|
188
213
|
io: CliIo = PROCESS_IO,
|
|
189
214
|
options: RunCliOptions = {},
|
|
190
215
|
): Promise<CliExitCode> {
|
|
216
|
+
if (args.length === 1 && (args[0] === '--version' || args[0] === '-V')) {
|
|
217
|
+
io.stdout(options.version ?? packageVersion());
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
191
220
|
if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
|
|
192
221
|
io.stdout(USAGE);
|
|
193
222
|
return 0;
|
|
@@ -222,13 +251,21 @@ export async function runCli(
|
|
|
222
251
|
// works inside a step of a run whose daemon is gone (kernel/DAEMON-LIFECYCLE.md §4).
|
|
223
252
|
if (parsed.command === 'status') {
|
|
224
253
|
// One verb, two sources. `--cloud` never reaches `runStatus`, so the
|
|
225
|
-
// offline reader stays offline (cli/status.ts).
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
254
|
+
// offline reader stays offline (cli/status.ts). Only `--watch` blocks, so
|
|
255
|
+
// only `--watch` takes a signal: a one-shot read keeps installing none.
|
|
256
|
+
if (parsed.cloud !== true) return runStatus(parsed, io, { tails: transcriptTailSource() });
|
|
257
|
+
const cloudStatus = parsed;
|
|
258
|
+
return cloudStatus.watch === true
|
|
259
|
+
? withInterrupt(options.signal, (signal) => runCloudStatusWatch(cloudStatus, io, { signal }))
|
|
260
|
+
: runCloudStatusCli(cloudStatus, io);
|
|
229
261
|
}
|
|
230
262
|
if (parsed.command === 'runs') return runCloudRunsCli(parsed, io);
|
|
231
|
-
if (parsed.command === 'logs')
|
|
263
|
+
if (parsed.command === 'logs') {
|
|
264
|
+
const logs = parsed;
|
|
265
|
+
return logs.follow
|
|
266
|
+
? withInterrupt(options.signal, (signal) => runCloudLogsFollow(logs, io, { signal }))
|
|
267
|
+
: runCloudLogsCli(logs, io);
|
|
268
|
+
}
|
|
232
269
|
if (parsed.command === 'answer') {
|
|
233
270
|
const execution = await answerFlow(parsed.runId, parsed.waitId, parsed.answer, parsed.dataDir, {
|
|
234
271
|
...(parsed.note === undefined ? {} : { note: parsed.note }),
|
|
@@ -250,8 +287,13 @@ export async function runCli(
|
|
|
250
287
|
// refuses `--data-dir` on `check`, so there is no data dir to attach to.
|
|
251
288
|
// `flows check` keeps working with no daemon, no relayflowd binary and no
|
|
252
289
|
// data directory at all -- a property worth keeping, not an omission.
|
|
290
|
+
// Only this invocation opts into `agent_worker_unresolved`: `flows check`
|
|
291
|
+
// attaches no worker and, being daemon-free, cannot see one attached
|
|
292
|
+
// elsewhere. The authored `.flow.ts` path checks header declarations
|
|
293
|
+
// without compiling steps, so it has no agent steps to count.
|
|
253
294
|
const checked = /\.(?:[cm]?[jt]s)$/.test(parsed.value)
|
|
254
|
-
? await checkAuthoredFlowComposed(parsed.value)
|
|
295
|
+
? await checkAuthoredFlowComposed(parsed.value)
|
|
296
|
+
: checkFlow(parsed.value, { warnUnresolvedAgentWorker: true });
|
|
255
297
|
emitCheckReport(checked.report, parsed.json, io);
|
|
256
298
|
return checked.report.ok ? 0 : 2;
|
|
257
299
|
}
|
|
@@ -287,20 +329,60 @@ export async function runCli(
|
|
|
287
329
|
// not here. Hoisting it above the dispatch would start a daemon as a side
|
|
288
330
|
// effect of an invocation that is about to be refused for bad input.
|
|
289
331
|
const startedSteps = new Map<string, number>();
|
|
332
|
+
const observer = parsed.noObserverLink ? undefined : createObserverSession(parsed.command, io);
|
|
333
|
+
const runnerLog: string[] = [];
|
|
334
|
+
// Opt-in, unlike the observer link beside it. The observer is the default
|
|
335
|
+
// way to watch a local run: it is free, it needs only a workspace key, and
|
|
336
|
+
// it carries a step projection. The dashboard is the richer, hosted view —
|
|
337
|
+
// it stores the flow source, every step's transcript and this invocation's
|
|
338
|
+
// own output — so a local run joins it because someone asked, never because
|
|
339
|
+
// a login happened to be lying around.
|
|
340
|
+
const mirror = !(parsed.cloudMirror || cloudMirrorRequested(process.env))
|
|
341
|
+
? undefined
|
|
342
|
+
: createCloudMirrorSession({
|
|
343
|
+
source: parsed.command === 'run'
|
|
344
|
+
? mirrorSourceFromPath(parsed.value, parsed.input, parsed.dataDir)
|
|
345
|
+
: mirrorSourceFromJournal(parsed.dataDir),
|
|
346
|
+
dataDir: parsed.dataDir,
|
|
347
|
+
log: () => runnerLog,
|
|
348
|
+
requested: parsed.cloudMirror ? 'flag' : 'env',
|
|
349
|
+
}, io);
|
|
350
|
+
// Everything this invocation prints, kept in order, so the mirror can upload
|
|
351
|
+
// it as the run's `runner.log` — the object the dashboard's log pane reads,
|
|
352
|
+
// and the sandbox's own equivalent. A declarative run emits no progress
|
|
353
|
+
// events at all, so building the log out of those would have left the pane
|
|
354
|
+
// empty for exactly the runs that are easiest to follow.
|
|
355
|
+
const logged: CliIo = mirror === undefined ? io : {
|
|
356
|
+
...io,
|
|
357
|
+
stdout: line => { runnerLog.push(line); io.stdout(line); },
|
|
358
|
+
stderr: line => { runnerLog.push(line); io.stderr(line); },
|
|
359
|
+
};
|
|
290
360
|
const showProgress = (event: ProgressEvent): void => {
|
|
291
361
|
if (event.type === 'step.started') startedSteps.set(event.stepId, performance.now());
|
|
292
|
-
if (!parsed.json) for (const line of renderProgress([event]))
|
|
362
|
+
if (!parsed.json) for (const line of renderProgress([event])) logged.stderr(line);
|
|
363
|
+
observer?.onProgress(event);
|
|
364
|
+
mirror?.onProgress(event);
|
|
293
365
|
};
|
|
294
366
|
const lifecycle = {
|
|
295
367
|
...(parsed.command === 'run' ? { bucket: parsed.bucket } : {}),
|
|
296
368
|
allowHumanInfluenced: parsed.allowHumanInfluenced,
|
|
297
|
-
onPtyReady: (path: string) =>
|
|
369
|
+
onPtyReady: (path: string) => logged.stderr(`PTY ${path}`),
|
|
298
370
|
...(parsed.command === 'run' && parsed.reuseFromRunId !== undefined ? { reuseFromRunId: parsed.reuseFromRunId } : {}),
|
|
299
371
|
localAgent: parsed.localAgent,
|
|
300
372
|
...(parsed.agentCapacity === undefined ? {} : { agentCapacity: parsed.agentCapacity }),
|
|
301
373
|
onProgress: showProgress,
|
|
374
|
+
...(observer === undefined && mirror === undefined ? {} : {
|
|
375
|
+
onJournalEntry: (entry: JournalEvent) => {
|
|
376
|
+
observer?.onJournalEntry(entry);
|
|
377
|
+
mirror?.onJournalEntry(entry);
|
|
378
|
+
},
|
|
379
|
+
onRunStarted: (run: { runId: string; flow: string; resumed?: boolean }) => {
|
|
380
|
+
observer?.onRunStarted(run);
|
|
381
|
+
mirror?.onRunStarted(run);
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
302
384
|
onWait: (progress: RunProgress) => {
|
|
303
|
-
emitWait(progress,
|
|
385
|
+
emitWait(progress, logged);
|
|
304
386
|
const now = performance.now();
|
|
305
387
|
if (!startedSteps.has(progress.stepId)) startedSteps.set(progress.stepId, now);
|
|
306
388
|
showProgress({ type: 'step.running', stepId: progress.stepId, stepType: progress.stepType,
|
|
@@ -308,16 +390,15 @@ export async function runCli(
|
|
|
308
390
|
},
|
|
309
391
|
daemon: { spawn: parsed.spawn && spawnAllowedByEnv() },
|
|
310
392
|
};
|
|
311
|
-
// Mint the observer token in parallel with the run so the mint round-trip
|
|
312
|
-
// never adds to the RUN summary latency. The outcome is only consulted at
|
|
313
|
-
// emit time; a rejected promise here can never fail the run (see
|
|
314
|
-
// `observerUrlFrom`, which swallows every failure into `warning`).
|
|
315
|
-
const observerMint = startObserverMint(parsed);
|
|
316
393
|
const execution = parsed.command === 'run'
|
|
317
394
|
? isAuthoredFlowPath(parsed.value)
|
|
318
395
|
? await runDirectFlow(parsed.value, parsed.input, parsed.dataDir, lifecycle)
|
|
319
396
|
: await runFlow(parsed.value, parsed.dataDir, lifecycle)
|
|
320
397
|
: await resumeFlow(parsed.value, parsed.dataDir, lifecycle);
|
|
398
|
+
// The link is scoped to the run's channel, so it exists only once the run
|
|
399
|
+
// does. `finish` drains the projection and settles the mint, both bounded;
|
|
400
|
+
// it never rejects (see `createObserverSession`).
|
|
401
|
+
const observerMint = observer?.finish(execution.report);
|
|
321
402
|
// In `--json` mode the report is a single machine-readable object that
|
|
322
403
|
// MUST carry `observerUrl` when one is available, so a consumer sees one
|
|
323
404
|
// authoritative signal. That justifies blocking up to `MINT_TIMEOUT_MS`
|
|
@@ -329,12 +410,17 @@ export async function runCli(
|
|
|
329
410
|
// preflight) is worse than printing `Observer:` on a later line, so we
|
|
330
411
|
// emit the run report immediately and finalize the observer link after.
|
|
331
412
|
if (parsed.json) {
|
|
332
|
-
const observerUrl = await observerUrlFrom(observerMint,
|
|
333
|
-
emitRunReport(execution, parsed.json,
|
|
413
|
+
const observerUrl = await observerUrlFrom(observerMint, logged);
|
|
414
|
+
emitRunReport(execution, parsed.json, logged, observerUrl, await mirror?.receipt());
|
|
415
|
+
// Last, and after the report: the run's own output is what the mirror
|
|
416
|
+
// uploads, and a run's exit code has never waited on Cloud. The mirror
|
|
417
|
+
// bounds itself and never rejects.
|
|
418
|
+
await mirror?.finish(execution.report);
|
|
334
419
|
return execution.exitCode;
|
|
335
420
|
}
|
|
336
|
-
emitRunReport(execution, parsed.json,
|
|
337
|
-
await finalizeObserverLine(observerMint,
|
|
421
|
+
emitRunReport(execution, parsed.json, logged);
|
|
422
|
+
await finalizeObserverLine(observerMint, logged);
|
|
423
|
+
await mirror?.finish(execution.report);
|
|
338
424
|
return execution.exitCode;
|
|
339
425
|
}
|
|
340
426
|
|
|
@@ -369,35 +455,6 @@ async function checkAuthoredFlowComposed(path: string): Promise<{ report: CheckR
|
|
|
369
455
|
};
|
|
370
456
|
}
|
|
371
457
|
|
|
372
|
-
/**
|
|
373
|
-
* Start the observer-token mint if the environment says one should happen.
|
|
374
|
-
* Returns `undefined` when no attempt should be made — no workspace key
|
|
375
|
-
* configured, or `FLOWS_NO_OBSERVER=1` / `--no-observer-link` — which is the
|
|
376
|
-
* silent-skip branch. The returned promise always resolves; a rejection here
|
|
377
|
-
* would slip past `observerUrlFrom` and could fail the run, which the feature
|
|
378
|
-
* expressly forbids.
|
|
379
|
-
*/
|
|
380
|
-
function startObserverMint(
|
|
381
|
-
parsed: { command: 'run' | 'resume'; noObserverLink: boolean },
|
|
382
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
383
|
-
mint: (options: MintObserverOptions) => Promise<{ observerUrl?: string; warning?: string }> = mintObserverUrl,
|
|
384
|
-
): Promise<{ observerUrl?: string; warning?: string }> | undefined {
|
|
385
|
-
if (parsed.noObserverLink) return undefined;
|
|
386
|
-
// `resolveObserverLinkEnv` (not `readObserverLinkEnv`) falls back to the
|
|
387
|
-
// `agent-relay cloud login` workspace store (~/.agentworkforce/relay/
|
|
388
|
-
// workspaces.json) when RELAYCAST_WORKSPACE_KEY is unset. Env wins if set;
|
|
389
|
-
// FLOWS_NO_OBSERVER=1 still suppresses regardless of source.
|
|
390
|
-
const link = resolveObserverLinkEnv(env);
|
|
391
|
-
if (link.suppressed || link.workspaceKey === undefined) return undefined;
|
|
392
|
-
return mint({
|
|
393
|
-
workspaceKey: link.workspaceKey,
|
|
394
|
-
...(link.baseUrl !== undefined ? { baseUrl: link.baseUrl } : {}),
|
|
395
|
-
...(link.dashboardUrl !== undefined ? { dashboardUrl: link.dashboardUrl } : {}),
|
|
396
|
-
}).catch((error) => ({
|
|
397
|
-
warning: error instanceof Error ? error.message : 'unknown mint error',
|
|
398
|
-
}));
|
|
399
|
-
}
|
|
400
|
-
|
|
401
458
|
/**
|
|
402
459
|
* Resolve the pending observer-mint into a URL (or nothing), and route any
|
|
403
460
|
* mint warning to stderr under a `[observer]` label. Silent-skip (`undefined`
|
|
@@ -590,6 +647,7 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
590
647
|
let sawDataDir = false;
|
|
591
648
|
let spawn = true;
|
|
592
649
|
let noObserverLink = false;
|
|
650
|
+
let cloudMirror = false;
|
|
593
651
|
let input: string | undefined;
|
|
594
652
|
let sawInput = false;
|
|
595
653
|
let reuseFromRunId: string | undefined;
|
|
@@ -648,6 +706,14 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
648
706
|
noObserverLink = true;
|
|
649
707
|
continue;
|
|
650
708
|
}
|
|
709
|
+
if (argument === '--cloud-mirror') {
|
|
710
|
+
// Only meaningful where a local run exists to mirror. Refused on `check`
|
|
711
|
+
// (which starts nothing) and, below, on `--cloud` (which IS the hosted
|
|
712
|
+
// run), so the flag never silently no-ops.
|
|
713
|
+
if (command === 'check' || cloudMirror) return undefined;
|
|
714
|
+
cloudMirror = true;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
651
717
|
if (argument === '--data-dir') {
|
|
652
718
|
const value = args[index + 1];
|
|
653
719
|
if (command === 'check' || sawDataDir || value === undefined || value.startsWith('-')) return undefined;
|
|
@@ -691,7 +757,8 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
691
757
|
// observer-link opt-out -- describes nothing there and is refused rather
|
|
692
758
|
// than ignored. `--input` is the authored body's argument and travels with
|
|
693
759
|
// the source, so it is accepted exactly where a local run accepts it.
|
|
694
|
-
if (allowHumanInfluenced || sawDataDir || !spawn || localAgent || noObserverLink ||
|
|
760
|
+
if (allowHumanInfluenced || sawDataDir || !spawn || localAgent || noObserverLink || cloudMirror
|
|
761
|
+
|| reuseFromRunId !== undefined) return undefined;
|
|
695
762
|
if (sawInput && !isAuthoredFlowPath(positionals[0]!)) return undefined;
|
|
696
763
|
return { command: 'cloud-run', value: positionals[0]!, json, wait, input, syncCode, noConnect };
|
|
697
764
|
}
|
|
@@ -702,8 +769,8 @@ function parseArgs(args: readonly string[]): ParsedArgs | undefined {
|
|
|
702
769
|
return command === 'check'
|
|
703
770
|
? { command, json, watch, value: positionals[0]! }
|
|
704
771
|
: command === 'run'
|
|
705
|
-
? { command, bucket, reuseFromRunId, localAgent, agentCapacity, dataDir, input, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! }
|
|
706
|
-
: { command, localAgent, agentCapacity, dataDir, json, spawn, noObserverLink, allowHumanInfluenced, value: positionals[0]! };
|
|
772
|
+
? { command, bucket, reuseFromRunId, localAgent, agentCapacity, dataDir, input, json, spawn, noObserverLink, cloudMirror, allowHumanInfluenced, value: positionals[0]! }
|
|
773
|
+
: { command, localAgent, agentCapacity, dataDir, json, spawn, noObserverLink, cloudMirror, allowHumanInfluenced, value: positionals[0]! };
|
|
707
774
|
}
|
|
708
775
|
|
|
709
776
|
/**
|
|
@@ -941,6 +1008,18 @@ function parseTickArgs(rest: readonly string[]): ParsedArgs | undefined {
|
|
|
941
1008
|
};
|
|
942
1009
|
}
|
|
943
1010
|
|
|
1011
|
+
/**
|
|
1012
|
+
* `agent_worker_unresolved` reads as a footnote to `REQUIRES codex (step
|
|
1013
|
+
* "implement"), …`: that line already names the steps that need an agent
|
|
1014
|
+
* worker, and this says what has to be true for one to be attached. So the
|
|
1015
|
+
* plain-text pass holds it back and emits it in that position, exactly once —
|
|
1016
|
+
* the leading batch below skips it rather than printing it twice. JSON mode
|
|
1017
|
+
* returns before any of this and keeps the single ordered diagnostics array.
|
|
1018
|
+
*/
|
|
1019
|
+
function isWorkerSurfaceWarning(diagnostic: CheckReport['diagnostics'][number]): boolean {
|
|
1020
|
+
return diagnostic.kind === 'agent_worker_unresolved';
|
|
1021
|
+
}
|
|
1022
|
+
|
|
944
1023
|
const MODEL_PROVENANCE: Readonly<Record<CliModelSource, string>> = {
|
|
945
1024
|
step: 'step',
|
|
946
1025
|
named: 'named agent',
|
|
@@ -953,16 +1032,20 @@ function modelProvenance(source: CliModelSource | undefined): string {
|
|
|
953
1032
|
}
|
|
954
1033
|
|
|
955
1034
|
function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
|
|
956
|
-
emitDiagnostics(report.diagnostics, io);
|
|
957
1035
|
if (json) {
|
|
1036
|
+
emitDiagnostics(report.diagnostics, io);
|
|
958
1037
|
io.stdout(JSON.stringify(report));
|
|
959
1038
|
return;
|
|
960
1039
|
}
|
|
1040
|
+
const deferred = report.diagnostics.filter(isWorkerSurfaceWarning);
|
|
1041
|
+
emitDiagnostics(report.diagnostics.filter((diagnostic) => !isWorkerSurfaceWarning(diagnostic)), io);
|
|
961
1042
|
for (const gate of report.gates) {
|
|
962
1043
|
// A gate that accepts every output is legal, but it must not read like a
|
|
963
1044
|
// gate that judges something.
|
|
964
1045
|
const vacuous = gate.acceptsAnyOutput === true ? ' [json_schema accepts any output]' : '';
|
|
965
|
-
|
|
1046
|
+
// Red-but-complete is a declared shape, so it is a visible one.
|
|
1047
|
+
const recorded = gate.recordsNonZeroExit === true ? ' [onNonZero: record]' : '';
|
|
1048
|
+
io.stdout(`GATE step "${gate.stepId}" ${gate.checks.join('+')} from data (kernel, journal-replayable)${vacuous}${recorded}`);
|
|
966
1049
|
}
|
|
967
1050
|
for (const schedule of report.schedules ?? []) {
|
|
968
1051
|
const declared = schedule.cron !== undefined
|
|
@@ -1001,6 +1084,7 @@ function emitCheckReport(report: CheckReport, json: boolean, io: CliIo): void {
|
|
|
1001
1084
|
// the hosted verbs check the same list against Cloud before submitting.
|
|
1002
1085
|
const requires = report.requirements === undefined ? '' : describeFlowRequirements(report.requirements);
|
|
1003
1086
|
if (requires) io.stdout(`REQUIRES ${requires}`);
|
|
1087
|
+
emitDiagnostics(deferred, io);
|
|
1004
1088
|
if (report.ok) io.stdout(`CHECK PASSED ${report.path ?? ''}`.trimEnd());
|
|
1005
1089
|
}
|
|
1006
1090
|
|
|
@@ -1009,6 +1093,7 @@ function emitRunReport(
|
|
|
1009
1093
|
json: boolean,
|
|
1010
1094
|
io: CliIo,
|
|
1011
1095
|
observerUrl?: string,
|
|
1096
|
+
mirror?: CloudMirrorReceipt,
|
|
1012
1097
|
): void {
|
|
1013
1098
|
const { report } = execution;
|
|
1014
1099
|
emitDiagnostics(report.diagnostics, io);
|
|
@@ -1016,7 +1101,17 @@ function emitRunReport(
|
|
|
1016
1101
|
// Fold `observerUrl` into the JSON report as a sibling of `runId`, so
|
|
1017
1102
|
// downstream tooling that consumes `--json` gets the same signal a
|
|
1018
1103
|
// human reader gets from the plain-text `Observer:` line.
|
|
1019
|
-
|
|
1104
|
+
//
|
|
1105
|
+
// `cloudRunId` and `dashboardUrl` ride beside it for the same reason, and
|
|
1106
|
+
// they close a sharper gap: the report's own `runId` is the *journal's*,
|
|
1107
|
+
// and every hosted read verb (`flows status --cloud`, `flows logs`,
|
|
1108
|
+
// `flows runs`) takes Cloud's. Without these a script that mirrored a run
|
|
1109
|
+
// had no handle on it at all, and a human had to read one out of a URL.
|
|
1110
|
+
const payload = {
|
|
1111
|
+
...report,
|
|
1112
|
+
...(observerUrl === undefined ? {} : { observerUrl }),
|
|
1113
|
+
...(mirror === undefined ? {} : mirror),
|
|
1114
|
+
};
|
|
1020
1115
|
io.stdout(JSON.stringify(payload));
|
|
1021
1116
|
return;
|
|
1022
1117
|
}
|
package/src/cloud-deploy.ts
CHANGED
|
@@ -23,18 +23,22 @@ import { assertNoUseDependencies, collectExtensionSubmissions } from './flow-ext
|
|
|
23
23
|
* inside a fresh branch of the deployment's repository.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
export const FLOW_TRIGGER_PROVIDERS = ['github', 'linear', 'jira', 'shortcut', 'slack'] as const;
|
|
26
|
+
export const FLOW_TRIGGER_PROVIDERS = ['github', 'gitlab', 'linear', 'jira', 'shortcut', 'slack'] as const;
|
|
27
27
|
export type FlowTriggerProvider = (typeof FLOW_TRIGGER_PROVIDERS)[number];
|
|
28
28
|
|
|
29
29
|
/** Settings Cloud's launcher prefilter reads per provider (`flow-trigger-sources.ts`). */
|
|
30
30
|
const PROVIDER_SETTINGS: Record<FlowTriggerProvider, readonly string[]> = {
|
|
31
31
|
// `events`: `issues` (default) or `pull_request` — which GitHub records
|
|
32
|
-
// wake the listener (AgentWorkforce/cloud#3772).
|
|
33
|
-
|
|
32
|
+
// wake the listener (AgentWorkforce/cloud#3772). `reviews`, `checks` and
|
|
33
|
+
// `comments` opt a pull_request source out of its extra subscriptions.
|
|
34
|
+
github: ['repository', 'labels', 'contains', 'events', 'reviews', 'checks', 'comments'],
|
|
35
|
+
gitlab: ['project', 'labels', 'contains', 'events'],
|
|
34
36
|
slack: ['channel', 'contains'],
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
// `events`: `issues` (default), `assigned` (issues assigned to the agent's
|
|
38
|
+
// connected app user), or `all` — which Linear records wake the listener.
|
|
39
|
+
linear: ['team', 'project', 'labels', 'contains', 'events'],
|
|
40
|
+
jira: ['project', 'labels', 'contains'],
|
|
41
|
+
shortcut: ['workspace', 'team', 'labels', 'contains'],
|
|
38
42
|
};
|
|
39
43
|
const MAX_SOURCE_BYTES = 256_000;
|
|
40
44
|
const MAX_SETTING_LENGTH = 500;
|
|
@@ -135,12 +139,26 @@ export function parseTriggerSource(value: string): FlowTriggerSource {
|
|
|
135
139
|
if (key === 'events') {
|
|
136
140
|
// Cloud's enum is lowercase; send it that way whatever the shell typed.
|
|
137
141
|
const events = setting.toLowerCase();
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
const valid =
|
|
143
|
+
provider === 'gitlab' ? ['issues', 'merge_request']
|
|
144
|
+
: provider === 'linear' ? ['issues', 'assigned', 'all']
|
|
145
|
+
: ['issues', 'pull_request'];
|
|
146
|
+
if (!valid.includes(events)) {
|
|
147
|
+
throw new CloudFlowError('invalid_input',
|
|
148
|
+
`${provider} events must be ${valid.map(v => `"${v}"`).join(' or ')}, got "${setting}".`);
|
|
140
149
|
}
|
|
141
150
|
settings[key] = events;
|
|
142
151
|
continue;
|
|
143
152
|
}
|
|
153
|
+
if (provider === 'github' && (key === 'reviews' || key === 'checks' || key === 'comments')) {
|
|
154
|
+
// Subscription opt-outs are boolean; Cloud refuses any other value.
|
|
155
|
+
const toggle = setting.toLowerCase();
|
|
156
|
+
if (!['true', 'false'].includes(toggle)) {
|
|
157
|
+
throw new CloudFlowError('invalid_input', `github ${key} must be "true" or "false", got "${setting}".`);
|
|
158
|
+
}
|
|
159
|
+
settings[key] = toggle;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
144
162
|
settings[key] = setting;
|
|
145
163
|
}
|
|
146
164
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Which Cloud run mirrored which journal, remembered between invocations.
|
|
2
|
+
//
|
|
3
|
+
// A journal outlives the process that started it: `flows run` parks on an
|
|
4
|
+
// `f.human`, exits, and some time later a different `flows resume` picks the
|
|
5
|
+
// same journal up. Cloud models that second attempt as its own run row — its
|
|
6
|
+
// own hosted resume mints a fresh run id too — so a parked-then-resumed flow
|
|
7
|
+
// is two rows however it is driven. What made that read as two *unrelated*
|
|
8
|
+
// pieces of work was that the resuming process had no idea the first attempt
|
|
9
|
+
// existed: nothing on disk connected the journal to the Cloud run mirroring
|
|
10
|
+
// it, so the second registration could not name its predecessor.
|
|
11
|
+
//
|
|
12
|
+
// This is that connection, and deliberately nothing more.
|
|
13
|
+
//
|
|
14
|
+
// ## What is stored, and what is not
|
|
15
|
+
//
|
|
16
|
+
// The run id and the deployment that issued it. **Never the credential.** The
|
|
17
|
+
// run token can write this run's steps and read the workspace's runs; leaving
|
|
18
|
+
// one in a dotfile under a developer's home directory, for every run they
|
|
19
|
+
// have ever started, would be a worse trade than the feature is worth. A
|
|
20
|
+
// resume mints its own credential the same way the first attempt did — the
|
|
21
|
+
// registration request it already makes.
|
|
22
|
+
//
|
|
23
|
+
// So an entry is not a capability. Everything in it is already in the URL the
|
|
24
|
+
// first attempt printed to the terminal.
|
|
25
|
+
//
|
|
26
|
+
// ## Failure is not an error
|
|
27
|
+
//
|
|
28
|
+
// Every operation here is best-effort and silent. A missing entry means the
|
|
29
|
+
// resumed run registers without naming a predecessor, which is exactly what
|
|
30
|
+
// happened before this file existed; an unreadable or corrupt one means the
|
|
31
|
+
// same. A mirror is an observer, and bookkeeping for an observer must never
|
|
32
|
+
// be able to fail a run.
|
|
33
|
+
|
|
34
|
+
import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises';
|
|
35
|
+
import { join, resolve } from 'node:path';
|
|
36
|
+
|
|
37
|
+
/** Where the ledger lives, beside the journals it describes. */
|
|
38
|
+
export const LEDGER_DIRECTORY = 'cloud-runs';
|
|
39
|
+
/**
|
|
40
|
+
* How long an entry is worth keeping.
|
|
41
|
+
*
|
|
42
|
+
* A resume of a two-week-old parked run is a real thing; a resume of a run
|
|
43
|
+
* from last quarter is not the case this serves, and a ledger that only grows
|
|
44
|
+
* is a directory nobody ever looks at filling up forever.
|
|
45
|
+
*/
|
|
46
|
+
export const LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
47
|
+
/** Entries kept. Past it, the oldest are dropped on the next write. */
|
|
48
|
+
export const LEDGER_MAX_ENTRIES = 500;
|
|
49
|
+
|
|
50
|
+
/** One journal's mirror, as the ledger records it. */
|
|
51
|
+
export interface LedgerEntry {
|
|
52
|
+
/** Cloud's run id for the attempt that mirrored this journal. */
|
|
53
|
+
cloudRunId: string;
|
|
54
|
+
/** The deployment that issued it; an entry from another one is not this one's. */
|
|
55
|
+
apiUrl: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const JOURNAL_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
|
|
59
|
+
const CLOUD_RUN_ID = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
60
|
+
|
|
61
|
+
function entryPath(dataDir: string, journalRunId: string): string {
|
|
62
|
+
return join(resolve(dataDir), LEDGER_DIRECTORY, `${journalRunId}.json`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Record that `journalRunId` is mirrored by `entry.cloudRunId`.
|
|
67
|
+
*
|
|
68
|
+
* Written with mode 0600 for the same reason the journals are private: it
|
|
69
|
+
* names runs in someone's workspace, and a shared machine should not publish
|
|
70
|
+
* that. It is not a secret — there is nothing here to steal — but it is
|
|
71
|
+
* nobody else's business either.
|
|
72
|
+
*/
|
|
73
|
+
export async function recordMirroredRun(
|
|
74
|
+
dataDir: string,
|
|
75
|
+
journalRunId: string,
|
|
76
|
+
entry: LedgerEntry,
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
if (!JOURNAL_RUN_ID.test(journalRunId) || !CLOUD_RUN_ID.test(entry.cloudRunId)) return;
|
|
79
|
+
const directory = join(resolve(dataDir), LEDGER_DIRECTORY);
|
|
80
|
+
try {
|
|
81
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
82
|
+
await writeFile(
|
|
83
|
+
entryPath(dataDir, journalRunId),
|
|
84
|
+
JSON.stringify({ cloudRunId: entry.cloudRunId, apiUrl: entry.apiUrl }),
|
|
85
|
+
{ mode: 0o600 },
|
|
86
|
+
);
|
|
87
|
+
} catch {
|
|
88
|
+
// A resume that cannot name its predecessor is the behaviour this file
|
|
89
|
+
// improves on, not a failure of the run.
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await prune(directory);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The Cloud run mirroring this journal, if one is recorded *for this
|
|
97
|
+
* deployment*.
|
|
98
|
+
*
|
|
99
|
+
* The deployment check is not bookkeeping. A developer who ran a flow against
|
|
100
|
+
* staging and resumes it against production must not have the resumed run
|
|
101
|
+
* claim to continue a run id that means something else there — or, worse,
|
|
102
|
+
* nothing at all, which Cloud would refuse and which would read as a bug in
|
|
103
|
+
* the resume rather than in the pairing.
|
|
104
|
+
*/
|
|
105
|
+
export async function readMirroredRun(
|
|
106
|
+
dataDir: string,
|
|
107
|
+
journalRunId: string,
|
|
108
|
+
apiUrl: string,
|
|
109
|
+
): Promise<string | undefined> {
|
|
110
|
+
if (!JOURNAL_RUN_ID.test(journalRunId)) return undefined;
|
|
111
|
+
let parsed: unknown;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(await readFile(entryPath(dataDir, journalRunId), 'utf8'));
|
|
114
|
+
} catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
|
|
118
|
+
const entry = parsed as Record<string, unknown>;
|
|
119
|
+
if (typeof entry['cloudRunId'] !== 'string' || !CLOUD_RUN_ID.test(entry['cloudRunId'])) return undefined;
|
|
120
|
+
if (entry['apiUrl'] !== apiUrl) return undefined;
|
|
121
|
+
return entry['cloudRunId'];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Drop entries past their age, then past the count. Never throws. */
|
|
125
|
+
async function prune(directory: string): Promise<void> {
|
|
126
|
+
try {
|
|
127
|
+
const names = (await readdir(directory)).filter(name => name.endsWith('.json'));
|
|
128
|
+
if (names.length <= LEDGER_MAX_ENTRIES) {
|
|
129
|
+
// Still age out: a data dir that never reaches the count cap would
|
|
130
|
+
// otherwise keep its first entry forever.
|
|
131
|
+
await dropOlderThan(directory, names, Date.now() - LEDGER_MAX_AGE_MS);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const ages = await Promise.all(names.map(async name => {
|
|
135
|
+
try {
|
|
136
|
+
return { name, at: (await stat(join(directory, name))).mtimeMs };
|
|
137
|
+
} catch {
|
|
138
|
+
return { name, at: 0 };
|
|
139
|
+
}
|
|
140
|
+
}));
|
|
141
|
+
ages.sort((left, right) => right.at - left.at);
|
|
142
|
+
await Promise.all(ages.slice(LEDGER_MAX_ENTRIES).map(entry =>
|
|
143
|
+
unlink(join(directory, entry.name)).catch(() => undefined)));
|
|
144
|
+
await dropOlderThan(directory, ages.slice(0, LEDGER_MAX_ENTRIES).map(entry => entry.name),
|
|
145
|
+
Date.now() - LEDGER_MAX_AGE_MS);
|
|
146
|
+
} catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function dropOlderThan(directory: string, names: string[], floor: number): Promise<void> {
|
|
152
|
+
await Promise.all(names.map(async name => {
|
|
153
|
+
try {
|
|
154
|
+
if ((await stat(join(directory, name))).mtimeMs < floor) {
|
|
155
|
+
await unlink(join(directory, name));
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
}
|