@the-open-engine/zeroshot 6.39.1 → 6.39.2

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 (73) hide show
  1. package/README.md +33 -1
  2. package/cli/index.js +30 -7
  3. package/cluster-templates/conductor-bootstrap.json +1 -1
  4. package/lib/agent-cli-provider/adapters/claude.js +1 -1
  5. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/adapters/codex.js +26 -1
  8. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  9. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/adapters/omp.js +1 -1
  11. package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
  12. package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts +4 -0
  13. package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts.map +1 -0
  14. package/lib/agent-cli-provider/omp/sdk-environment-policy.js +37 -0
  15. package/lib/agent-cli-provider/omp/sdk-environment-policy.js.map +1 -0
  16. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.d.ts.map +1 -1
  17. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js +4 -15
  18. package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js.map +1 -1
  19. package/lib/agent-cli-provider/omp/sdk-process-result.d.ts +2 -0
  20. package/lib/agent-cli-provider/omp/sdk-process-result.d.ts.map +1 -1
  21. package/lib/agent-cli-provider/omp/sdk-process-result.js +2 -2
  22. package/lib/agent-cli-provider/omp/sdk-process-result.js.map +1 -1
  23. package/lib/agent-cli-provider/omp/sdk-process-runner.d.ts.map +1 -1
  24. package/lib/agent-cli-provider/omp/sdk-process-runner.js +6 -1
  25. package/lib/agent-cli-provider/omp/sdk-process-runner.js.map +1 -1
  26. package/lib/agent-cli-provider/provider-registry.d.ts +3 -1
  27. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  28. package/lib/agent-cli-provider/provider-registry.js +9 -2
  29. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  30. package/lib/agent-cli-provider/single-agent-runtime.d.ts +4 -1
  31. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  32. package/lib/agent-cli-provider/single-agent-runtime.js +18 -15
  33. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  34. package/lib/agent-cli-provider/types.d.ts +3 -0
  35. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  36. package/lib/agent-cli-provider/types.js.map +1 -1
  37. package/lib/cluster-worker/engine-adapter-common.js +18 -0
  38. package/lib/cluster-worker/engine-adapter.js +2 -1
  39. package/lib/git-remote-utils.js +22 -5
  40. package/npm-shrinkwrap.json +2 -2
  41. package/package.json +2 -1
  42. package/scripts/omp/runtime-identities.js +31 -18
  43. package/scripts/omp/runtime.js +2 -0
  44. package/src/agent/agent-lifecycle.js +17 -1
  45. package/src/agent/output-extraction-failures.js +73 -0
  46. package/src/agent/output-extraction-failures.ts +100 -0
  47. package/src/agent/output-extraction-types.ts +5 -0
  48. package/src/agent/provider-terminal-failure.js +2 -2
  49. package/src/agent/provider-terminal-failure.ts +4 -2
  50. package/src/agent-cli-provider/adapters/claude.ts +1 -1
  51. package/src/agent-cli-provider/adapters/codex.ts +36 -1
  52. package/src/agent-cli-provider/adapters/omp.ts +2 -1
  53. package/src/agent-cli-provider/omp/sdk-environment-policy.ts +38 -0
  54. package/src/agent-cli-provider/omp/sdk-process-private-runtime.ts +4 -16
  55. package/src/agent-cli-provider/omp/sdk-process-result.ts +4 -2
  56. package/src/agent-cli-provider/omp/sdk-process-runner.ts +7 -1
  57. package/src/agent-cli-provider/provider-registry.ts +10 -2
  58. package/src/agent-cli-provider/single-agent-runtime.ts +24 -15
  59. package/src/agent-cli-provider/types.ts +3 -0
  60. package/src/agents/git-pusher-template.js +61 -71
  61. package/src/attach/socket-paths.js +1 -1
  62. package/src/attach/socket-paths.ts +1 -1
  63. package/src/foreground-benchmark-run.js +24 -12
  64. package/src/legacy-lib/git-remote-utils.ts +23 -5
  65. package/src/omp-session-verifier.js +8 -5
  66. package/src/orchestrator.js +42 -0
  67. package/src/preflight.js +31 -6
  68. package/src/providers/index.js +3 -3
  69. package/src/task-execution-context.js +22 -0
  70. package/src/watcher-prompt-channel.js +1 -1
  71. package/src/worktree-claude-config.js +25 -0
  72. package/src/worktree-claude-config.ts +28 -0
  73. package/task-lib/runner.js +57 -22
@@ -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)
@@ -32,13 +32,13 @@ class RuntimeProvider extends BaseProvider {
32
32
  return this._adapter;
33
33
  }
34
34
 
35
- isAvailable() {
35
+ isAvailable(context = {}) {
36
36
  if (this._usesBundledRunner()) {
37
- return helper.probeRuntimeProviderCli(this.name).available;
37
+ return helper.probeRuntimeProviderCli(this.name, undefined, undefined, context).available;
38
38
  }
39
39
  const { command } = resolveProviderCommand(this.name);
40
40
  if (!commandExists(command)) return false;
41
- return helper.probeRuntimeProviderCli(this.name).available;
41
+ return helper.probeRuntimeProviderCli(this.name, undefined, undefined, context).available;
42
42
  }
43
43
 
44
44
  getCliPath() {
@@ -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
+ };
@@ -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 });
@@ -1,5 +1,5 @@
1
- import { fork } from 'child_process';
2
- import { join, dirname, resolve as resolvePath } from 'path';
1
+ import { execFileSync, spawn } from 'child_process';
2
+ import { isAbsolute, join, dirname, relative, resolve as resolvePath } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { mkdirSync } from 'fs';
5
5
  import { LOGS_DIR } from './config.js';
@@ -17,6 +17,8 @@ import {
17
17
  import { createRequire } from 'module';
18
18
 
19
19
  const require = createRequire(import.meta.url);
20
+ const { resolveTaskExecutionContext } = require('../src/task-execution-context.js');
21
+ export { resolveTaskExecutionContext };
20
22
  const {
21
23
  buildOmpPrompt,
22
24
  getProviderRegistryEntry,
@@ -43,8 +45,6 @@ const {
43
45
  partitionPathFor,
44
46
  createOmpSessionPartitionDirectory,
45
47
  } = require('../src/omp-session-partition');
46
- const TASK_EXECUTION_CONTEXT_ENV = 'ZEROSHOT_TASK_EXECUTION_CONTEXT';
47
- const TASK_EXECUTION_CONTEXTS = new Set(['host', 'detached', 'docker', 'benchmark']);
48
48
  export {
49
49
  isOwnedProcessTreeRunning,
50
50
  isProcessRunning,
@@ -401,8 +401,22 @@ function resolveOutputFormat(options) {
401
401
  }
402
402
 
403
403
  function resolveJsonSchema(options, outputFormat) {
404
- let jsonSchema = options.jsonSchema || null;
405
- if (jsonSchema && outputFormat !== 'json') {
404
+ let jsonSchema = options.jsonSchema ?? null;
405
+ if (typeof jsonSchema === 'string') {
406
+ try {
407
+ jsonSchema = JSON.parse(jsonSchema);
408
+ } catch (error) {
409
+ throw new Error(`--json-schema must be valid JSON: ${error.message}`);
410
+ }
411
+ }
412
+ if (
413
+ jsonSchema !== null &&
414
+ typeof jsonSchema !== 'boolean' &&
415
+ (typeof jsonSchema !== 'object' || Array.isArray(jsonSchema))
416
+ ) {
417
+ throw new Error('--json-schema must be a boolean or JSON Schema object.');
418
+ }
419
+ if (jsonSchema !== null && outputFormat !== 'json') {
406
420
  console.warn('Warning: --json-schema requires --output-format json, ignoring schema');
407
421
  jsonSchema = null;
408
422
  }
@@ -411,11 +425,13 @@ function resolveJsonSchema(options, outputFormat) {
411
425
 
412
426
  function buildProviderOptions(options, runtime, modelSelection) {
413
427
  const structuredOutputRecovery = options.structuredOutputRecovery === true;
428
+ const executionContext = resolveTaskExecutionContext();
414
429
  return {
415
430
  outputFormat: runtime.outputFormat,
416
431
  jsonSchema: runtime.jsonSchema,
417
432
  cwd: runtime.cwd,
418
- executionContext: resolveTaskExecutionContext(),
433
+ ...codexGitMetadataOption(options, runtime.cwd, executionContext),
434
+ executionContext,
419
435
  autoApprove: !structuredOutputRecovery,
420
436
  ...(modelSelection === undefined ? {} : { modelSpec: modelSelection.modelSpec }),
421
437
  ...(structuredOutputRecovery ? {} : mcpConfigOption(options)),
@@ -430,15 +446,35 @@ function buildProviderOptions(options, runtime, modelSelection) {
430
446
  };
431
447
  }
432
448
 
433
- export function resolveTaskExecutionContext(environment = process.env) {
434
- const context = environment[TASK_EXECUTION_CONTEXT_ENV];
435
- if (context === undefined) return 'detached';
436
- if (!TASK_EXECUTION_CONTEXTS.has(context)) {
437
- throw new Error(
438
- `${TASK_EXECUTION_CONTEXT_ENV} must be one of: ${[...TASK_EXECUTION_CONTEXTS].join(', ')}.`
439
- );
449
+ function codexGitMetadataOption(options, cwd, executionContext) {
450
+ const settings = loadSettings();
451
+ const providerName = normalizeProviderName(
452
+ options.provider || settings.defaultProvider || getDefaultProviderId()
453
+ );
454
+ if (providerName !== 'codex') return {};
455
+ if (executionContext === 'docker' || executionContext === 'benchmark') return {};
456
+ const directories = resolveCodexGitMetadataDirectories(cwd);
457
+ return directories.length === 0 ? {} : { additionalWritableDirectories: directories };
458
+ }
459
+
460
+ export function resolveCodexGitMetadataDirectories(cwd, runGit = execFileSync) {
461
+ try {
462
+ const raw = runGit('git', ['rev-parse', '--git-common-dir'], {
463
+ cwd,
464
+ encoding: 'utf8',
465
+ stdio: ['ignore', 'pipe', 'ignore'],
466
+ });
467
+ const commonDirectory = resolvePath(cwd, raw.trim());
468
+ const fromWorkspace = relative(resolvePath(cwd), commonDirectory);
469
+ const outsideWorkspace =
470
+ isAbsolute(fromWorkspace) ||
471
+ fromWorkspace === '..' ||
472
+ fromWorkspace.startsWith('../') ||
473
+ fromWorkspace.startsWith('..\\');
474
+ return outsideWorkspace ? [commonDirectory] : [];
475
+ } catch {
476
+ return [];
440
477
  }
441
- return context;
442
478
  }
443
479
 
444
480
  function claudeSettingsFileOption() {
@@ -655,14 +691,14 @@ function resolveWatcherScript(options, providerName, invoke) {
655
691
  function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfig, rpcPrompt }) {
656
692
  const sendsPrompt = typeof rpcPrompt === 'string';
657
693
  const watcherEnv = buildWatcherEnv();
658
- const watcher = fork(
659
- watcherScript,
660
- [id, cwd, logFile, JSON.stringify(finalArgs), JSON.stringify(watcherConfig)],
694
+ const watcher = spawn(
695
+ process.execPath,
696
+ [watcherScript, id, cwd, logFile, JSON.stringify(finalArgs), JSON.stringify(watcherConfig)],
661
697
  {
662
698
  detached: true,
663
- // A prompt-carrying lane needs a real pipe on fd 0 to receive it; every other lane keeps
664
- // stdio fully ignored. fork() requires an explicit 'ipc' slot once stdio is an array.
665
- stdio: sendsPrompt ? ['pipe', 'ignore', 'ignore', 'ipc'] : 'ignore',
699
+ // A prompt-carrying lane needs only a private stdin pipe; detached watchers have no parent
700
+ // messaging contract, so an IPC channel would add a second, racy wrapper-lifetime owner.
701
+ stdio: sendsPrompt ? ['pipe', 'ignore', 'ignore'] : 'ignore',
666
702
  windowsHide: true,
667
703
  env: watcherEnv,
668
704
  }
@@ -673,7 +709,6 @@ function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfi
673
709
  if (sendsPrompt) sendWatcherPrompt(watcher.stdin, rpcPrompt);
674
710
 
675
711
  watcher.unref();
676
- watcher.disconnect(); // Close IPC channel so parent can exit
677
712
  }
678
713
 
679
714
  export function buildWatcherEnv(sourceEnv = process.env) {