@the-open-engine/zeroshot 6.10.0 → 6.10.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 (33) hide show
  1. package/cli/index.js +16 -0
  2. package/cluster-hooks/block-ask-user-question.py +2 -3
  3. package/cluster-hooks/block-dangerous-git.py +2 -3
  4. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  5. package/lib/agent-cli-provider/adapters/claude.js +42 -1
  6. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  7. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/contract-options.js +2 -0
  9. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  10. package/lib/agent-cli-provider/types.d.ts +6 -2
  11. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/types.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/agent/agent-lifecycle.js +13 -6
  15. package/src/agent/agent-task-executor.js +527 -313
  16. package/src/agent-cli-provider/adapters/claude.ts +46 -1
  17. package/src/agent-cli-provider/contract-options.ts +6 -0
  18. package/src/agent-cli-provider/types.ts +9 -6
  19. package/src/claude-task-runner.js +168 -44
  20. package/src/darwin-keychain-boundary.js +174 -0
  21. package/src/task-spawn-cleanup-ownership.js +82 -0
  22. package/src/worktree-claude-config.js +193 -85
  23. package/src/worktree-tooling-env.js +3 -0
  24. package/task-lib/attachable-watcher.js +93 -267
  25. package/task-lib/command-spec-cleanup.js +219 -0
  26. package/task-lib/commands/clean.js +35 -5
  27. package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
  28. package/task-lib/commands/kill.js +117 -4
  29. package/task-lib/commands/status.js +1 -0
  30. package/task-lib/runner.js +61 -4
  31. package/task-lib/store.js +68 -6
  32. package/task-lib/watcher-output-runtime.js +284 -0
  33. package/task-lib/watcher.js +124 -257
@@ -5,17 +5,19 @@
5
5
  * Runs detached from parent, updates task status on completion
6
6
  */
7
7
 
8
- import { spawn } from 'child_process';
9
- import { appendFileSync, unlinkSync } from 'fs';
10
- import { unlink } from 'fs/promises';
8
+ import { appendFileSync } from 'fs';
11
9
  import { getTask, updateTask } from './store.js';
12
- import {
13
- detectProviderFatalError,
14
- detectProviderStreamingModeError,
15
- recoverProviderStructuredOutput,
16
- supportsProviderStructuredOutputRecovery,
17
- } from './provider-helper-runtime.js';
10
+ import { createCommandSpecCleanup } from './command-spec-cleanup.js';
18
11
  import { createProviderSessionCapture } from './provider-session-capture.js';
12
+ import {
13
+ completeWatcherFailure,
14
+ completePendingWatcherCancellation,
15
+ completeWatcherTask,
16
+ createWatcherOutputRuntime,
17
+ resolveWatcherCommand,
18
+ spawnWatcherProvider,
19
+ terminateWatcherProvider,
20
+ } from './watcher-output-runtime.js';
19
21
  import { createRequire } from 'module';
20
22
 
21
23
  const require = createRequire(import.meta.url);
@@ -35,8 +37,24 @@ function log(msg) {
35
37
  appendFileSync(logFile, msg);
36
38
  }
37
39
 
38
- const providerName = normalizeProviderName(config.provider || 'claude');
39
- const enableRecovery = supportsProviderStructuredOutputRecovery(providerName);
40
+ function emergencyLog(msg) {
41
+ try {
42
+ log(msg);
43
+ } catch {
44
+ process.stderr.write(msg);
45
+ }
46
+ }
47
+
48
+ const commandCleanup = createCommandSpecCleanup(commandSpec, (cleanupPath, error) => {
49
+ emergencyLog(`[${Date.now()}][CLEANUP] Failed to delete ${cleanupPath}: ${error.message}\n`);
50
+ });
51
+
52
+ const { providerName, env, command, finalArgs } = resolveWatcherCommand(
53
+ config,
54
+ commandSpec,
55
+ args,
56
+ normalizeProviderName
57
+ );
40
58
  const storedTask = getTask(taskId);
41
59
  const providerSessionCapture = createProviderSessionCapture({
42
60
  providerName,
@@ -47,64 +65,14 @@ const providerSessionCapture = createProviderSessionCapture({
47
65
  initialSessionId: storedTask?.sessionId || null,
48
66
  initialSessionIdConflict: storedTask?.sessionIdConflict === true,
49
67
  });
50
- const maybeCaptureProviderSession = providerSessionCapture.captureLine;
51
-
52
- const env = { ...process.env, ...(commandSpec.env || {}) };
53
- const command = commandSpec.binary;
54
- const finalArgs = [...(commandSpec.args || args)];
55
-
56
- const child = spawn(command, finalArgs, {
57
- cwd: commandSpec.cwd || cwd,
58
- env,
59
- stdio: ['ignore', 'pipe', 'pipe'],
60
- detached: process.platform !== 'win32',
61
- windowsHide: true,
62
- });
63
-
64
- updateTask(taskId, {
65
- pid: child.pid,
66
- processGroupId: process.platform === 'win32' ? null : child.pid,
67
- terminationStrategy: process.platform === 'win32' ? 'process-tree' : 'process-group',
68
- });
69
-
70
- const silentJsonMode =
71
- config.outputFormat === 'json' &&
72
- config.jsonSchema &&
73
- config.silentJsonOutput &&
74
- supportsProviderStructuredOutputRecovery(providerName);
75
-
76
- let finalResultJson = null;
77
- let streamingModeError = null;
78
- let fatalError = null;
79
- let cleanupStarted = false;
80
68
 
69
+ let crashStarted = false;
70
+ let child = null;
81
71
  let stdoutBuffer = '';
82
72
  let stderrBuffer = '';
83
73
 
84
- function splitBufferLines(buffer, chunk) {
85
- const nextBuffer = buffer + chunk;
86
- const lines = nextBuffer.split('\n');
87
- const remaining = lines.pop() || '';
88
- return { lines, remaining };
89
- }
90
-
91
- function maybeHandleFatalError(line, timestamp) {
92
- if (fatalError) {
93
- return false;
94
- }
95
-
96
- const detected = detectProviderFatalError(providerName, line);
97
- if (!detected) {
98
- return false;
99
- }
100
-
101
- fatalError = detected;
102
-
103
- if (silentJsonMode) {
104
- log(`[${timestamp}]${line}\n`);
105
- }
106
- log(`[${timestamp}][FATAL] ${detected}\n`);
107
-
74
+ function stopProviderAfterFatalOutput() {
75
+ if (!child) return;
108
76
  try {
109
77
  child.kill('SIGTERM');
110
78
  } catch {
@@ -120,221 +88,120 @@ function maybeHandleFatalError(line, timestamp) {
120
88
  }
121
89
  }
122
90
  }, 5000);
123
-
124
- return true;
125
91
  }
126
92
 
127
- function captureStreamingError(line, timestamp) {
128
- const detectedError = detectProviderStreamingModeError(providerName, line);
129
- if (!detectedError) {
130
- return false;
131
- }
132
-
133
- streamingModeError = { ...detectedError, timestamp };
134
- return true;
135
- }
136
-
137
- function maybeCaptureStructuredOutput(line) {
138
- try {
139
- const json = JSON.parse(line);
140
- if (json.structured_output) {
141
- finalResultJson = line;
142
- }
143
- } catch {
144
- // Not JSON, skip
145
- }
146
- }
147
-
148
- function handleSilentJsonLines(lines, timestamp) {
149
- for (const line of lines) {
150
- if (!line.trim()) continue;
151
- maybeCaptureProviderSession(line);
152
- maybeHandleFatalError(line, timestamp);
153
- if (captureStreamingError(line, timestamp)) {
154
- continue;
155
- }
156
- maybeCaptureStructuredOutput(line);
157
- }
158
- }
159
-
160
- function handleStreamingLines(lines, timestamp) {
161
- for (const line of lines) {
162
- maybeCaptureProviderSession(line);
163
- maybeHandleFatalError(line, timestamp);
164
- if (captureStreamingError(line, timestamp)) {
165
- continue;
166
- }
167
- log(`[${timestamp}]${line}\n`);
168
- }
169
- }
170
-
171
- function flushStdoutBuffer(timestamp) {
172
- if (!stdoutBuffer.trim()) {
173
- return;
174
- }
175
-
176
- maybeCaptureProviderSession(stdoutBuffer);
177
- if (!enableRecovery) {
178
- if (!silentJsonMode) {
179
- log(`[${timestamp}]${stdoutBuffer}\n`);
180
- }
181
- return;
182
- }
183
-
184
- maybeHandleFatalError(stdoutBuffer, timestamp);
185
- if (captureStreamingError(stdoutBuffer, timestamp)) {
186
- return;
187
- }
188
-
189
- if (silentJsonMode) {
190
- maybeCaptureStructuredOutput(stdoutBuffer);
191
- return;
192
- }
93
+ const outputRuntime = createWatcherOutputRuntime({
94
+ config,
95
+ providerName,
96
+ log,
97
+ stopProvider: stopProviderAfterFatalOutput,
98
+ providerSessionCapture,
99
+ });
193
100
 
194
- log(`[${timestamp}]${stdoutBuffer}\n`);
101
+ function terminateOwnedProviderBoundary(exitObserved = false) {
102
+ return terminateWatcherProvider(child, { exitObserved });
195
103
  }
196
104
 
197
- function flushStderrBuffer(timestamp) {
198
- if (stderrBuffer.trim()) {
199
- maybeHandleFatalError(stderrBuffer, timestamp);
200
- log(`[${timestamp}]${stderrBuffer}\n`);
201
- }
105
+ async function crashWithError(error, source) {
106
+ if (crashStarted) return;
107
+ crashStarted = true;
108
+ await completeWatcherFailure({
109
+ taskId,
110
+ error,
111
+ source,
112
+ commandCleanup,
113
+ terminateProvider: terminateOwnedProviderBoundary,
114
+ updateTask,
115
+ emergencyLog,
116
+ });
117
+ process.exit(1);
202
118
  }
203
119
 
204
- function attemptRecovery(code, timestamp) {
205
- if (!(code !== 0 && streamingModeError?.sessionId)) {
206
- return null;
207
- }
208
-
209
- const recovered = recoverProviderStructuredOutput(providerName, streamingModeError.sessionId);
210
- if (recovered?.payload) {
211
- const recoveredLine = JSON.stringify(recovered.payload);
212
- if (silentJsonMode) {
213
- finalResultJson = recoveredLine;
214
- } else {
215
- log(`[${timestamp}]${recoveredLine}\n`);
216
- }
217
- } else if (streamingModeError.line) {
218
- if (silentJsonMode) {
219
- log(streamingModeError.line + '\n');
220
- } else {
221
- log(`[${streamingModeError.timestamp}]${streamingModeError.line}\n`);
222
- }
223
- }
224
-
225
- return recovered;
226
- }
120
+ process.on('uncaughtException', (error) => {
121
+ void crashWithError(error, 'uncaughtException');
122
+ });
227
123
 
228
- async function cleanupCommandSpec() {
229
- if (cleanupStarted) return;
230
- cleanupStarted = true;
231
- for (const file of commandSpec.cleanup || []) {
232
- try {
233
- await unlink(file);
234
- } catch (error) {
235
- log(`[${Date.now()}][CLEANUP] Failed to delete ${file}: ${error.message}\n`);
236
- }
237
- }
238
- }
124
+ process.on('unhandledRejection', (reason) => {
125
+ void crashWithError(reason, 'unhandledRejection');
126
+ });
239
127
 
240
- function cleanupCommandSpecSync() {
241
- if (cleanupStarted) return;
242
- cleanupStarted = true;
243
- for (const file of commandSpec.cleanup || []) {
244
- try {
245
- unlinkSync(file);
246
- } catch (error) {
247
- log(`[${Date.now()}][CLEANUP] Failed to delete ${file}: ${error.message}\n`);
248
- }
249
- }
128
+ if (
129
+ await completePendingWatcherCancellation({
130
+ taskId,
131
+ getTask,
132
+ commandCleanup,
133
+ terminateProvider: () => true,
134
+ updateTask,
135
+ emergencyLog,
136
+ })
137
+ ) {
138
+ process.exit(0);
250
139
  }
251
140
 
252
- function writeCompletionFooter(code, signal) {
253
- if (config.outputFormat === 'json') {
254
- return;
255
- }
256
-
257
- log(`\n${'='.repeat(50)}\n`);
258
- log(`Finished: ${new Date().toISOString()}\n`);
259
- log(`Exit code: ${code}, Signal: ${signal}\n`);
260
- }
141
+ child = spawnWatcherProvider(command, finalArgs, {
142
+ cwd: commandSpec.cwd || cwd,
143
+ env,
144
+ stdio: ['ignore', 'pipe', 'pipe'],
145
+ detached: process.platform !== 'win32',
146
+ });
261
147
 
262
148
  child.stdout.on('data', (data) => {
263
- const chunk = data.toString();
264
- const timestamp = Date.now();
265
-
266
- const { lines, remaining } = splitBufferLines(stdoutBuffer, chunk);
267
- stdoutBuffer = remaining;
268
-
269
- if (silentJsonMode) {
270
- handleSilentJsonLines(lines, timestamp);
271
- } else {
272
- handleStreamingLines(lines, timestamp);
273
- }
149
+ stdoutBuffer = outputRuntime.consumeOutput(stdoutBuffer, data);
274
150
  });
275
151
 
276
152
  child.stderr.on('data', (data) => {
277
- const chunk = data.toString();
278
- const timestamp = Date.now();
279
-
280
- const { lines, remaining } = splitBufferLines(stderrBuffer, chunk);
281
- stderrBuffer = remaining;
282
-
283
- for (const line of lines) {
284
- log(`[${timestamp}]${line}\n`);
285
- }
153
+ stderrBuffer = outputRuntime.consumeStderr(stderrBuffer, data);
286
154
  });
287
155
 
288
156
  child.on('close', async (code, signal) => {
289
- const timestamp = Date.now();
290
-
291
- flushStdoutBuffer(timestamp);
292
- flushStderrBuffer(timestamp);
293
-
294
- const recovered = attemptRecovery(code, timestamp);
295
-
296
- if (silentJsonMode && finalResultJson) {
297
- log(finalResultJson + '\n');
298
- }
299
-
300
- const sessionIdentityError = providerSessionCapture.getCompletionError();
301
- const resolvedCode =
302
- fatalError || sessionIdentityError ? 1 : recovered?.payload ? 0 : code;
303
- const status = resolvedCode === 0 ? 'completed' : 'failed';
304
-
305
- writeCompletionFooter(resolvedCode, signal);
306
- await cleanupCommandSpec();
307
-
308
- try {
309
- await updateTask(taskId, {
310
- status,
311
- pid: null,
312
- processGroupId: null,
313
- exitCode: resolvedCode,
314
- ...providerSessionCapture.getCompletionUpdate(resolvedCode),
315
- error:
316
- fatalError ||
317
- sessionIdentityError ||
318
- (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
319
- });
320
- } catch (updateError) {
321
- log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
322
- }
157
+ if (crashStarted) return;
158
+ const completion = outputRuntime.complete({
159
+ code,
160
+ signal,
161
+ outputBuffer: stdoutBuffer,
162
+ stderrBuffer,
163
+ });
164
+ await completeWatcherTask({
165
+ taskId,
166
+ completion,
167
+ commandCleanup,
168
+ terminateProvider: () => terminateOwnedProviderBoundary(true),
169
+ updateTask,
170
+ emergencyLog,
171
+ });
323
172
  process.exit(0);
324
173
  });
325
174
 
326
175
  child.on('error', async (err) => {
176
+ if (crashStarted) return;
177
+ crashStarted = true;
327
178
  log(`\nError: ${err.message}\n`);
328
- cleanupCommandSpecSync();
329
- try {
330
- await updateTask(taskId, {
331
- status: 'failed',
332
- pid: null,
333
- processGroupId: null,
334
- error: err.message,
335
- });
336
- } catch (updateError) {
337
- log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
338
- }
179
+ await completeWatcherTask({
180
+ taskId,
181
+ completion: { status: 'failed', resolvedCode: 1, error: err.message },
182
+ commandCleanup,
183
+ terminateProvider: terminateOwnedProviderBoundary,
184
+ updateTask,
185
+ emergencyLog,
186
+ });
339
187
  process.exit(1);
340
188
  });
189
+
190
+ updateTask(taskId, {
191
+ pid: child.pid,
192
+ processGroupId: process.platform === 'win32' ? null : child.pid,
193
+ terminationStrategy: process.platform === 'win32' ? 'process-tree' : 'process-group',
194
+ });
195
+
196
+ if (getTask(taskId)?.cancelRequested) {
197
+ crashStarted = true;
198
+ await completePendingWatcherCancellation({
199
+ taskId,
200
+ getTask,
201
+ commandCleanup,
202
+ terminateProvider: terminateOwnedProviderBoundary,
203
+ updateTask,
204
+ emergencyLog,
205
+ });
206
+ process.exit(0);
207
+ }