@the-open-engine/zeroshot 6.39.1 → 6.40.0

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 (121) hide show
  1. package/README.md +45 -1
  2. package/cli/agent-provider-boundary.js +19 -0
  3. package/cli/agent-provider-boundary.ts +79 -0
  4. package/cli/export-stream.js +94 -0
  5. package/cli/export-stream.ts +118 -0
  6. package/cli/index.js +132 -61
  7. package/cli/json-export.js +2 -38
  8. package/cli/json-export.ts +2 -56
  9. package/cli/semantic-canonical.js +60 -0
  10. package/cli/semantic-canonical.ts +59 -0
  11. package/cli/semantic-contract.js +97 -0
  12. package/cli/semantic-contract.ts +153 -0
  13. package/cli/semantic-events.js +53 -0
  14. package/cli/semantic-events.ts +65 -0
  15. package/cli/semantic-evidence.js +63 -0
  16. package/cli/semantic-evidence.ts +65 -0
  17. package/cli/semantic-export.js +186 -0
  18. package/cli/semantic-export.ts +245 -0
  19. package/cli/semantic-json.js +69 -0
  20. package/cli/semantic-json.ts +69 -0
  21. package/cli/semantic-line-scanner.js +56 -0
  22. package/cli/semantic-line-scanner.ts +54 -0
  23. package/cli/semantic-parser.js +78 -0
  24. package/cli/semantic-parser.ts +95 -0
  25. package/cli/semantic-provider-line.js +91 -0
  26. package/cli/semantic-provider-line.ts +106 -0
  27. package/cli/trace-evidence.js +86 -0
  28. package/cli/trace-evidence.ts +115 -0
  29. package/cli/trace-export.js +120 -0
  30. package/cli/trace-export.ts +164 -0
  31. package/cli/trace-output-record.js +15 -0
  32. package/cli/trace-output-record.ts +18 -0
  33. package/cli/trace-output.js +98 -0
  34. package/cli/trace-output.ts +119 -0
  35. package/cluster-templates/conductor-bootstrap.json +1 -1
  36. package/lib/agent-cli-provider/adapters/claude.js +1 -1
  37. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  38. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  39. package/lib/agent-cli-provider/adapters/codex.js +26 -1
  40. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  41. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
  42. package/lib/agent-cli-provider/adapters/omp.js +1 -1
  43. package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
  44. package/lib/agent-cli-provider/log-prefix.d.ts.map +1 -1
  45. package/lib/agent-cli-provider/log-prefix.js +8 -6
  46. package/lib/agent-cli-provider/log-prefix.js.map +1 -1
  47. package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts +4 -0
  48. package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts.map +1 -0
  49. package/lib/agent-cli-provider/omp/sdk-environment-policy.js +37 -0
  50. package/lib/agent-cli-provider/omp/sdk-environment-policy.js.map +1 -0
  51. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.d.ts.map +1 -1
  52. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js +4 -15
  53. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js.map +1 -1
  54. package/lib/agent-cli-provider/omp/sdk-process-result.d.ts +2 -0
  55. package/lib/agent-cli-provider/omp/sdk-process-result.d.ts.map +1 -1
  56. package/lib/agent-cli-provider/omp/sdk-process-result.js +2 -2
  57. package/lib/agent-cli-provider/omp/sdk-process-result.js.map +1 -1
  58. package/lib/agent-cli-provider/omp/sdk-process-runner.d.ts.map +1 -1
  59. package/lib/agent-cli-provider/omp/sdk-process-runner.js +6 -1
  60. package/lib/agent-cli-provider/omp/sdk-process-runner.js.map +1 -1
  61. package/lib/agent-cli-provider/provider-registry.d.ts +3 -1
  62. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  63. package/lib/agent-cli-provider/provider-registry.js +9 -2
  64. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  65. package/lib/agent-cli-provider/single-agent-runtime.d.ts +4 -1
  66. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  67. package/lib/agent-cli-provider/single-agent-runtime.js +18 -15
  68. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  69. package/lib/agent-cli-provider/types.d.ts +3 -0
  70. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  71. package/lib/agent-cli-provider/types.js.map +1 -1
  72. package/lib/cluster-worker/engine-adapter-common.js +18 -0
  73. package/lib/cluster-worker/engine-adapter.js +2 -1
  74. package/lib/git-remote-utils.js +22 -5
  75. package/lib/stream-json-parser.js +6 -5
  76. package/npm-shrinkwrap.json +2 -2
  77. package/package.json +7 -4
  78. package/scripts/omp/runtime-identities.js +31 -18
  79. package/scripts/omp/runtime.js +2 -0
  80. package/src/agent/agent-lifecycle.js +17 -1
  81. package/src/agent/agent-task-executor.js +36 -40
  82. package/src/agent/output-extraction-failures.js +73 -0
  83. package/src/agent/output-extraction-failures.ts +100 -0
  84. package/src/agent/output-extraction-json.js +5 -6
  85. package/src/agent/output-extraction-json.ts +4 -7
  86. package/src/agent/output-extraction-types.ts +5 -0
  87. package/src/agent/provider-terminal-failure.js +2 -2
  88. package/src/agent/provider-terminal-failure.ts +4 -2
  89. package/src/agent-cli-provider/adapters/claude.ts +1 -1
  90. package/src/agent-cli-provider/adapters/codex.ts +36 -1
  91. package/src/agent-cli-provider/adapters/omp.ts +2 -1
  92. package/src/agent-cli-provider/log-prefix.ts +12 -7
  93. package/src/agent-cli-provider/omp/sdk-environment-policy.ts +38 -0
  94. package/src/agent-cli-provider/omp/sdk-process-private-runtime.ts +4 -16
  95. package/src/agent-cli-provider/omp/sdk-process-result.ts +4 -2
  96. package/src/agent-cli-provider/omp/sdk-process-runner.ts +7 -1
  97. package/src/agent-cli-provider/provider-registry.ts +10 -2
  98. package/src/agent-cli-provider/single-agent-runtime.ts +24 -15
  99. package/src/agent-cli-provider/types.ts +3 -0
  100. package/src/agents/git-pusher-template.js +61 -71
  101. package/src/attach/socket-paths.js +1 -1
  102. package/src/attach/socket-paths.ts +1 -1
  103. package/src/claude-task-runner.js +9 -9
  104. package/src/foreground-benchmark-run.js +24 -12
  105. package/src/legacy-lib/git-remote-utils.ts +23 -5
  106. package/src/legacy-lib/stream-json-parser.ts +10 -5
  107. package/src/omp-session-verifier.js +8 -5
  108. package/src/orchestrator.js +42 -0
  109. package/src/preflight.js +31 -6
  110. package/src/providers/index.js +4 -22
  111. package/src/task-execution-context.js +22 -0
  112. package/src/task-log-line.d.ts +20 -0
  113. package/src/task-log-line.js +80 -0
  114. package/src/watcher-prompt-channel.js +1 -1
  115. package/src/worktree-claude-config.js +25 -0
  116. package/src/worktree-claude-config.ts +28 -0
  117. package/task-lib/commands/logs.js +5 -8
  118. package/task-lib/runner.js +57 -22
  119. package/task-lib/sdk-watcher-output.js +31 -0
  120. package/task-lib/sdk-watcher.js +4 -11
  121. package/task-lib/watcher-output-runtime.js +30 -16
@@ -11,6 +11,7 @@ const TaskRunner = require('./task-runner');
11
11
  const { loadSettings } = require('../lib/settings');
12
12
  const { normalizeProviderName, getDefaultProviderId } = require('../lib/provider-names');
13
13
  const { getProvider } = require('./providers');
14
+ const { decodeTaskLogLine } = require('./task-log-line');
14
15
  const { prependWorktreeToolBinToEnv } = require('./worktree-tooling-env');
15
16
  const { applyDarwinKeychainBoundaryToEnv } = require('./darwin-keychain-boundary');
16
17
  const { getTask, getTaskBySpawnOwnershipToken } = require('../task-lib/store.js');
@@ -556,18 +557,17 @@ class ClaudeTaskRunner extends TaskRunner {
556
557
  const broadcastLine = (line) => {
557
558
  if (!line.trim()) return;
558
559
 
559
- let content = line;
560
- const timestampMatch = line.match(/^\[(\d{13})\](.*)$/);
561
- if (timestampMatch) {
562
- content = timestampMatch[2];
563
- }
560
+ const decoded = decodeTaskLogLine(line);
561
+ if (!decoded.providerOutput) return;
562
+ const content = decoded.content;
564
563
 
565
564
  // Skip non-JSON patterns
566
565
  if (
567
- content.startsWith('===') ||
568
- content.startsWith('Finished:') ||
569
- content.startsWith('Exit code:') ||
570
- (content.includes('"type":"system"') && content.includes('"subtype":"init"'))
566
+ decoded.channel !== 'provider_stdout' &&
567
+ (content.startsWith('===') ||
568
+ content.startsWith('Finished:') ||
569
+ content.startsWith('Exit code:') ||
570
+ (content.includes('"type":"system"') && content.includes('"subtype":"init"')))
571
571
  ) {
572
572
  return;
573
573
  }
@@ -4,6 +4,7 @@ const { writeBenchmarkResultBundle } = require('./foreground-benchmark-files');
4
4
  const TERMINAL_TOPICS = ['CLUSTER_COMPLETE', 'CLUSTER_FAILED'];
5
5
  const VERIFIER_ELIGIBLE = new Set(['completed', 'task_failure']);
6
6
  const EXIT_CODES = Object.freeze({
7
+ task_failure: 23,
7
8
  provider_failure: 20,
8
9
  engine_failure: 21,
9
10
  cancelled: 22,
@@ -40,20 +41,24 @@ function requireSettledStatus(orchestrator, clusterId) {
40
41
  return status;
41
42
  }
42
43
 
43
- function writeForegroundResult({ orchestrator, cluster, clusterId, resultPath, cancelled }) {
44
- const status = requireSettledStatus(orchestrator, clusterId);
45
- const terminals = terminalMessages(cluster, clusterId);
46
- let result;
44
+ function buildForegroundResult({ orchestrator, cluster, clusterId, cancelled }) {
45
+ const finalRun = orchestrator.getFinalRun?.(clusterId);
46
+ const status = finalRun?.status || requireSettledStatus(orchestrator, clusterId);
47
+ const terminals = finalRun?.terminalMessages || terminalMessages(cluster, clusterId);
47
48
  if (cancelled && terminals.length === 0) {
48
- result = buildCancelledResult({ runId: clusterId, agents: status.agents });
49
- } else {
50
- result = buildBenchmarkResult({
51
- runId: clusterId,
52
- terminalMessages: terminals,
53
- agents: status.agents,
54
- });
49
+ return buildCancelledResult({ runId: clusterId, agents: status.agents });
55
50
  }
56
- const snapshot = cluster.messageBus.readSnapshot(clusterId);
51
+ return buildBenchmarkResult({
52
+ runId: clusterId,
53
+ terminalMessages: terminals,
54
+ agents: status.agents,
55
+ });
56
+ }
57
+
58
+ function writeForegroundResult({ orchestrator, cluster, clusterId, resultPath, cancelled }) {
59
+ const result = buildForegroundResult({ orchestrator, cluster, clusterId, cancelled });
60
+ const snapshot =
61
+ orchestrator.getFinalRun?.(clusterId)?.snapshot || cluster.messageBus.readSnapshot(clusterId);
57
62
  return writeBenchmarkResultBundle(resultPath, result, snapshot);
58
63
  }
59
64
 
@@ -64,8 +69,15 @@ function exitCodeForResult(result) {
64
69
  return exitCode;
65
70
  }
66
71
 
72
+ function exitCodeForForegroundResult(result) {
73
+ if (result.outcome === 'completed') return 0;
74
+ return EXIT_CODES[result.outcome] ?? exitCodeForResult(result);
75
+ }
76
+
67
77
  module.exports = {
78
+ buildForegroundResult,
68
79
  exitCodeForResult,
80
+ exitCodeForForegroundResult,
69
81
  isForegroundStatusSettled,
70
82
  terminalMessages,
71
83
  writeForegroundResult,
@@ -181,6 +181,25 @@ function parseGitRemoteUrl(remoteUrl: unknown): GitContext | null {
181
181
  return null;
182
182
  }
183
183
 
184
+ function parseFetchRemoteLine(line: string): readonly [string, string] | null {
185
+ // Partial clones append their filter after the fetch marker, for example
186
+ // `origin https://github.com/org/repo.git (fetch) [blob:none]`.
187
+ const fetchMarker = ' (fetch)';
188
+ const fetchMarkerIndex = line.lastIndexOf(fetchMarker);
189
+ if (fetchMarkerIndex < 1) return null;
190
+ const suffix = line.slice(fetchMarkerIndex + fetchMarker.length);
191
+ if (suffix.length > 0 && !(suffix.startsWith(' [') && suffix.endsWith(']'))) return null;
192
+
193
+ const remoteLine = line.slice(0, fetchMarkerIndex);
194
+ const separatorIndex = remoteLine.search(/\s/);
195
+ if (separatorIndex < 1) {
196
+ return null;
197
+ }
198
+
199
+ const remoteUrl = remoteLine.slice(separatorIndex).trim();
200
+ return remoteUrl.length > 0 ? [remoteLine.slice(0, separatorIndex), remoteUrl] : null;
201
+ }
202
+
184
203
  /**
185
204
  * Detect git repository context from current working directory.
186
205
  * Returns provider context extracted from git remote URL.
@@ -205,15 +224,14 @@ function detectGitContext(cwd = process.cwd()): GitContextWithRemote | null {
205
224
 
206
225
  const supportedRemotes = new Map<string, GitContextWithRemote>();
207
226
  for (const line of remoteOutput.split(/\r?\n/)) {
208
- const match = /^(\S+)\s+(.+)\s+\(fetch\)$/.exec(line);
209
- if (!match) {
227
+ const remoteParts = parseFetchRemoteLine(line);
228
+ if (!remoteParts) {
210
229
  continue;
211
230
  }
212
231
 
213
- const remoteCandidate = match[1];
214
- const remoteUrl = match[2];
232
+ const [remoteCandidate, remoteUrl] = remoteParts;
215
233
  const remote = normalizeGitRemoteName(remoteCandidate);
216
- if (!remote || remoteUrl === undefined) {
234
+ if (!remote) {
217
235
  continue;
218
236
  }
219
237
  const context = parseGitRemoteUrl(remoteUrl);
@@ -16,9 +16,16 @@ interface ProviderFacade {
16
16
  listProviders(): string[];
17
17
  }
18
18
 
19
+ interface TaskLogDecoder {
20
+ decodeTaskLogLine(line: string): { content: string; providerOutput: boolean };
21
+ }
22
+
19
23
  // The runtime facade is maintained JavaScript; keep its original emitted require path.
20
24
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
21
25
  const { getProvider, listProviders }: ProviderFacade = require('../src/providers');
26
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
27
+ const taskLogDecoder: TaskLogDecoder = require('../src/task-log-line');
28
+ const { decodeTaskLogLine } = taskLogDecoder;
22
29
 
23
30
  function createProviderParsers(): ProviderParser[] {
24
31
  return listProviders().map((name) => getProvider(name));
@@ -26,11 +33,9 @@ function createProviderParsers(): ProviderParser[] {
26
33
 
27
34
  function stripTimestampPrefix(line: unknown): string {
28
35
  if (!line || typeof line !== 'string') return '';
29
- let trimmed = line.trim().replace(/\r$/, '');
30
- if (!trimmed) return '';
31
-
32
- const tsMatch = /^\[(\d{13})\](.*)$/.exec(trimmed);
33
- if (tsMatch) trimmed = (tsMatch[2] || '').trimStart();
36
+ const decoded = decodeTaskLogLine(line.trim());
37
+ if (!decoded.providerOutput) return '';
38
+ const trimmed = decoded.content;
34
39
 
35
40
  if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
36
41
  const pipeMatch = /^[^|]{1,40}\|\s*(.*)$/.exec(trimmed);
@@ -399,7 +399,10 @@ function streamSessionJsonl(fd, describePath, limits) {
399
399
  `${describePath} record ${records} is not valid JSON: ${error.message}`
400
400
  );
401
401
  }
402
- if (header === null) header = parsed;
402
+ // OMP 17.2.1 may reserve its first JSONL record for mutable title metadata before writing the
403
+ // authoritative session record. Accept exactly that one known preamble position; every other
404
+ // ordering still fails closed in parseSessionHeader below.
405
+ if (header === null && !(records === 1 && parsed?.type === 'title')) header = parsed;
403
406
  collectCanonicalBlobRefs(parsed, blobRefs, limits);
404
407
  }
405
408
 
@@ -430,9 +433,9 @@ function streamSessionJsonl(fd, describePath, limits) {
430
433
  };
431
434
  }
432
435
 
433
- /** The session's first record is OMP's session header (`{type:"session", version, id, cwd, ...}`
434
- * session-manager.ts `#resetToNewSession`). Its `id` is the authoritative session identity written
435
- * to disk and its `cwd` is the workspace the session belongs to. */
436
+ /** The session header (`{type:"session", version, id, cwd, ...}`) is either the first record or
437
+ * follows OMP 17.2.1's single leading title-metadata record. Its `id` is the authoritative session
438
+ * identity written to disk and its `cwd` is the workspace the session belongs to. */
436
439
  function parseSessionHeader(header, describePath) {
437
440
  if (!header || typeof header !== 'object' || Array.isArray(header)) {
438
441
  fail('session-header-missing', `${describePath} has no session header record.`);
@@ -440,7 +443,7 @@ function parseSessionHeader(header, describePath) {
440
443
  if (header.type !== 'session') {
441
444
  fail(
442
445
  'session-header-invalid',
443
- `${describePath} first record is type ${JSON.stringify(header.type)}, not "session".`
446
+ `${describePath} first non-title-preamble record is type ${JSON.stringify(header.type)}, not "session".`
444
447
  );
445
448
  }
446
449
  if (typeof header.id !== 'string' || header.id.length === 0) {
@@ -307,6 +307,9 @@ class Orchestrator {
307
307
  this.closed = false;
308
308
  this._conductorWatchdogs = new Set();
309
309
  this._clusterRunBoundaries = new Map();
310
+ // Successful auto-cleanup closes and removes the live cluster before a foreground caller can
311
+ // build its authoritative result. Preserve only the bounded terminal handoff for this process.
312
+ this._finalRuns = new Map();
310
313
 
311
314
  // Track if clusters are loaded (for lazy loading pattern)
312
315
  this._clustersLoaded = options.skipLoad === true;
@@ -2430,6 +2433,7 @@ class Orchestrator {
2430
2433
  cluster.pid = null; // Clear PID - cluster is no longer running
2431
2434
 
2432
2435
  if (shouldAutoCleanWorktree) {
2436
+ this._captureFinalRun(clusterId, cluster);
2433
2437
  // Close message bus and ledger (same as kill() path)
2434
2438
  cluster.messageBus.close();
2435
2439
  }
@@ -2562,6 +2566,35 @@ class Orchestrator {
2562
2566
  cluster.messageBus.close();
2563
2567
  }
2564
2568
  }
2569
+ this._finalRuns.clear();
2570
+ }
2571
+
2572
+ _captureFinalRun(clusterId, cluster) {
2573
+ const snapshot = cluster.messageBus.readSnapshot(clusterId);
2574
+ const terminalMessages = ['CLUSTER_COMPLETE', 'CLUSTER_FAILED']
2575
+ .flatMap((topic) => cluster.messageBus.query({ cluster_id: clusterId, topic }))
2576
+ .sort((left, right) => {
2577
+ const a = BigInt(left.sequence);
2578
+ const b = BigInt(right.sequence);
2579
+ return a < b ? -1 : a > b ? 1 : 0;
2580
+ });
2581
+ this._finalRuns.set(clusterId, {
2582
+ snapshot,
2583
+ terminalMessages,
2584
+ status: {
2585
+ id: clusterId,
2586
+ state: cluster.state,
2587
+ isZombie: false,
2588
+ pid: null,
2589
+ createdAt: cluster.createdAt,
2590
+ agents: cluster.agents.map((agent) => agent.getState()),
2591
+ messageCount: snapshot.messageCount,
2592
+ setupLogPath: cluster.setupLogPath || null,
2593
+ setupStage: cluster.setupStage || null,
2594
+ failureInfo: cluster.failureInfo || null,
2595
+ runMode: cluster.runMode || null,
2596
+ },
2597
+ });
2565
2598
  }
2566
2599
 
2567
2600
  /**
@@ -4300,11 +4333,16 @@ Continue from where you left off. Review your previous output to understand what
4300
4333
  * @returns {Object} Cluster status
4301
4334
  */
4302
4335
  getStatus(clusterId) {
4336
+ const finalRun = this._finalRuns.get(clusterId);
4337
+ if (finalRun) return finalRun.status;
4303
4338
  const cluster = this.clusters.get(clusterId);
4304
4339
  if (!cluster) {
4305
4340
  throw new Error(`Cluster ${clusterId} not found`);
4306
4341
  }
4342
+ return this._getLiveStatus(clusterId, cluster);
4343
+ }
4307
4344
 
4345
+ _getLiveStatus(clusterId, cluster) {
4308
4346
  // Detect zombie clusters: state=running but no backing process
4309
4347
  let state = cluster.state;
4310
4348
  let isZombie = false;
@@ -4388,6 +4426,10 @@ Continue from where you left off. Review your previous output to understand what
4388
4426
  return this.clusters.get(clusterId);
4389
4427
  }
4390
4428
 
4429
+ getFinalRun(clusterId) {
4430
+ return this._finalRuns.get(clusterId) || null;
4431
+ }
4432
+
4391
4433
  /**
4392
4434
  * Export cluster conversation
4393
4435
  * @param {String} clusterId - Cluster ID
package/src/preflight.js CHANGED
@@ -29,6 +29,10 @@ const {
29
29
  } = require('../lib/provider-names');
30
30
  const { detectGitContext } = require('../lib/git-remote-utils');
31
31
  const { readKeychainCredentials } = require('./claude-credentials');
32
+ const {
33
+ TASK_EXECUTION_CONTEXT_ENV,
34
+ resolveTaskExecutionContext,
35
+ } = require('./task-execution-context');
32
36
 
33
37
  /**
34
38
  * Validation result
@@ -164,6 +168,13 @@ function checkClaudeAuth() {
164
168
  ...(method && { method }),
165
169
  });
166
170
 
171
+ // Claude Code supports gateway bearer authentication independently of Anthropic API keys.
172
+ // This is the documented path for OpenRouter and enterprise LLM gateways, where
173
+ // ANTHROPIC_API_KEY is intentionally present but empty to avoid credential conflicts.
174
+ if (process.env.ANTHROPIC_AUTH_TOKEN) {
175
+ return authResult(true, null, 'auth_token');
176
+ }
177
+
167
178
  // Check for Bedrock bearer token (highest priority)
168
179
  if (process.env.AWS_BEARER_TOKEN_BEDROCK) {
169
180
  if (!process.env.AWS_REGION) {
@@ -481,7 +492,7 @@ function validateProviderExecutionSettings(providerName, settings, options) {
481
492
  return [];
482
493
  }
483
494
 
484
- const executionContext = options.requireDocker ? 'docker' : 'detached';
495
+ const executionContext = resolvePreflightExecutionContext(options);
485
496
  const error = metadata.settingsValidator(providerSettings, { executionContext });
486
497
  if (!error) return [];
487
498
 
@@ -519,11 +530,20 @@ function validateProvider(providerName, options) {
519
530
  return { errors: [...isolationErrors, ...result.errors], warnings: result.warnings };
520
531
  }
521
532
 
522
- const result = validateRegistryCliProvider(providerName);
533
+ const result = validateRegistryCliProvider(
534
+ providerName,
535
+ resolvePreflightExecutionContext(options)
536
+ );
523
537
  return { errors: [...isolationErrors, ...result.errors], warnings: result.warnings };
524
538
  }
525
539
 
526
- function validateRegistryCliProvider(providerName) {
540
+ function resolvePreflightExecutionContext(options) {
541
+ const executionContext =
542
+ options.executionContext ?? (options.requireDocker ? 'docker' : 'detached');
543
+ return resolveTaskExecutionContext({ [TASK_EXECUTION_CONTEXT_ENV]: executionContext });
544
+ }
545
+
546
+ function validateRegistryCliProvider(providerName, executionContext) {
527
547
  const metadata = getProviderMetadata(providerName);
528
548
  const { command } = resolveProviderCommand(providerName);
529
549
  const installSteps = metadata.installInstructions.split('\n').filter(Boolean);
@@ -537,7 +557,7 @@ function validateRegistryCliProvider(providerName) {
537
557
  }
538
558
 
539
559
  const { getProvider } = require('./providers');
540
- if (getProvider(providerName).isAvailable()) {
560
+ if (getProvider(providerName).isAvailable({ executionContext })) {
541
561
  return { errors: [], warnings: [] };
542
562
  }
543
563
 
@@ -642,6 +662,7 @@ function validateGitRequirement() {
642
662
  * @param {boolean} options.quiet - Suppress success messages
643
663
  * @param {string} options.claudeCommand - Custom Claude command (from settings)
644
664
  * @param {string} options.provider - Provider override
665
+ * @param {'host'|'detached'|'docker'|'benchmark'} options.executionContext - Provider execution boundary
645
666
  * @returns {ValidationResult}
646
667
  */
647
668
  async function runPreflight(options = {}) {
@@ -670,10 +691,14 @@ async function runPreflight(options = {}) {
670
691
  const providerName = normalizeProviderName(
671
692
  options.provider || settings.defaultProvider || getDefaultProviderId()
672
693
  );
694
+ const providerOptions = {
695
+ ...options,
696
+ executionContext: resolvePreflightExecutionContext(options),
697
+ };
673
698
 
674
- const providerResult = validateProvider(providerName, options);
699
+ const providerResult = validateProvider(providerName, providerOptions);
675
700
  errors.push(...providerResult.errors);
676
- errors.push(...validateProviderExecutionSettings(providerName, settings, options));
701
+ errors.push(...validateProviderExecutionSettings(providerName, settings, providerOptions));
677
702
  warnings.push(...providerResult.warnings);
678
703
 
679
704
  // 4. Check issue provider CLI (if required)
@@ -6,6 +6,7 @@ const {
6
6
  } = require('../../lib/provider-names');
7
7
  const { commandExists, getCommandPath } = require('../../lib/provider-detection');
8
8
  const helper = require('../../lib/agent-cli-provider');
9
+ const { stripTimestampPrefix } = require('../../lib/agent-cli-provider/log-prefix');
9
10
 
10
11
  const warned = new Set();
11
12
 
@@ -32,13 +33,13 @@ class RuntimeProvider extends BaseProvider {
32
33
  return this._adapter;
33
34
  }
34
35
 
35
- isAvailable() {
36
+ isAvailable(context = {}) {
36
37
  if (this._usesBundledRunner()) {
37
- return helper.probeRuntimeProviderCli(this.name).available;
38
+ return helper.probeRuntimeProviderCli(this.name, undefined, undefined, context).available;
38
39
  }
39
40
  const { command } = resolveProviderCommand(this.name);
40
41
  if (!commandExists(command)) return false;
41
- return helper.probeRuntimeProviderCli(this.name).available;
42
+ return helper.probeRuntimeProviderCli(this.name, undefined, undefined, context).available;
42
43
  }
43
44
 
44
45
  getCliPath() {
@@ -218,25 +219,6 @@ function listProviders() {
218
219
  return helper.listProviderAdapters();
219
220
  }
220
221
 
221
- function stripTimestampPrefix(line) {
222
- if (!line || typeof line !== 'string') return '';
223
- let trimmed = line.trim().replace(/\r$/, '');
224
- if (!trimmed) return '';
225
-
226
- const tsMatch = trimmed.match(/^\[(\d{13})\](.*)$/);
227
- if (tsMatch) trimmed = (tsMatch[2] || '').trimStart();
228
-
229
- if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
230
- const pipeMatch = trimmed.match(/^[^|]{1,40}\|\s*(.*)$/);
231
- if (pipeMatch) {
232
- const afterPipe = (pipeMatch[1] || '').trimStart();
233
- if (afterPipe.startsWith('{') || afterPipe.startsWith('[')) return afterPipe;
234
- }
235
- }
236
-
237
- return trimmed;
238
- }
239
-
240
222
  function collectEvents(events, event) {
241
223
  if (!event) return;
242
224
  if (Array.isArray(event)) {
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ const TASK_EXECUTION_CONTEXT_ENV = 'ZEROSHOT_TASK_EXECUTION_CONTEXT';
4
+ const TASK_EXECUTION_CONTEXTS = Object.freeze(['host', 'detached', 'docker', 'benchmark']);
5
+ const taskExecutionContexts = new Set(TASK_EXECUTION_CONTEXTS);
6
+
7
+ function resolveTaskExecutionContext(environment = process.env) {
8
+ const context = environment[TASK_EXECUTION_CONTEXT_ENV];
9
+ if (context === undefined) return 'detached';
10
+ if (!taskExecutionContexts.has(context)) {
11
+ throw new Error(
12
+ `${TASK_EXECUTION_CONTEXT_ENV} must be one of: ${TASK_EXECUTION_CONTEXTS.join(', ')}.`
13
+ );
14
+ }
15
+ return context;
16
+ }
17
+
18
+ module.exports = {
19
+ TASK_EXECUTION_CONTEXT_ENV,
20
+ TASK_EXECUTION_CONTEXTS,
21
+ resolveTaskExecutionContext,
22
+ };
@@ -0,0 +1,20 @@
1
+ export const TASK_LOG_V2_MARKER: '[ZEROSHOT][LOG_FORMAT] channel-framed-v2';
2
+ export const TASK_LOG_STDOUT_PREFIX: '[ZEROSHOT][PROVIDER_STDOUT] ';
3
+ export const TASK_LOG_STDERR_PREFIX: '[ZEROSHOT][PROVIDER_STDERR] ';
4
+
5
+ export type TaskLogChannel = 'provider_stdout' | 'provider_stderr' | 'control' | 'legacy';
6
+ export type TaskLogFormat = 'stderr-tagged-v1' | 'channel-framed-v2' | null;
7
+
8
+ export interface DecodedTaskLogLine {
9
+ readonly channel: TaskLogChannel;
10
+ readonly content: string;
11
+ readonly format: TaskLogFormat;
12
+ readonly providerOutput: boolean;
13
+ readonly timestamp: number | null;
14
+ readonly timestamped: boolean;
15
+ }
16
+
17
+ export function decodeTaskLogLine(line: string): DecodedTaskLogLine;
18
+ export function formatTaskLogMarker(timestamp: number): string;
19
+ export function formatTaskLogStderr(timestamp: number, content: string): string;
20
+ export function formatTaskLogStdout(timestamp: number, content: string): string;
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const TASK_LOG_V2_MARKER = '[ZEROSHOT][LOG_FORMAT] channel-framed-v2';
4
+ const TASK_LOG_STDOUT_PREFIX = '[ZEROSHOT][PROVIDER_STDOUT] ';
5
+ const TASK_LOG_STDERR_PREFIX = '[ZEROSHOT][PROVIDER_STDERR] ';
6
+ const TASK_LOG_V1_MARKER = '[ZEROSHOT][LOG_FORMAT] stderr-tagged-v1';
7
+ const CONTROL_LINE =
8
+ /^\[(?:ATTACH|CLEANUP|CRASH|DETACH|ERROR|OMP-OWNERSHIP|SDK-DIAGNOSTIC|SYSTEM)\](?:\s|$)/;
9
+
10
+ function timestampParts(line) {
11
+ const normalized = line.replace(/\r$/, '');
12
+ const match = /^\[(\d{13}|\d{4}-\d{2}-\d{2}T[^\]]+)\](.*)$/.exec(normalized);
13
+ if (!match) return { content: normalized, timestamp: null };
14
+ const rawTimestamp = match[1];
15
+ const epochTimestamp = /^\d{13}$/.test(rawTimestamp);
16
+ const timestamp = epochTimestamp
17
+ ? Number.parseInt(rawTimestamp, 10)
18
+ : new Date(rawTimestamp).getTime();
19
+ return {
20
+ content: match[2].trimStart(),
21
+ timestamp: Number.isFinite(timestamp) ? timestamp : null,
22
+ };
23
+ }
24
+
25
+ function decodeTaskLogLine(line) {
26
+ const { content, timestamp } = timestampParts(line);
27
+ const common = { timestamp, timestamped: timestamp !== null };
28
+ if (content.startsWith(TASK_LOG_STDOUT_PREFIX)) {
29
+ return {
30
+ ...common,
31
+ channel: 'provider_stdout',
32
+ content: content.slice(TASK_LOG_STDOUT_PREFIX.length),
33
+ format: null,
34
+ providerOutput: true,
35
+ };
36
+ }
37
+ if (content.startsWith(TASK_LOG_STDERR_PREFIX)) {
38
+ return {
39
+ ...common,
40
+ channel: 'provider_stderr',
41
+ content: content.slice(TASK_LOG_STDERR_PREFIX.length),
42
+ format: null,
43
+ providerOutput: false,
44
+ };
45
+ }
46
+ let format = null;
47
+ if (content === TASK_LOG_V2_MARKER) format = 'channel-framed-v2';
48
+ else if (content === TASK_LOG_V1_MARKER) format = 'stderr-tagged-v1';
49
+ const control =
50
+ format !== null || content.startsWith('[ZEROSHOT][FATAL] ') || CONTROL_LINE.test(content);
51
+ return {
52
+ ...common,
53
+ channel: control ? 'control' : 'legacy',
54
+ content,
55
+ format,
56
+ providerOutput: !control,
57
+ };
58
+ }
59
+
60
+ function formatTaskLogMarker(timestamp) {
61
+ return `[${timestamp}]${TASK_LOG_V2_MARKER}\n`;
62
+ }
63
+
64
+ function formatTaskLogStdout(timestamp, content) {
65
+ return `[${timestamp}]${TASK_LOG_STDOUT_PREFIX}${content}\n`;
66
+ }
67
+
68
+ function formatTaskLogStderr(timestamp, content) {
69
+ return `[${timestamp}]${TASK_LOG_STDERR_PREFIX}${content}\n`;
70
+ }
71
+
72
+ module.exports = {
73
+ TASK_LOG_STDERR_PREFIX,
74
+ TASK_LOG_STDOUT_PREFIX,
75
+ TASK_LOG_V2_MARKER,
76
+ decodeTaskLogLine,
77
+ formatTaskLogMarker,
78
+ formatTaskLogStderr,
79
+ formatTaskLogStdout,
80
+ };
@@ -4,7 +4,7 @@
4
4
  * The OMP prompt is task content (repository text, instructions, whatever a caller pasted in), so
5
5
  * it must never travel in the watcher's argv: `ps` and /proc/<pid>/cmdline expose argv to every
6
6
  * local user for the entire lifetime of a long-lived watcher. task-lib/runner.js therefore hands
7
- * the prompt to task-lib/rpc-watcher.js over the anonymous stdin pipe fork() creates for it. The
7
+ * the prompt to task-lib/rpc-watcher.js over its anonymous stdin pipe. The
8
8
  * pipe has exactly two ends and no name, so nothing is written to the filesystem, the task log, or
9
9
  * the ledger while the prompt is in transit.
10
10
  *
@@ -12,6 +12,11 @@ const CLAUDE_SETTINGS_ENV = 'ZEROSHOT_CLAUDE_SETTINGS_FILE';
12
12
  const CLAUDE_MCP_CONFIG_ENV = 'ZEROSHOT_CLAUDE_MCP_CONFIG_FILE';
13
13
  const ASK_USER_HOOK = 'block-ask-user-question.py';
14
14
  const DANGEROUS_GIT_HOOK = 'block-dangerous-git.py';
15
+ const ALTERNATE_BACKEND_ENV = [
16
+ 'CLAUDE_CODE_USE_BEDROCK',
17
+ 'CLAUDE_CODE_USE_VERTEX',
18
+ 'CLAUDE_CODE_USE_FOUNDRY',
19
+ ];
15
20
  function isMutableRecord(value) {
16
21
  return typeof value === 'object' && value !== null;
17
22
  }
@@ -109,6 +114,25 @@ function ensureDangerousGitHook(targetClaudeDir) {
109
114
  writeSettings(settingsPath, settings);
110
115
  }
111
116
  }
117
+ function ensureExplicitGatewayBackend(targetClaudeDir, environment) {
118
+ if (!environment.ANTHROPIC_AUTH_TOKEN?.trim() || !environment.ANTHROPIC_BASE_URL?.trim())
119
+ return;
120
+ const overlayDir = requireTargetClaudeDir(targetClaudeDir);
121
+ const settingsPath = path.join(overlayDir, SETTINGS_BASENAME);
122
+ const settings = readSettings(settingsPath);
123
+ const existingEnv = settings.env;
124
+ let env = {};
125
+ if (existingEnv !== undefined) {
126
+ if (!isMutableRecord(existingEnv)) {
127
+ throw new TypeError('Claude settings env must be an object.');
128
+ }
129
+ env = { ...existingEnv };
130
+ }
131
+ for (const name of ALTERNATE_BACKEND_ENV)
132
+ env[name] = '0';
133
+ settings.env = env;
134
+ writeSettings(settingsPath, settings);
135
+ }
112
136
  function prepareClaudeSettingsOverlay(options = {}) {
113
137
  const overlayDir = fs.mkdtempSync(path.join(os.tmpdir(), OVERLAY_PREFIX));
114
138
  fs.chmodSync(overlayDir, 0o700);
@@ -116,6 +140,7 @@ function prepareClaudeSettingsOverlay(options = {}) {
116
140
  ensureAskUserQuestionHook(overlayDir);
117
141
  if (options.includeDangerousGit)
118
142
  ensureDangerousGitHook(overlayDir);
143
+ ensureExplicitGatewayBackend(overlayDir, options.environment || process.env);
119
144
  return path.join(overlayDir, SETTINGS_BASENAME);
120
145
  }
121
146
  catch (error) {
@@ -19,6 +19,7 @@ interface HookEntry {
19
19
 
20
20
  interface OverlayOptions {
21
21
  includeDangerousGit?: boolean;
22
+ environment?: NodeJS.ProcessEnv;
22
23
  }
23
24
 
24
25
  interface WorktreeOptions {
@@ -34,6 +35,11 @@ const CLAUDE_SETTINGS_ENV = 'ZEROSHOT_CLAUDE_SETTINGS_FILE';
34
35
  const CLAUDE_MCP_CONFIG_ENV = 'ZEROSHOT_CLAUDE_MCP_CONFIG_FILE';
35
36
  const ASK_USER_HOOK = 'block-ask-user-question.py';
36
37
  const DANGEROUS_GIT_HOOK = 'block-dangerous-git.py';
38
+ const ALTERNATE_BACKEND_ENV = [
39
+ 'CLAUDE_CODE_USE_BEDROCK',
40
+ 'CLAUDE_CODE_USE_VERTEX',
41
+ 'CLAUDE_CODE_USE_FOUNDRY',
42
+ ] as const;
37
43
 
38
44
  function isMutableRecord(value: unknown): value is MutableRecord {
39
45
  return typeof value === 'object' && value !== null;
@@ -155,12 +161,34 @@ function ensureDangerousGitHook(targetClaudeDir: unknown): void {
155
161
  }
156
162
  }
157
163
 
164
+ function ensureExplicitGatewayBackend(
165
+ targetClaudeDir: unknown,
166
+ environment: NodeJS.ProcessEnv
167
+ ): void {
168
+ if (!environment.ANTHROPIC_AUTH_TOKEN?.trim() || !environment.ANTHROPIC_BASE_URL?.trim()) return;
169
+ const overlayDir = requireTargetClaudeDir(targetClaudeDir);
170
+ const settingsPath = path.join(overlayDir, SETTINGS_BASENAME);
171
+ const settings = readSettings(settingsPath);
172
+ const existingEnv = settings.env;
173
+ let env: MutableRecord = {};
174
+ if (existingEnv !== undefined) {
175
+ if (!isMutableRecord(existingEnv)) {
176
+ throw new TypeError('Claude settings env must be an object.');
177
+ }
178
+ env = { ...existingEnv };
179
+ }
180
+ for (const name of ALTERNATE_BACKEND_ENV) env[name] = '0';
181
+ settings.env = env;
182
+ writeSettings(settingsPath, settings);
183
+ }
184
+
158
185
  function prepareClaudeSettingsOverlay(options: OverlayOptions = {}): string {
159
186
  const overlayDir = fs.mkdtempSync(path.join(os.tmpdir(), OVERLAY_PREFIX));
160
187
  fs.chmodSync(overlayDir, 0o700);
161
188
  try {
162
189
  ensureAskUserQuestionHook(overlayDir);
163
190
  if (options.includeDangerousGit) ensureDangerousGitHook(overlayDir);
191
+ ensureExplicitGatewayBackend(overlayDir, options.environment || process.env);
164
192
  return path.join(overlayDir, SETTINGS_BASENAME);
165
193
  } catch (error) {
166
194
  fs.rmSync(overlayDir, { recursive: true, force: true });