@covibes/zeroshot 1.0.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/CHANGELOG.md +167 -0
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/index.js +3990 -0
- package/cluster-templates/base-templates/debug-workflow.json +181 -0
- package/cluster-templates/base-templates/full-workflow.json +455 -0
- package/cluster-templates/base-templates/single-worker.json +48 -0
- package/cluster-templates/base-templates/worker-validator.json +131 -0
- package/cluster-templates/conductor-bootstrap.json +122 -0
- package/cluster-templates/conductor-junior-bootstrap.json +69 -0
- package/docker/zeroshot-cluster/Dockerfile +132 -0
- package/lib/completion.js +174 -0
- package/lib/id-detector.js +53 -0
- package/lib/settings.js +97 -0
- package/lib/stream-json-parser.js +236 -0
- package/package.json +121 -0
- package/src/agent/agent-config.js +121 -0
- package/src/agent/agent-context-builder.js +241 -0
- package/src/agent/agent-hook-executor.js +329 -0
- package/src/agent/agent-lifecycle.js +555 -0
- package/src/agent/agent-stuck-detector.js +256 -0
- package/src/agent/agent-task-executor.js +1034 -0
- package/src/agent/agent-trigger-evaluator.js +67 -0
- package/src/agent-wrapper.js +459 -0
- package/src/agents/git-pusher-agent.json +20 -0
- package/src/attach/attach-client.js +438 -0
- package/src/attach/attach-server.js +543 -0
- package/src/attach/index.js +35 -0
- package/src/attach/protocol.js +220 -0
- package/src/attach/ring-buffer.js +121 -0
- package/src/attach/socket-discovery.js +242 -0
- package/src/claude-task-runner.js +468 -0
- package/src/config-router.js +80 -0
- package/src/config-validator.js +598 -0
- package/src/github.js +103 -0
- package/src/isolation-manager.js +1042 -0
- package/src/ledger.js +429 -0
- package/src/logic-engine.js +223 -0
- package/src/message-bus-bridge.js +139 -0
- package/src/message-bus.js +202 -0
- package/src/name-generator.js +232 -0
- package/src/orchestrator.js +1938 -0
- package/src/schemas/sub-cluster.js +156 -0
- package/src/sub-cluster-wrapper.js +545 -0
- package/src/task-runner.js +28 -0
- package/src/template-resolver.js +347 -0
- package/src/tui/CHANGES.txt +133 -0
- package/src/tui/LAYOUT.md +261 -0
- package/src/tui/README.txt +192 -0
- package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
- package/src/tui/data-poller.js +325 -0
- package/src/tui/demo.js +208 -0
- package/src/tui/formatters.js +123 -0
- package/src/tui/index.js +193 -0
- package/src/tui/keybindings.js +383 -0
- package/src/tui/layout.js +317 -0
- package/src/tui/renderer.js +194 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeTaskRunner - Production implementation of TaskRunner
|
|
3
|
+
*
|
|
4
|
+
* Executes Claude tasks by spawning the `zeroshot task run` CLI command,
|
|
5
|
+
* following logs, and assembling results.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { spawn, exec, execSync } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const TaskRunner = require('./task-runner');
|
|
11
|
+
|
|
12
|
+
class ClaudeTaskRunner extends TaskRunner {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @param {Object} [options.messageBus] - MessageBus for streaming output
|
|
16
|
+
* @param {boolean} [options.quiet] - Suppress console logging
|
|
17
|
+
* @param {number} [options.timeout] - Task timeout in ms (default: 1 hour)
|
|
18
|
+
* @param {Function} [options.onOutput] - Callback for output lines
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
super();
|
|
22
|
+
this.messageBus = options.messageBus || null;
|
|
23
|
+
this.quiet = options.quiet || false;
|
|
24
|
+
this.timeout = options.timeout || 60 * 60 * 1000;
|
|
25
|
+
this.onOutput = options.onOutput || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {...any} args
|
|
30
|
+
*/
|
|
31
|
+
_log(...args) {
|
|
32
|
+
if (!this.quiet) {
|
|
33
|
+
console.log(...args);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute a Claude task via zeroshot CLI
|
|
39
|
+
*
|
|
40
|
+
* @param {string} context - Full prompt/context
|
|
41
|
+
* @param {{agentId?: string, model?: string, outputFormat?: string, jsonSchema?: any, strictSchema?: boolean, cwd?: string, isolation?: any}} options - Execution options
|
|
42
|
+
* @returns {Promise<{success: boolean, output: string, error: string|null, taskId?: string}>}
|
|
43
|
+
*/
|
|
44
|
+
async run(context, options = {}) {
|
|
45
|
+
const {
|
|
46
|
+
agentId = 'unknown',
|
|
47
|
+
model = 'sonnet',
|
|
48
|
+
outputFormat = 'stream-json',
|
|
49
|
+
jsonSchema = null,
|
|
50
|
+
strictSchema = false, // false = live streaming (default), true = CLI schema enforcement (no streaming)
|
|
51
|
+
cwd = process.cwd(),
|
|
52
|
+
isolation = null,
|
|
53
|
+
} = options;
|
|
54
|
+
|
|
55
|
+
// Isolation mode delegates to separate method
|
|
56
|
+
if (isolation?.enabled) {
|
|
57
|
+
return this._runIsolated(context, options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ctPath = 'zeroshot';
|
|
61
|
+
|
|
62
|
+
// Build args.
|
|
63
|
+
// json output does not stream; if a jsonSchema is configured we run stream-json
|
|
64
|
+
// for live logs and validate/parse JSON after completion.
|
|
65
|
+
// Set strictSchema=true to disable live streaming and use CLI's native schema enforcement.
|
|
66
|
+
const desiredOutputFormat = outputFormat;
|
|
67
|
+
const runOutputFormat =
|
|
68
|
+
jsonSchema && desiredOutputFormat === 'json' && !strictSchema
|
|
69
|
+
? 'stream-json'
|
|
70
|
+
: desiredOutputFormat;
|
|
71
|
+
const args = ['task', 'run', '--output-format', runOutputFormat];
|
|
72
|
+
|
|
73
|
+
// Pass schema to CLI only when using json output (strictSchema=true or no conflict)
|
|
74
|
+
if (jsonSchema && runOutputFormat === 'json') {
|
|
75
|
+
args.push('--json-schema', JSON.stringify(jsonSchema));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
args.push(context);
|
|
79
|
+
|
|
80
|
+
// Spawn and get task ID
|
|
81
|
+
const taskId = await this._spawnAndGetTaskId(ctPath, args, cwd, model, agentId);
|
|
82
|
+
|
|
83
|
+
this._log(`📋 [${agentId}]: Following zeroshot logs for ${taskId}`);
|
|
84
|
+
|
|
85
|
+
// Wait for task registration
|
|
86
|
+
await this._waitForTaskReady(ctPath, taskId);
|
|
87
|
+
|
|
88
|
+
// Follow logs until completion
|
|
89
|
+
return this._followLogs(ctPath, taskId, agentId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} ctPath
|
|
94
|
+
* @param {string[]} args
|
|
95
|
+
* @param {string} cwd
|
|
96
|
+
* @param {string} model
|
|
97
|
+
* @param {string} _agentId
|
|
98
|
+
* @returns {Promise<string>}
|
|
99
|
+
*/
|
|
100
|
+
_spawnAndGetTaskId(ctPath, args, cwd, model, _agentId) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const proc = spawn(ctPath, args, {
|
|
103
|
+
cwd,
|
|
104
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
105
|
+
env: {
|
|
106
|
+
...process.env,
|
|
107
|
+
ANTHROPIC_MODEL: model,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
let stdout = '';
|
|
112
|
+
let stderr = '';
|
|
113
|
+
|
|
114
|
+
proc.stdout.on('data', (data) => {
|
|
115
|
+
stdout += data.toString();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
proc.stderr.on('data', (data) => {
|
|
119
|
+
stderr += data.toString();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
proc.on('close', (code) => {
|
|
123
|
+
if (code === 0) {
|
|
124
|
+
const match = stdout.match(/Task spawned: ((?:task-)?[a-z]+-[a-z]+-[a-z0-9]+)/);
|
|
125
|
+
if (match) {
|
|
126
|
+
resolve(match[1]);
|
|
127
|
+
} else {
|
|
128
|
+
reject(new Error(`Could not parse task ID from output: ${stdout}`));
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
reject(new Error(`zeroshot task run failed with code ${code}: ${stderr}`));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
proc.on('error', (error) => {
|
|
136
|
+
reject(error);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} ctPath
|
|
143
|
+
* @param {string} taskId
|
|
144
|
+
* @param {number} maxRetries
|
|
145
|
+
* @param {number} delayMs
|
|
146
|
+
* @returns {Promise<void>}
|
|
147
|
+
*/
|
|
148
|
+
async _waitForTaskReady(ctPath, taskId, maxRetries = 10, delayMs = 200) {
|
|
149
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
150
|
+
const exists = await new Promise((resolve) => {
|
|
151
|
+
exec(`${ctPath} status ${taskId}`, (error, stdout) => {
|
|
152
|
+
resolve(!error && !stdout.includes('Task not found'));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (exists) return;
|
|
157
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
158
|
+
}
|
|
159
|
+
console.warn(
|
|
160
|
+
`⚠️ Task ${taskId} not yet visible after ${maxRetries} retries, continuing anyway`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} ctPath
|
|
166
|
+
* @param {string} taskId
|
|
167
|
+
* @param {string} agentId
|
|
168
|
+
* @returns {Promise<{success: boolean, output: string, error: string|null, taskId: string}>}
|
|
169
|
+
*/
|
|
170
|
+
_followLogs(ctPath, taskId, agentId) {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
let output = '';
|
|
173
|
+
/** @type {string|null} */
|
|
174
|
+
let logFilePath = null;
|
|
175
|
+
let lastSize = 0;
|
|
176
|
+
/** @type {NodeJS.Timeout|null} */
|
|
177
|
+
let pollInterval = null;
|
|
178
|
+
/** @type {NodeJS.Timeout|null} */
|
|
179
|
+
let statusCheckInterval = null;
|
|
180
|
+
let resolved = false;
|
|
181
|
+
let lineBuffer = '';
|
|
182
|
+
|
|
183
|
+
// Get log file path
|
|
184
|
+
try {
|
|
185
|
+
logFilePath = execSync(`${ctPath} get-log-path ${taskId}`, {
|
|
186
|
+
encoding: 'utf-8',
|
|
187
|
+
}).trim();
|
|
188
|
+
} catch {
|
|
189
|
+
this._log(`⏳ [${agentId}]: Waiting for log file...`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {string} line
|
|
194
|
+
*/
|
|
195
|
+
const broadcastLine = (line) => {
|
|
196
|
+
if (!line.trim()) return;
|
|
197
|
+
|
|
198
|
+
let content = line;
|
|
199
|
+
const timestampMatch = line.match(/^\[(\d{13})\](.*)$/);
|
|
200
|
+
if (timestampMatch) {
|
|
201
|
+
content = timestampMatch[2];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Skip non-JSON patterns
|
|
205
|
+
if (
|
|
206
|
+
content.startsWith('===') ||
|
|
207
|
+
content.startsWith('Finished:') ||
|
|
208
|
+
content.startsWith('Exit code:') ||
|
|
209
|
+
(content.includes('"type":"system"') && content.includes('"subtype":"init"'))
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!content.trim().startsWith('{')) return;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
JSON.parse(content);
|
|
218
|
+
} catch {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
output += content + '\n';
|
|
223
|
+
|
|
224
|
+
// Callback for output streaming
|
|
225
|
+
if (this.onOutput) {
|
|
226
|
+
this.onOutput(content, agentId);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @param {string} content
|
|
232
|
+
*/
|
|
233
|
+
const processNewContent = (content) => {
|
|
234
|
+
lineBuffer += content;
|
|
235
|
+
const lines = lineBuffer.split('\n');
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
238
|
+
broadcastLine(lines[i]);
|
|
239
|
+
}
|
|
240
|
+
lineBuffer = lines[lines.length - 1];
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const pollLogFile = () => {
|
|
244
|
+
if (!logFilePath) {
|
|
245
|
+
try {
|
|
246
|
+
logFilePath = execSync(`${ctPath} get-log-path ${taskId}`, {
|
|
247
|
+
encoding: 'utf-8',
|
|
248
|
+
}).trim();
|
|
249
|
+
} catch {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!fs.existsSync(logFilePath)) return;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const stats = fs.statSync(logFilePath);
|
|
258
|
+
const currentSize = stats.size;
|
|
259
|
+
|
|
260
|
+
if (currentSize > lastSize) {
|
|
261
|
+
const fd = fs.openSync(logFilePath, 'r');
|
|
262
|
+
const buffer = Buffer.alloc(currentSize - lastSize);
|
|
263
|
+
fs.readSync(fd, buffer, 0, buffer.length, lastSize);
|
|
264
|
+
fs.closeSync(fd);
|
|
265
|
+
|
|
266
|
+
processNewContent(buffer.toString('utf-8'));
|
|
267
|
+
lastSize = currentSize;
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
const error = /** @type {Error} */ (err);
|
|
271
|
+
console.warn(`⚠️ [${agentId}]: Error reading log: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
pollInterval = setInterval(pollLogFile, 300);
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {boolean} success
|
|
279
|
+
* @param {string} stdout
|
|
280
|
+
* @returns {string|null}
|
|
281
|
+
*/
|
|
282
|
+
const extractErrorContext = (success, stdout) => {
|
|
283
|
+
if (success) return null;
|
|
284
|
+
|
|
285
|
+
// Try to extract error from status output first
|
|
286
|
+
const statusErrorMatch = stdout.match(/Error:\s*(.+)/);
|
|
287
|
+
if (statusErrorMatch) {
|
|
288
|
+
return statusErrorMatch[1].trim();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Fall back to last 500 chars of output
|
|
292
|
+
const lastOutput = output.slice(-500).trim();
|
|
293
|
+
if (!lastOutput) {
|
|
294
|
+
return 'Task failed with no output';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const errorPatterns = [
|
|
298
|
+
/Error:\s*(.+)/i,
|
|
299
|
+
/error:\s*(.+)/i,
|
|
300
|
+
/failed:\s*(.+)/i,
|
|
301
|
+
/Exception:\s*(.+)/i,
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
for (const pattern of errorPatterns) {
|
|
305
|
+
const match = lastOutput.match(pattern);
|
|
306
|
+
if (match) {
|
|
307
|
+
return match[1].slice(0, 200);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return `Task failed. Last output: ${lastOutput.slice(-200)}`;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
statusCheckInterval = setInterval(() => {
|
|
315
|
+
exec(`${ctPath} status ${taskId}`, (error, stdout) => {
|
|
316
|
+
if (resolved) return;
|
|
317
|
+
|
|
318
|
+
if (
|
|
319
|
+
!error &&
|
|
320
|
+
(stdout.includes('Status: completed') || stdout.includes('Status: failed'))
|
|
321
|
+
) {
|
|
322
|
+
const success = stdout.includes('Status: completed');
|
|
323
|
+
|
|
324
|
+
pollLogFile();
|
|
325
|
+
|
|
326
|
+
setTimeout(() => {
|
|
327
|
+
if (resolved) return;
|
|
328
|
+
resolved = true;
|
|
329
|
+
|
|
330
|
+
if (pollInterval) clearInterval(pollInterval);
|
|
331
|
+
if (statusCheckInterval) clearInterval(statusCheckInterval);
|
|
332
|
+
|
|
333
|
+
const errorContext = extractErrorContext(success, stdout);
|
|
334
|
+
|
|
335
|
+
resolve({
|
|
336
|
+
success,
|
|
337
|
+
output,
|
|
338
|
+
error: errorContext,
|
|
339
|
+
taskId,
|
|
340
|
+
});
|
|
341
|
+
}, 500);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}, 1000);
|
|
345
|
+
|
|
346
|
+
// Timeout
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
if (resolved) return;
|
|
349
|
+
resolved = true;
|
|
350
|
+
|
|
351
|
+
clearInterval(pollInterval);
|
|
352
|
+
clearInterval(statusCheckInterval);
|
|
353
|
+
|
|
354
|
+
const timeoutMinutes = Math.round(this.timeout / 60000);
|
|
355
|
+
reject(new Error(`Task timed out after ${timeoutMinutes} minutes`));
|
|
356
|
+
}, this.timeout);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Run task in isolated Docker container
|
|
362
|
+
* @param {string} context
|
|
363
|
+
* @param {{agentId?: string, model?: string, outputFormat?: string, jsonSchema?: any, strictSchema?: boolean, isolation?: any}} options
|
|
364
|
+
* @returns {Promise<{success: boolean, output: string, error: string|null}>}
|
|
365
|
+
*/
|
|
366
|
+
_runIsolated(context, options) {
|
|
367
|
+
const {
|
|
368
|
+
agentId = 'unknown',
|
|
369
|
+
model = 'sonnet',
|
|
370
|
+
outputFormat = 'stream-json',
|
|
371
|
+
jsonSchema = null,
|
|
372
|
+
strictSchema = false,
|
|
373
|
+
isolation,
|
|
374
|
+
} = options;
|
|
375
|
+
const { manager, clusterId } = isolation;
|
|
376
|
+
|
|
377
|
+
this._log(`📦 [${agentId}]: Running task in isolated container...`);
|
|
378
|
+
|
|
379
|
+
// Determine output format: stream-json for live logs unless strictSchema=true
|
|
380
|
+
const desiredOutputFormat = outputFormat;
|
|
381
|
+
const runOutputFormat =
|
|
382
|
+
jsonSchema && desiredOutputFormat === 'json' && !strictSchema
|
|
383
|
+
? 'stream-json'
|
|
384
|
+
: desiredOutputFormat;
|
|
385
|
+
|
|
386
|
+
const command = [
|
|
387
|
+
'claude',
|
|
388
|
+
'--print',
|
|
389
|
+
'--dangerously-skip-permissions',
|
|
390
|
+
'--output-format',
|
|
391
|
+
runOutputFormat,
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
if (runOutputFormat === 'stream-json') {
|
|
395
|
+
command.push('--verbose');
|
|
396
|
+
command.push('--include-partial-messages');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Pass schema to CLI only when using json output (strictSchema=true or no conflict)
|
|
400
|
+
if (jsonSchema && runOutputFormat === 'json') {
|
|
401
|
+
command.push('--json-schema', JSON.stringify(jsonSchema));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (model) {
|
|
405
|
+
command.push('--model', model);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
command.push(context);
|
|
409
|
+
|
|
410
|
+
return new Promise((resolve, reject) => {
|
|
411
|
+
let output = '';
|
|
412
|
+
let resolved = false;
|
|
413
|
+
|
|
414
|
+
const proc = manager.spawnInContainer(clusterId, command, {
|
|
415
|
+
env: { ANTHROPIC_MODEL: model },
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
proc.stdout.on('data', (/** @type {Buffer} */ data) => {
|
|
419
|
+
const chunk = data.toString();
|
|
420
|
+
output += chunk;
|
|
421
|
+
|
|
422
|
+
if (this.onOutput) {
|
|
423
|
+
this.onOutput(chunk, agentId);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
proc.stderr.on('data', (/** @type {Buffer} */ data) => {
|
|
428
|
+
const chunk = data.toString();
|
|
429
|
+
if (!this.quiet) {
|
|
430
|
+
console.error(`[${agentId}] stderr:`, chunk);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
proc.on('close', (/** @type {number|null} */ code) => {
|
|
435
|
+
if (resolved) return;
|
|
436
|
+
resolved = true;
|
|
437
|
+
|
|
438
|
+
resolve({
|
|
439
|
+
success: code === 0,
|
|
440
|
+
output,
|
|
441
|
+
error: code === 0 ? null : `Container exited with code ${code}`,
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
proc.on('error', (/** @type {Error} */ error) => {
|
|
446
|
+
if (resolved) return;
|
|
447
|
+
resolved = true;
|
|
448
|
+
reject(error);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
setTimeout(() => {
|
|
452
|
+
if (resolved) return;
|
|
453
|
+
resolved = true;
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
proc.kill('SIGKILL');
|
|
457
|
+
} catch {
|
|
458
|
+
// Ignore - process may already be dead
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const timeoutMinutes = Math.round(this.timeout / 60000);
|
|
462
|
+
reject(new Error(`Isolated task timed out after ${timeoutMinutes} minutes`));
|
|
463
|
+
}, this.timeout);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
module.exports = ClaudeTaskRunner;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Router - Maps 2D classification to parameterized templates
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for: Complexity × TaskType → { base, params }
|
|
5
|
+
* Used by both logic-engine.js (trigger evaluation) and agent-wrapper.js (transform scripts)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { DEFAULT_MAX_ITERATIONS } = require('./agent/agent-config');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get cluster config based on complexity and task type
|
|
12
|
+
* @param {string} complexity - TRIVIAL, SIMPLE, STANDARD, CRITICAL
|
|
13
|
+
* @param {string} taskType - INQUIRY, TASK, DEBUG
|
|
14
|
+
* @returns {{ base: string, params: object }}
|
|
15
|
+
*/
|
|
16
|
+
function getConfig(complexity, taskType) {
|
|
17
|
+
const getBase = () => {
|
|
18
|
+
if (taskType === 'DEBUG' && complexity !== 'TRIVIAL') {
|
|
19
|
+
return 'debug-workflow';
|
|
20
|
+
}
|
|
21
|
+
if (complexity === 'TRIVIAL') {
|
|
22
|
+
return 'single-worker';
|
|
23
|
+
}
|
|
24
|
+
if (complexity === 'SIMPLE') {
|
|
25
|
+
return 'worker-validator';
|
|
26
|
+
}
|
|
27
|
+
return 'full-workflow';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getModel = (role) => {
|
|
31
|
+
if (complexity === 'CRITICAL' && role === 'planner') return 'opus';
|
|
32
|
+
if (complexity === 'TRIVIAL') return 'haiku';
|
|
33
|
+
return 'sonnet';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getValidatorCount = () => {
|
|
37
|
+
if (complexity === 'TRIVIAL') return 0;
|
|
38
|
+
if (complexity === 'SIMPLE') return 1;
|
|
39
|
+
if (complexity === 'STANDARD') return 2;
|
|
40
|
+
if (complexity === 'CRITICAL') return 4;
|
|
41
|
+
return 1;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getMaxTokens = () => {
|
|
45
|
+
if (complexity === 'TRIVIAL') return 50000;
|
|
46
|
+
if (complexity === 'SIMPLE') return 100000;
|
|
47
|
+
if (complexity === 'STANDARD') return 100000;
|
|
48
|
+
if (complexity === 'CRITICAL') return 150000;
|
|
49
|
+
return 100000;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const base = getBase();
|
|
53
|
+
|
|
54
|
+
const params = {
|
|
55
|
+
task_type: taskType,
|
|
56
|
+
complexity,
|
|
57
|
+
max_tokens: getMaxTokens(),
|
|
58
|
+
max_iterations: DEFAULT_MAX_ITERATIONS,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (base === 'single-worker') {
|
|
62
|
+
params.worker_model = getModel('worker');
|
|
63
|
+
} else if (base === 'worker-validator') {
|
|
64
|
+
params.worker_model = getModel('worker');
|
|
65
|
+
params.validator_model = getModel('validator');
|
|
66
|
+
} else if (base === 'debug-workflow') {
|
|
67
|
+
params.investigator_model = getModel('planner');
|
|
68
|
+
params.fixer_model = getModel('worker');
|
|
69
|
+
params.tester_model = getModel('validator');
|
|
70
|
+
} else if (base === 'full-workflow') {
|
|
71
|
+
params.planner_model = getModel('planner');
|
|
72
|
+
params.worker_model = getModel('worker');
|
|
73
|
+
params.validator_model = getModel('validator');
|
|
74
|
+
params.validator_count = getValidatorCount();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { base, params };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { getConfig };
|