@the-open-engine/zeroshot 6.7.1 → 6.8.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 (44) hide show
  1. package/cli/index.js +114 -16
  2. package/lib/agent-cli-provider/adapters/common.js +1 -1
  3. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  4. package/lib/agent-cli-provider/adapters/opencode-models.d.ts +7 -0
  5. package/lib/agent-cli-provider/adapters/opencode-models.d.ts.map +1 -0
  6. package/lib/agent-cli-provider/adapters/opencode-models.js +87 -0
  7. package/lib/agent-cli-provider/adapters/opencode-models.js.map +1 -0
  8. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/opencode.js +6 -58
  10. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  11. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/single-agent-runtime.js +27 -13
  13. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  14. package/lib/agent-cli-provider/types.d.ts +1 -0
  15. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/types.js.map +1 -1
  17. package/lib/git-remote-utils.js +90 -7
  18. package/package.json +6 -15
  19. package/protocol/openengine-cluster/v1/worker.schema.json +47 -0
  20. package/scripts/assert-release-published.js +161 -2
  21. package/scripts/release-dry-run.js +83 -0
  22. package/scripts/release-preflight.js +24 -12
  23. package/scripts/release-recovery.js +149 -0
  24. package/scripts/semantic-release-notes.js +44 -0
  25. package/scripts/setup-merge-queue.sh +104 -118
  26. package/src/agent/agent-task-executor.js +18 -9
  27. package/src/agent-cli-provider/adapters/common.ts +1 -1
  28. package/src/agent-cli-provider/adapters/opencode-models.ts +100 -0
  29. package/src/agent-cli-provider/adapters/opencode.ts +8 -65
  30. package/src/agent-cli-provider/single-agent-runtime.ts +51 -33
  31. package/src/agent-cli-provider/types.ts +1 -0
  32. package/src/agent-wrapper.js +14 -2
  33. package/src/agents/git-pusher-template.js +94 -19
  34. package/src/attach/socket-discovery.js +9 -19
  35. package/src/attach/socket-paths.js +85 -0
  36. package/src/claude-task-runner.js +121 -37
  37. package/src/config-validator.js +2 -2
  38. package/src/isolation-manager.js +164 -74
  39. package/src/orchestrator.js +83 -34
  40. package/src/providers/index.js +5 -0
  41. package/src/task-run-model-args.js +155 -0
  42. package/task-lib/attachable-watcher.js +3 -9
  43. package/task-lib/commands/run.js +17 -2
  44. package/task-lib/runner.js +87 -29
@@ -224,6 +224,7 @@ function buildPrOptions(options, requiredQualityGates) {
224
224
  prBase: options.prBase || null,
225
225
  mergeQueue: options.mergeQueue || false,
226
226
  closeIssue: options.closeIssue || null,
227
+ gitRemote: options.gitRemote || null,
227
228
  autoMerge,
228
229
  ...(requiredQualityGates.length > 0 ? { requiredQualityGates } : {}),
229
230
  cwd: options.cwd || process.cwd(),
@@ -1266,6 +1267,8 @@ class Orchestrator {
1266
1267
  const { detectGitContext } = require('../lib/git-remote-utils');
1267
1268
  const gitContext = detectGitContext(options.cwd);
1268
1269
  cluster.gitPlatform = gitContext?.provider || null;
1270
+ options.gitRemote = gitContext?.remote || options.gitRemote || null;
1271
+ cluster.prOptions = buildPrOptions(options, requiredQualityGates);
1269
1272
 
1270
1273
  if (cluster.gitPlatform) {
1271
1274
  this._log(`[Orchestrator] Git platform detected: ${cluster.gitPlatform.toUpperCase()}`);
@@ -1796,18 +1799,30 @@ class Orchestrator {
1796
1799
  const workDir = options.cwd || process.cwd();
1797
1800
 
1798
1801
  isolationManager = new IsolationManager({});
1799
- // `prBase` is the PR target branch. Use that same local ref as the
1800
- // worktree base so repo-local integration commits are not skipped.
1802
+ const { detectGitContext } = require('../lib/git-remote-utils');
1803
+ const gitContext = detectGitContext(workDir);
1804
+ if (gitContext?.remote) {
1805
+ options.gitRemote = gitContext.remote;
1806
+ }
1807
+
1808
+ // `prBase` is the PR target branch. Base isolated work on its refreshed
1809
+ // remote-tracking ref so a stale local branch cannot change the plan.
1801
1810
  const worktreeOptions = {};
1802
1811
  if (options.prBase) {
1803
- worktreeOptions.baseRef = options.prBase;
1804
- this._log(`[Orchestrator] Using worktree base ref: ${options.prBase}`);
1812
+ const remoteName = options.gitRemote || 'origin';
1813
+ worktreeOptions.baseRef = `${remoteName}/${options.prBase}`;
1814
+ if (remoteName !== 'origin') {
1815
+ worktreeOptions.remoteName = remoteName;
1816
+ }
1817
+ worktreeOptions.requireFreshBase = true;
1818
+ this._log(`[Orchestrator] Using worktree base ref: ${worktreeOptions.baseRef}`);
1805
1819
  }
1806
1820
  worktreeInfo = isolationManager.createWorktreeIsolation(clusterId, workDir, worktreeOptions);
1807
1821
 
1808
1822
  this._log(`[Orchestrator] Starting cluster in worktree isolation mode`);
1809
1823
  this._log(`[Orchestrator] Worktree: ${worktreeInfo.path}`);
1810
1824
  this._log(`[Orchestrator] Branch: ${worktreeInfo.branch}`);
1825
+ this._log(`[Orchestrator] Worktree base commit: ${worktreeInfo.baseSha}`);
1811
1826
  }
1812
1827
 
1813
1828
  return { isolationManager, containerId, worktreeInfo, image: isolationImage };
@@ -1870,16 +1885,13 @@ class Orchestrator {
1870
1885
  closeIssue: options.closeIssue,
1871
1886
  requiredQualityGates: options.requiredQualityGates,
1872
1887
  autoMerge: resolveRunPlan(options).autoMerge,
1888
+ gitRemote: gitContext?.remote || options.gitRemote,
1889
+ issueNumber: inputData.number,
1890
+ issueTitle: inputData.title,
1891
+ includeIssueReference: !skipCloseIssue,
1873
1892
  cwd: options.cwd,
1874
1893
  });
1875
1894
 
1876
- // Template replacement for issue context
1877
- const issueRef = skipCloseIssue ? '' : `Closes #${inputData.number || 'unknown'}`;
1878
- gitPusherConfig.prompt = gitPusherConfig.prompt
1879
- .replace(/\{\{issue_number\}\}/g, inputData.number || 'unknown')
1880
- .replace(/\{\{issue_title\}\}/g, inputData.title || 'Implementation')
1881
- .replace(/Closes #\{\{issue_number\}\}/g, issueRef);
1882
-
1883
1895
  config.agents.push(gitPusherConfig);
1884
1896
  this._log(
1885
1897
  `[Orchestrator] Injected ${platform}-git-pusher agent (issue: ${inputData.number || 'N/A'}, PR platform: ${platform.toUpperCase()})`
@@ -2649,31 +2661,68 @@ class Orchestrator {
2649
2661
  }
2650
2662
 
2651
2663
  _resolveFailureInfo(cluster, clusterId) {
2652
- if (cluster.failureInfo) {
2653
- return cluster.failureInfo.agentId ? cluster.failureInfo : null;
2664
+ const persistedFailure = cluster.failureInfo?.agentId ? cluster.failureInfo : null;
2665
+ if (persistedFailure) {
2666
+ if (!this._hasDurableProgressAfterFailure(cluster, clusterId, persistedFailure)) {
2667
+ return persistedFailure;
2668
+ }
2669
+ this._log(
2670
+ `[Orchestrator] Ignoring recovered registry failure from ${persistedFailure.agentId}; durable task progress followed it`
2671
+ );
2654
2672
  }
2655
2673
 
2656
2674
  const errors = cluster.messageBus.query({
2657
2675
  cluster_id: clusterId,
2658
2676
  topic: 'AGENT_ERROR',
2659
- limit: 10,
2660
2677
  order: 'desc',
2661
2678
  });
2662
2679
 
2663
- if (errors.length === 0) {
2664
- return null;
2680
+ for (const error of errors) {
2681
+ const failureInfo = {
2682
+ agentId: error.sender,
2683
+ taskId: error.content?.data?.taskId || null,
2684
+ iteration: error.content?.data?.iteration || 0,
2685
+ error: error.content?.data?.error || error.content?.text,
2686
+ timestamp: error.timestamp,
2687
+ };
2688
+ if (this._hasDurableProgressAfterFailure(cluster, clusterId, failureInfo)) {
2689
+ this._log(
2690
+ `[Orchestrator] Ignoring recovered ledger failure from ${error.sender}; durable task progress followed it`
2691
+ );
2692
+ continue;
2693
+ }
2694
+
2695
+ this._log(`[Orchestrator] Found failure from ledger: ${failureInfo.agentId}`);
2696
+ return failureInfo;
2665
2697
  }
2666
2698
 
2667
- const firstError = errors[0];
2668
- const failureInfo = {
2669
- agentId: firstError.sender,
2670
- taskId: firstError.content?.data?.taskId || null,
2671
- iteration: firstError.content?.data?.iteration || 0,
2672
- error: firstError.content?.data?.error || firstError.content?.text,
2673
- timestamp: firstError.timestamp,
2674
- };
2675
- this._log(`[Orchestrator] Found failure from ledger: ${failureInfo.agentId}`);
2676
- return failureInfo;
2699
+ return null;
2700
+ }
2701
+
2702
+ _hasDurableProgressAfterFailure(cluster, clusterId, failureInfo) {
2703
+ const failureIteration = failureInfo.iteration;
2704
+ const failureTimestamp = failureInfo.timestamp;
2705
+ return cluster.messageBus
2706
+ .query({
2707
+ cluster_id: clusterId,
2708
+ topic: 'AGENT_LIFECYCLE',
2709
+ sender: failureInfo.agentId,
2710
+ since: failureTimestamp,
2711
+ })
2712
+ .some((message) => {
2713
+ const event = message.content?.data?.event;
2714
+ if (!['TASK_STARTED', 'TASK_COMPLETED'].includes(event)) {
2715
+ return false;
2716
+ }
2717
+
2718
+ const iteration = message.content?.data?.iteration;
2719
+ return (
2720
+ (Number.isFinite(failureTimestamp) && message.timestamp > failureTimestamp) ||
2721
+ (Number.isInteger(failureIteration) &&
2722
+ Number.isInteger(iteration) &&
2723
+ iteration > failureIteration)
2724
+ );
2725
+ });
2677
2726
  }
2678
2727
 
2679
2728
  _requireFailedAgent(clusterId, cluster, failureInfo) {
@@ -3996,7 +4045,6 @@ Continue from where you left off. Review your previous output to understand what
3996
4045
  }
3997
4046
  }
3998
4047
 
3999
- // Generate platform-specific git-pusher agent from template
4000
4048
  const {
4001
4049
  generateGitPusherAgent,
4002
4050
  isPlatformSupported,
@@ -4008,18 +4056,19 @@ Continue from where you left off. Review your previous output to understand what
4008
4056
  );
4009
4057
  }
4010
4058
 
4011
- // Use persisted PR options from cluster state (or empty for repo settings fallback)
4012
- const gitPusherConfig = generateGitPusherAgent(platform, cluster.prOptions || {});
4013
-
4014
4059
  // Get issue context from ledger
4015
4060
  const issueMsg = cluster.messageBus.ledger.findLast({ topic: 'ISSUE_OPENED' });
4016
4061
  const issueNumber = issueMsg?.content?.data?.number || 'unknown';
4017
4062
  const issueTitle = issueMsg?.content?.data?.title || 'Implementation';
4018
4063
 
4019
- // Inject issue context into prompt
4020
- gitPusherConfig.prompt = gitPusherConfig.prompt
4021
- .replace(/\{\{issue_number\}\}/g, issueNumber)
4022
- .replace(/\{\{issue_title\}\}/g, issueTitle);
4064
+ // Generate the final prompt in one typed assembly pass. Issue values are
4065
+ // resolved before the shell-quoted remote is inserted, so placeholder-like
4066
+ // remote names cannot be rewritten by issue context.
4067
+ const gitPusherConfig = generateGitPusherAgent(platform, {
4068
+ ...(cluster.prOptions || {}),
4069
+ issueNumber,
4070
+ issueTitle,
4071
+ });
4023
4072
 
4024
4073
  await this._opAddAgents(cluster, { agents: [gitPusherConfig] }, context);
4025
4074
  this._log(` [--pr mode] Injected ${platform}-git-pusher agent`);
@@ -129,6 +129,11 @@ class RuntimeProvider extends BaseProvider {
129
129
  return this._adapter.validateModelId(modelId);
130
130
  }
131
131
 
132
+ validateConfiguredModelId(modelId) {
133
+ const validator = this._adapter.validateConfiguredModelId || this._adapter.validateModelId;
134
+ return validator(modelId);
135
+ }
136
+
132
137
  isRetryableError(err) {
133
138
  return helper.classifyProviderError(this.name, err).retryable;
134
139
  }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Append a resolved model selection to a nested `zeroshot task run` invocation.
3
+ *
4
+ * Direct requests use the public, catalog-strict `--model` channel.
5
+ * Provider-level selections carry only their level. The child must resolve the
6
+ * concrete model again from its effective provider settings.
7
+ *
8
+ * @param {string[]} args
9
+ * @param {Object|null|undefined} modelSpec
10
+ * @param {'direct'|'provider-level'} [modelSpecSource]
11
+ * @returns {string[]}
12
+ */
13
+ function appendTaskRunModelArgs(args, modelSpec, modelSpecSource = 'direct') {
14
+ if (modelSpecSource === 'provider-level') {
15
+ if (!modelSpec?.level) {
16
+ throw new Error('Provider-level task model selections require a model level');
17
+ }
18
+ args.push('--model-level', modelSpec.level);
19
+ } else if (modelSpec?.model) {
20
+ args.push('--model', modelSpec.model);
21
+ }
22
+
23
+ if (modelSpec?.reasoningEffort) {
24
+ args.push('--reasoning-effort', modelSpec.reasoningEffort);
25
+ }
26
+
27
+ return args;
28
+ }
29
+
30
+ const ISOLATED_SETTINGS_FILE_ENV = 'ZEROSHOT_SETTINGS_FILE';
31
+ const ISOLATED_SETTINGS_FILE_MARKER = 'ZEROSHOT_DOCKER_SETTINGS_FILE';
32
+ const LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV = 'ZEROSHOT_ISOLATED_PROVIDER_SETTINGS_JSON';
33
+ const SETTINGS_BOOTSTRAP_SCRIPT = String.raw`
34
+ const childProcess = require('node:child_process');
35
+ const fs = require('node:fs');
36
+ const os = require('node:os');
37
+ const path = require('node:path');
38
+
39
+ const snapshot = process.argv[1];
40
+ const command = process.argv[2];
41
+ const args = process.argv.slice(3);
42
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'zeroshot-isolated-settings-'));
43
+ const settingsFile = path.join(directory, 'settings.json');
44
+
45
+ try {
46
+ fs.writeFileSync(settingsFile, snapshot, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
47
+ const result = childProcess.spawnSync(command, args, {
48
+ stdio: 'inherit',
49
+ env: {
50
+ ...process.env,
51
+ ${ISOLATED_SETTINGS_FILE_ENV}: settingsFile,
52
+ ${ISOLATED_SETTINGS_FILE_MARKER}: '1',
53
+ },
54
+ });
55
+ if (result.error) throw result.error;
56
+ process.exitCode = result.status === null ? 1 : result.status;
57
+ } finally {
58
+ fs.rmSync(directory, { recursive: true, force: true });
59
+ }
60
+ `.trim();
61
+
62
+ /**
63
+ * Wrap an isolated task command with a Docker-only, temporary settings-file
64
+ * bootstrap. The snapshot is a closed projection containing only the selected
65
+ * OpenCode level and its model. It never contains arbitrary provider settings,
66
+ * credentials, or caller-owned keys.
67
+ *
68
+ * @param {string[]} command
69
+ * @param {{providerName: string, settings: Object, modelSpecSource: 'direct'|'provider-level', modelSpec: Object|null|undefined}} context
70
+ * @returns {string[]}
71
+ */
72
+ function wrapTaskRunWithIsolatedSettings(command, context) {
73
+ const { providerName, settings, modelSpecSource, modelSpec } = context;
74
+ if (providerName !== 'opencode' || modelSpecSource !== 'provider-level') return command;
75
+ const snapshot = buildIsolatedSettingsSnapshot(settings, modelSpec);
76
+ if (snapshot === null) return command;
77
+ return ['node', '-e', SETTINGS_BOOTSTRAP_SCRIPT, snapshot, ...command];
78
+ }
79
+
80
+ function buildIsolatedSettingsSnapshot(settings, modelSpec) {
81
+ const level = modelSpec?.level;
82
+ if (!['level1', 'level2', 'level3'].includes(level)) {
83
+ throw permanentError(
84
+ 'Provider-level isolated OpenCode selections require a valid model level.'
85
+ );
86
+ }
87
+
88
+ const providerSettings = ownRecordValue(settings, 'providerSettings', 'settings') ?? {};
89
+ const opencodeSettings = ownRecordValue(
90
+ providerSettings,
91
+ 'opencode',
92
+ 'settings.providerSettings'
93
+ );
94
+ const levelOverrides = ownRecordValue(
95
+ opencodeSettings,
96
+ 'levelOverrides',
97
+ 'settings.providerSettings.opencode'
98
+ );
99
+ const levelOverride = ownRecordValue(
100
+ levelOverrides,
101
+ level,
102
+ 'settings.providerSettings.opencode.levelOverrides'
103
+ );
104
+ const configuredModel =
105
+ levelOverride && Object.prototype.hasOwnProperty.call(levelOverride, 'model')
106
+ ? levelOverride.model
107
+ : null;
108
+ if (configuredModel !== null && typeof configuredModel !== 'string') {
109
+ throw permanentError(`Configured isolated OpenCode ${level} model must be a string or null.`);
110
+ }
111
+ if (modelSpec?.model !== configuredModel) {
112
+ throw permanentError(
113
+ `Provider-level model "${modelSpec?.model}" does not match the effective isolated ${modelSpec?.level} model "${configuredModel}".`
114
+ );
115
+ }
116
+ if (configuredModel === null) return null;
117
+
118
+ return JSON.stringify({
119
+ providerSettings: {
120
+ opencode: {
121
+ levelOverrides: {
122
+ [level]: { model: configuredModel },
123
+ },
124
+ },
125
+ },
126
+ });
127
+ }
128
+
129
+ function ownRecordValue(record, key, field) {
130
+ if (record === null || record === undefined) return undefined;
131
+ if (typeof record !== 'object' || Array.isArray(record)) {
132
+ throw permanentError(`${field} must be an object.`);
133
+ }
134
+ if (!Object.prototype.hasOwnProperty.call(record, key)) return undefined;
135
+ const value = record[key];
136
+ if (value === null || value === undefined) return undefined;
137
+ if (typeof value !== 'object' || Array.isArray(value)) {
138
+ throw permanentError(`${field}.${key} must be an object.`);
139
+ }
140
+ return value;
141
+ }
142
+
143
+ function permanentError(message) {
144
+ const error = new Error(message);
145
+ error.permanent = true;
146
+ return error;
147
+ }
148
+
149
+ module.exports = {
150
+ ISOLATED_SETTINGS_FILE_ENV,
151
+ ISOLATED_SETTINGS_FILE_MARKER,
152
+ LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV,
153
+ appendTaskRunModelArgs,
154
+ wrapTaskRunWithIsolatedSettings,
155
+ };
@@ -5,10 +5,8 @@
5
5
  * Runs detached from parent, provides Unix socket for attach clients.
6
6
  */
7
7
 
8
- import { appendFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
8
+ import { appendFileSync, unlinkSync } from 'fs';
9
9
  import { unlink } from 'fs/promises';
10
- import { join } from 'path';
11
- import { homedir } from 'os';
12
10
  import { updateTask } from './store.js';
13
11
  import {
14
12
  detectProviderFatalError,
@@ -72,6 +70,7 @@ process.on('unhandledRejection', (reason) => {
72
70
 
73
71
  const require = createRequire(import.meta.url);
74
72
  const { AttachServer } = require('../src/attach');
73
+ const { getTaskSocketPath } = require('../src/attach/socket-paths');
75
74
  const { normalizeProviderName } = require('../lib/provider-names');
76
75
 
77
76
  const taskId = taskIdArg;
@@ -88,12 +87,7 @@ const commandSpec = config.commandSpec || {
88
87
  commandSpecCleanup = commandSpec.cleanup || [];
89
88
  let server = null;
90
89
 
91
- const SOCKET_DIR = join(homedir(), '.zeroshot', 'sockets');
92
- const socketPath = join(SOCKET_DIR, `${taskId}.sock`);
93
-
94
- if (!existsSync(SOCKET_DIR)) {
95
- mkdirSync(SOCKET_DIR, { recursive: true });
96
- }
90
+ const socketPath = getTaskSocketPath(taskId);
97
91
 
98
92
  function log(msg) {
99
93
  appendFileSync(logFile, msg);
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { spawnTask } from '../runner.js';
2
+ import { shouldUseAttachableWatcher, spawnTask } from '../runner.js';
3
3
 
4
4
  export async function runTask(prompt, options = {}) {
5
5
  if (!prompt || prompt.trim().length === 0) {
@@ -46,8 +46,23 @@ export async function runTask(prompt, options = {}) {
46
46
  console.log(chalk.dim(` Log: ${task.logFile}`));
47
47
  console.log(chalk.dim(` CWD: ${task.cwd}`));
48
48
 
49
+ const attachSupported = shouldUseAttachableWatcher(
50
+ {
51
+ jsonSchema: outputFormat === 'json' ? jsonSchema : null,
52
+ },
53
+ task.provider
54
+ );
55
+
49
56
  console.log(chalk.dim('\nCommands:'));
50
- console.log(chalk.dim(` zeroshot attach ${task.id} # Attach to task (Ctrl+B d to detach)`));
57
+ if (attachSupported) {
58
+ console.log(chalk.dim(` zeroshot attach ${task.id} # Attach to task (Ctrl+B d to detach)`));
59
+ } else {
60
+ console.log(
61
+ chalk.dim(
62
+ ` Attach unavailable: ${task.provider} strict structured output uses a non-PTY watcher`
63
+ )
64
+ );
65
+ }
51
66
  console.log(chalk.dim(` zeroshot logs ${task.id} # View output`));
52
67
  console.log(chalk.dim(` zeroshot logs -f ${task.id} # Follow output`));
53
68
  console.log(chalk.dim(` zeroshot status ${task.id} # Check status`));
@@ -7,6 +7,11 @@ import { createRequire } from 'module';
7
7
 
8
8
  const require = createRequire(import.meta.url);
9
9
  const { prepareSingleAgentProviderCommand } = require('./provider-helper-runtime.js');
10
+ const {
11
+ ISOLATED_SETTINGS_FILE_ENV,
12
+ ISOLATED_SETTINGS_FILE_MARKER,
13
+ LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV,
14
+ } = require('../src/task-run-model-args.js');
10
15
  export {
11
16
  isOwnedProcessTreeRunning,
12
17
  isProcessRunning,
@@ -25,10 +30,10 @@ export function spawnTask(prompt, options = {}) {
25
30
 
26
31
  const outputFormat = resolveOutputFormat(options);
27
32
  const jsonSchema = resolveJsonSchema(options, outputFormat);
28
- const prepared = prepareSingleAgentProviderCommand({
29
- provider: options.provider || null,
30
- context: prompt,
31
- options: buildProviderOptions(options, outputFormat, jsonSchema, cwd),
33
+ const prepared = prepareTaskProviderCommandFromResolved(prompt, options, {
34
+ outputFormat,
35
+ jsonSchema,
36
+ cwd,
32
37
  });
33
38
  const providerName = prepared.adapter.id;
34
39
  const modelSpec = prepared.options.modelSpec;
@@ -53,7 +58,13 @@ export function spawnTask(prompt, options = {}) {
53
58
  providerName,
54
59
  commandSpec
55
60
  );
56
- const watcherScript = resolveWatcherScript(options);
61
+ const watcherScript = resolveWatcherScript(
62
+ {
63
+ attachable: options.attachable,
64
+ jsonSchema,
65
+ },
66
+ providerName
67
+ );
57
68
  spawnWatcher({
58
69
  watcherScript,
59
70
  id,
@@ -66,6 +77,24 @@ export function spawnTask(prompt, options = {}) {
66
77
  return task;
67
78
  }
68
79
 
80
+ export function prepareTaskProviderCommand(prompt, options = {}) {
81
+ const outputFormat = resolveOutputFormat(options);
82
+ return prepareTaskProviderCommandFromResolved(prompt, options, {
83
+ outputFormat,
84
+ jsonSchema: resolveJsonSchema(options, outputFormat),
85
+ cwd: options.cwd || process.cwd(),
86
+ });
87
+ }
88
+
89
+ function prepareTaskProviderCommandFromResolved(prompt, options, runtime) {
90
+ const modelSelection = resolveRequestedModelSelection(options);
91
+ return prepareSingleAgentProviderCommand({
92
+ provider: options.provider || null,
93
+ context: prompt,
94
+ options: buildProviderOptions(options, runtime, modelSelection),
95
+ });
96
+ }
97
+
69
98
  function resolveOutputFormat(options) {
70
99
  return options.outputFormat || 'stream-json';
71
100
  }
@@ -79,13 +108,13 @@ function resolveJsonSchema(options, outputFormat) {
79
108
  return jsonSchema;
80
109
  }
81
110
 
82
- function buildProviderOptions(options, outputFormat, jsonSchema, cwd) {
111
+ function buildProviderOptions(options, runtime, modelSelection) {
83
112
  return {
84
- outputFormat,
85
- jsonSchema,
86
- cwd,
113
+ outputFormat: runtime.outputFormat,
114
+ jsonSchema: runtime.jsonSchema,
115
+ cwd: runtime.cwd,
87
116
  autoApprove: true,
88
- ...modelSpecOption(options),
117
+ ...(modelSelection === undefined ? {} : { modelSpec: modelSelection.modelSpec }),
89
118
  ...mcpConfigOption(options),
90
119
  ...(options.resume ? { resumeSessionId: options.resume } : {}),
91
120
  ...(options.continue ? { continueSession: true } : {}),
@@ -98,27 +127,32 @@ function mcpConfigOption(options) {
98
127
  return { mcpConfig: entries };
99
128
  }
100
129
 
101
- function modelSpecOption(options) {
102
- const modelSpec = resolveRequestedModelSpec(options);
103
- return modelSpec === undefined ? {} : { modelSpec };
104
- }
130
+ function resolveRequestedModelSelection(options) {
131
+ if (Object.prototype.hasOwnProperty.call(options, 'configuredModel')) {
132
+ throw new Error(
133
+ '--configured-model is not supported; configure providerSettings levelOverrides instead'
134
+ );
135
+ }
105
136
 
106
- function resolveRequestedModelSpec(options) {
107
137
  if (options.model) {
108
- return {
109
- model: options.model,
110
- ...(options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}),
111
- };
138
+ return directModelSelection(options);
112
139
  }
113
140
 
114
- if (options.reasoningEffort) {
115
- return {
116
- ...(options.modelLevel ? { level: options.modelLevel } : {}),
117
- reasoningEffort: options.reasoningEffort,
118
- };
119
- }
120
- if (options.modelLevel) return { level: options.modelLevel };
121
- return undefined;
141
+ return providerLevelSelection(options);
142
+ }
143
+
144
+ function directModelSelection(options) {
145
+ const modelSpec = { model: options.model };
146
+ if (options.reasoningEffort) modelSpec.reasoningEffort = options.reasoningEffort;
147
+ return { modelSpec };
148
+ }
149
+
150
+ function providerLevelSelection(options) {
151
+ if (!options.reasoningEffort && !options.modelLevel) return undefined;
152
+ const modelSpec = {};
153
+ if (options.modelLevel) modelSpec.level = options.modelLevel;
154
+ if (options.reasoningEffort) modelSpec.reasoningEffort = options.reasoningEffort;
155
+ return { modelSpec };
122
156
  }
123
157
 
124
158
  function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, modelSpec }) {
@@ -165,12 +199,25 @@ function buildWatcherCommandSpec(commandSpec) {
165
199
  return watcherCommandSpec;
166
200
  }
167
201
 
168
- function resolveWatcherScript(options) {
169
- const useAttachable = options.attachable !== false && !options.jsonSchema;
202
+ export function shouldUseAttachableWatcher(options, providerName) {
203
+ if (options.attachable === false) {
204
+ return false;
205
+ }
206
+
207
+ // Claude strict structured output still needs the non-PTY watcher. Claude
208
+ // can treat PTY notifications as streaming commands and reject the run.
209
+ // Other providers, including Codex, support their structured-output mode in
210
+ // the attachable PTY watcher and must not lose the advertised attach socket.
211
+ return !(providerName === 'claude' && options.jsonSchema);
212
+ }
213
+
214
+ function resolveWatcherScript(options, providerName) {
215
+ const useAttachable = shouldUseAttachableWatcher(options, providerName);
170
216
  return useAttachable ? join(__dirname, 'attachable-watcher.js') : join(__dirname, 'watcher.js');
171
217
  }
172
218
 
173
219
  function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfig }) {
220
+ const watcherEnv = buildWatcherEnv();
174
221
  const watcher = fork(
175
222
  watcherScript,
176
223
  [id, cwd, logFile, JSON.stringify(finalArgs), JSON.stringify(watcherConfig)],
@@ -178,9 +225,20 @@ function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfi
178
225
  detached: true,
179
226
  stdio: 'ignore',
180
227
  windowsHide: true,
228
+ env: watcherEnv,
181
229
  }
182
230
  );
183
231
 
184
232
  watcher.unref();
185
233
  watcher.disconnect(); // Close IPC channel so parent can exit
186
234
  }
235
+
236
+ export function buildWatcherEnv(sourceEnv = process.env) {
237
+ const watcherEnv = { ...sourceEnv };
238
+ delete watcherEnv[LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV];
239
+ if (watcherEnv[ISOLATED_SETTINGS_FILE_MARKER] === '1') {
240
+ delete watcherEnv[ISOLATED_SETTINGS_FILE_ENV];
241
+ delete watcherEnv[ISOLATED_SETTINGS_FILE_MARKER];
242
+ }
243
+ return watcherEnv;
244
+ }