@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.
- package/cli/index.js +114 -16
- package/lib/agent-cli-provider/adapters/common.js +1 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode-models.d.ts +7 -0
- package/lib/agent-cli-provider/adapters/opencode-models.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/opencode-models.js +87 -0
- package/lib/agent-cli-provider/adapters/opencode-models.js.map +1 -0
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +6 -58
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +27 -13
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +1 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/git-remote-utils.js +90 -7
- package/package.json +6 -15
- package/protocol/openengine-cluster/v1/worker.schema.json +47 -0
- package/scripts/assert-release-published.js +161 -2
- package/scripts/release-dry-run.js +83 -0
- package/scripts/release-preflight.js +24 -12
- package/scripts/release-recovery.js +149 -0
- package/scripts/semantic-release-notes.js +44 -0
- package/scripts/setup-merge-queue.sh +104 -118
- package/src/agent/agent-task-executor.js +18 -9
- package/src/agent-cli-provider/adapters/common.ts +1 -1
- package/src/agent-cli-provider/adapters/opencode-models.ts +100 -0
- package/src/agent-cli-provider/adapters/opencode.ts +8 -65
- package/src/agent-cli-provider/single-agent-runtime.ts +51 -33
- package/src/agent-cli-provider/types.ts +1 -0
- package/src/agent-wrapper.js +14 -2
- package/src/agents/git-pusher-template.js +94 -19
- package/src/attach/socket-discovery.js +9 -19
- package/src/attach/socket-paths.js +85 -0
- package/src/claude-task-runner.js +121 -37
- package/src/config-validator.js +2 -2
- package/src/isolation-manager.js +164 -74
- package/src/orchestrator.js +83 -34
- package/src/providers/index.js +5 -0
- package/src/task-run-model-args.js +155 -0
- package/task-lib/attachable-watcher.js +3 -9
- package/task-lib/commands/run.js +17 -2
- package/task-lib/runner.js +87 -29
package/src/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
1800
|
-
|
|
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
|
-
|
|
1804
|
-
|
|
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
|
-
|
|
2653
|
-
|
|
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
|
-
|
|
2664
|
-
|
|
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
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
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
|
-
//
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
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`);
|
package/src/providers/index.js
CHANGED
|
@@ -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,
|
|
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
|
|
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);
|
package/task-lib/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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`));
|
package/task-lib/runner.js
CHANGED
|
@@ -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 =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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(
|
|
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,
|
|
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
|
-
...
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
169
|
-
|
|
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
|
+
}
|