@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/LICENSE +21 -0
  3. package/README.md +364 -0
  4. package/cli/index.js +3990 -0
  5. package/cluster-templates/base-templates/debug-workflow.json +181 -0
  6. package/cluster-templates/base-templates/full-workflow.json +455 -0
  7. package/cluster-templates/base-templates/single-worker.json +48 -0
  8. package/cluster-templates/base-templates/worker-validator.json +131 -0
  9. package/cluster-templates/conductor-bootstrap.json +122 -0
  10. package/cluster-templates/conductor-junior-bootstrap.json +69 -0
  11. package/docker/zeroshot-cluster/Dockerfile +132 -0
  12. package/lib/completion.js +174 -0
  13. package/lib/id-detector.js +53 -0
  14. package/lib/settings.js +97 -0
  15. package/lib/stream-json-parser.js +236 -0
  16. package/package.json +121 -0
  17. package/src/agent/agent-config.js +121 -0
  18. package/src/agent/agent-context-builder.js +241 -0
  19. package/src/agent/agent-hook-executor.js +329 -0
  20. package/src/agent/agent-lifecycle.js +555 -0
  21. package/src/agent/agent-stuck-detector.js +256 -0
  22. package/src/agent/agent-task-executor.js +1034 -0
  23. package/src/agent/agent-trigger-evaluator.js +67 -0
  24. package/src/agent-wrapper.js +459 -0
  25. package/src/agents/git-pusher-agent.json +20 -0
  26. package/src/attach/attach-client.js +438 -0
  27. package/src/attach/attach-server.js +543 -0
  28. package/src/attach/index.js +35 -0
  29. package/src/attach/protocol.js +220 -0
  30. package/src/attach/ring-buffer.js +121 -0
  31. package/src/attach/socket-discovery.js +242 -0
  32. package/src/claude-task-runner.js +468 -0
  33. package/src/config-router.js +80 -0
  34. package/src/config-validator.js +598 -0
  35. package/src/github.js +103 -0
  36. package/src/isolation-manager.js +1042 -0
  37. package/src/ledger.js +429 -0
  38. package/src/logic-engine.js +223 -0
  39. package/src/message-bus-bridge.js +139 -0
  40. package/src/message-bus.js +202 -0
  41. package/src/name-generator.js +232 -0
  42. package/src/orchestrator.js +1938 -0
  43. package/src/schemas/sub-cluster.js +156 -0
  44. package/src/sub-cluster-wrapper.js +545 -0
  45. package/src/task-runner.js +28 -0
  46. package/src/template-resolver.js +347 -0
  47. package/src/tui/CHANGES.txt +133 -0
  48. package/src/tui/LAYOUT.md +261 -0
  49. package/src/tui/README.txt +192 -0
  50. package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
  51. package/src/tui/data-poller.js +325 -0
  52. package/src/tui/demo.js +208 -0
  53. package/src/tui/formatters.js +123 -0
  54. package/src/tui/index.js +193 -0
  55. package/src/tui/keybindings.js +383 -0
  56. package/src/tui/layout.js +317 -0
  57. 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 };