@the-open-engine/zeroshot 6.10.0 → 6.10.1
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 +16 -0
- package/cluster-hooks/block-ask-user-question.py +2 -3
- package/cluster-hooks/block-dangerous-git.py +2 -3
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +42 -1
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +2 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +6 -2
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +13 -6
- package/src/agent/agent-task-executor.js +526 -313
- package/src/agent-cli-provider/adapters/claude.ts +46 -1
- package/src/agent-cli-provider/contract-options.ts +6 -0
- package/src/agent-cli-provider/types.ts +9 -6
- package/src/claude-task-runner.js +153 -44
- package/src/task-spawn-cleanup-ownership.js +82 -0
- package/src/worktree-claude-config.js +193 -85
- package/task-lib/attachable-watcher.js +93 -267
- package/task-lib/command-spec-cleanup.js +219 -0
- package/task-lib/commands/clean.js +35 -5
- package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
- package/task-lib/commands/kill.js +117 -4
- package/task-lib/commands/status.js +1 -0
- package/task-lib/runner.js +61 -4
- package/task-lib/store.js +68 -6
- package/task-lib/watcher-output-runtime.js +284 -0
- package/task-lib/watcher.js +124 -257
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
|
|
2
|
+
import { contractError } from '../contract-errors';
|
|
2
3
|
import {
|
|
3
4
|
type BuildProviderCommandOptions,
|
|
4
5
|
type ClaudeCliFeatures,
|
|
@@ -66,6 +67,10 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
|
|
|
66
67
|
supportsVerbose: unknown ? true : /--verbose/.test(help),
|
|
67
68
|
supportsModel: unknown ? true : /--model/.test(help),
|
|
68
69
|
supportsEffort: unknown ? true : /--effort/.test(help),
|
|
70
|
+
// Settings carry mandatory safety hooks, so an unprobed or old CLI must
|
|
71
|
+
// fail closed before a provider process is spawned.
|
|
72
|
+
supportsSettings: !unknown && /--settings/.test(help),
|
|
73
|
+
supportsMcpConfig: !unknown && /--mcp-config/.test(help),
|
|
69
74
|
supportsResume: unknown ? true : /--resume/.test(help),
|
|
70
75
|
unknown,
|
|
71
76
|
};
|
|
@@ -131,6 +136,40 @@ function addSessionArgs(args: string[], options: BuildProviderCommandOptions): v
|
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
function addSettingsArgs(args: string[], options: BuildProviderCommandOptions): void {
|
|
140
|
+
const settingsPath = options.claudeSettingsFile?.trim();
|
|
141
|
+
if (settingsPath) {
|
|
142
|
+
args.push('--settings', settingsPath);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function addMcpConfigArgs(args: string[], options: BuildProviderCommandOptions): void {
|
|
147
|
+
if (!options.mcpConfig?.length) return;
|
|
148
|
+
args.push('--mcp-config', ...options.mcpConfig);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function failClosedUnsupportedRunConfig(options: BuildProviderCommandOptions): void {
|
|
152
|
+
const features = optionFeatures(options);
|
|
153
|
+
if (options.claudeSettingsFile?.trim() && features.supportsSettings === false) {
|
|
154
|
+
throw contractError({
|
|
155
|
+
code: 'invalid-field',
|
|
156
|
+
field: 'options.cliFeatures.supportsSettings',
|
|
157
|
+
exitCode: 2,
|
|
158
|
+
message:
|
|
159
|
+
'Claude CLI does not advertise --settings support required for Zeroshot safety hooks. Upgrade Claude Code before running this task.',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (options.mcpConfig?.length && features.supportsMcpConfig === false) {
|
|
163
|
+
throw contractError({
|
|
164
|
+
code: 'invalid-field',
|
|
165
|
+
field: 'options.cliFeatures.supportsMcpConfig',
|
|
166
|
+
exitCode: 2,
|
|
167
|
+
message:
|
|
168
|
+
'Claude CLI does not advertise --mcp-config support required to preserve repository MCP tools. Upgrade Claude Code before running this task.',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
134
173
|
function extractSessionId(line: string): string | null {
|
|
135
174
|
const event = tryParseJson(line.trim());
|
|
136
175
|
if (!isRecord(event)) return null;
|
|
@@ -185,15 +224,21 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
|
|
|
185
224
|
}
|
|
186
225
|
|
|
187
226
|
function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
|
|
227
|
+
failClosedUnsupportedRunConfig(options);
|
|
188
228
|
const { command, args: commandPrefix } = resolveClaudeCommand();
|
|
189
|
-
const args: string[] = [...commandPrefix, '--print'
|
|
229
|
+
const args: string[] = [...commandPrefix, '--print'];
|
|
190
230
|
const authEnv = options.authEnv ?? {};
|
|
191
231
|
|
|
232
|
+
// --mcp-config is variadic. A following option terminates its values before
|
|
233
|
+
// the positional prompt.
|
|
234
|
+
addMcpConfigArgs(args, options);
|
|
235
|
+
args.push('--input-format', 'text');
|
|
192
236
|
addOutputArgs(args, options);
|
|
193
237
|
addSchemaArgs(args, options);
|
|
194
238
|
addModelArgs(args, options);
|
|
195
239
|
addAutoApproveArgs(args, options);
|
|
196
240
|
addSessionArgs(args, options);
|
|
241
|
+
addSettingsArgs(args, options);
|
|
197
242
|
|
|
198
243
|
args.push(context);
|
|
199
244
|
|
|
@@ -23,6 +23,7 @@ const CLI_FEATURE_FIELDS = [
|
|
|
23
23
|
'supportsVerbose',
|
|
24
24
|
'supportsModel',
|
|
25
25
|
'supportsEffort',
|
|
26
|
+
'supportsSettings',
|
|
26
27
|
'supportsJson',
|
|
27
28
|
'supportsOutputSchema',
|
|
28
29
|
'supportsDir',
|
|
@@ -209,6 +210,11 @@ function normalizeBuildOptions(value: Record<string, unknown>): BuildProviderCom
|
|
|
209
210
|
'continueSession',
|
|
210
211
|
optionalBooleanValue(value.continueSession, 'options.continueSession')
|
|
211
212
|
);
|
|
213
|
+
addDefined(
|
|
214
|
+
result,
|
|
215
|
+
'claudeSettingsFile',
|
|
216
|
+
optionalStringValue(value.claudeSettingsFile, 'options.claudeSettingsFile')
|
|
217
|
+
);
|
|
212
218
|
addDefined(result, 'cliFeatures', optionalCliFeatures(value.cliFeatures));
|
|
213
219
|
addDefined(result, 'mcpConfig', optionalMcpConfig(value.mcpConfig));
|
|
214
220
|
addDefined(
|
|
@@ -101,6 +101,8 @@ export interface ClaudeCliFeatures extends BaseCliFeatures {
|
|
|
101
101
|
readonly supportsVerbose: boolean;
|
|
102
102
|
readonly supportsModel: boolean;
|
|
103
103
|
readonly supportsEffort: boolean;
|
|
104
|
+
readonly supportsSettings: boolean;
|
|
105
|
+
readonly supportsMcpConfig: boolean;
|
|
104
106
|
readonly supportsResume: boolean;
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -195,6 +197,7 @@ export interface CliFeatureOverrides {
|
|
|
195
197
|
readonly supportsVerbose?: boolean;
|
|
196
198
|
readonly supportsModel?: boolean;
|
|
197
199
|
readonly supportsEffort?: boolean;
|
|
200
|
+
readonly supportsSettings?: boolean;
|
|
198
201
|
readonly supportsJson?: boolean;
|
|
199
202
|
readonly supportsOutputSchema?: boolean;
|
|
200
203
|
readonly supportsDir?: boolean;
|
|
@@ -231,10 +234,10 @@ export interface CliFeatureOverrides {
|
|
|
231
234
|
}
|
|
232
235
|
|
|
233
236
|
export interface CleanupMetadata {
|
|
234
|
-
readonly kind: 'temp-file';
|
|
237
|
+
readonly kind: 'temp-file' | 'temp-directory';
|
|
235
238
|
readonly provider: ProviderId;
|
|
236
239
|
readonly path: string;
|
|
237
|
-
readonly reason: 'output-schema';
|
|
240
|
+
readonly reason: 'output-schema' | 'settings-overlay';
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
export interface WarningMetadata {
|
|
@@ -268,14 +271,14 @@ export interface BuildProviderCommandOptions {
|
|
|
268
271
|
readonly autoApprove?: boolean;
|
|
269
272
|
readonly resumeSessionId?: string;
|
|
270
273
|
readonly continueSession?: boolean;
|
|
274
|
+
readonly claudeSettingsFile?: string;
|
|
271
275
|
readonly cliFeatures?: CliFeatureOverrides;
|
|
272
276
|
readonly authEnv?: Readonly<Record<string, string>>;
|
|
273
277
|
readonly strictSchema?: boolean;
|
|
274
278
|
readonly gateway?: GatewayBuildOptions;
|
|
275
|
-
// MCP server configs forwarded to providers that accept an MCP config CLI flag
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
// entry. Providers whose adapter models no MCP flag ignore this field.
|
|
279
|
+
// MCP server configs forwarded to providers that accept an MCP config CLI flag. Claude accepts
|
|
280
|
+
// one variadic `--mcp-config` followed by file paths; adapters such as Copilot accept repeated
|
|
281
|
+
// flags with inline JSON so configs survive container path translation.
|
|
279
282
|
readonly mcpConfig?: readonly string[];
|
|
280
283
|
}
|
|
281
284
|
|
|
@@ -12,7 +12,23 @@ const { loadSettings } = require('../lib/settings');
|
|
|
12
12
|
const { normalizeProviderName } = require('../lib/provider-names');
|
|
13
13
|
const { getProvider } = require('./providers');
|
|
14
14
|
const { prependWorktreeToolBinToEnv } = require('./worktree-tooling-env');
|
|
15
|
-
const {
|
|
15
|
+
const { getTask, getTaskBySpawnOwnershipToken } = require('../task-lib/store.js');
|
|
16
|
+
const {
|
|
17
|
+
TASK_SPAWN_OWNERSHIP_TOKEN_ENV,
|
|
18
|
+
cleanupCallerOwnedCommand,
|
|
19
|
+
callerOwnsCommandCleanup,
|
|
20
|
+
createTaskSpawnOwnershipToken,
|
|
21
|
+
requireTaskIdFromWrapperResult,
|
|
22
|
+
trackTaskWrapperCleanupOwnership,
|
|
23
|
+
} = require('./task-spawn-cleanup-ownership');
|
|
24
|
+
const {
|
|
25
|
+
CLAUDE_MCP_CONFIG_ENV,
|
|
26
|
+
CLAUDE_SETTINGS_ENV,
|
|
27
|
+
cleanupClaudeSettingsOverlay,
|
|
28
|
+
prepareClaudeSettingsOverlay,
|
|
29
|
+
resolveContainerMcpConfigPath,
|
|
30
|
+
resolveRepoMcpConfigPath,
|
|
31
|
+
} = require('./worktree-claude-config');
|
|
16
32
|
const {
|
|
17
33
|
appendTaskRunModelArgs,
|
|
18
34
|
wrapTaskRunWithIsolatedSettings,
|
|
@@ -80,6 +96,28 @@ function runCommand(command, args, options = {}, callback = null) {
|
|
|
80
96
|
});
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
const TASK_TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale', 'cancelled']);
|
|
100
|
+
|
|
101
|
+
async function cleanupPersistedTaskAfterLaunchFailure(ctPath, taskId) {
|
|
102
|
+
let lastError = null;
|
|
103
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
104
|
+
let commandError = null;
|
|
105
|
+
try {
|
|
106
|
+
await runCommand(ctPath, ['kill', taskId], { timeout: 10000 });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
commandError = error;
|
|
109
|
+
}
|
|
110
|
+
const task = getTask(taskId);
|
|
111
|
+
if (!task || (TASK_TERMINAL_STATUSES.has(task.status) && !task.commandCleanup)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
lastError =
|
|
115
|
+
commandError ||
|
|
116
|
+
new Error(`Task ${taskId} termination and command cleanup were not confirmed`);
|
|
117
|
+
}
|
|
118
|
+
throw lastError || new Error(`Task ${taskId} cleanup failed`);
|
|
119
|
+
}
|
|
120
|
+
|
|
83
121
|
function runCommandSync(command, args, options = {}) {
|
|
84
122
|
const timeout = options.timeout ?? 30000;
|
|
85
123
|
const result = spawnSync(command, args, { ...options, timeout });
|
|
@@ -95,6 +133,17 @@ function runCommandSync(command, args, options = {}) {
|
|
|
95
133
|
return result.stdout?.toString() || '';
|
|
96
134
|
}
|
|
97
135
|
|
|
136
|
+
function appendIsolatedMcpConfigArgs(command, provider, options) {
|
|
137
|
+
if (provider !== 'claude') return;
|
|
138
|
+
const mcpConfigPath = resolveContainerMcpConfigPath({
|
|
139
|
+
cwd: options.cwd || process.cwd(),
|
|
140
|
+
worktreePath: options.worktreePath || null,
|
|
141
|
+
});
|
|
142
|
+
if (mcpConfigPath) {
|
|
143
|
+
command.push('--mcp-config', mcpConfigPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
98
147
|
class ClaudeTaskRunner extends TaskRunner {
|
|
99
148
|
/**
|
|
100
149
|
* @param {Object} options
|
|
@@ -191,14 +240,20 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
191
240
|
worktreePath,
|
|
192
241
|
});
|
|
193
242
|
|
|
194
|
-
|
|
243
|
+
let taskId;
|
|
244
|
+
try {
|
|
245
|
+
taskId = await this._spawnAndGetTaskId(ctPath, args, cwd, spawnEnv, agentId);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
cleanupCallerOwnedCommand(error, () =>
|
|
248
|
+
cleanupClaudeSettingsOverlay(spawnEnv[CLAUDE_SETTINGS_ENV])
|
|
249
|
+
);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
195
252
|
|
|
253
|
+
// Once a task ID is returned, the detached watcher owns provider
|
|
254
|
+
// termination and command cleanup.
|
|
196
255
|
this._log(`📋 [${agentId}]: Following zeroshot logs for ${taskId}`);
|
|
197
|
-
|
|
198
|
-
// Wait for task registration
|
|
199
256
|
await this._waitForTaskReady(ctPath, taskId);
|
|
200
|
-
|
|
201
|
-
// Follow logs until completion
|
|
202
257
|
return this._followLogs(ctPath, taskId, agentId);
|
|
203
258
|
}
|
|
204
259
|
|
|
@@ -325,9 +380,12 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
325
380
|
spawnEnv.ANTHROPIC_MODEL = resolvedModelSpec.model;
|
|
326
381
|
}
|
|
327
382
|
if (providerName === 'claude') {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
383
|
+
spawnEnv[CLAUDE_SETTINGS_ENV] = prepareClaudeSettingsOverlay({
|
|
384
|
+
includeDangerousGit: Boolean(worktreePath),
|
|
385
|
+
});
|
|
386
|
+
const mcpConfigPath = resolveRepoMcpConfigPath({ cwd, worktreePath });
|
|
387
|
+
if (mcpConfigPath) {
|
|
388
|
+
spawnEnv[CLAUDE_MCP_CONFIG_ENV] = mcpConfigPath;
|
|
331
389
|
}
|
|
332
390
|
}
|
|
333
391
|
|
|
@@ -345,16 +403,49 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
345
403
|
* @returns {Promise<string>}
|
|
346
404
|
*/
|
|
347
405
|
_spawnAndGetTaskId(ctPath, args, cwd, spawnEnv, _agentId) {
|
|
406
|
+
const ownershipToken = createTaskSpawnOwnershipToken();
|
|
407
|
+
const findPersistedTaskId = () => getTaskBySpawnOwnershipToken(ownershipToken)?.id || null;
|
|
348
408
|
return new Promise((resolve, reject) => {
|
|
349
409
|
const proc = spawn(ctPath, args, {
|
|
350
410
|
cwd,
|
|
351
411
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
352
|
-
env: spawnEnv,
|
|
412
|
+
env: { ...spawnEnv, [TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken },
|
|
353
413
|
windowsHide: true,
|
|
354
414
|
});
|
|
355
415
|
|
|
356
416
|
let stdout = '';
|
|
357
417
|
let stderr = '';
|
|
418
|
+
let settled = false;
|
|
419
|
+
const classifyCleanupOwnership = trackTaskWrapperCleanupOwnership(findPersistedTaskId);
|
|
420
|
+
const rejectWithOwnership = async (error) => {
|
|
421
|
+
if (settled) return;
|
|
422
|
+
settled = true;
|
|
423
|
+
const classifiedError = classifyCleanupOwnership(error);
|
|
424
|
+
if (!callerOwnsCommandCleanup(classifiedError)) {
|
|
425
|
+
classifiedError.spawnOwnershipToken = ownershipToken;
|
|
426
|
+
let persistedTaskId = null;
|
|
427
|
+
let lookupError = null;
|
|
428
|
+
try {
|
|
429
|
+
persistedTaskId = findPersistedTaskId();
|
|
430
|
+
} catch (lookupFailure) {
|
|
431
|
+
lookupError = lookupFailure;
|
|
432
|
+
}
|
|
433
|
+
classifiedError.taskId = persistedTaskId;
|
|
434
|
+
try {
|
|
435
|
+
if (lookupError) throw lookupError;
|
|
436
|
+
if (persistedTaskId) {
|
|
437
|
+
await cleanupPersistedTaskAfterLaunchFailure(ctPath, persistedTaskId);
|
|
438
|
+
}
|
|
439
|
+
} catch (cleanupError) {
|
|
440
|
+
classifiedError.message += ` Task cleanup was not confirmed: ${cleanupError.message}`;
|
|
441
|
+
classifiedError.permanent = true;
|
|
442
|
+
classifiedError.restartExhausted = true;
|
|
443
|
+
classifiedError.terminationExhausted = true;
|
|
444
|
+
classifiedError.terminationAttempts = persistedTaskId ? 3 : 1;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
reject(classifiedError);
|
|
448
|
+
};
|
|
358
449
|
|
|
359
450
|
proc.stdout.on('data', (data) => {
|
|
360
451
|
stdout += data.toString();
|
|
@@ -364,21 +455,26 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
364
455
|
stderr += data.toString();
|
|
365
456
|
});
|
|
366
457
|
|
|
367
|
-
proc.on('close', (code) => {
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
458
|
+
proc.on('close', async (code) => {
|
|
459
|
+
if (settled) return;
|
|
460
|
+
try {
|
|
461
|
+
const taskId = requireTaskIdFromWrapperResult({
|
|
462
|
+
code,
|
|
463
|
+
stdout,
|
|
464
|
+
stderr,
|
|
465
|
+
parseTaskId: (output) =>
|
|
466
|
+
output.match(/Task spawned: ((?:task-)?[a-z]+-[a-z]+-[a-z0-9]+)/)?.[1],
|
|
467
|
+
persistedTaskId: findPersistedTaskId(),
|
|
468
|
+
});
|
|
469
|
+
settled = true;
|
|
470
|
+
resolve(taskId);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
await rejectWithOwnership(error);
|
|
377
473
|
}
|
|
378
474
|
});
|
|
379
475
|
|
|
380
|
-
proc.on('error', (error) => {
|
|
381
|
-
|
|
476
|
+
proc.on('error', async (error) => {
|
|
477
|
+
await rejectWithOwnership(error);
|
|
382
478
|
});
|
|
383
479
|
});
|
|
384
480
|
}
|
|
@@ -424,6 +520,7 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
424
520
|
let statusCheckInterval = null;
|
|
425
521
|
let resolved = false;
|
|
426
522
|
let lineBuffer = '';
|
|
523
|
+
let cleanupRecoveryPending = false;
|
|
427
524
|
|
|
428
525
|
// Get log file path
|
|
429
526
|
try {
|
|
@@ -558,33 +655,43 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
558
655
|
|
|
559
656
|
statusCheckInterval = setInterval(() => {
|
|
560
657
|
runCommand(ctPath, ['status', taskId], {}, (error, stdout) => {
|
|
561
|
-
if (resolved) return;
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
(
|
|
566
|
-
|
|
567
|
-
|
|
658
|
+
if (resolved || error) return;
|
|
659
|
+
const terminalMatch = stdout.match(/Status:\s+(completed|failed)/i);
|
|
660
|
+
if (!terminalMatch) return;
|
|
661
|
+
if (/Cleanup:\s+pending/i.test(stdout)) {
|
|
662
|
+
if (!cleanupRecoveryPending) {
|
|
663
|
+
cleanupRecoveryPending = true;
|
|
664
|
+
runCommand(ctPath, ['kill', taskId], { timeout: 10000 }, (cleanupError) => {
|
|
665
|
+
cleanupRecoveryPending = false;
|
|
666
|
+
if (cleanupError) {
|
|
667
|
+
this._log(
|
|
668
|
+
`⚠️ [${agentId}]: Terminal cleanup recovery will retry: ${cleanupError.message}`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
568
675
|
|
|
569
|
-
|
|
676
|
+
const success = terminalMatch[1].toLowerCase() === 'completed';
|
|
677
|
+
pollLogFile();
|
|
570
678
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
679
|
+
setTimeout(() => {
|
|
680
|
+
if (resolved) return;
|
|
681
|
+
resolved = true;
|
|
574
682
|
|
|
575
|
-
|
|
576
|
-
|
|
683
|
+
clearInterval(pollInterval);
|
|
684
|
+
clearInterval(statusCheckInterval);
|
|
577
685
|
|
|
578
|
-
|
|
686
|
+
const errorContext = extractErrorContext(success, stdout);
|
|
579
687
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
688
|
+
resolve({
|
|
689
|
+
success,
|
|
690
|
+
output,
|
|
691
|
+
error: errorContext,
|
|
692
|
+
taskId,
|
|
693
|
+
});
|
|
694
|
+
}, 500);
|
|
588
695
|
});
|
|
589
696
|
}, 1000);
|
|
590
697
|
|
|
@@ -664,6 +771,8 @@ class ClaudeTaskRunner extends TaskRunner {
|
|
|
664
771
|
command.push('--json-schema', JSON.stringify(jsonSchema));
|
|
665
772
|
}
|
|
666
773
|
|
|
774
|
+
appendIsolatedMcpConfigArgs(command, provider, options);
|
|
775
|
+
|
|
667
776
|
let finalContext = context;
|
|
668
777
|
if (jsonSchema && desiredOutputFormat === 'json' && runOutputFormat === 'stream-json') {
|
|
669
778
|
finalContext += `\n\n## Output Format (REQUIRED)\n\nReturn a JSON object that matches this schema exactly.\n\nSchema:\n\`\`\`json\n${JSON.stringify(
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const { randomUUID } = require('node:crypto');
|
|
2
|
+
|
|
3
|
+
const TASK_SPAWN_OWNERSHIP_TOKEN_ENV = 'ZEROSHOT_TASK_SPAWN_OWNERSHIP_TOKEN';
|
|
4
|
+
|
|
5
|
+
const COMMAND_CLEANUP_OWNER = Object.freeze({
|
|
6
|
+
CALLER: 'caller',
|
|
7
|
+
TASK_LIFECYCLE: 'task-lifecycle',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function normalizeError(error) {
|
|
11
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mark a wrapper failure after the detached task record durably accepted the
|
|
16
|
+
* launch ownership token. Human stdout and process spawn events are not
|
|
17
|
+
* ownership receipts.
|
|
18
|
+
*/
|
|
19
|
+
function transferCommandCleanupOwnership(error) {
|
|
20
|
+
const normalized = normalizeError(error);
|
|
21
|
+
normalized.commandCleanupOwner = COMMAND_CLEANUP_OWNER.TASK_LIFECYCLE;
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function callerOwnsCommandCleanup(error) {
|
|
26
|
+
return error?.commandCleanupOwner !== COMMAND_CLEANUP_OWNER.TASK_LIFECYCLE;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cleanupCallerOwnedCommand(error, cleanup) {
|
|
30
|
+
if (callerOwnsCommandCleanup(error)) cleanup();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function requireTaskIdFromWrapperResult({
|
|
34
|
+
code,
|
|
35
|
+
stdout,
|
|
36
|
+
stderr,
|
|
37
|
+
parseTaskId,
|
|
38
|
+
persistedTaskId = null,
|
|
39
|
+
}) {
|
|
40
|
+
if (code !== 0) {
|
|
41
|
+
throw new Error(`zeroshot task run failed with code ${code}: ${stderr}`);
|
|
42
|
+
}
|
|
43
|
+
if (!persistedTaskId) {
|
|
44
|
+
const printedTaskId = parseTaskId(stdout);
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Detached task ownership receipt was not persisted${
|
|
47
|
+
printedTaskId ? ` for wrapper output ${printedTaskId}` : ''
|
|
48
|
+
}.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const printedTaskId = parseTaskId(stdout);
|
|
52
|
+
if (printedTaskId && printedTaskId !== persistedTaskId) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Task ownership receipt ${persistedTaskId} did not match wrapper output ${printedTaskId}.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return persistedTaskId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createTaskSpawnOwnershipToken() {
|
|
61
|
+
return randomUUID();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function trackTaskWrapperCleanupOwnership(findPersistedTaskId) {
|
|
65
|
+
return (error) => {
|
|
66
|
+
try {
|
|
67
|
+
return findPersistedTaskId() ? transferCommandCleanupOwnership(error) : error;
|
|
68
|
+
} catch {
|
|
69
|
+
return transferCommandCleanupOwnership(error);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
COMMAND_CLEANUP_OWNER,
|
|
76
|
+
TASK_SPAWN_OWNERSHIP_TOKEN_ENV,
|
|
77
|
+
cleanupCallerOwnedCommand,
|
|
78
|
+
callerOwnsCommandCleanup,
|
|
79
|
+
createTaskSpawnOwnershipToken,
|
|
80
|
+
requireTaskIdFromWrapperResult,
|
|
81
|
+
trackTaskWrapperCleanupOwnership,
|
|
82
|
+
};
|