@relayflows/sdk 2.0.55 → 2.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/dist/adapters/base.d.ts +2 -0
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/devin.d.ts +4 -0
  4. package/dist/adapters/devin.d.ts.map +1 -0
  5. package/dist/adapters/devin.js +20 -0
  6. package/dist/adapters/devin.js.map +1 -0
  7. package/dist/adapters/index.d.ts +3 -2
  8. package/dist/adapters/index.d.ts.map +1 -1
  9. package/dist/adapters/index.js +5 -1
  10. package/dist/adapters/index.js.map +1 -1
  11. package/dist/authored-broker.d.ts +13 -0
  12. package/dist/authored-broker.d.ts.map +1 -0
  13. package/dist/authored-broker.js +75 -0
  14. package/dist/authored-broker.js.map +1 -0
  15. package/dist/authored-budget.d.ts +1 -1
  16. package/dist/authored-budget.d.ts.map +1 -1
  17. package/dist/authored-budget.js +8 -2
  18. package/dist/authored-budget.js.map +1 -1
  19. package/dist/authored-flow-error.d.ts +6 -0
  20. package/dist/authored-flow-error.d.ts.map +1 -1
  21. package/dist/authored-flow-error.js.map +1 -1
  22. package/dist/authored-flow-executor.d.ts +11 -0
  23. package/dist/authored-flow-executor.d.ts.map +1 -1
  24. package/dist/authored-flow-executor.js +104 -20
  25. package/dist/authored-flow-executor.js.map +1 -1
  26. package/dist/authored-flow-loader.d.ts.map +1 -1
  27. package/dist/authored-flow-loader.js +42 -3
  28. package/dist/authored-flow-loader.js.map +1 -1
  29. package/dist/authored-helper-effect.d.ts.map +1 -1
  30. package/dist/authored-helper-effect.js +1 -1
  31. package/dist/authored-helper-effect.js.map +1 -1
  32. package/dist/authored-hooks.d.ts +2 -1
  33. package/dist/authored-hooks.d.ts.map +1 -1
  34. package/dist/authored-hooks.js +2 -2
  35. package/dist/authored-hooks.js.map +1 -1
  36. package/dist/authored-human.d.ts +18 -0
  37. package/dist/authored-human.d.ts.map +1 -1
  38. package/dist/authored-human.js +32 -4
  39. package/dist/authored-human.js.map +1 -1
  40. package/dist/authored-mcp.d.ts.map +1 -1
  41. package/dist/authored-mcp.js +3 -2
  42. package/dist/authored-mcp.js.map +1 -1
  43. package/dist/authored-node-entry.js +4 -0
  44. package/dist/authored-node-entry.js.map +1 -1
  45. package/dist/authored-node-runner.d.ts.map +1 -1
  46. package/dist/authored-node-runner.js +1 -0
  47. package/dist/authored-node-runner.js.map +1 -1
  48. package/dist/authored-plugin-effect.d.ts.map +1 -1
  49. package/dist/authored-plugin-effect.js +3 -2
  50. package/dist/authored-plugin-effect.js.map +1 -1
  51. package/dist/authored-preflight.d.ts.map +1 -1
  52. package/dist/authored-preflight.js +2 -1
  53. package/dist/authored-preflight.js.map +1 -1
  54. package/dist/authored-root.d.ts.map +1 -1
  55. package/dist/authored-root.js +13 -3
  56. package/dist/authored-root.js.map +1 -1
  57. package/dist/authored-serial-ordinals.d.ts +41 -0
  58. package/dist/authored-serial-ordinals.d.ts.map +1 -0
  59. package/dist/authored-serial-ordinals.js +45 -0
  60. package/dist/authored-serial-ordinals.js.map +1 -0
  61. package/dist/authored-worker-step.d.ts +5 -1
  62. package/dist/authored-worker-step.d.ts.map +1 -1
  63. package/dist/authored-worker-step.js +19 -2
  64. package/dist/authored-worker-step.js.map +1 -1
  65. package/dist/budget-preflight.d.ts.map +1 -1
  66. package/dist/budget-preflight.js +18 -3
  67. package/dist/budget-preflight.js.map +1 -1
  68. package/dist/bundle-transport.d.ts +2 -0
  69. package/dist/bundle-transport.d.ts.map +1 -1
  70. package/dist/bundle-transport.js +4 -0
  71. package/dist/bundle-transport.js.map +1 -1
  72. package/dist/cached-flow-runtime.d.ts +3 -0
  73. package/dist/cached-flow-runtime.d.ts.map +1 -0
  74. package/dist/cached-flow-runtime.js +37 -0
  75. package/dist/cached-flow-runtime.js.map +1 -0
  76. package/dist/cli/check.d.ts.map +1 -1
  77. package/dist/cli/check.js +13 -1
  78. package/dist/cli/check.js.map +1 -1
  79. package/dist/cli/cli-probe.d.ts.map +1 -1
  80. package/dist/cli/cli-probe.js +72 -11
  81. package/dist/cli/cli-probe.js.map +1 -1
  82. package/dist/cli/cloud-deploy.js +6 -5
  83. package/dist/cli/cloud-deploy.js.map +1 -1
  84. package/dist/cli/cloud-mirror-session.d.ts +8 -2
  85. package/dist/cli/cloud-mirror-session.d.ts.map +1 -1
  86. package/dist/cli/cloud-mirror-session.js +20 -27
  87. package/dist/cli/cloud-mirror-session.js.map +1 -1
  88. package/dist/cli/direct-run.d.ts.map +1 -1
  89. package/dist/cli/direct-run.js +2 -1
  90. package/dist/cli/direct-run.js.map +1 -1
  91. package/dist/cli/run.d.ts.map +1 -1
  92. package/dist/cli/run.js +19 -4
  93. package/dist/cli/run.js.map +1 -1
  94. package/dist/cli.d.ts.map +1 -1
  95. package/dist/cli.js +39 -18
  96. package/dist/cli.js.map +1 -1
  97. package/dist/cloud-deploy.d.ts +3 -3
  98. package/dist/cloud-deploy.d.ts.map +1 -1
  99. package/dist/cloud-deploy.js +15 -9
  100. package/dist/cloud-deploy.js.map +1 -1
  101. package/dist/cloud-flow-source.d.ts +13 -0
  102. package/dist/cloud-flow-source.d.ts.map +1 -0
  103. package/dist/cloud-flow-source.js +55 -0
  104. package/dist/cloud-flow-source.js.map +1 -0
  105. package/dist/cloud-mirror-transport.d.ts +2 -0
  106. package/dist/cloud-mirror-transport.d.ts.map +1 -1
  107. package/dist/cloud-mirror-transport.js +2 -0
  108. package/dist/cloud-mirror-transport.js.map +1 -1
  109. package/dist/cloud-mirror.d.ts.map +1 -1
  110. package/dist/cloud-mirror.js +10 -1
  111. package/dist/cloud-mirror.js.map +1 -1
  112. package/dist/communication/broker-preflight.d.ts +3 -0
  113. package/dist/communication/broker-preflight.d.ts.map +1 -0
  114. package/dist/communication/broker-preflight.js +22 -0
  115. package/dist/communication/broker-preflight.js.map +1 -0
  116. package/dist/communication/broker-spec.d.ts +20 -0
  117. package/dist/communication/broker-spec.d.ts.map +1 -0
  118. package/dist/communication/broker-spec.js +46 -0
  119. package/dist/communication/broker-spec.js.map +1 -0
  120. package/dist/communication/broker-worker.d.ts +17 -0
  121. package/dist/communication/broker-worker.d.ts.map +1 -0
  122. package/dist/communication/broker-worker.js +214 -0
  123. package/dist/communication/broker-worker.js.map +1 -0
  124. package/dist/communication/environment.d.ts.map +1 -1
  125. package/dist/communication/environment.js +1 -0
  126. package/dist/communication/environment.js.map +1 -1
  127. package/dist/communication/pty.d.ts +29 -0
  128. package/dist/communication/pty.d.ts.map +1 -0
  129. package/dist/communication/pty.js +44 -0
  130. package/dist/communication/pty.js.map +1 -0
  131. package/dist/communication/relay.d.ts +18 -0
  132. package/dist/communication/relay.d.ts.map +1 -1
  133. package/dist/communication/relay.js +4 -0
  134. package/dist/communication/relay.js.map +1 -1
  135. package/dist/communication/skills.d.ts +3 -0
  136. package/dist/communication/skills.d.ts.map +1 -0
  137. package/dist/communication/skills.js +18 -0
  138. package/dist/communication/skills.js.map +1 -0
  139. package/dist/communication/worker.d.ts.map +1 -1
  140. package/dist/communication/worker.js +9 -26
  141. package/dist/communication/worker.js.map +1 -1
  142. package/dist/devin-runtime.d.ts +10 -0
  143. package/dist/devin-runtime.d.ts.map +1 -0
  144. package/dist/devin-runtime.js +37 -0
  145. package/dist/devin-runtime.js.map +1 -0
  146. package/dist/failure-kinds.d.ts +1 -1
  147. package/dist/failure-kinds.d.ts.map +1 -1
  148. package/dist/failure-kinds.js +2 -0
  149. package/dist/failure-kinds.js.map +1 -1
  150. package/dist/plugin-manifest.js +1 -1
  151. package/dist/plugin-manifest.js.map +1 -1
  152. package/dist/preflight.d.ts +1 -1
  153. package/dist/preflight.d.ts.map +1 -1
  154. package/dist/preflight.js +14 -4
  155. package/dist/preflight.js.map +1 -1
  156. package/dist/step-secrets.d.ts.map +1 -1
  157. package/dist/step-secrets.js +1 -0
  158. package/dist/step-secrets.js.map +1 -1
  159. package/dist/worker-cli.d.ts.map +1 -1
  160. package/dist/worker-cli.js +15 -3
  161. package/dist/worker-cli.js.map +1 -1
  162. package/dist/worker.d.ts +1 -0
  163. package/dist/worker.d.ts.map +1 -1
  164. package/dist/worker.js +12 -0
  165. package/dist/worker.js.map +1 -1
  166. package/package.json +2 -2
  167. package/src/adapters/base.ts +2 -0
  168. package/src/adapters/devin.ts +22 -0
  169. package/src/adapters/index.ts +5 -2
  170. package/src/authored-broker.ts +73 -0
  171. package/src/authored-budget.ts +6 -2
  172. package/src/authored-flow-error.ts +6 -0
  173. package/src/authored-flow-executor.ts +115 -20
  174. package/src/authored-flow-loader.ts +43 -3
  175. package/src/authored-helper-effect.ts +2 -1
  176. package/src/authored-hooks.ts +4 -3
  177. package/src/authored-human.ts +35 -4
  178. package/src/authored-mcp.ts +7 -2
  179. package/src/authored-node-entry.ts +4 -1
  180. package/src/authored-node-runner.ts +1 -0
  181. package/src/authored-plugin-effect.ts +7 -2
  182. package/src/authored-preflight.ts +2 -1
  183. package/src/authored-root.ts +13 -3
  184. package/src/authored-serial-ordinals.ts +76 -0
  185. package/src/authored-worker-step.ts +17 -2
  186. package/src/budget-preflight.ts +16 -2
  187. package/src/bundle-transport.ts +4 -0
  188. package/src/cached-flow-runtime.ts +32 -0
  189. package/src/cli/check.ts +11 -1
  190. package/src/cli/cli-probe.ts +65 -11
  191. package/src/cli/cloud-deploy.ts +5 -5
  192. package/src/cli/cloud-mirror-session.ts +23 -29
  193. package/src/cli/direct-run.ts +2 -1
  194. package/src/cli/run.ts +18 -3
  195. package/src/cli.ts +31 -13
  196. package/src/cloud-deploy.ts +19 -14
  197. package/src/cloud-flow-source.ts +58 -0
  198. package/src/cloud-mirror-transport.ts +4 -0
  199. package/src/cloud-mirror.ts +9 -1
  200. package/src/communication/broker-preflight.ts +22 -0
  201. package/src/communication/broker-spec.ts +48 -0
  202. package/src/communication/broker-worker.ts +198 -0
  203. package/src/communication/environment.ts +1 -0
  204. package/src/communication/pty.ts +51 -0
  205. package/src/communication/relay.ts +12 -1
  206. package/src/communication/skills.ts +17 -0
  207. package/src/communication/worker.ts +9 -26
  208. package/src/devin-runtime.ts +33 -0
  209. package/src/failure-kinds.ts +2 -0
  210. package/src/plugin-manifest.ts +1 -1
  211. package/src/preflight.ts +15 -4
  212. package/src/step-secrets.ts +1 -0
  213. package/src/worker-cli.ts +13 -5
  214. package/src/worker.ts +11 -0
@@ -0,0 +1,76 @@
1
+ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
2
+
3
+ /**
4
+ * Serial step ids are `<kind>-<N>`, with ONE ordinal counter shared by every
5
+ * kind and assigned in call order (authored-flow-executor `nextId`). Inside a
6
+ * `Promise.all` that call order follows result arrival: each branch's next
7
+ * call is made when its previous step resolves. A resume replays completed
8
+ * results from the journal, in whatever order they come back, so two
9
+ * branches can take each other's ordinals.
10
+ *
11
+ * The kernel catches part of that: an admission key is bound to its spec, so
12
+ * `run-3` re-requested with a different command is `run_admission_conflict`.
13
+ * It cannot catch a KIND swap. `run-3` and `llm-3` are different admission
14
+ * keys, so a resumed `f.run` that lands on the ordinal an `llm` already
15
+ * holds gets a fresh key and runs again, exiting 0. This guard refuses that
16
+ * from the root's journaled step index before the step is admitted.
17
+ *
18
+ * The root driver reads the index, plus the root's `wait.human` ids, once
19
+ * per re-dispatched attempt (authored-root.ts); a first attempt reads
20
+ * nothing. The guard adds no journal record of its own. It sees what those
21
+ * hold — `f.llm`/`f.agent` from admission, an `f.dispatch` from its child's
22
+ * first step, an `f.human` from its park, other steps from completion — so an
23
+ * ordinal whose only holder was an `f.run` or effect still in flight at the
24
+ * crash is invisible to it. Stable per-branch identities (a new identity
25
+ * version) remove the class.
26
+ */
27
+ export type SerialOrdinals = ReadonlyMap<number, ReadonlyArray<{ readonly kind: string; readonly step: string }>>;
28
+
29
+ const SERIAL_ID = /^(.+)-([1-9][0-9]*)$/u;
30
+
31
+ /**
32
+ * The ordinals journaled directly under `prefix` (a dispatched child's
33
+ * `dispatch-N--`, or '' for the root body). A descendant's step claims its
34
+ * dispatch's ordinal here: `dispatch-3--run-1` holds ordinal 3 for `dispatch`
35
+ * even before the child's receipt is journaled. Lowered `.gate` steps and
36
+ * anything not shaped `<kind>-<N>` hold none.
37
+ */
38
+ export function serialOrdinals(journaledSteps: readonly string[], prefix: string): SerialOrdinals {
39
+ const ordinals = new Map<number, Array<{ kind: string; step: string }>>();
40
+ for (const step of journaledSteps) {
41
+ if (!step.startsWith(prefix)) continue;
42
+ const local = step.slice(prefix.length);
43
+ const descendant = local.indexOf('--');
44
+ const match = SERIAL_ID.exec(descendant < 0 ? local : local.slice(0, descendant));
45
+ if (match === null || (descendant >= 0 && match[1] !== 'dispatch')) continue;
46
+ const ordinal = Number(match[2]);
47
+ if (!Number.isSafeInteger(ordinal)) continue;
48
+ const holders = ordinals.get(ordinal) ?? [];
49
+ holders.push({ kind: match[1]!, step });
50
+ ordinals.set(ordinal, holders);
51
+ }
52
+ return ordinals;
53
+ }
54
+
55
+ /** Refuse `<kind>-<ordinal>` when the journal holds that ordinal under another kind. */
56
+ export function assertSerialOrdinal(ordinals: SerialOrdinals, kind: string, ordinal: number): void {
57
+ const journaled = ordinals.get(ordinal)?.find(holder => holder.kind !== kind);
58
+ if (journaled === undefined) return;
59
+ throw serialReplayOrderMismatch(
60
+ `ordinal ${ordinal} is journaled as ${kindName(journaled.kind)} step "${journaled.step}", `
61
+ + `but this resume reached it with ${kindName(kind)}`,
62
+ );
63
+ }
64
+
65
+ export function serialReplayOrderMismatch(detail: string): AuthoredFlowExecutionError {
66
+ return new AuthoredFlowExecutionError(
67
+ 'serial_replay_order_mismatch',
68
+ `serial replay order mismatch: ${detail}. Concurrent branches resolved in a different order `
69
+ + 'than the journaled run, so the step was refused instead of executed.',
70
+ );
71
+ }
72
+
73
+ /** Helper ids embed the root run id (`slack-<root>-N`); name the provider only. */
74
+ function kindName(kind: string): string {
75
+ return kind.replace(/-[0-9A-HJKMNP-TV-Z]{26}$/u, '');
76
+ }
@@ -1,3 +1,5 @@
1
+ import { authoredBroker } from './authored-broker.js';
2
+ import { brokerInstruction, brokerOptionsError } from './communication/broker-spec.js';
1
3
  import { dirname, resolve, isAbsolute, relative, sep } from 'node:path';
2
4
  import type { AuthoredBudget } from './authored-budget.js';
3
5
  import { parseBudget } from './budget.js';
@@ -159,14 +161,24 @@ export function authoredWorkerRunner(
159
161
  onCompleted === undefined ? context : { ...context, onCompleted }, execution.report.status);
160
162
  };
161
163
  const admissionKey = authoredChildAdmissionKey(rootRunId, id);
164
+ const cleanup = step.type === 'agent' && brokerInstruction(step.instruction)?.operation === 'release';
162
165
  const admit = async () => budget === undefined
163
166
  ? consume(await journal.runStart(spec, undefined, admissionKey))
164
- : budget.execute(journal, spec, consume, admissionKey);
167
+ : budget.execute(journal, spec, consume, admissionKey, false, cleanup);
165
168
  return slotScopes === undefined ? admit() : slotScopes[step.type === 'llm' ? 'llm' : 'agent'].run(admit);
166
169
  }
167
170
 
171
+ const broker = authoredBroker(definition, rootRunId, localAgentStream, async step => {
172
+ const output = await run(step);
173
+ if (typeof output === 'object' && output !== null && AGENT_TIMEOUT in output) {
174
+ return { completionReason: 'timeout', summary: String(output[AGENT_TIMEOUT]), artifacts: [] };
175
+ }
176
+ return output;
177
+ });
168
178
  return {
169
179
  slots,
180
+ message: broker.message,
181
+ release: broker.release,
170
182
  /** Refuse every agent/LLM call still waiting for a worker slot (body teardown). */
171
183
  stop(reason: unknown): void {
172
184
  if (ownsSlots) {
@@ -177,9 +189,12 @@ export function authoredWorkerRunner(
177
189
  slotScopes?.llm.close(reason);
178
190
  }
179
191
  },
180
- async agent(id: string, options: AgentOptions, verification?: NamedGate): Promise<AgentResult> {
192
+ async agent(id: string, options: AgentOptions, verification?: NamedGate, name = id): Promise<AgentResult> {
181
193
  const secrets = validateStepSecrets((options as AgentOptions & { secrets?: unknown }).secrets,
182
194
  agentEnvironment ?? process.env);
195
+ if (options.transport === 'broker') return broker.agent(id, name, options, verification, secrets);
196
+ const brokerProblem = brokerOptionsError(options);
197
+ if (brokerProblem) throw new AuthoredFlowExecutionError('agent_cli_unresolved', brokerProblem);
183
198
  if (options.workspace !== undefined && localAgentStream !== undefined) {
184
199
  throw new AuthoredFlowExecutionError('unsupported_workspace_permission',
185
200
  'The local agent worker accepts stream-only steps. Remove workspace or attach a worker that holds its revision pins.');
@@ -1,3 +1,4 @@
1
+ import { brokerInstruction } from './communication/broker-spec.js';
1
2
  import type { CompiledFlowSpec } from './compile.js';
2
3
  import type { PreflightWarning } from './preflight.js';
3
4
  import { hasPricing } from './model-pricing.js';
@@ -34,16 +35,29 @@ export function budgetDiagnostics(
34
35
  flow: CompiledFlowSpec,
35
36
  resolved: ReadonlyMap<string, BudgetStepResolution> = new Map(),
36
37
  ): PreflightWarning[] {
37
- if (flow.budget?.pricing !== 'frozen' || flow.budget.maxDollars === undefined) return [];
38
+ const dollarBudget = flow.budget?.pricing === 'frozen' && flow.budget.maxDollars !== undefined;
38
39
  const warnings: PreflightWarning[] = [];
39
40
  for (const step of flow.steps) {
40
41
  if (step.type === 'deterministic') continue;
42
+ const broker = step.type === 'agent' ? brokerInstruction(step.instruction) : undefined;
43
+ if (broker) {
44
+ if (broker.operation !== 'release') warnings.push({ severity: 'warning', kind: 'budget_unmetered', stepId: step.id,
45
+ message: `Step "${step.id}" uses a broker session that reports no token or dollar usage. Token and dollar ceilings cannot bound its spend; timeout bounds its duration.` });
46
+ continue;
47
+ }
41
48
  const resolution = resolved.get(step.id);
49
+ const cli = resolution?.cli ?? step.cli ?? (step.type === 'agent' && step.agent !== undefined
50
+ ? flow.agents?.[step.agent]?.cli : undefined);
51
+ if (cli !== undefined && resolveAdapterKind(cli) === 'devin') {
52
+ warnings.push({ severity: 'warning', kind: 'budget_unmetered', stepId: step.id,
53
+ message: `Step "${step.id}" uses Devin print mode, which reports no token or dollar usage. Token and dollar ceilings cannot bound its spend; timeout bounds its duration.` });
54
+ continue;
55
+ }
56
+ if (!dollarBudget) continue;
42
57
  const model = resolution?.model
43
58
  ?? step.model ?? (step.type === 'agent' && step.agent !== undefined
44
59
  ? flow.agents?.[step.agent]?.model : undefined);
45
60
  if (hasPricing(model)) continue;
46
- const cli = resolution?.cli ?? step.cli;
47
61
  const reason = model !== undefined
48
62
  ? `model "${model}" has no frozen price`
49
63
  : cli !== undefined && resolveAdapterKind(cli) === 'codex'
@@ -11,6 +11,10 @@ export class BundleFailure extends Error {
11
11
  constructor(readonly kind: BundleFailureKind, message: string) { super(message); }
12
12
  }
13
13
  export interface DigestReference { name: string; digest: string }
14
+ /** Bare cloud names exclude file extensions and directory separators. */
15
+ export function isFlowName(value: string): boolean {
16
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/u.test(value);
17
+ }
14
18
  export function parseDigestReference(value: string): DigestReference | undefined {
15
19
  // Match the safe single-component name grammar used by sealBundle.
16
20
  const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)@sha256:([0-9a-f]{64})$/.exec(value);
@@ -0,0 +1,32 @@
1
+ import { createRequire } from 'node:module';
2
+ import { mkdir, readFile, realpath, symlink } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ /** Cached single-file flows use the CLI's installed authoring runtime. */
7
+ export async function prepareCachedFlowRuntime(directory: string): Promise<void> {
8
+ const require = createRequire(import.meta.url);
9
+ const packages = [
10
+ ['@relayflows/surface', require.resolve('@relayflows/surface/runtime')],
11
+ ['@relayflows/sdk', fileURLToPath(import.meta.url)],
12
+ ] as const;
13
+ for (const [name, entry] of packages) {
14
+ let root = dirname(entry);
15
+ for (;;) {
16
+ const manifest = await readFile(join(root, 'package.json'), 'utf8').catch(() => '{}');
17
+ if ((JSON.parse(manifest) as { name?: string }).name === name) break;
18
+ const parent = dirname(root);
19
+ if (parent === root) throw new Error(`Cannot locate installed ${name}.`);
20
+ root = parent;
21
+ }
22
+ const target = join(directory, 'node_modules', name);
23
+ await mkdir(dirname(target), { recursive: true });
24
+ try { await symlink(root, target, 'dir'); }
25
+ catch (error) {
26
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
27
+ if (await realpath(target) !== await realpath(root)) {
28
+ throw new Error(`Cached runtime at ${target} differs from this CLI. Remove that link and retry.`);
29
+ }
30
+ }
31
+ }
32
+ }
package/src/cli/check.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { checkBrokerEnvironment } from '../communication/broker-preflight.js';
2
+ import { brokerInstruction } from '../communication/broker-spec.js';
1
3
  import { validateSpecWithDaemon, type DaemonValidation, type DaemonValidationMode, type DaemonValidationDeps } from '../daemon-spec-validation.js';
2
4
  import { communicationInstruction } from '../communication/spec.js';
3
5
  import { rememberResolvedCliIdentities } from '../resolved-cli-identity.js';
@@ -246,6 +248,14 @@ export function checkAuthoredFlow(
246
248
  effectiveProbeCache,
247
249
  )
248
250
  : undefined;
251
+ if (flow?.steps.some(step => step.type === 'agent' && brokerInstruction(step.instruction)?.operation === 'spawn')) {
252
+ try { checkBrokerEnvironment(); }
253
+ catch (error) {
254
+ result.ok = false;
255
+ result.diagnostics.push({ severity: 'refusal', kind: 'probe_failed',
256
+ message: error instanceof Error ? error.message : 'Broker environment could not be checked.' });
257
+ }
258
+ }
249
259
  if (flow?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
250
260
  try { if (options?.communicationChecked !== true) checkCommunicationEnvironment(flow); }
251
261
  catch (error) {
@@ -520,7 +530,7 @@ function bindResolvedCliPaths(
520
530
  const resolution = byStep.get(step.id);
521
531
  if (resolution === undefined) return step;
522
532
  const directory = resolution.source === 'project' ? configDirectory : flowDirectory;
523
- const managed = step.type === 'agent' && communicationInstruction(step.instruction) !== undefined;
533
+ const managed = step.type === 'agent' && (communicationInstruction(step.instruction) !== undefined || brokerInstruction(step.instruction) !== undefined);
524
534
  const outcome = probeCache.get(cliProbeKey(resolution, managed, step.secrets));
525
535
  const executable = outcome !== undefined && 'result' in outcome
526
536
  ? outcome.result.executable
@@ -1,12 +1,14 @@
1
1
  import { accessSync, constants, realpathSync } from 'node:fs';
2
2
  import { basename, isAbsolute, resolve } from 'node:path';
3
3
  import { execFile, spawnSync } from 'node:child_process';
4
+ import { redactText } from '../agent-transcript.js';
4
5
  import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
5
6
  import { adapterIdentification, authenticationProbe, classifyModelProbeFailure, cliAdapterKind,
6
7
  displayInvocation, modelReadinessProbe, type CliInvocation } from '../cli-adapter.js';
7
8
  import { MODEL_ENV } from '../worker-cli.js';
8
9
  import { CliProbeError, type CliProbeResult } from '../preflight.js';
9
10
  import { pinCliAlias, pinCliAliasSync } from './pinned-cli-alias.js';
11
+ import { prepareDevinRuntime } from '../devin-runtime.js';
10
12
  import { agentProbeEnvironment } from '../step-secrets.js';
11
13
 
12
14
  interface ProbeRequest {
@@ -40,15 +42,18 @@ function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
40
42
  while (!next.done) {
41
43
  const request = next.value;
42
44
  const pinned = pinCliAliasSync(request.executable, request.argv0 ?? basename(request.executable));
45
+ let prepared: ReturnType<typeof prepareProbe> | undefined;
43
46
  const result = (() => {
44
47
  try {
45
- return spawnSync(pinned.executable, request.invocation.args, {
46
- ...probeOptions(request),
48
+ prepared = prepareProbe(request);
49
+ return spawnSync(pinned.executable, prepared.args, {
50
+ ...probeOptions({ ...request, environment: prepared.env }),
47
51
  // Preserve the old synchronous probe contract: provider CLIs must not
48
52
  // inherit a readable stdin that can block auth/identify probes.
49
53
  stdio: ['ignore', 'pipe', 'pipe'],
50
54
  });
51
55
  } finally {
56
+ prepared?.close();
52
57
  pinned.release();
53
58
  }
54
59
  })();
@@ -69,9 +74,12 @@ function probeOptions({ argv0, directory, invocation, environment }: ProbeReques
69
74
 
70
75
  async function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
71
76
  const pinned = await pinCliAlias(request.executable, request.argv0 ?? basename(request.executable));
77
+ let prepared: ReturnType<typeof prepareProbe> | undefined;
72
78
  try {
79
+ prepared = prepareProbe(request);
80
+ const current = prepared;
73
81
  return await new Promise((resolve, reject) => {
74
- const child = execFile(pinned.executable, request.invocation.args, probeOptions(request),
82
+ const child = execFile(pinned.executable, current.args, probeOptions({ ...request, environment: current.env }),
75
83
  (error, stdout, stderr) => {
76
84
  // Numeric exit codes are probe results; launch, timeout, signal and buffer
77
85
  // errors are failures to collect a fact, just as in the synchronous driver.
@@ -86,6 +94,7 @@ async function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
86
94
  child.stdin?.end();
87
95
  });
88
96
  } finally {
97
+ prepared?.close();
89
98
  await pinned.release();
90
99
  }
91
100
  }
@@ -133,12 +142,16 @@ function* probeSequence(
133
142
  const auth = authenticationProbe(kind);
134
143
  const authCommand = displayInvocation(cli, auth);
135
144
  if (model === undefined) {
145
+ const authProbe = yield probe(auth);
146
+ const authenticated = authProbe.status === 0;
136
147
  return {
137
148
  exists: true,
138
149
  supported: true,
139
- authenticated: (yield probe(auth)).status === 0,
150
+ authenticated,
140
151
  executable,
141
152
  authCommand,
153
+ ...(authenticated ? {} : { authExitCode: authProbe.status,
154
+ authFailureDetail: redactProbeOutput(`${authProbe.stderr}${authProbe.stdout}`, sourceEnvironment).trim().slice(0, 500) }),
142
155
  };
143
156
  }
144
157
 
@@ -168,7 +181,7 @@ function* probeSequence(
168
181
  // because Claude Code prints the provider's API error there and its own
169
182
  // catalog warnings on stderr.
170
183
  const failure = classifyModelProbeFailure(kind, `${scopedProbe.stdout}\n${scopedProbe.stderr}`);
171
- const modelDetail = redactProbeOutput(scopedProbe.stdout.trim() || scopedProbe.stderr.trim()).slice(0, 500);
184
+ const modelDetail = redactProbeOutput(scopedProbe.stdout.trim() || scopedProbe.stderr.trim(), sourceEnvironment).slice(0, 500);
172
185
  return {
173
186
  exists: true,
174
187
  supported: true,
@@ -187,7 +200,7 @@ function* probeSequence(
187
200
  : {
188
201
  authExitCode: authProbe.status,
189
202
  authFailureDetail: redactProbeOutput(
190
- `${authProbe.stderr}${authProbe.stdout}`,
203
+ `${authProbe.stderr}${authProbe.stdout}`, sourceEnvironment,
191
204
  ).trim().slice(0, 500),
192
205
  }),
193
206
  };
@@ -201,11 +214,42 @@ function* probeSequence(
201
214
  * say WHY a probe failed, which survives redaction; leaking an identity into a
202
215
  * refusal message that gets pasted into issues does not.
203
216
  */
204
- function redactProbeOutput(text: string): string {
205
- return text
206
- .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
207
- .replace(/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, '<redacted-token>')
208
- .replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
217
+ function redactProbeOutput(text: string, environment: NodeJS.ProcessEnv): string {
218
+ const key = environment.DEVIN_API_KEY?.trim();
219
+ const normalized = { ...environment, ...(key === undefined ? {} : { DEVIN_API_KEY: key }) };
220
+ // Preserve the shared redactor's known-value, header and cookie handling.
221
+ const redacted = redactText(text, normalized);
222
+ // One byte per diagnostic character bounds auxiliary memory even when a
223
+ // one-character credential matches the entire probe buffer. No span lists.
224
+ const covered = new Uint8Array(redacted.length);
225
+ const replacements = ['', '<redacted-email>', '<redacted-token>', '<redacted-hex>', '[redacted:DEVIN_API_KEY]'];
226
+ for (const [pattern, tag] of [
227
+ [/(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, 1],
228
+ [/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, 2],
229
+ [/\b[A-Fa-f0-9]{32,}\b/g, 3],
230
+ ] as const) {
231
+ for (const match of redacted.matchAll(pattern)) covered.fill(tag, match.index, match.index + match[0].length);
232
+ }
233
+ // Mark short credentials AFTER the shapes, on the same text: replacing
234
+ // either first can split the other and leak a suffix. Connected marks are
235
+ // redacted together; a known credential's marker takes precedence.
236
+ if (key && key.length < 8) {
237
+ for (let start = redacted.indexOf(key); start !== -1; start = redacted.indexOf(key, start + 1)) {
238
+ covered.fill(4, start, start + key.length);
239
+ }
240
+ }
241
+ let result = ''; let cursor = 0;
242
+ for (let start = 0; start < covered.length;) {
243
+ if (covered[start] === 0) { start++; continue; }
244
+ let tag = covered[start]!; let end = start + 1;
245
+ while (end < covered.length && covered[end] !== 0) {
246
+ if (covered[end] === 4) tag = 4;
247
+ end++;
248
+ }
249
+ result += redacted.slice(cursor, start) + replacements[tag];
250
+ cursor = end; start = end;
251
+ }
252
+ return result + redacted.slice(cursor);
209
253
  }
210
254
 
211
255
  function* executableSequence(command: string, directory: string, environment: NodeJS.ProcessEnv): Generator<ProbeRequest, string | undefined, ProbeOutput> {
@@ -245,3 +289,13 @@ function classifySpawnFailure(
245
289
  }
246
290
  return signal === null ? undefined : new CliProbeError(`signal:${signal}`);
247
291
  }
292
+
293
+ function prepareProbe(request: ProbeRequest) {
294
+ if (cliAdapterKind(request.argv0 ?? request.executable) === 'devin'
295
+ && request.environment.DEVIN_API_KEY !== undefined && !request.environment.DEVIN_API_KEY.trim()) {
296
+ throw new CliProbeError('devin_api_key_empty');
297
+ }
298
+ return cliAdapterKind(request.argv0 ?? request.executable) === 'devin'
299
+ ? prepareDevinRuntime(request.environment, request.invocation)
300
+ : { args: request.invocation.args, env: request.environment, close() {} };
301
+ }
@@ -98,7 +98,7 @@ export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs |
98
98
  value = arg;
99
99
  }
100
100
  if (value === undefined) return undefined;
101
- if (flow === undefined && (repo === undefined || on.length === 0)) return undefined;
101
+ if (flow === undefined && on.length > 0 && repo === undefined) return undefined;
102
102
  return { command: 'cloud-deploy', value, flow, repo, on, approver, name, agents, draft, noConnect, json, plugins };
103
103
  }
104
104
 
@@ -119,7 +119,7 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
119
119
  // by deployToCloud, not here.
120
120
  let harnesses: readonly string[] = agents instanceof CloudFlowError ? [] : agents ?? [];
121
121
  try {
122
- if (args.approver === undefined) {
122
+ if (args.on.length > 0 && args.approver === undefined) {
123
123
  throw new CloudFlowError('invalid_input',
124
124
  '--approver <handle> is required: every launched run receives it as input.approver for f.human.');
125
125
  }
@@ -128,7 +128,7 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
128
128
  if (agents === undefined) harnesses = (await flowRequirementsForPath(args.value))?.harnesses ?? [];
129
129
  const deployment = await deployToCloud({
130
130
  path: args.value,
131
- repository: parseRepository(args.repo!),
131
+ ...(args.repo === undefined ? {} : { repository: parseRepository(args.repo) }),
132
132
  sources: args.on.map(parseTriggerSource),
133
133
  approver: args.approver,
134
134
  draft: args.draft,
@@ -144,12 +144,12 @@ export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promi
144
144
  io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`
145
145
  + (deployment.version === undefined ? '' : ` · ${describeVersionChange(deployment.version)}`));
146
146
  io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
147
- io.stdout(` repository: ${describeRepository(deployment.repository)}`);
147
+ if (deployment.repository) io.stdout(` repository: ${describeRepository(deployment.repository)}`);
148
148
  for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
149
149
  const requires = describeFlowRequirements(deployment.requirements);
150
150
  if (requires) io.stdout(` requires: ${requires}`);
151
151
  for (const provider of deployment.connected) io.stdout(` connected: ${provider}`);
152
- io.stdout(deployment.status === 'draft'
152
+ io.stdout(deployment.sources.length === 0 ? `Run locally: flows run ${deployment.name}` : deployment.status === 'draft'
153
153
  ? 'Saved without activating; activate it from the Cloud dashboard, or redeploy without --draft.'
154
154
  : 'Each matching ticket launches a run of this source in a fresh branch; list with: flows deployments');
155
155
  return 0;
@@ -1,26 +1,5 @@
1
- // The Cloud half of `flows run` / `flows resume`: also put this local run on
2
- // the Cloud dashboard.
3
- //
4
- // Opt-in, and deliberately the only opt-in thing on this path.
5
- //
6
- // A local run is already watchable by default: `observer-session.ts` beside
7
- // this one projects the run into its own channel and prints a read-only link,
8
- // off a workspace key, for free. That is the right default — it is a step
9
- // projection, it costs nothing, and a run that never joins a workspace is not
10
- // a defect (RFC-0001 settled decision 7).
11
- //
12
- // The dashboard is the richer, hosted view of the same run: the flow source,
13
- // every step's transcript, the run graph, the logs, and the run sitting in the
14
- // same history as the hosted ones. It is also the one that *stores* all of
15
- // that. So it is asked for — `--cloud-mirror`, or `FLOWS_CLOUD_MIRROR=1` for a
16
- // shell — and never turned on by a login happening to be present. Mirroring
17
- // someone's local runs because they once signed in is not a default anyone
18
- // consented to.
19
- //
20
- // Best-effort once it is on, like the observer: a registration failure is one
21
- // labeled stderr line, and nothing here can change a run's exit code or its
22
- // journal. But it is a *louder* line than it used to be, because the run asked
23
- // for this and did not get it.
1
+ // Signed-in local runs project their journal onto the Cloud dashboard.
2
+ // Reporting is best-effort and never changes the durable run outcome.
24
3
 
25
4
  import { readFile } from 'node:fs/promises';
26
5
  import type { CliIo } from '../cli.js';
@@ -59,7 +38,7 @@ export interface CloudMirrorSession {
59
38
  */
60
39
  receipt(): Promise<CloudMirrorReceipt | undefined>;
61
40
  /** A journaled entry; the first one names the run for a declarative flow. */
62
- onJournalEntry(entry: { run_id: string }): void;
41
+ onJournalEntry(entry: { run_id: string; entry_type?: string; step_id?: string | null }): void;
63
42
  /** Step transitions, pushed as lifecycle events the run page's stream shows. */
64
43
  onProgress(event: ProgressEvent): void;
65
44
  /** Publish the final report and the terminal status. Never rejects. */
@@ -82,7 +61,7 @@ export interface CloudMirrorRequest {
82
61
  /** Lines the CLI has printed for this run, uploaded as the run's `runner.log`. */
83
62
  log: () => readonly string[];
84
63
  /** How the dashboard was asked for, so a refusal can name the right switch. */
85
- requested: 'flag' | 'env';
64
+ requested: 'flag' | 'env' | 'login';
86
65
  }
87
66
 
88
67
  /**
@@ -147,7 +126,14 @@ export function createCloudMirrorSession(
147
126
  void open(run.runId);
148
127
  },
149
128
  onJournalEntry(entry) {
150
- void open(entry.run_id);
129
+ const opened = open(entry.run_id);
130
+ if (entry.step_id && (entry.entry_type === 'step.attempt.started' || entry.entry_type === 'step.completed')) {
131
+ void opened.then(mirror => mirror?.event({
132
+ eventType: entry.entry_type === 'step.completed' ? 'relayflow.step.completed' : 'relayflow.step.started',
133
+ stepName: entry.step_id!,
134
+ payload: { journalRunId: entry.run_id },
135
+ }));
136
+ }
151
137
  },
152
138
  async receipt() {
153
139
  if (opening === undefined) return undefined;
@@ -194,6 +180,7 @@ export function mirrorSourceFromPath(
194
180
  path: string,
195
181
  inputArgument: string | undefined,
196
182
  dataDir: string,
183
+ named?: { flowId: string; flowVersionId: string; source: string },
197
184
  ): (runId: string) => Promise<MirrorRunSource> {
198
185
  const fromJournal = mirrorSourceFromJournal(dataDir);
199
186
  return async (runId) => {
@@ -203,9 +190,10 @@ export function mirrorSourceFromPath(
203
190
  // the dashboard, so fall back to what the journal recorded — the same
204
191
  // source a resume mirrors.
205
192
  if (parseDigestReference(path)) return fromJournal(runId);
206
- const workflow = await readFile(path, 'utf8');
193
+ const workflow = named?.source ?? await readFile(path, 'utf8');
207
194
  if (!isAuthoredFlowPath(path)) return { workflow, fileType: 'yaml' };
208
- return { workflow, fileType: 'ts', inputs: parseDirectInput(inputArgument) };
195
+ return { workflow, fileType: 'ts', inputs: parseDirectInput(inputArgument),
196
+ ...(named === undefined ? {} : { flowId: named.flowId, flowVersionId: named.flowVersionId }) };
209
197
  };
210
198
  }
211
199
 
@@ -305,7 +293,13 @@ function completionReport(report: RunReport): Record<string, unknown> {
305
293
  * missing-login case is the common one and names the two commands that fix
306
294
  * it: sign in, or stop asking.
307
295
  */
308
- function mirrorRefusal(error: unknown, requested: 'flag' | 'env'): string {
296
+ function mirrorRefusal(error: unknown, requested: 'flag' | 'env' | 'login'): string {
297
+ if (requested === 'login') {
298
+ if (error instanceof CloudFlowError && error.reason === 'auth_missing') {
299
+ return 'Not signed in; this run stays local. Sign in with `agent-relay cloud login` to report runs.';
300
+ }
301
+ return `Local run reporting unavailable: ${error instanceof Error ? error.message : String(error)}; the run itself is unaffected.`;
302
+ }
309
303
  const asked = requested === 'flag' ? '--cloud-mirror' : `${MIRROR_ENV}=1`;
310
304
  if (error instanceof CloudFlowError && error.code === 'configuration') {
311
305
  return error.reason === 'auth_missing'
@@ -221,7 +221,8 @@ export async function runDirectFlow(
221
221
  // diagnostic carried up from `classifyOutcome` already names the step, its
222
222
  // exit code and its output tail; this branch is what lets it reach the
223
223
  // terminal. `resumeFlow` takes the same branch, through the same helper.
224
- if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
224
+ if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed'
225
+ || error.code === 'serial_replay_order_mismatch')) {
225
226
  return authoredStepFailure('run', base, socketPath, error);
226
227
  }
227
228
  if (error instanceof AuthoredFlowExecutionError && error.code === 'result_unreadable') {
package/src/cli/run.ts CHANGED
@@ -7,6 +7,7 @@ import { communicationInstruction } from '../communication/spec.js';
7
7
  import { checkCommunicationEnvironment, CommunicationEnvironmentError } from '../communication/preflight.js';
8
8
  import { parseHumanRecipient } from '../human-to.js';
9
9
  import { parseDigestReference } from '../bundle-transport.js';
10
+ import { resolveNamedFlow } from '../cloud-flow-source.js';
10
11
  import { prepareDigestRun } from './run-digest.js';
11
12
  import { attachReuseSummary } from './reuse.js';
12
13
  import { resumeHelperEffect } from '../authored-helper-effect.js';
@@ -47,6 +48,7 @@ import { DEFAULT_LOCAL_AGENT_CAPACITY } from '../worker-slots.js';
47
48
  import { localAgentEnvironment } from '../local-agent-environment.js';
48
49
  import {
49
50
  checkFlow,
51
+ inputFailureReport,
50
52
  type CheckReport,
51
53
  } from './check.js';
52
54
 
@@ -182,6 +184,17 @@ export async function runFlow(
182
184
  dataDir: string,
183
185
  options: RunLifecycleOptions = {},
184
186
  ): Promise<RunExecution> {
187
+ try {
188
+ const named = await resolveNamedFlow(path);
189
+ if (named) {
190
+ const { runDirectFlow } = await import('./direct-run.js');
191
+ return runDirectFlow(named.path, '{}', dataDir, options);
192
+ }
193
+ } catch (error) {
194
+ return { exitCode: 2, report: fromCheckReport('run', inputFailureReport({
195
+ kind: 'input_unreadable', message: error instanceof Error ? error.message : String(error),
196
+ }, path)) };
197
+ }
185
198
  const agentEnvironment = localAgentEnvironment();
186
199
  const prepared = parseDigestReference(path) ? await prepareDigestRun(path, options.bucket) : undefined;
187
200
  if (prepared && 'exitCode' in prepared) return prepared;
@@ -432,7 +445,8 @@ export async function resumeFlow(
432
445
  // leaving it on `protocolFailure` meant `flows run` printed the evidence
433
446
  // while `flows resume` still printed `protocol_error` and
434
447
  // `RUN <id> unknown` for the identical failure.
435
- if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed')) {
448
+ if (error instanceof AuthoredFlowExecutionError && (error.code === 'step_failed' || error.code === 'gate_failed'
449
+ || error.code === 'serial_replay_order_mismatch')) {
436
450
  return authoredStepFailure('resume', base, socketPath, error, runId);
437
451
  }
438
452
  if (error instanceof AuthoredFlowExecutionError && error.code === 'result_unreadable') {
@@ -632,10 +646,11 @@ export function authoredStepFailure(
632
646
  severity: 'failure',
633
647
  // A predicate gate that judged false is a run failure with its own
634
648
  // name, so the report says which kind of check the body did not pass.
635
- kind: error.code === 'gate_failed' ? 'gate_failed' : 'step_failed',
649
+ // A refused out-of-order replay names itself the same way.
650
+ kind: error.code === 'gate_failed' || error.code === 'serial_replay_order_mismatch' ? error.code : 'step_failed',
636
651
  // The `step_failed: ` prefix `AuthoredFlowExecutionError` adds is
637
652
  // redundant once the diagnostic is labelled `[step_failed]`.
638
- message: error.message.replace(/^(?:step_failed|gate_failed): /, ''),
653
+ message: error.message.replace(/^(?:step_failed|gate_failed|serial_replay_order_mismatch): /, ''),
639
654
  }],
640
655
  },
641
656
  };