@relayflows/sdk 2.0.56 → 2.0.58

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 (52) 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/budget-preflight.d.ts.map +1 -1
  12. package/dist/budget-preflight.js +8 -2
  13. package/dist/budget-preflight.js.map +1 -1
  14. package/dist/cli/cli-probe.d.ts.map +1 -1
  15. package/dist/cli/cli-probe.js +72 -11
  16. package/dist/cli/cli-probe.js.map +1 -1
  17. package/dist/communication/environment.d.ts.map +1 -1
  18. package/dist/communication/environment.js +1 -0
  19. package/dist/communication/environment.js.map +1 -1
  20. package/dist/communication/pty.d.ts.map +1 -1
  21. package/dist/communication/pty.js +6 -2
  22. package/dist/communication/pty.js.map +1 -1
  23. package/dist/communication/skills.d.ts.map +1 -1
  24. package/dist/communication/skills.js +2 -1
  25. package/dist/communication/skills.js.map +1 -1
  26. package/dist/devin-runtime.d.ts +10 -0
  27. package/dist/devin-runtime.d.ts.map +1 -0
  28. package/dist/devin-runtime.js +37 -0
  29. package/dist/devin-runtime.js.map +1 -0
  30. package/dist/preflight.d.ts +1 -1
  31. package/dist/preflight.d.ts.map +1 -1
  32. package/dist/preflight.js +12 -3
  33. package/dist/preflight.js.map +1 -1
  34. package/dist/step-secrets.d.ts.map +1 -1
  35. package/dist/step-secrets.js +1 -0
  36. package/dist/step-secrets.js.map +1 -1
  37. package/dist/worker-cli.d.ts.map +1 -1
  38. package/dist/worker-cli.js +15 -3
  39. package/dist/worker-cli.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/adapters/base.ts +2 -0
  42. package/src/adapters/devin.ts +22 -0
  43. package/src/adapters/index.ts +5 -2
  44. package/src/budget-preflight.ts +8 -2
  45. package/src/cli/cli-probe.ts +65 -11
  46. package/src/communication/environment.ts +1 -0
  47. package/src/communication/pty.ts +7 -3
  48. package/src/communication/skills.ts +2 -1
  49. package/src/devin-runtime.ts +33 -0
  50. package/src/preflight.ts +12 -3
  51. package/src/step-secrets.ts +1 -0
  52. package/src/worker-cli.ts +13 -5
@@ -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
+ }
@@ -25,6 +25,7 @@ const providerNames: Readonly<Record<string, readonly string[]>> = {
25
25
  // AgentWorkforce/cloud#4026's house-provider run contract gives Grok an
26
26
  // isolated home plus an xAI-compatible base URL. These remain Grok-scoped.
27
27
  grok: ['XAI_API_KEY', 'GROK_XAI_API_BASE_URL', 'GROK_HOME'],
28
+ devin: ['DEVIN_API_KEY', 'DEVIN_MODEL'],
28
29
  ...Object.fromEntries(['opencode', 'aider', 'goose', 'pi', 'deepagents'].map(cli => [cli, multiProvider])),
29
30
  };
30
31
  /** @internal Names-only test seam; credential lists remain private. */
@@ -1,5 +1,6 @@
1
1
  import { mkdir, mkdtemp, rm, symlink } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
+ import { prepareDevinRuntime } from '../devin-runtime.js';
3
4
  import { agentEnvironment } from './environment.js';
4
5
  import { resolveCliModel } from '../cli-adapter.js';
5
6
  import type { RelayBroker } from './relay.js';
@@ -19,6 +20,9 @@ export async function spawnFlowPty(broker: Pick<RelayBroker, 'spawnPty'>, option
19
20
  const argv0 = basename(identity);
20
21
  let command = options.cli;
21
22
  let link: string | undefined;
23
+ const environment = agentEnvironment(identity, options.environment ?? process.env,
24
+ options.secrets, options.scopeSecrets ?? true);
25
+ const prepared = argv0.replace(/\.exe$/i, '') === 'devin' ? prepareDevinRuntime(environment) : undefined;
22
26
  try {
23
27
  if (basename(command) !== argv0) {
24
28
  const root = join(options.dataDir, 'communication');
@@ -32,15 +36,15 @@ export async function spawnFlowPty(broker: Pick<RelayBroker, 'spawnPty'>, option
32
36
  ...(options.team === undefined ? {} : { team: options.team }),
33
37
  model: resolveCliModel(identity, options.model), cwd: options.cwd,
34
38
  harnessConfig: { runtime: 'pty', command: shellQuote(command), args: [], cwd: options.cwd,
35
- env: { ...agentEnvironment(identity, options.environment ?? process.env,
36
- options.secrets, options.scopeSecrets ?? true),
39
+ env: { ...(prepared?.env ?? environment),
37
40
  RELAYFLOW_COMMUNICATION_SOCKET: options.socket, RELAYFLOW_COMMUNICATION_TOKEN: options.token,
38
41
  ...(options.metadata === undefined ? {} : {
39
42
  RELAYFLOW_RUN_ID: options.metadata.runId, RELAYFLOW_FLOW_NAME: options.metadata.flow,
40
43
  RELAYFLOW_STEP_NAME: options.metadata.step,
41
44
  }) }, delivery: { mode: 'pty-injection', format: 'relay-block' } } });
42
- return { handle, close: async () => { if (link) await rm(link, { recursive: true, force: true }); } };
45
+ return { handle, close: async () => { prepared?.close(); if (link) await rm(link, { recursive: true, force: true }); } };
43
46
  } catch (error) {
47
+ prepared?.close();
44
48
  if (link) await rm(link, { recursive: true, force: true });
45
49
  throw error;
46
50
  }
@@ -7,8 +7,9 @@ const execute = promisify(execFile);
7
7
  export async function installBrokerSkills(skills: string[], cli: string, cwd: string, signal: AbortSignal): Promise<void> {
8
8
  if (!skills.length) return;
9
9
  const identity = basename(cli).replace(/\.exe$/i, '');
10
+ // Devin reads .windsurf/skills, which the canonical skills installer supports.
10
11
  const harness = ({ claude: 'claude-code', codex: 'codex', gemini: 'gemini-cli',
11
- 'cursor-agent': 'cursor', opencode: 'opencode' } as Record<string, string>)[identity];
12
+ 'cursor-agent': 'cursor', opencode: 'opencode', devin: 'windsurf' } as Record<string, string>)[identity];
12
13
  if (!harness) throw new Error(`Skill installation is unsupported for broker CLI ${identity}`);
13
14
  await execute('npx', ['--yes', 'skills', 'add', 'https://github.com/AgentWorkforce/skills',
14
15
  '--skill', ...skills, '--agent', harness, '--yes', '--copy'],
@@ -0,0 +1,33 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import type { CliInvocation } from './adapters/base.js';
5
+
6
+ /** Private attempt/probe state, never the user's interactive Devin login. */
7
+ export function prepareDevinRuntime(environment: NodeJS.ProcessEnv, invocation?: CliInvocation) {
8
+ let root: string | undefined;
9
+ const env = { ...environment };
10
+ const args = [...(invocation?.args ?? [])];
11
+ const directory = () => root ??= mkdtempSync(join(environment.TMPDIR ?? tmpdir(), 'relayflows-devin-'));
12
+ const close = () => { if (root !== undefined) rmSync(root, { recursive: true, force: true }); };
13
+ try {
14
+ if (env.DEVIN_API_KEY !== undefined) {
15
+ const key = env.DEVIN_API_KEY.trim();
16
+ if (!key) throw new Error('DEVIN_API_KEY must not be empty');
17
+ const data = join(directory(), 'data');
18
+ mkdirSync(join(data, 'devin'), { recursive: true, mode: 0o700 });
19
+ writeFileSync(join(data, 'devin', 'credentials.toml'),
20
+ `windsurf_api_key = ${JSON.stringify(key)}\n`, { mode: 0o600 });
21
+ env.XDG_DATA_HOME = data;
22
+ env.XDG_CONFIG_HOME = join(directory(), 'config');
23
+ env.XDG_CACHE_HOME = join(directory(), 'cache');
24
+ delete env.DEVIN_API_KEY;
25
+ }
26
+ if (invocation?.promptFile !== undefined) {
27
+ const path = join(directory(), 'prompt.txt');
28
+ writeFileSync(path, invocation.promptFile, { mode: 0o600 });
29
+ args.push('--prompt-file', path);
30
+ }
31
+ return { env, args, close };
32
+ } catch (error) { close(); throw error; }
33
+ }
package/src/preflight.ts CHANGED
@@ -11,7 +11,7 @@ import type { TriggerSource } from '@relayflows/surface';
11
11
  import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js';
12
12
  import { compileSpec, CompileError } from './compile.js';
13
13
  import { helperCall } from './yaml-helpers.js';
14
- import { resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
14
+ import { cliAdapterKind, resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
15
15
  import { isNamedGate, NAMED_GATE_FAILURE_KINDS, type NamedGateFailureKind } from './named-gates.js';
16
16
  import { namedGateScanCoverageDiagnostics } from './named-gate-preflight.js';
17
17
  import { compileScopes, type ScopeInput, type MountRegistry } from './scope-compiler.js';
@@ -65,6 +65,7 @@ export interface CliProbeResult {
65
65
 
66
66
  export type CliProbeFailureDetail =
67
67
  | 'spawn_failed'
68
+ | 'devin_api_key_empty'
68
69
  | `timeout:${number}ms`
69
70
  | `signal:${string}`;
70
71
 
@@ -286,6 +287,10 @@ export function resolvePreflight(flow: unknown, options: ResolutionOptions): Pre
286
287
  message: unresolvedCliMessage(step.id, options),
287
288
  });
288
289
  } else {
290
+ if (step.type === 'llm' && cliAdapterKind(resolution.cli) === 'devin') {
291
+ cliResolutionDiagnostics.push({ severity: 'refusal', kind: 'cli_unsupported', stepId: step.id,
292
+ cli: resolution.cli, message: `Step "${step.id}" uses Devin for an llm step; Devin supports agent steps only.` });
293
+ }
289
294
  resolutions.push(resolution);
290
295
  resolutionByStep.set(step.id, resolution);
291
296
  }
@@ -594,13 +599,16 @@ function probeResolvedCli(
594
599
  kind: 'cli_unsupported',
595
600
  stepId: resolution.stepId,
596
601
  cli: resolution.cli,
597
- message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it is neither a supported raw Claude/Codex executable nor a conforming Relayflows wrapper; custom wrappers must identify with the relayflows-agent-cli-v1 contract.`,
602
+ message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it is neither a supported raw Claude/Codex/Devin executable nor a conforming Relayflows wrapper; custom wrappers must identify with the relayflows-agent-cli-v1 contract.`,
598
603
  });
599
604
  } else if (managed && result.authenticated === 'unverified') {
600
605
  diagnostics.push({ severity: 'warning', kind: 'managed_cli_unverified', stepId: resolution.stepId,
601
606
  message: `Step "${resolution.stepId}" uses Relay's interactive CLI transport. The executable exists, but authentication${resolution.model ? ' and access to model "' + resolution.model + '"' : ''} could not be verified before startup. The managed session must execute its task and report completion; startup or task failure fails the step.` });
602
607
  } else if (result.authenticated !== true) {
603
608
  const command = result.authCommand ?? `${resolution.cli} auth status`;
609
+ const loginHint = cliAdapterKind(resolution.cli) === 'devin'
610
+ ? ' For hosted flows, set FLOW_DEVIN_API_KEY to a Devin CLI login token; local runs use DEVIN_API_KEY. A REST API key is a different credential.'
611
+ : '';
604
612
  // Say what the probe reported. A refusal that names only the command turns
605
613
  // a transient provider rejection and a genuinely unauthenticated CLI into
606
614
  // the same message, and the difference decides whether retrying is
@@ -616,7 +624,7 @@ function probeResolvedCli(
616
624
  kind: 'cli_unauthenticated',
617
625
  stepId: resolution.stepId,
618
626
  cli: resolution.cli,
619
- message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero${exitCode}; authenticate it or repair that adapter's authentication probe.${detail}`,
627
+ message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero${exitCode}; authenticate it or repair that adapter's authentication probe.${detail}${loginHint}`,
620
628
  });
621
629
  } else if (resolution.model !== undefined && result.modelAvailable !== true) {
622
630
  diagnostics.push(modelProbeRefusal(resolution as CliResolution & { model: string }, result));
@@ -668,6 +676,7 @@ function probeFailedMessage(
668
676
  ): string {
669
677
  const prefix = `Could not verify CLI "${resolution.cli}" for step "${resolution.stepId}"`;
670
678
  if (detail === undefined) return `${prefix}.`;
679
+ if (detail === 'devin_api_key_empty') return `${prefix}: DEVIN_API_KEY must contain a non-empty Devin CLI login token. For hosted flows, set FLOW_DEVIN_API_KEY.`;
671
680
  if (detail === 'spawn_failed') return `${prefix}: the probe process could not be started.`;
672
681
  if (detail.startsWith('signal:')) {
673
682
  return `${prefix}: the probe was terminated by signal "${detail.slice('signal:'.length)}".`;
@@ -47,6 +47,7 @@ const CLI_LOGIN: Record<CliAdapterKind, readonly string[]> = {
47
47
  'AWS_SESSION_TOKEN', 'AWS_REGION', 'AWS_DEFAULT_REGION',
48
48
  ],
49
49
  codex: ['CODEX_HOME', 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENROUTER_API_KEY'],
50
+ devin: ['DEVIN_API_KEY', 'DEVIN_MODEL'],
50
51
  'relayflows-wrapper-v1': [],
51
52
  };
52
53
 
package/src/worker-cli.ts CHANGED
@@ -45,6 +45,7 @@ import {
45
45
  type AgentTransport,
46
46
  } from './agent-relay-transport.js';
47
47
  import { pinCliAlias } from './cli/pinned-cli-alias.js';
48
+ import { prepareDevinRuntime } from './devin-runtime.js';
48
49
  import { promptOperand } from './adapters/base.js';
49
50
 
50
51
  export type { CliTransportCause, CliTransportEvidence, CliTransportPhase } from './cli-transport-evidence.js';
@@ -277,15 +278,22 @@ export async function runAgentCli(
277
278
  // The flags go before the `--` that ends the task operand (promptOperand):
278
279
  // after it they would reach the CLI as task text, not options.
279
280
  const args = [...invocation.args];
280
- args.splice(args.length - promptOperand('').length, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
281
+ if (kind !== 'devin') args.splice(args.length - promptOperand('').length, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
281
282
  const completion = kind === 'claude' ? claudeResultOutcome : undefined;
282
283
  // Retain the source values for output redaction even though the child
283
284
  // receives only its filtered environment. A provider can echo a value from
284
285
  // its prompt or fixture that was not granted as an environment variable.
285
- const redactionEnvironment = { ...processEnvironment, ...env };
286
- return requirePricedUsage(decodeProviderResult(await spawnInvocation(
287
- cli, { ...invocation, args }, env, signal, sidechannel, cwd, completion, argv0, kind, redactionEnvironment,
288
- ), kind, redactionEnvironment), effectiveModel);
286
+ const redactionEnvironment = { ...processEnvironment, ...env,
287
+ ...(kind === 'devin' && env.DEVIN_API_KEY !== undefined ? { DEVIN_API_KEY: env.DEVIN_API_KEY.trim() } : {}) };
288
+ const prepared = kind === 'devin' ? prepareDevinRuntime(env, { ...invocation, args }) : undefined;
289
+ try {
290
+ const result = await spawnInvocation(cli,
291
+ { ...invocation, args: prepared?.args ?? args }, prepared?.env ?? env,
292
+ signal, sidechannel, cwd, completion, argv0, kind, redactionEnvironment);
293
+ // Print mode has neither provider frames nor authoritative token counts.
294
+ return kind === 'devin' ? { ...result, stdout_tail: redactText(result.stdout_tail, redactionEnvironment) }
295
+ : requirePricedUsage(decodeProviderResult(result, kind, redactionEnvironment), effectiveModel);
296
+ } finally { prepared?.close(); }
289
297
  }
290
298
  }
291
299