@covibes/zeroshot 5.2.1 → 5.3.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 (62) hide show
  1. package/CHANGELOG.md +174 -189
  2. package/README.md +199 -248
  3. package/cli/commands/providers.js +150 -0
  4. package/cli/index.js +214 -58
  5. package/cli/lib/first-run.js +40 -3
  6. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  7. package/cluster-templates/base-templates/full-workflow.json +44 -145
  8. package/cluster-templates/base-templates/single-worker.json +23 -15
  9. package/cluster-templates/base-templates/worker-validator.json +47 -34
  10. package/cluster-templates/conductor-bootstrap.json +7 -5
  11. package/lib/docker-config.js +6 -1
  12. package/lib/provider-detection.js +59 -0
  13. package/lib/provider-names.js +56 -0
  14. package/lib/settings.js +191 -6
  15. package/lib/stream-json-parser.js +4 -238
  16. package/package.json +21 -5
  17. package/scripts/validate-templates.js +100 -0
  18. package/src/agent/agent-config.js +37 -13
  19. package/src/agent/agent-context-builder.js +64 -2
  20. package/src/agent/agent-hook-executor.js +82 -9
  21. package/src/agent/agent-lifecycle.js +53 -14
  22. package/src/agent/agent-task-executor.js +196 -194
  23. package/src/agent/output-extraction.js +200 -0
  24. package/src/agent/output-reformatter.js +175 -0
  25. package/src/agent/schema-utils.js +111 -0
  26. package/src/agent-wrapper.js +102 -30
  27. package/src/agents/git-pusher-agent.json +1 -1
  28. package/src/claude-task-runner.js +80 -30
  29. package/src/config-router.js +13 -13
  30. package/src/config-validator.js +231 -10
  31. package/src/github.js +36 -0
  32. package/src/isolation-manager.js +243 -154
  33. package/src/ledger.js +28 -6
  34. package/src/orchestrator.js +391 -96
  35. package/src/preflight.js +85 -82
  36. package/src/providers/anthropic/cli-builder.js +45 -0
  37. package/src/providers/anthropic/index.js +134 -0
  38. package/src/providers/anthropic/models.js +23 -0
  39. package/src/providers/anthropic/output-parser.js +159 -0
  40. package/src/providers/base-provider.js +181 -0
  41. package/src/providers/capabilities.js +51 -0
  42. package/src/providers/google/cli-builder.js +55 -0
  43. package/src/providers/google/index.js +116 -0
  44. package/src/providers/google/models.js +24 -0
  45. package/src/providers/google/output-parser.js +92 -0
  46. package/src/providers/index.js +75 -0
  47. package/src/providers/openai/cli-builder.js +122 -0
  48. package/src/providers/openai/index.js +135 -0
  49. package/src/providers/openai/models.js +21 -0
  50. package/src/providers/openai/output-parser.js +129 -0
  51. package/src/sub-cluster-wrapper.js +18 -3
  52. package/src/task-runner.js +8 -6
  53. package/src/tui/layout.js +20 -3
  54. package/task-lib/attachable-watcher.js +80 -78
  55. package/task-lib/claude-recovery.js +119 -0
  56. package/task-lib/commands/list.js +1 -1
  57. package/task-lib/commands/resume.js +3 -2
  58. package/task-lib/commands/run.js +12 -3
  59. package/task-lib/runner.js +59 -38
  60. package/task-lib/scheduler.js +2 -2
  61. package/task-lib/store.js +43 -30
  62. package/task-lib/watcher.js +81 -62
@@ -1,42 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Attachable Watcher - spawns Claude with PTY for attach/detach support
5
- *
4
+ * Attachable Watcher - spawns a CLI process with PTY for attach/detach support
6
5
  * Runs detached from parent, provides Unix socket for attach clients.
7
- * Uses node-pty for proper terminal emulation.
8
- *
9
- * Key differences from legacy watcher.js:
10
- * - Uses AttachServer (node-pty) instead of child_process.spawn
11
- * - Creates Unix socket at ~/.zeroshot/sockets/task-<id>.sock
12
- * - Supports multiple attached clients
13
- * - Still writes to log file for backward compatibility
14
- *
15
- * CRITICAL: Global error handlers installed FIRST to catch silent crashes
16
6
  */
17
7
 
18
8
  import { appendFileSync, existsSync, mkdirSync } from 'fs';
19
9
  import { join } from 'path';
20
10
  import { homedir } from 'os';
21
11
  import { updateTask } from './store.js';
12
+ import { detectStreamingModeError, recoverStructuredOutput } from './claude-recovery.js';
13
+ import { createRequire } from 'module';
22
14
 
23
15
  // ═══════════════════════════════════════════════════════════════════════════
24
16
  // 🔴 CRITICAL: Global error handlers - MUST be installed BEFORE any async ops
25
17
  // Without these, uncaught errors cause SILENT process death (no logs, no status)
26
18
  // ═══════════════════════════════════════════════════════════════════════════
27
19
 
28
- // Parse args early so we can log errors to the correct file
29
20
  const [, , taskIdArg, cwdArg, logFileArg, argsJsonArg, configJsonArg] = process.argv;
30
21
 
31
- /**
32
- * Emergency logger - works even if main log function isn't ready
33
- */
34
22
  function emergencyLog(msg) {
35
23
  if (logFileArg) {
36
24
  try {
37
25
  appendFileSync(logFileArg, msg);
38
26
  } catch {
39
- // Last resort - stderr
40
27
  process.stderr.write(msg);
41
28
  }
42
29
  } else {
@@ -44,9 +31,6 @@ function emergencyLog(msg) {
44
31
  }
45
32
  }
46
33
 
47
- /**
48
- * Mark task as failed and exit
49
- */
50
34
  function crashWithError(error, source) {
51
35
  const timestamp = Date.now();
52
36
  const errorMsg = error instanceof Error ? error.stack || error.message : String(error);
@@ -54,7 +38,6 @@ function crashWithError(error, source) {
54
38
  emergencyLog(`\n[${timestamp}][CRASH] ${source}: ${errorMsg}\n`);
55
39
  emergencyLog(`[${timestamp}][CRASH] Process terminating due to unhandled error\n`);
56
40
 
57
- // Try to update task status - may fail if error is in store.js itself
58
41
  if (taskIdArg) {
59
42
  try {
60
43
  updateTask(taskIdArg, {
@@ -67,11 +50,9 @@ function crashWithError(error, source) {
67
50
  }
68
51
  }
69
52
 
70
- // Exit with error code
71
53
  process.exit(1);
72
54
  }
73
55
 
74
- // Install handlers IMMEDIATELY
75
56
  process.on('uncaughtException', (error) => {
76
57
  crashWithError(error, 'uncaughtException');
77
58
  });
@@ -80,24 +61,19 @@ process.on('unhandledRejection', (reason) => {
80
61
  crashWithError(reason, 'unhandledRejection');
81
62
  });
82
63
 
83
- // Import attach infrastructure from src package (CommonJS)
84
- import { createRequire } from 'module';
85
64
  const require = createRequire(import.meta.url);
86
65
  const { AttachServer } = require('../src/attach');
87
- const { getClaudeCommand } = require('../lib/settings.js');
66
+ const { normalizeProviderName } = require('../lib/provider-names');
88
67
 
89
- // Use the args parsed earlier (during error handler setup)
90
68
  const taskId = taskIdArg;
91
69
  const cwd = cwdArg;
92
70
  const logFile = logFileArg;
93
71
  const args = JSON.parse(argsJsonArg);
94
72
  const config = configJsonArg ? JSON.parse(configJsonArg) : {};
95
73
 
96
- // Socket path for attach
97
74
  const SOCKET_DIR = join(homedir(), '.zeroshot', 'sockets');
98
75
  const socketPath = join(SOCKET_DIR, `${taskId}.sock`);
99
76
 
100
- // Ensure socket directory exists
101
77
  if (!existsSync(SOCKET_DIR)) {
102
78
  mkdirSync(SOCKET_DIR, { recursive: true });
103
79
  }
@@ -106,33 +82,24 @@ function log(msg) {
106
82
  appendFileSync(logFile, msg);
107
83
  }
108
84
 
109
- // Build environment - inherit user's auth method (API key or subscription)
110
- const env = { ...process.env };
111
-
112
- // Add model flag - priority: config.model > ANTHROPIC_MODEL env var
113
- const claudeArgs = [...args];
114
- const model = config.model || env.ANTHROPIC_MODEL;
115
- if (model && !claudeArgs.includes('--model')) {
116
- claudeArgs.unshift('--model', model);
117
- }
85
+ const providerName = normalizeProviderName(config.provider || 'claude');
86
+ const enableRecovery = providerName === 'claude';
118
87
 
119
- // Get configured Claude command (supports custom commands like 'ccr code')
120
- const { command: claudeCommand, args: claudeExtraArgs } = getClaudeCommand();
121
- const finalArgs = [...claudeExtraArgs, ...claudeArgs];
88
+ const env = { ...process.env, ...(config.env || {}) };
89
+ const command = config.command || 'claude';
90
+ const finalArgs = [...args];
122
91
 
123
- // For JSON schema output with silent mode, track final result
124
92
  const silentJsonMode =
125
- config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput;
126
- let finalResultJson = null;
93
+ config.outputFormat === 'json' && config.jsonSchema && config.silentJsonOutput && enableRecovery;
127
94
 
128
- // Buffer for incomplete lines
95
+ let finalResultJson = null;
129
96
  let outputBuffer = '';
97
+ let streamingModeError = null;
130
98
 
131
- // Create AttachServer to spawn Claude with PTY
132
99
  const server = new AttachServer({
133
100
  id: taskId,
134
101
  socketPath,
135
- command: claudeCommand,
102
+ command,
136
103
  args: finalArgs,
137
104
  cwd,
138
105
  env,
@@ -140,19 +107,24 @@ const server = new AttachServer({
140
107
  rows: 30,
141
108
  });
142
109
 
143
- // Handle output from PTY
144
110
  server.on('output', (data) => {
145
111
  const chunk = data.toString();
146
112
  const timestamp = Date.now();
147
113
 
148
114
  if (silentJsonMode) {
149
- // Parse each line to find structured_output
150
115
  outputBuffer += chunk;
151
116
  const lines = outputBuffer.split('\n');
152
117
  outputBuffer = lines.pop() || '';
153
118
 
154
119
  for (const line of lines) {
155
120
  if (!line.trim()) continue;
121
+ if (enableRecovery) {
122
+ const detectedError = detectStreamingModeError(line);
123
+ if (detectedError) {
124
+ streamingModeError = { ...detectedError, timestamp };
125
+ continue;
126
+ }
127
+ }
156
128
  try {
157
129
  const json = JSON.parse(line);
158
130
  if (json.structured_output) {
@@ -163,73 +135,106 @@ server.on('output', (data) => {
163
135
  }
164
136
  }
165
137
  } else {
166
- // Normal mode - stream with timestamps
167
138
  outputBuffer += chunk;
168
139
  const lines = outputBuffer.split('\n');
169
140
  outputBuffer = lines.pop() || '';
170
141
 
171
142
  for (const line of lines) {
143
+ if (enableRecovery) {
144
+ const detectedError = detectStreamingModeError(line);
145
+ if (detectedError) {
146
+ streamingModeError = { ...detectedError, timestamp };
147
+ continue;
148
+ }
149
+ }
172
150
  log(`[${timestamp}]${line}\n`);
173
151
  }
174
152
  }
175
153
  });
176
154
 
177
- // Handle process exit
178
- server.on('exit', ({ exitCode, signal }) => {
155
+ server.on('exit', async ({ exitCode, signal }) => {
179
156
  const timestamp = Date.now();
180
157
  const code = exitCode;
181
158
 
182
- // Flush remaining buffered output
183
159
  if (outputBuffer.trim()) {
184
- if (silentJsonMode) {
185
- try {
186
- const json = JSON.parse(outputBuffer);
187
- if (json.structured_output) {
188
- finalResultJson = outputBuffer;
160
+ if (enableRecovery) {
161
+ const detectedError = detectStreamingModeError(outputBuffer);
162
+ if (detectedError) {
163
+ streamingModeError = { ...detectedError, timestamp };
164
+ } else if (silentJsonMode) {
165
+ try {
166
+ const json = JSON.parse(outputBuffer);
167
+ if (json.structured_output) {
168
+ finalResultJson = outputBuffer;
169
+ }
170
+ } catch {
171
+ // Not valid JSON
189
172
  }
190
- } catch {
191
- // Not valid JSON
173
+ } else {
174
+ log(`[${timestamp}]${outputBuffer}\n`);
192
175
  }
193
- } else {
176
+ } else if (!silentJsonMode) {
194
177
  log(`[${timestamp}]${outputBuffer}\n`);
195
178
  }
196
179
  }
197
180
 
198
- // In silent JSON mode, log ONLY the final structured_output JSON
181
+ let recovered = null;
182
+ if (enableRecovery && code !== 0 && streamingModeError?.sessionId) {
183
+ recovered = recoverStructuredOutput(streamingModeError.sessionId);
184
+ if (recovered?.payload) {
185
+ const recoveredLine = JSON.stringify(recovered.payload);
186
+ if (silentJsonMode) {
187
+ finalResultJson = recoveredLine;
188
+ } else {
189
+ log(`[${timestamp}]${recoveredLine}\n`);
190
+ }
191
+ } else if (streamingModeError.line) {
192
+ if (silentJsonMode) {
193
+ log(streamingModeError.line + '\n');
194
+ } else {
195
+ log(`[${streamingModeError.timestamp}]${streamingModeError.line}\n`);
196
+ }
197
+ }
198
+ }
199
+
199
200
  if (silentJsonMode && finalResultJson) {
200
201
  log(finalResultJson + '\n');
201
202
  }
202
203
 
203
- // Skip footer for pure JSON output
204
204
  if (config.outputFormat !== 'json') {
205
205
  log(`\n${'='.repeat(50)}\n`);
206
206
  log(`Finished: ${new Date().toISOString()}\n`);
207
207
  log(`Exit code: ${code}, Signal: ${signal}\n`);
208
208
  }
209
209
 
210
- // Simple status: completed if exit 0, failed otherwise
211
- const status = code === 0 ? 'completed' : 'failed';
212
- updateTask(taskId, {
213
- status,
214
- exitCode: code,
215
- error: signal ? `Killed by ${signal}` : null,
216
- socketPath: null, // Clear socket path on exit
217
- });
210
+ const resolvedCode = recovered?.payload ? 0 : code;
211
+ const status = resolvedCode === 0 ? 'completed' : 'failed';
212
+ try {
213
+ await updateTask(taskId, {
214
+ status,
215
+ exitCode: resolvedCode,
216
+ error: resolvedCode === 0 ? null : signal ? `Killed by ${signal}` : null,
217
+ socketPath: null,
218
+ });
219
+ } catch (updateError) {
220
+ log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
221
+ }
218
222
 
219
- // Give clients time to receive exit message before exiting
220
223
  setTimeout(() => {
221
224
  process.exit(0);
222
225
  }, 500);
223
226
  });
224
227
 
225
- // Handle errors
226
- server.on('error', (err) => {
228
+ server.on('error', async (err) => {
227
229
  log(`\nError: ${err.message}\n`);
228
- updateTask(taskId, { status: 'failed', error: err.message });
230
+ try {
231
+ await updateTask(taskId, { status: 'failed', error: err.message });
232
+ } catch (updateError) {
233
+ log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
234
+ }
229
235
  process.exit(1);
230
236
  });
231
237
 
232
- // Handle client attach/detach for logging
233
238
  server.on('clientAttach', ({ clientId }) => {
234
239
  log(`[${Date.now()}][ATTACH] Client attached: ${clientId.slice(0, 8)}...\n`);
235
240
  });
@@ -238,11 +243,9 @@ server.on('clientDetach', ({ clientId }) => {
238
243
  log(`[${Date.now()}][DETACH] Client detached: ${clientId.slice(0, 8)}...\n`);
239
244
  });
240
245
 
241
- // Start the server
242
246
  try {
243
247
  await server.start();
244
248
 
245
- // Update task with PID and socket path
246
249
  updateTask(taskId, {
247
250
  pid: server.pid,
248
251
  socketPath,
@@ -258,7 +261,6 @@ try {
258
261
  process.exit(1);
259
262
  }
260
263
 
261
- // Handle process signals for cleanup
262
264
  process.on('SIGTERM', async () => {
263
265
  log(`[${Date.now()}][SYSTEM] Received SIGTERM, stopping...\n`);
264
266
  await server.stop('SIGTERM');
@@ -0,0 +1,119 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ export const STREAMING_MODE_ERROR = 'only prompt commands are supported in streaming mode';
6
+
7
+ export function detectStreamingModeError(line) {
8
+ const trimmed = typeof line === 'string' ? line.trim() : '';
9
+ if (!trimmed.startsWith('{')) return null;
10
+
11
+ try {
12
+ const parsed = JSON.parse(trimmed);
13
+ if (
14
+ parsed &&
15
+ parsed.type === 'result' &&
16
+ parsed.is_error === true &&
17
+ Array.isArray(parsed.errors) &&
18
+ parsed.errors.includes(STREAMING_MODE_ERROR) &&
19
+ typeof parsed.session_id === 'string'
20
+ ) {
21
+ return {
22
+ sessionId: parsed.session_id,
23
+ line: trimmed,
24
+ };
25
+ }
26
+ } catch {
27
+ // Ignore parse errors - not JSON
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ function findSessionJsonlPath(sessionId) {
34
+ const claudeDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
35
+ const projectsDir = join(claudeDir, 'projects');
36
+ if (!existsSync(projectsDir)) return null;
37
+
38
+ const target = `${sessionId}.jsonl`;
39
+ const queue = [projectsDir];
40
+
41
+ while (queue.length > 0) {
42
+ const dir = queue.pop();
43
+ if (!dir) continue;
44
+
45
+ let entries;
46
+ try {
47
+ entries = readdirSync(dir, { withFileTypes: true });
48
+ } catch {
49
+ continue;
50
+ }
51
+
52
+ for (const entry of entries) {
53
+ if (entry.isFile() && entry.name === target) {
54
+ return join(dir, entry.name);
55
+ }
56
+ if (entry.isDirectory()) {
57
+ queue.push(join(dir, entry.name));
58
+ }
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ export function recoverStructuredOutput(sessionId) {
66
+ const jsonlPath = findSessionJsonlPath(sessionId);
67
+ if (!jsonlPath) return null;
68
+
69
+ let fileContents;
70
+ try {
71
+ fileContents = readFileSync(jsonlPath, 'utf8');
72
+ } catch {
73
+ return null;
74
+ }
75
+
76
+ const lines = fileContents.split('\n');
77
+ let structuredOutput = null;
78
+ let usage = null;
79
+
80
+ for (const line of lines) {
81
+ if (!line.trim()) continue;
82
+ try {
83
+ const entry = JSON.parse(line);
84
+ const message = entry?.message;
85
+ const content = message?.content;
86
+ if (!Array.isArray(content)) continue;
87
+
88
+ for (const block of content) {
89
+ if (block?.type === 'tool_use' && block?.name === 'StructuredOutput' && block?.input) {
90
+ structuredOutput = block.input;
91
+ if (message?.usage && typeof message.usage === 'object') {
92
+ usage = message.usage;
93
+ }
94
+ }
95
+ }
96
+ } catch {
97
+ // Skip invalid JSON lines
98
+ }
99
+ }
100
+
101
+ if (!structuredOutput) return null;
102
+
103
+ const payload = {
104
+ type: 'result',
105
+ subtype: 'success',
106
+ is_error: false,
107
+ structured_output: structuredOutput,
108
+ session_id: sessionId,
109
+ };
110
+
111
+ if (usage) {
112
+ payload.usage = usage;
113
+ }
114
+
115
+ return {
116
+ payload,
117
+ sourcePath: jsonlPath,
118
+ };
119
+ }
@@ -27,7 +27,7 @@ export function listTasks(options = {}) {
27
27
  // Table format (default) or verbose format
28
28
  if (options.verbose) {
29
29
  // Verbose format (old behavior)
30
- console.log(chalk.bold(`\nClaude Tasks (${filtered.length}/${taskList.length})\n`));
30
+ console.log(chalk.bold(`\nTasks (${filtered.length}/${taskList.length})\n`));
31
31
 
32
32
  for (const task of filtered) {
33
33
  // Verify running status
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import { getTask } from '../store.js';
3
3
  import { spawnTask } from '../runner.js';
4
4
 
5
- export function resumeTask(taskId, newPrompt) {
5
+ export async function resumeTask(taskId, newPrompt) {
6
6
  const task = getTask(taskId);
7
7
 
8
8
  if (!task) {
@@ -23,10 +23,11 @@ export function resumeTask(taskId, newPrompt) {
23
23
  console.log(chalk.dim(`Original prompt: ${task.prompt}`));
24
24
  console.log(chalk.dim(`Resume prompt: ${prompt}`));
25
25
 
26
- const newTask = spawnTask(prompt, {
26
+ const newTask = await spawnTask(prompt, {
27
27
  cwd: task.cwd,
28
28
  continue: true, // Use --continue to load most recent session in that directory
29
29
  sessionId: task.sessionId,
30
+ provider: task.provider,
30
31
  });
31
32
 
32
33
  console.log(chalk.green(`\n✓ Resumed as new task: ${chalk.cyan(newTask.id)}`));
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { spawnTask } from '../runner.js';
3
3
 
4
- export function runTask(prompt, options = {}) {
4
+ export async function runTask(prompt, options = {}) {
5
5
  if (!prompt || prompt.trim().length === 0) {
6
6
  console.log(chalk.red('Error: Prompt is required'));
7
7
  process.exit(1);
@@ -11,10 +11,16 @@ export function runTask(prompt, options = {}) {
11
11
  const jsonSchema = options.jsonSchema;
12
12
  const silentJsonOutput = options.silentJsonOutput || false;
13
13
 
14
- console.log(chalk.dim('Spawning Claude task...'));
14
+ console.log(chalk.dim('Spawning task...'));
15
+ if (options.provider) {
16
+ console.log(chalk.dim(` Provider: ${options.provider}`));
17
+ }
15
18
  if (options.model) {
16
19
  console.log(chalk.dim(` Model: ${options.model}`));
17
20
  }
21
+ if (options.modelLevel) {
22
+ console.log(chalk.dim(` Level: ${options.modelLevel}`));
23
+ }
18
24
  if (jsonSchema && outputFormat === 'json') {
19
25
  console.log(chalk.dim(` JSON Schema: enforced`));
20
26
  if (silentJsonOutput) {
@@ -22,9 +28,12 @@ export function runTask(prompt, options = {}) {
22
28
  }
23
29
  }
24
30
 
25
- const task = spawnTask(prompt, {
31
+ const task = await spawnTask(prompt, {
26
32
  cwd: options.cwd || process.cwd(),
27
33
  model: options.model,
34
+ modelLevel: options.modelLevel,
35
+ reasoningEffort: options.reasoningEffort,
36
+ provider: options.provider,
28
37
  resume: options.resume,
29
38
  continue: options.continue,
30
39
  outputFormat,
@@ -1,55 +1,76 @@
1
1
  import { fork } from 'child_process';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { LOGS_DIR, DEFAULT_MODEL } from './config.js';
4
+ import { LOGS_DIR } from './config.js';
5
5
  import { addTask, generateId, ensureDirs } from './store.js';
6
+ import { createRequire } from 'module';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const { loadSettings } = require('../lib/settings.js');
10
+ const { normalizeProviderName } = require('../lib/provider-names');
11
+ const { getProvider } = require('../src/providers');
6
12
 
7
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
14
 
9
- export function spawnTask(prompt, options = {}) {
15
+ export async function spawnTask(prompt, options = {}) {
10
16
  ensureDirs();
11
17
 
12
18
  const id = generateId();
13
19
  const logFile = join(LOGS_DIR, `${id}.log`);
14
20
  const cwd = options.cwd || process.cwd();
15
- const model = options.model || process.env.ANTHROPIC_MODEL || DEFAULT_MODEL;
16
21
 
17
- // Build claude command args
18
- // --print: non-interactive mode
19
- // --dangerously-skip-permissions: background tasks can't prompt for approval (CRITICAL)
20
- // --output-format: stream-json (default) for real-time, text for clean output, json for structured
22
+ const settings = loadSettings();
23
+ const providerName = normalizeProviderName(
24
+ options.provider || settings.defaultProvider || 'claude'
25
+ );
26
+ const provider = getProvider(providerName);
27
+ const providerSettings = settings.providerSettings?.[providerName] || {};
28
+ const levelOverrides = providerSettings.levelOverrides || {};
29
+
21
30
  const outputFormat = options.outputFormat || 'stream-json';
22
- const args = ['--print', '--dangerously-skip-permissions', '--output-format', outputFormat];
23
31
 
24
- // Only add streaming options for stream-json format
25
- if (outputFormat === 'stream-json') {
26
- args.push('--verbose');
27
- // Include partial messages to get streaming updates before completion (required for stream-json format)
28
- args.push('--include-partial-messages');
32
+ let jsonSchema = options.jsonSchema || null;
33
+ if (jsonSchema && outputFormat !== 'json') {
34
+ console.warn('Warning: --json-schema requires --output-format json, ignoring schema');
35
+ jsonSchema = null;
29
36
  }
30
37
 
31
- // Add JSON schema if provided (only works with --output-format json)
32
- if (options.jsonSchema) {
33
- if (outputFormat !== 'json') {
34
- console.warn('Warning: --json-schema requires --output-format json, ignoring schema');
35
- } else {
36
- // CRITICAL: Must stringify schema object before passing to CLI (like zeroshot does)
37
- const schemaString =
38
- typeof options.jsonSchema === 'string'
39
- ? options.jsonSchema
40
- : JSON.stringify(options.jsonSchema);
41
- args.push('--json-schema', schemaString);
38
+ let modelSpec;
39
+ if (options.model) {
40
+ modelSpec = {
41
+ model: options.model,
42
+ reasoningEffort: options.reasoningEffort,
43
+ };
44
+ } else {
45
+ const level = options.modelLevel || providerSettings.defaultLevel || provider.getDefaultLevel();
46
+ modelSpec = provider.resolveModelSpec(level, levelOverrides);
47
+ if (options.reasoningEffort) {
48
+ modelSpec = { ...modelSpec, reasoningEffort: options.reasoningEffort };
42
49
  }
43
50
  }
44
51
 
45
- if (options.resume) {
46
- args.push('--resume', options.resume);
47
- } else if (options.continue) {
48
- args.push('--continue');
52
+ const cliFeatures = await provider.getCliFeatures();
53
+ const commandSpec = provider.buildCommand(prompt, {
54
+ modelSpec,
55
+ outputFormat,
56
+ jsonSchema,
57
+ cwd,
58
+ autoApprove: true,
59
+ cliFeatures,
60
+ });
61
+
62
+ const finalArgs = [...commandSpec.args];
63
+ if (providerName === 'claude') {
64
+ const promptIndex = finalArgs.length - 1;
65
+ if (options.resume) {
66
+ finalArgs.splice(promptIndex, 0, '--resume', options.resume);
67
+ } else if (options.continue) {
68
+ finalArgs.splice(promptIndex, 0, '--continue');
69
+ }
70
+ } else if (options.resume || options.continue) {
71
+ console.warn('Warning: resume/continue is only supported for Claude CLI; ignoring.');
49
72
  }
50
73
 
51
- args.push(prompt);
52
-
53
74
  const task = {
54
75
  id,
55
76
  prompt: prompt.slice(0, 200) + (prompt.length > 200 ? '...' : ''),
@@ -63,6 +84,8 @@ export function spawnTask(prompt, options = {}) {
63
84
  updatedAt: new Date().toISOString(),
64
85
  exitCode: null,
65
86
  error: null,
87
+ provider: providerName,
88
+ model: modelSpec?.model || null,
66
89
  // Schedule reference (if spawned by scheduler)
67
90
  scheduleId: options.scheduleId || null,
68
91
  // Attach support
@@ -72,24 +95,23 @@ export function spawnTask(prompt, options = {}) {
72
95
 
73
96
  addTask(task);
74
97
 
75
- // Fork a watcher process that will manage the claude process
76
98
  const watcherConfig = {
77
99
  outputFormat,
78
- jsonSchema: options.jsonSchema || null,
100
+ jsonSchema,
79
101
  silentJsonOutput: options.silentJsonOutput || false,
80
- model,
102
+ provider: providerName,
103
+ command: commandSpec.binary,
104
+ env: commandSpec.env || {},
81
105
  };
82
106
 
83
- // Use attachable watcher by default (unless explicitly disabled)
84
- // Attachable watcher uses node-pty and creates a Unix socket for attach/detach
85
- const useAttachable = options.attachable !== false;
107
+ const useAttachable = options.attachable !== false && !options.jsonSchema;
86
108
  const watcherScript = useAttachable
87
109
  ? join(__dirname, 'attachable-watcher.js')
88
110
  : join(__dirname, 'watcher.js');
89
111
 
90
112
  const watcher = fork(
91
113
  watcherScript,
92
- [id, cwd, logFile, JSON.stringify(args), JSON.stringify(watcherConfig)],
114
+ [id, cwd, logFile, JSON.stringify(finalArgs), JSON.stringify(watcherConfig)],
93
115
  {
94
116
  detached: true,
95
117
  stdio: 'ignore',
@@ -98,7 +120,6 @@ export function spawnTask(prompt, options = {}) {
98
120
 
99
121
  watcher.unref();
100
122
 
101
- // Return task immediately - watcher will update PID async
102
123
  return task;
103
124
  }
104
125
 
@@ -108,7 +108,7 @@ function log(msg) {
108
108
  /**
109
109
  * Check and run due schedules
110
110
  */
111
- function checkSchedules() {
111
+ async function checkSchedules() {
112
112
  const schedules = loadSchedules();
113
113
  const now = new Date();
114
114
 
@@ -122,7 +122,7 @@ function checkSchedules() {
122
122
  log(`Running scheduled task: ${schedule.id} - "${schedule.prompt.slice(0, 50)}..."`);
123
123
 
124
124
  try {
125
- const task = spawnTask(schedule.prompt, {
125
+ const task = await spawnTask(schedule.prompt, {
126
126
  cwd: schedule.cwd,
127
127
  scheduleId: schedule.id,
128
128
  });