@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
@@ -9,7 +9,9 @@
9
9
  * - maxModel ceiling enforcement at config time
10
10
  */
11
11
 
12
- const { loadSettings, validateModelAgainstMax } = require('../../lib/settings');
12
+ const { loadSettings, validateModelAgainstMax, VALID_MODELS } = require('../../lib/settings');
13
+
14
+ const VALID_LEVELS = ['level1', 'level2', 'level3'];
13
15
 
14
16
  // Default max iterations (high limit - let the user decide when to give up)
15
17
  const DEFAULT_MAX_ITERATIONS = 100;
@@ -58,39 +60,61 @@ function validateAgentConfig(config, options = {}) {
58
60
  }
59
61
 
60
62
  // Model configuration: support both static model and dynamic rules
61
- // If no model specified, model is null - _selectModel() will use maxModel as default
63
+ // If no model specified, model is null - _selectModel() will use provider defaults
62
64
  let modelConfig;
63
65
  if (config.modelRules) {
64
66
  modelConfig = { type: 'rules', rules: config.modelRules };
65
67
  } else {
66
- modelConfig = { type: 'static', model: config.model || null };
68
+ modelConfig = {
69
+ type: 'static',
70
+ model: config.model || null,
71
+ modelLevel: config.modelLevel || null,
72
+ };
67
73
  }
68
74
 
69
- // COST CEILING ENFORCEMENT: Validate model(s) against maxModel at config time
75
+ // COST CEILING/FLOOR ENFORCEMENT: Validate model(s) against maxModel and minModel at config time
70
76
  // Catches violations EARLY (config load) instead of at runtime (iteration N)
71
77
  const settings = loadSettings();
72
78
  const maxModel = settings.maxModel || 'sonnet';
79
+ const minModel = settings.minModel || null;
80
+
81
+ if (modelConfig.type === 'static') {
82
+ if (modelConfig.model && VALID_MODELS.includes(modelConfig.model)) {
83
+ // Static model: validate once (legacy Claude models only)
84
+ try {
85
+ validateModelAgainstMax(modelConfig.model, maxModel, minModel);
86
+ } catch (error) {
87
+ throw new Error(`Agent "${config.id}": ${error.message}`);
88
+ }
89
+ }
73
90
 
74
- if (modelConfig.type === 'static' && modelConfig.model) {
75
- // Static model: validate once
76
- try {
77
- validateModelAgainstMax(modelConfig.model, maxModel);
78
- } catch (error) {
79
- throw new Error(`Agent "${config.id}": ${error.message}`);
91
+ if (modelConfig.modelLevel && !VALID_LEVELS.includes(modelConfig.modelLevel)) {
92
+ throw new Error(
93
+ `Agent "${config.id}": invalid modelLevel "${modelConfig.modelLevel}". ` +
94
+ `Valid: ${VALID_LEVELS.join(', ')}`
95
+ );
80
96
  }
81
97
  } else if (modelConfig.type === 'rules') {
82
98
  // Dynamic rules: validate ALL rules upfront (don't wait until iteration N)
83
99
  for (const rule of modelConfig.rules) {
84
- if (rule.model) {
100
+ if (rule.model && VALID_MODELS.includes(rule.model)) {
85
101
  try {
86
- validateModelAgainstMax(rule.model, maxModel);
102
+ validateModelAgainstMax(rule.model, maxModel, minModel);
87
103
  } catch {
88
104
  throw new Error(
89
105
  `Agent "${config.id}": modelRule "${rule.iterations}" requests "${rule.model}" ` +
90
- `but maxModel is "${maxModel}". Either lower the rule's model or raise maxModel.`
106
+ `but maxModel is "${maxModel}"${minModel ? ` and minModel is "${minModel}"` : ''}. ` +
107
+ `Either adjust the rule's model or change maxModel/minModel settings.`
91
108
  );
92
109
  }
93
110
  }
111
+
112
+ if (rule.modelLevel && !VALID_LEVELS.includes(rule.modelLevel)) {
113
+ throw new Error(
114
+ `Agent "${config.id}": modelRule "${rule.iterations}" has invalid modelLevel ` +
115
+ `"${rule.modelLevel}". Valid: ${VALID_LEVELS.join(', ')}`
116
+ );
117
+ }
94
118
  }
95
119
  }
96
120
 
@@ -13,6 +13,44 @@
13
13
  // Prevents "Prompt is too long" errors that kill tasks
14
14
  const MAX_CONTEXT_CHARS = 500000;
15
15
 
16
+ /**
17
+ * Generate an example object from a JSON schema
18
+ * Used to show models a concrete example of expected output
19
+ *
20
+ * @param {object} schema - JSON schema
21
+ * @returns {object|null} Example object or null if generation fails
22
+ */
23
+ function generateExampleFromSchema(schema) {
24
+ if (!schema || schema.type !== 'object' || !schema.properties) {
25
+ return null;
26
+ }
27
+
28
+ const example = {};
29
+
30
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
31
+ if (propSchema.enum && propSchema.enum.length > 0) {
32
+ // Use first enum value as example
33
+ example[key] = propSchema.enum[0];
34
+ } else if (propSchema.type === 'string') {
35
+ example[key] = propSchema.description || `${key} value`;
36
+ } else if (propSchema.type === 'boolean') {
37
+ example[key] = true;
38
+ } else if (propSchema.type === 'number' || propSchema.type === 'integer') {
39
+ example[key] = 0;
40
+ } else if (propSchema.type === 'array') {
41
+ if (propSchema.items?.type === 'string') {
42
+ example[key] = [];
43
+ } else {
44
+ example[key] = [];
45
+ }
46
+ } else if (propSchema.type === 'object') {
47
+ example[key] = generateExampleFromSchema(propSchema) || {};
48
+ }
49
+ }
50
+
51
+ return example;
52
+ }
53
+
16
54
  /**
17
55
  * Build execution context for an agent
18
56
  * @param {object} params - Context building parameters
@@ -91,7 +129,8 @@ function buildContext({
91
129
  // Add prompt from config (system prompt, instructions, output format)
92
130
  // If selectedPrompt is provided (iteration-based), use it directly
93
131
  // Otherwise fall back to legacy config.prompt handling
94
- const promptText = selectedPrompt || (typeof config.prompt === 'string' ? config.prompt : config.prompt?.system);
132
+ const promptText =
133
+ selectedPrompt || (typeof config.prompt === 'string' ? config.prompt : config.prompt?.system);
95
134
 
96
135
  if (promptText) {
97
136
  context += `## Instructions\n\n${promptText}\n\n`;
@@ -103,7 +142,7 @@ function buildContext({
103
142
  );
104
143
  }
105
144
 
106
- // Output format schema (if configured)
145
+ // Output format schema (if configured via legacy format)
107
146
  if (config.prompt?.outputFormat) {
108
147
  context += `## Output Schema (REQUIRED)\n\n`;
109
148
  context += `\`\`\`json\n${JSON.stringify(config.prompt.outputFormat.example, null, 2)}\n\`\`\`\n\n`;
@@ -116,6 +155,29 @@ function buildContext({
116
155
  context += '\n';
117
156
  }
118
157
 
158
+ // AUTO-INJECT JSON OUTPUT INSTRUCTIONS when jsonSchema is defined
159
+ // This ensures ALL agents with structured output schemas get explicit "output ONLY JSON" instructions
160
+ // Critical for less capable models (Codex, Gemini) that output prose without explicit instructions
161
+ if (config.jsonSchema && config.outputFormat === 'json') {
162
+ context += `## 🔴 OUTPUT FORMAT - JSON ONLY\n\n`;
163
+ context += `Your response must be ONLY valid JSON. No other text before or after.\n`;
164
+ context += `Start with { and end with }. Nothing else.\n\n`;
165
+ context += `Required schema:\n`;
166
+ context += `\`\`\`json\n${JSON.stringify(config.jsonSchema, null, 2)}\n\`\`\`\n\n`;
167
+
168
+ // Generate example from schema
169
+ const example = generateExampleFromSchema(config.jsonSchema);
170
+ if (example) {
171
+ context += `Example output:\n`;
172
+ context += `\`\`\`json\n${JSON.stringify(example, null, 2)}\n\`\`\`\n\n`;
173
+ }
174
+
175
+ context += `CRITICAL RULES:\n`;
176
+ context += `- Output ONLY the JSON object - no explanation, no thinking, no preamble\n`;
177
+ context += `- Use EXACTLY the enum values specified (case-sensitive)\n`;
178
+ context += `- Include ALL required fields\n\n`;
179
+ }
180
+
119
181
  // Add sources
120
182
  for (const source of strategy.sources) {
121
183
  // Resolve special 'since' values
@@ -22,7 +22,7 @@ const vm = require('vm');
22
22
  * @param {Object} params.orchestrator - Orchestrator instance
23
23
  * @returns {Promise<void>}
24
24
  */
25
- function executeHook(params) {
25
+ async function executeHook(params) {
26
26
  const { hook, agent, message, result, cluster } = params;
27
27
 
28
28
  if (!hook) {
@@ -43,14 +43,14 @@ function executeHook(params) {
43
43
 
44
44
  if (hook.transform) {
45
45
  // NEW: Execute transform script to generate message
46
- messageToPublish = executeTransform({
46
+ messageToPublish = await executeTransform({
47
47
  transform: hook.transform,
48
48
  context,
49
49
  agent,
50
50
  });
51
51
  } else {
52
52
  // Existing: Use template substitution
53
- messageToPublish = substituteTemplate({
53
+ messageToPublish = await substituteTemplate({
54
54
  config: hook.config,
55
55
  context,
56
56
  agent,
@@ -77,9 +77,9 @@ function executeHook(params) {
77
77
  * @param {Object} params.transform - Transform configuration
78
78
  * @param {Object} params.context - Execution context
79
79
  * @param {Object} params.agent - Agent instance
80
- * @returns {Object} Message to publish
80
+ * @returns {Promise<Object>} Message to publish
81
81
  */
82
- function executeTransform(params) {
82
+ async function executeTransform(params) {
83
83
  const { transform, context, agent } = params;
84
84
  const { engine, script } = transform;
85
85
 
@@ -93,7 +93,46 @@ function executeTransform(params) {
93
93
  let resultData = null;
94
94
 
95
95
  if (context.result?.output) {
96
- resultData = agent._parseResultOutput(context.result.output);
96
+ try {
97
+ resultData = await agent._parseResultOutput(context.result.output);
98
+ } catch (parseError) {
99
+ // FAIL FAST: Result parsing failed - don't continue with null data
100
+ const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
101
+ console.error(`\n${'='.repeat(80)}`);
102
+ console.error(`🔴 TRANSFORM SCRIPT BLOCKED - RESULT PARSING FAILED`);
103
+ console.error(`${'='.repeat(80)}`);
104
+ console.error(`Agent: ${agent.id}, Role: ${agent.role}`);
105
+ console.error(`TaskID: ${taskId}`);
106
+ console.error(`Parse error: ${parseError.message}`);
107
+ console.error(`Output (last 500 chars): ${(context.result.output || '').slice(-500)}`);
108
+ console.error(`${'='.repeat(80)}\n`);
109
+ throw new Error(
110
+ `Transform script cannot run: result parsing failed. ` +
111
+ `Agent: ${agent.id}, Error: ${parseError.message}`
112
+ );
113
+ }
114
+
115
+ // DEFENSIVE: Validate result has expected fields if script accesses them
116
+ // Extract field names from script (e.g., result.complexity, result.taskType)
117
+ const accessedFields = [...script.matchAll(/result\.([a-zA-Z_]+)/g)].map((m) => m[1]);
118
+ const missingFields = accessedFields.filter((f) => resultData[f] === undefined);
119
+ if (missingFields.length > 0) {
120
+ const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
121
+ console.error(`\n${'='.repeat(80)}`);
122
+ console.error(`🔴 TRANSFORM SCRIPT BLOCKED - MISSING REQUIRED FIELDS`);
123
+ console.error(`${'='.repeat(80)}`);
124
+ console.error(`Agent: ${agent.id}, Role: ${agent.role}, TaskID: ${taskId}`);
125
+ console.error(`Script accesses: ${accessedFields.join(', ')}`);
126
+ console.error(`Missing from result: ${missingFields.join(', ')}`);
127
+ console.error(`Result keys: ${Object.keys(resultData).join(', ')}`);
128
+ console.error(`Result data: ${JSON.stringify(resultData, null, 2)}`);
129
+ console.error(`${'='.repeat(80)}\n`);
130
+ throw new Error(
131
+ `Transform script accesses undefined fields: ${missingFields.join(', ')}. ` +
132
+ `Agent ${agent.id} (task ${taskId}) output missing required fields. ` +
133
+ `Check agent's jsonSchema and output format.`
134
+ );
135
+ }
97
136
  } else if (scriptUsesResult) {
98
137
  const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
99
138
  const outputLength = (context.result?.output || '').length;
@@ -151,6 +190,40 @@ function executeTransform(params) {
151
190
  throw new Error(`Transform script result must have a 'content' property`);
152
191
  }
153
192
 
193
+ // CRITICAL: Extra validation for CLUSTER_OPERATIONS - this is the make-or-break message
194
+ // If this message is malformed, the cluster will hang forever
195
+ if (result.topic === 'CLUSTER_OPERATIONS') {
196
+ const operations = result.content?.data?.operations;
197
+ if (!operations) {
198
+ console.error(`\n${'='.repeat(80)}`);
199
+ console.error(`🔴 CLUSTER_OPERATIONS MALFORMED - MISSING OPERATIONS ARRAY`);
200
+ console.error(`${'='.repeat(80)}`);
201
+ console.error(`Agent: ${agent.id}`);
202
+ console.error(`Result: ${JSON.stringify(result, null, 2)}`);
203
+ console.error(`${'='.repeat(80)}\n`);
204
+ throw new Error(
205
+ `CLUSTER_OPERATIONS message missing operations array. ` +
206
+ `Agent ${agent.id} transform script returned invalid structure.`
207
+ );
208
+ }
209
+ if (!Array.isArray(operations)) {
210
+ throw new Error(`CLUSTER_OPERATIONS.operations must be an array, got: ${typeof operations}`);
211
+ }
212
+ if (operations.length === 0) {
213
+ throw new Error(`CLUSTER_OPERATIONS.operations is empty - no operations to execute`);
214
+ }
215
+
216
+ // Validate each operation has required 'action' field
217
+ for (let i = 0; i < operations.length; i++) {
218
+ const op = operations[i];
219
+ if (!op || !op.action) {
220
+ throw new Error(`CLUSTER_OPERATIONS.operations[${i}] missing required 'action' field`);
221
+ }
222
+ }
223
+
224
+ agent._log(`✅ CLUSTER_OPERATIONS validated: ${operations.length} operations`);
225
+ }
226
+
154
227
  return result;
155
228
  }
156
229
 
@@ -163,9 +236,9 @@ function executeTransform(params) {
163
236
  * @param {Object} params.context - Execution context
164
237
  * @param {Object} params.agent - Agent instance
165
238
  * @param {Object} params.cluster - Cluster object
166
- * @returns {Object} Substituted configuration
239
+ * @returns {Promise<Object>} Substituted configuration
167
240
  */
168
- function substituteTemplate(params) {
241
+ async function substituteTemplate(params) {
169
242
  const { config, context, agent, cluster } = params;
170
243
 
171
244
  if (!config) {
@@ -243,7 +316,7 @@ function substituteTemplate(params) {
243
316
  );
244
317
  }
245
318
  // Parse result output - WILL THROW if no JSON block
246
- resultData = agent._parseResultOutput(context.result.output);
319
+ resultData = await agent._parseResultOutput(context.result.output);
247
320
  }
248
321
 
249
322
  // Helper to escape a value for JSON string substitution
@@ -281,7 +281,7 @@ async function executeTask(agent, triggeringMessage) {
281
281
  console.log(`${'='.repeat(80)}\n`);
282
282
  }
283
283
 
284
- // Spawn claude-zeroshots
284
+ // Spawn provider task
285
285
  agent.state = 'executing_task';
286
286
 
287
287
  // LOCK CONTENTION FIX: Add random jitter for validators to prevent thundering herd
@@ -292,14 +292,19 @@ async function executeTask(agent, triggeringMessage) {
292
292
  if (agent.role === 'validator' && !agent.testMode) {
293
293
  const jitterMs = Math.floor(Math.random() * 15000); // 0-15 seconds
294
294
  if (!agent.quiet) {
295
- agent._log(`[Agent ${agent.id}] Adding ${Math.round(jitterMs / 1000)}s jitter to prevent lock contention`);
295
+ agent._log(
296
+ `[Agent ${agent.id}] Adding ${Math.round(jitterMs / 1000)}s jitter to prevent lock contention`
297
+ );
296
298
  }
297
299
  await new Promise((resolve) => setTimeout(resolve, jitterMs));
298
300
  }
299
301
 
302
+ const modelSpec = agent._resolveModelSpec ? agent._resolveModelSpec() : null;
300
303
  agent._publishLifecycle('TASK_STARTED', {
301
304
  iteration: agent.iteration,
302
305
  model: agent._selectModel(),
306
+ provider: agent._resolveProvider ? agent._resolveProvider() : 'claude',
307
+ modelSpec,
303
308
  triggeredBy: triggeringMessage.topic,
304
309
  triggerFrom: triggeringMessage.sender,
305
310
  });
@@ -352,16 +357,48 @@ async function executeTask(agent, triggeringMessage) {
352
357
  });
353
358
  }
354
359
 
355
- // Execute onComplete hook
356
- await executeHook({
357
- hook: agent.config.hooks?.onComplete,
358
- agent: agent,
359
- message: triggeringMessage,
360
- result: result,
361
- messageBus: agent.messageBus,
362
- cluster: agent.cluster,
363
- orchestrator: agent.orchestrator,
364
- });
360
+ // Execute onComplete hook WITH RETRY
361
+ // Hook failure shouldn't retry the entire task - just the hook
362
+ const hookMaxRetries = 3;
363
+ const hookBaseDelay = 1000;
364
+ let hookSuccess = false;
365
+
366
+ for (let hookAttempt = 1; hookAttempt <= hookMaxRetries && !hookSuccess; hookAttempt++) {
367
+ try {
368
+ await executeHook({
369
+ hook: agent.config.hooks?.onComplete,
370
+ agent: agent,
371
+ message: triggeringMessage,
372
+ result: result,
373
+ messageBus: agent.messageBus,
374
+ cluster: agent.cluster,
375
+ orchestrator: agent.orchestrator,
376
+ });
377
+ hookSuccess = true;
378
+ } catch (hookError) {
379
+ console.error(`\n${'='.repeat(80)}`);
380
+ console.error(
381
+ `🔴 HOOK EXECUTION FAILED - AGENT: ${agent.id} (Attempt ${hookAttempt}/${hookMaxRetries})`
382
+ );
383
+ console.error(`${'='.repeat(80)}`);
384
+ console.error(`Error: ${hookError.message}`);
385
+
386
+ if (hookAttempt < hookMaxRetries) {
387
+ const delay = hookBaseDelay * Math.pow(2, hookAttempt - 1);
388
+ console.error(`Will retry hook in ${delay}ms...`);
389
+ console.error(`${'='.repeat(80)}\n`);
390
+ await new Promise((resolve) => setTimeout(resolve, delay));
391
+ } else {
392
+ console.error(`${'='.repeat(80)}\n`);
393
+ // All hook retries exhausted - throw to trigger task-level handling
394
+ throw new Error(
395
+ `Hook execution failed after ${hookMaxRetries} attempts. ` +
396
+ `Task completed successfully but hook could not publish result. ` +
397
+ `Original error: ${hookError.message}`
398
+ );
399
+ }
400
+ }
401
+ }
365
402
 
366
403
  // ✅ SUCCESS - exit retry loop
367
404
  return;
@@ -381,8 +418,10 @@ async function executeTask(agent, triggeringMessage) {
381
418
  if (isLockError) {
382
419
  // Lock contention - add significant jittered delay
383
420
  const lockDelay = 10000 + Math.floor(Math.random() * 20000); // 10-30 seconds
384
- console.error(`⚠️ Lock contention detected - waiting ${Math.round(lockDelay / 1000)}s before retry`);
385
- await new Promise(resolve => setTimeout(resolve, lockDelay));
421
+ console.error(
422
+ `⚠️ Lock contention detected - waiting ${Math.round(lockDelay / 1000)}s before retry`
423
+ );
424
+ await new Promise((resolve) => setTimeout(resolve, lockDelay));
386
425
  } else if (attempt < maxRetries) {
387
426
  console.error(`Will retry in ${baseDelay * Math.pow(2, attempt - 1)}ms...`);
388
427
  }