@covibes/zeroshot 5.3.0 → 5.4.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 (82) hide show
  1. package/README.md +94 -14
  2. package/cli/commands/providers.js +8 -9
  3. package/cli/index.js +3032 -2409
  4. package/cli/message-formatters-normal.js +28 -6
  5. package/cluster-templates/base-templates/debug-workflow.json +1 -1
  6. package/cluster-templates/base-templates/full-workflow.json +72 -188
  7. package/cluster-templates/base-templates/worker-validator.json +59 -3
  8. package/cluster-templates/conductor-bootstrap.json +4 -4
  9. package/lib/docker-config.js +8 -0
  10. package/lib/git-remote-utils.js +165 -0
  11. package/lib/id-detector.js +10 -7
  12. package/lib/provider-defaults.js +62 -0
  13. package/lib/provider-names.js +2 -1
  14. package/lib/settings/claude-auth.js +78 -0
  15. package/lib/settings.js +161 -63
  16. package/package.json +7 -2
  17. package/scripts/setup-merge-queue.sh +170 -0
  18. package/src/agent/agent-config.js +135 -82
  19. package/src/agent/agent-context-builder.js +297 -188
  20. package/src/agent/agent-hook-executor.js +310 -113
  21. package/src/agent/agent-lifecycle.js +385 -325
  22. package/src/agent/agent-stuck-detector.js +7 -7
  23. package/src/agent/agent-task-executor.js +824 -565
  24. package/src/agent/output-extraction.js +41 -24
  25. package/src/agent/output-reformatter.js +1 -1
  26. package/src/agent/schema-utils.js +108 -73
  27. package/src/agent-wrapper.js +10 -1
  28. package/src/agents/git-pusher-template.js +285 -0
  29. package/src/claude-task-runner.js +85 -34
  30. package/src/config-validator.js +922 -657
  31. package/src/input-helpers.js +65 -0
  32. package/src/isolation-manager.js +289 -199
  33. package/src/issue-providers/README.md +305 -0
  34. package/src/issue-providers/azure-devops-provider.js +273 -0
  35. package/src/issue-providers/base-provider.js +232 -0
  36. package/src/issue-providers/github-provider.js +179 -0
  37. package/src/issue-providers/gitlab-provider.js +241 -0
  38. package/src/issue-providers/index.js +196 -0
  39. package/src/issue-providers/jira-provider.js +239 -0
  40. package/src/ledger.js +22 -5
  41. package/src/lib/safe-exec.js +88 -0
  42. package/src/orchestrator.js +1107 -811
  43. package/src/preflight.js +313 -159
  44. package/src/process-metrics.js +98 -56
  45. package/src/providers/anthropic/cli-builder.js +53 -25
  46. package/src/providers/anthropic/index.js +72 -2
  47. package/src/providers/anthropic/output-parser.js +32 -14
  48. package/src/providers/base-provider.js +70 -0
  49. package/src/providers/capabilities.js +9 -0
  50. package/src/providers/google/output-parser.js +47 -38
  51. package/src/providers/index.js +19 -3
  52. package/src/providers/openai/cli-builder.js +14 -3
  53. package/src/providers/openai/index.js +1 -0
  54. package/src/providers/openai/output-parser.js +44 -30
  55. package/src/providers/opencode/cli-builder.js +42 -0
  56. package/src/providers/opencode/index.js +103 -0
  57. package/src/providers/opencode/models.js +55 -0
  58. package/src/providers/opencode/output-parser.js +122 -0
  59. package/src/schemas/sub-cluster.js +68 -39
  60. package/src/status-footer.js +94 -48
  61. package/src/sub-cluster-wrapper.js +76 -35
  62. package/src/template-resolver.js +12 -9
  63. package/src/tui/data-poller.js +123 -99
  64. package/src/tui/formatters.js +4 -3
  65. package/src/tui/keybindings.js +259 -318
  66. package/src/tui/renderer.js +11 -21
  67. package/task-lib/attachable-watcher.js +118 -81
  68. package/task-lib/claude-recovery.js +61 -24
  69. package/task-lib/commands/episodes.js +105 -0
  70. package/task-lib/commands/list.js +2 -2
  71. package/task-lib/commands/logs.js +231 -189
  72. package/task-lib/commands/schedules.js +111 -61
  73. package/task-lib/config.js +0 -2
  74. package/task-lib/runner.js +84 -27
  75. package/task-lib/scheduler.js +1 -1
  76. package/task-lib/store.js +467 -168
  77. package/task-lib/tui/formatters.js +94 -45
  78. package/task-lib/tui/renderer.js +13 -3
  79. package/task-lib/tui.js +73 -32
  80. package/task-lib/watcher.js +128 -90
  81. package/src/agents/git-pusher-agent.json +0 -20
  82. package/src/github.js +0 -139
@@ -12,7 +12,6 @@
12
12
  * State machine: idle → evaluating → building_context → executing → idle
13
13
  */
14
14
 
15
- const { buildContext } = require('./agent-context-builder');
16
15
  const { findMatchingTrigger, evaluateTrigger } = require('./agent-trigger-evaluator');
17
16
  const { executeHook } = require('./agent-hook-executor');
18
17
  const {
@@ -200,377 +199,438 @@ async function executeTriggerAction(agent, trigger, message) {
200
199
  * @param {AgentWrapper} agent - Agent instance
201
200
  * @param {Object} triggeringMessage - Message that triggered execution
202
201
  */
203
- async function executeTask(agent, triggeringMessage) {
204
- // Early exit if agent was stopped
205
- if (!agent.running) {
202
+ function handleMaxIterations(agent) {
203
+ if (agent.iteration < agent.maxIterations) {
204
+ return false;
205
+ }
206
+
207
+ agent._log(`[Agent ${agent.id}] Hit max iterations (${agent.maxIterations}), stopping cluster`);
208
+ agent._publishLifecycle('MAX_ITERATIONS_REACHED', {
209
+ iteration: agent.iteration,
210
+ maxIterations: agent.maxIterations,
211
+ });
212
+ // Publish failure message - orchestrator watches for this and auto-stops
213
+ agent._publish({
214
+ topic: 'CLUSTER_FAILED',
215
+ receiver: 'system',
216
+ content: {
217
+ text: `Agent ${agent.id} hit max iterations limit (${agent.maxIterations}). Stopping cluster.`,
218
+ data: {
219
+ reason: 'max_iterations',
220
+ iteration: agent.iteration,
221
+ maxIterations: agent.maxIterations,
222
+ },
223
+ },
224
+ });
225
+ agent.state = 'failed';
226
+ return true;
227
+ }
228
+
229
+ function logInputContext(agent, context) {
230
+ if (agent.quiet) {
206
231
  return;
207
232
  }
208
233
 
209
- // Default: no retries (maxRetries=1 means 1 attempt only)
210
- // Set agent config `maxRetries: 3` to enable exponential backoff retries
211
- const maxRetries = agent.config.maxRetries ?? 1;
212
- const baseDelay = 2000; // 2 seconds
234
+ console.log(`
235
+ ${'='.repeat(80)}`);
236
+ console.log(`📥 INPUT CONTEXT - Agent: ${agent.id} (Iteration: ${agent.iteration})`);
237
+ console.log(`${'='.repeat(80)}`);
238
+ console.log(context);
239
+ console.log(`${'='.repeat(80)}
240
+ `);
241
+ }
213
242
 
214
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
215
- // Check if agent was stopped between retries
216
- if (!agent.running) {
217
- return;
218
- }
243
+ async function applyValidatorJitter(agent) {
244
+ // LOCK CONTENTION FIX: Add random jitter for validators to prevent thundering herd
245
+ // When multiple validators wake on the same trigger (e.g., IMPLEMENTATION_READY),
246
+ // they all try to spawn Claude CLI at the same time. Claude CLI uses a lock file
247
+ // per workspace, so only one can run. Adding jitter staggers their starts.
248
+ // SKIP in testMode - tests use mocks and don't need jitter
249
+ if (agent.role !== 'validator' || agent.testMode) {
250
+ return;
251
+ }
252
+
253
+ const jitterMs = Math.floor(Math.random() * 15000); // 0-15 seconds
254
+ if (!agent.quiet) {
255
+ agent._log(
256
+ `[Agent ${agent.id}] Adding ${Math.round(jitterMs / 1000)}s jitter to prevent lock contention`
257
+ );
258
+ }
259
+ await new Promise((resolve) => setTimeout(resolve, jitterMs));
260
+ }
261
+
262
+ function publishTaskStarted(agent, triggeringMessage) {
263
+ const modelSpec = agent._resolveModelSpec ? agent._resolveModelSpec() : null;
264
+ agent._publishLifecycle('TASK_STARTED', {
265
+ iteration: agent.iteration,
266
+ model: agent._selectModel(),
267
+ provider: agent._resolveProvider ? agent._resolveProvider() : 'claude',
268
+ modelSpec,
269
+ triggeredBy: triggeringMessage.topic,
270
+ triggerFrom: triggeringMessage.sender,
271
+ });
272
+ }
273
+
274
+ function attachResultMetadata(agent, result) {
275
+ // Add task ID to result for debugging and hooks
276
+ result.taskId = agent.currentTaskId;
277
+ result.agentId = agent.id;
278
+ result.iteration = agent.iteration;
279
+ }
280
+
281
+ function publishTaskCompleted(agent, result) {
282
+ agent._publishLifecycle('TASK_COMPLETED', {
283
+ iteration: agent.iteration,
284
+ success: true,
285
+ taskId: agent.currentTaskId,
286
+ tokenUsage: result.tokenUsage || null,
287
+ });
288
+ }
289
+
290
+ function publishTokenUsage(agent, result) {
291
+ // Publish TOKEN_USAGE event for aggregation and tracking
292
+ // CRITICAL: Include taskId for causal linking - allows consumers to group
293
+ // messages by task regardless of interleaved timing from async hooks
294
+ if (!result.tokenUsage) {
295
+ return;
296
+ }
219
297
 
298
+ agent.messageBus.publish({
299
+ cluster_id: agent.cluster.id,
300
+ topic: 'TOKEN_USAGE',
301
+ sender: agent.id,
302
+ content: {
303
+ text: `${agent.id} used ${result.tokenUsage.inputTokens} input + ${result.tokenUsage.outputTokens} output tokens`,
304
+ data: {
305
+ agentId: agent.id,
306
+ role: agent.role,
307
+ model: agent._selectModel(),
308
+ iteration: agent.iteration,
309
+ taskId: agent.currentTaskId, // Causal linking for message ordering
310
+ ...result.tokenUsage,
311
+ },
312
+ },
313
+ });
314
+ }
315
+
316
+ async function executeOnCompleteHookWithRetry(agent, triggeringMessage, result) {
317
+ // Execute onComplete hook WITH RETRY
318
+ // Hook failure shouldn't retry the entire task - just the hook
319
+ const hookMaxRetries = 3;
320
+ const hookBaseDelay = 1000;
321
+ let hookSuccess = false;
322
+
323
+ for (let hookAttempt = 1; hookAttempt <= hookMaxRetries && !hookSuccess; hookAttempt++) {
220
324
  try {
221
- // Execute onStart hook
222
325
  await executeHook({
223
- hook: agent.config.hooks?.onStart,
326
+ hook: agent.config.hooks?.onComplete,
224
327
  agent: agent,
225
328
  message: triggeringMessage,
226
- result: undefined,
329
+ result: result,
227
330
  messageBus: agent.messageBus,
228
331
  cluster: agent.cluster,
229
332
  orchestrator: agent.orchestrator,
230
333
  });
231
-
232
- // Check max iterations limit BEFORE incrementing (prevents infinite rejection loops)
233
- if (agent.iteration >= agent.maxIterations) {
234
- agent._log(
235
- `[Agent ${agent.id}] Hit max iterations (${agent.maxIterations}), stopping cluster`
334
+ hookSuccess = true;
335
+ } catch (hookError) {
336
+ console.error(`
337
+ ${'='.repeat(80)}`);
338
+ console.error(
339
+ `🔴 HOOK EXECUTION FAILED - AGENT: ${agent.id} (Attempt ${hookAttempt}/${hookMaxRetries})`
340
+ );
341
+ console.error(`${'='.repeat(80)}`);
342
+ console.error(`Error: ${hookError.message}`);
343
+
344
+ if (hookAttempt < hookMaxRetries) {
345
+ const delay = hookBaseDelay * Math.pow(2, hookAttempt - 1);
346
+ console.error(`Will retry hook in ${delay}ms...`);
347
+ console.error(`${'='.repeat(80)}
348
+ `);
349
+ await new Promise((resolve) => setTimeout(resolve, delay));
350
+ } else {
351
+ console.error(`${'='.repeat(80)}
352
+ `);
353
+ // All hook retries exhausted - throw to trigger task-level handling
354
+ throw new Error(
355
+ `Hook execution failed after ${hookMaxRetries} attempts. ` +
356
+ `Task completed successfully but hook could not publish result. ` +
357
+ `Original error: ${hookError.message}`
236
358
  );
237
- agent._publishLifecycle('MAX_ITERATIONS_REACHED', {
238
- iteration: agent.iteration,
239
- maxIterations: agent.maxIterations,
240
- });
241
- // Publish failure message - orchestrator watches for this and auto-stops
242
- agent._publish({
243
- topic: 'CLUSTER_FAILED',
244
- receiver: 'system',
245
- content: {
246
- text: `Agent ${agent.id} hit max iterations limit (${agent.maxIterations}). Stopping cluster.`,
247
- data: {
248
- reason: 'max_iterations',
249
- iteration: agent.iteration,
250
- maxIterations: agent.maxIterations,
251
- },
252
- },
253
- });
254
- agent.state = 'failed';
255
- return;
256
359
  }
360
+ }
361
+ }
362
+ }
257
363
 
258
- // Increment iteration BEFORE building context so worker knows current iteration
259
- agent.iteration++;
364
+ async function runTaskAttempt(agent, triggeringMessage) {
365
+ // Execute onStart hook
366
+ await executeHook({
367
+ hook: agent.config.hooks?.onStart,
368
+ agent: agent,
369
+ message: triggeringMessage,
370
+ result: undefined,
371
+ messageBus: agent.messageBus,
372
+ cluster: agent.cluster,
373
+ orchestrator: agent.orchestrator,
374
+ });
260
375
 
261
- // Build context
262
- agent.state = 'building_context';
263
- const context = buildContext({
264
- id: agent.id,
265
- role: agent.role,
266
- iteration: agent.iteration,
267
- config: agent.config,
268
- messageBus: agent.messageBus,
269
- cluster: agent.cluster,
270
- lastTaskEndTime: agent.lastTaskEndTime,
271
- triggeringMessage,
272
- selectedPrompt: agent._selectPrompt(),
273
- });
376
+ // Check max iterations limit BEFORE incrementing (prevents infinite rejection loops)
377
+ if (handleMaxIterations(agent)) {
378
+ return;
379
+ }
274
380
 
275
- // Log input context (helps debug what each agent sees)
276
- if (!agent.quiet) {
277
- console.log(`\n${'='.repeat(80)}`);
278
- console.log(`📥 INPUT CONTEXT - Agent: ${agent.id} (Iteration: ${agent.iteration})`);
279
- console.log(`${'='.repeat(80)}`);
280
- console.log(context);
281
- console.log(`${'='.repeat(80)}\n`);
282
- }
381
+ // Increment iteration BEFORE building context so worker knows current iteration
382
+ agent.iteration++;
283
383
 
284
- // Spawn provider task
285
- agent.state = 'executing_task';
286
-
287
- // LOCK CONTENTION FIX: Add random jitter for validators to prevent thundering herd
288
- // When multiple validators wake on the same trigger (e.g., IMPLEMENTATION_READY),
289
- // they all try to spawn Claude CLI at the same time. Claude CLI uses a lock file
290
- // per workspace, so only one can run. Adding jitter staggers their starts.
291
- // SKIP in testMode - tests use mocks and don't need jitter
292
- if (agent.role === 'validator' && !agent.testMode) {
293
- const jitterMs = Math.floor(Math.random() * 15000); // 0-15 seconds
294
- if (!agent.quiet) {
295
- agent._log(
296
- `[Agent ${agent.id}] Adding ${Math.round(jitterMs / 1000)}s jitter to prevent lock contention`
297
- );
298
- }
299
- await new Promise((resolve) => setTimeout(resolve, jitterMs));
300
- }
384
+ // Build context
385
+ agent.state = 'building_context';
386
+ const context = agent._buildContext(triggeringMessage);
301
387
 
302
- const modelSpec = agent._resolveModelSpec ? agent._resolveModelSpec() : null;
303
- agent._publishLifecycle('TASK_STARTED', {
304
- iteration: agent.iteration,
305
- model: agent._selectModel(),
306
- provider: agent._resolveProvider ? agent._resolveProvider() : 'claude',
307
- modelSpec,
308
- triggeredBy: triggeringMessage.topic,
309
- triggerFrom: triggeringMessage.sender,
310
- });
388
+ // Log input context (helps debug what each agent sees)
389
+ logInputContext(agent, context);
311
390
 
312
- const result = await agent._spawnClaudeTask(context);
391
+ // Spawn provider task
392
+ agent.state = 'executing_task';
393
+ await applyValidatorJitter(agent);
394
+ publishTaskStarted(agent, triggeringMessage);
313
395
 
314
- // Add task ID to result for debugging and hooks
315
- result.taskId = agent.currentTaskId;
316
- result.agentId = agent.id;
317
- result.iteration = agent.iteration;
396
+ const result = await agent._spawnClaudeTask(context);
397
+ attachResultMetadata(agent, result);
318
398
 
319
- // Check if task execution failed
320
- if (!result.success) {
321
- throw new Error(result.error || 'Task execution failed');
322
- }
399
+ // Check if task execution failed
400
+ if (!result.success) {
401
+ throw new Error(result.error || 'Task execution failed');
402
+ }
323
403
 
324
- // Set state to idle BEFORE publishing lifecycle event
325
- // (so lifecycle message includes correct state)
326
- agent.state = 'idle';
404
+ // Set state to idle BEFORE publishing lifecycle event
405
+ // (so lifecycle message includes correct state)
406
+ agent.state = 'idle';
327
407
 
328
- // Track completion time for context filtering (used by "since: last_task_end")
329
- agent.lastTaskEndTime = Date.now();
408
+ // Track completion time for context filtering (used by "since: last_task_end")
409
+ agent.lastTaskEndTime = Date.now();
330
410
 
331
- agent._publishLifecycle('TASK_COMPLETED', {
332
- iteration: agent.iteration,
333
- success: true,
334
- taskId: agent.currentTaskId,
335
- tokenUsage: result.tokenUsage || null,
336
- });
411
+ publishTaskCompleted(agent, result);
412
+ publishTokenUsage(agent, result);
413
+ await executeOnCompleteHookWithRetry(agent, triggeringMessage, result);
414
+ }
415
+
416
+ function logTaskAttemptFailure(agent, attempt, maxRetries, error) {
417
+ // Log attempt failure
418
+ console.error(`
419
+ ${'='.repeat(80)}`);
420
+ console.error(`🔴 TASK EXECUTION FAILED - AGENT: ${agent.id} (Attempt ${attempt}/${maxRetries})`);
421
+ console.error(`${'='.repeat(80)}`);
422
+ console.error(`Error: ${error.message}`);
423
+ }
337
424
 
338
- // Publish TOKEN_USAGE event for aggregation and tracking
339
- // CRITICAL: Include taskId for causal linking - allows consumers to group
340
- // messages by task regardless of interleaved timing from async hooks
341
- if (result.tokenUsage) {
342
- agent.messageBus.publish({
343
- cluster_id: agent.cluster.id,
344
- topic: 'TOKEN_USAGE',
345
- sender: agent.id,
346
- content: {
347
- text: `${agent.id} used ${result.tokenUsage.inputTokens} input + ${result.tokenUsage.outputTokens} output tokens`,
348
- data: {
349
- agentId: agent.id,
350
- role: agent.role,
351
- model: agent._selectModel(),
352
- iteration: agent.iteration,
353
- taskId: agent.currentTaskId, // Causal linking for message ordering
354
- ...result.tokenUsage,
355
- },
425
+ async function handleLockContention() {
426
+ // Lock contention - add significant jittered delay
427
+ const lockDelay = 10000 + Math.floor(Math.random() * 20000); // 10-30 seconds
428
+ console.error(
429
+ `⚠️ Lock contention detected - waiting ${Math.round(lockDelay / 1000)}s before retry`
430
+ );
431
+ await new Promise((resolve) => setTimeout(resolve, lockDelay));
432
+ }
433
+
434
+ async function handleFinalFailure(agent, triggeringMessage, error, maxRetries) {
435
+ console.error(`
436
+ ${'='.repeat(80)}`);
437
+ console.error(`🔴🔴🔴 MAX RETRIES EXHAUSTED - AGENT: ${agent.id} 🔴🔴🔴`);
438
+ console.error(`${'='.repeat(80)}`);
439
+ console.error(`All ${maxRetries} attempts failed`);
440
+ console.error(`Final error: ${error.message}`);
441
+ console.error(`Stack: ${error.stack}`);
442
+ console.error(`${'='.repeat(80)}
443
+ `);
444
+
445
+ // CRITICAL FIX: Validator crash = REJECTION (not auto-approval)
446
+ // Auto-approval on crash allowed broken code to be merged - unacceptable!
447
+ // If validator crashed 3x, something is fundamentally wrong - REJECT and investigate
448
+ if (agent.role === 'validator') {
449
+ console.error(`
450
+ ${'='.repeat(80)}`);
451
+ console.error(`❌ VALIDATOR CRASHED - REJECTING (NOT AUTO-APPROVING)`);
452
+ console.error(`${'='.repeat(80)}`);
453
+ console.error(`Validator ${agent.id} crashed ${maxRetries} times`);
454
+ console.error(`Error: ${error.message}`);
455
+ console.error(`REJECTING validation - broken code will NOT be merged`);
456
+ console.error(`Investigation required before retry`);
457
+ console.error(`${'='.repeat(80)}
458
+ `);
459
+
460
+ // Publish REJECTION message (NOT approval!)
461
+ const hook = agent.config.hooks?.onComplete;
462
+ if (hook && hook.action === 'publish_message') {
463
+ agent._publish({
464
+ topic: hook.config.topic,
465
+ receiver: hook.config.receiver || 'broadcast',
466
+ content: {
467
+ text: `REJECTED: Validator crashed ${maxRetries} times - ${error.message}`,
468
+ data: {
469
+ approved: false, // REJECT!
470
+ crashedAfterRetries: true,
471
+ errors: JSON.stringify([
472
+ `VALIDATOR CRASHED ${maxRetries}x: ${error.message}`,
473
+ `Validation could not be performed - REJECTING to prevent broken code merge`,
474
+ `Investigation required before retry`,
475
+ ]),
476
+ attempts: maxRetries,
477
+ requiresInvestigation: true,
356
478
  },
357
- });
358
- }
479
+ },
480
+ });
481
+ }
359
482
 
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
- }
483
+ agent.state = 'error';
484
+ // Don't return - fall through to publish AGENT_ERROR and save failure info
485
+ // This allows the cluster to stop and be resumed after investigation
486
+ }
402
487
 
403
- // SUCCESS - exit retry loop
404
- return;
405
- } catch (error) {
406
- // LOCK CONTENTION: Add extra jittered delay for lock file errors
407
- // This happens when multiple validators try to run Claude CLI in the same workspace
408
- const isLockError = error.message && error.message.includes('Lock file');
488
+ // Non-validator agents: publish error and stop
489
+ agent.state = 'error';
409
490
 
410
- // Log attempt failure
411
- console.error(`\n${'='.repeat(80)}`);
412
- console.error(
413
- `🔴 TASK EXECUTION FAILED - AGENT: ${agent.id} (Attempt ${attempt}/${maxRetries})`
414
- );
415
- console.error(`${'='.repeat(80)}`);
416
- console.error(`Error: ${error.message}`);
491
+ // Save failure info to cluster for resume capability
492
+ agent.cluster.failureInfo = {
493
+ agentId: agent.id,
494
+ taskId: agent.currentTaskId,
495
+ iteration: agent.iteration,
496
+ error: error.message,
497
+ attempts: maxRetries,
498
+ timestamp: Date.now(),
499
+ };
417
500
 
418
- if (isLockError) {
419
- // Lock contention - add significant jittered delay
420
- const lockDelay = 10000 + Math.floor(Math.random() * 20000); // 10-30 seconds
421
- console.error(
422
- `⚠️ Lock contention detected - waiting ${Math.round(lockDelay / 1000)}s before retry`
423
- );
424
- await new Promise((resolve) => setTimeout(resolve, lockDelay));
425
- } else if (attempt < maxRetries) {
426
- console.error(`Will retry in ${baseDelay * Math.pow(2, attempt - 1)}ms...`);
427
- }
428
- console.error(`${'='.repeat(80)}\n`);
501
+ // Publish error to message bus for visibility in logs
502
+ agent._publish({
503
+ topic: 'AGENT_ERROR',
504
+ receiver: 'broadcast',
505
+ content: {
506
+ text: `Task execution failed after ${maxRetries} attempts: ${error.message}`,
507
+ data: {
508
+ error: error.message,
509
+ stack: error.stack,
510
+ agent: agent.id,
511
+ role: agent.role,
512
+ iteration: agent.iteration,
513
+ taskId: agent.currentTaskId,
514
+ attempts: maxRetries,
515
+ hookFailureContext: error.message.includes('Hook uses result')
516
+ ? {
517
+ taskId: agent.currentTaskId || 'UNKNOWN',
518
+ retrieveLogs: agent.currentTaskId
519
+ ? `zeroshot task logs ${agent.currentTaskId}`
520
+ : 'N/A',
521
+ }
522
+ : undefined,
523
+ },
524
+ },
525
+ metadata: {
526
+ triggeringTopic: triggeringMessage.topic,
527
+ },
528
+ });
429
529
 
430
- // Last attempt - give up
431
- if (attempt >= maxRetries) {
432
- console.error(`\n${'='.repeat(80)}`);
433
- console.error(`🔴🔴🔴 MAX RETRIES EXHAUSTED - AGENT: ${agent.id} 🔴🔴🔴`);
434
- console.error(`${'='.repeat(80)}`);
435
- console.error(`All ${maxRetries} attempts failed`);
436
- console.error(`Final error: ${error.message}`);
437
- console.error(`Stack: ${error.stack}`);
438
- console.error(`${'='.repeat(80)}\n`);
530
+ // Execute onError hook
531
+ await executeHook({
532
+ hook: agent.config.hooks?.onError,
533
+ agent: agent,
534
+ message: triggeringMessage,
535
+ result: { error },
536
+ messageBus: agent.messageBus,
537
+ cluster: agent.cluster,
538
+ orchestrator: agent.orchestrator,
539
+ });
439
540
 
440
- // CRITICAL FIX: Validator crash = REJECTION (not auto-approval)
441
- // Auto-approval on crash allowed broken code to be merged - unacceptable!
442
- // If validator crashed 3x, something is fundamentally wrong - REJECT and investigate
443
- if (agent.role === 'validator') {
444
- console.error(`\n${'='.repeat(80)}`);
445
- console.error(`❌ VALIDATOR CRASHED - REJECTING (NOT AUTO-APPROVING)`);
446
- console.error(`${'='.repeat(80)}`);
447
- console.error(`Validator ${agent.id} crashed ${maxRetries} times`);
448
- console.error(`Error: ${error.message}`);
449
- console.error(`REJECTING validation - broken code will NOT be merged`);
450
- console.error(`Investigation required before retry`);
451
- console.error(`${'='.repeat(80)}\n`);
452
-
453
- // Publish REJECTION message (NOT approval!)
454
- const hook = agent.config.hooks?.onComplete;
455
- if (hook && hook.action === 'publish_message') {
456
- agent._publish({
457
- topic: hook.config.topic,
458
- receiver: hook.config.receiver || 'broadcast',
459
- content: {
460
- text: `REJECTED: Validator crashed ${maxRetries} times - ${error.message}`,
461
- data: {
462
- approved: false, // REJECT!
463
- crashedAfterRetries: true,
464
- errors: JSON.stringify([
465
- `VALIDATOR CRASHED ${maxRetries}x: ${error.message}`,
466
- `Validation could not be performed - REJECTING to prevent broken code merge`,
467
- `Investigation required before retry`,
468
- ]),
469
- attempts: maxRetries,
470
- requiresInvestigation: true,
471
- },
472
- },
473
- });
474
- }
475
-
476
- agent.state = 'error';
477
- // Don't return - fall through to publish AGENT_ERROR and save failure info
478
- // This allows the cluster to stop and be resumed after investigation
479
- }
480
-
481
- // Non-validator agents: publish error and stop
482
- agent.state = 'error';
483
-
484
- // Save failure info to cluster for resume capability
485
- agent.cluster.failureInfo = {
486
- agentId: agent.id,
487
- taskId: agent.currentTaskId,
488
- iteration: agent.iteration,
489
- error: error.message,
490
- attempts: maxRetries,
491
- timestamp: Date.now(),
492
- };
493
-
494
- // Publish error to message bus for visibility in logs
495
- agent._publish({
496
- topic: 'AGENT_ERROR',
497
- receiver: 'broadcast',
498
- content: {
499
- text: `Task execution failed after ${maxRetries} attempts: ${error.message}`,
500
- data: {
501
- error: error.message,
502
- stack: error.stack,
503
- agent: agent.id,
504
- role: agent.role,
505
- iteration: agent.iteration,
506
- taskId: agent.currentTaskId,
507
- attempts: maxRetries,
508
- hookFailureContext: error.message.includes('Hook uses result')
509
- ? {
510
- taskId: agent.currentTaskId || 'UNKNOWN',
511
- retrieveLogs: agent.currentTaskId
512
- ? `zeroshot task logs ${agent.currentTaskId}`
513
- : 'N/A',
514
- }
515
- : undefined,
516
- },
517
- },
518
- metadata: {
519
- triggeringTopic: triggeringMessage.topic,
520
- },
521
- });
541
+ agent.state = 'idle';
542
+ }
522
543
 
523
- // Execute onError hook
524
- await executeHook({
525
- hook: agent.config.hooks?.onError,
526
- agent: agent,
527
- message: triggeringMessage,
528
- result: { error },
529
- messageBus: agent.messageBus,
530
- cluster: agent.cluster,
531
- orchestrator: agent.orchestrator,
532
- });
544
+ async function scheduleRetry(agent, error, attempt, maxRetries, baseDelay) {
545
+ const delay = baseDelay * Math.pow(2, attempt - 1); // 2s, 4s, 8s
533
546
 
534
- agent.state = 'idle';
535
- return; // Give up
536
- }
547
+ agent._publishLifecycle('RETRY_SCHEDULED', {
548
+ attempt,
549
+ maxRetries,
550
+ delayMs: delay,
551
+ error: error.message,
552
+ });
537
553
 
538
- // Not the last attempt - prepare for retry
539
- const delay = baseDelay * Math.pow(2, attempt - 1); // 2s, 4s, 8s
554
+ agent._log(`[${agent.id}] ⚠️ Retrying in ${delay}ms... (${attempt + 1}/${maxRetries})`);
540
555
 
541
- agent._publishLifecycle('RETRY_SCHEDULED', {
542
- attempt,
543
- maxRetries,
544
- delayMs: delay,
545
- error: error.message,
546
- });
556
+ // Exponential backoff
557
+ await new Promise((resolve) => setTimeout(resolve, delay));
547
558
 
548
- agent._log(`[${agent.id}] ⚠️ Retrying in ${delay}ms... (${attempt + 1}/${maxRetries})`);
559
+ agent._log(`[${agent.id}] 🔄 Starting retry attempt ${attempt + 1}/${maxRetries}`);
560
+ }
549
561
 
550
- // Exponential backoff
551
- await new Promise((resolve) => setTimeout(resolve, delay));
562
+ async function handleTaskAttemptFailure({
563
+ agent,
564
+ triggeringMessage,
565
+ error,
566
+ attempt,
567
+ maxRetries,
568
+ baseDelay,
569
+ }) {
570
+ // LOCK CONTENTION: Add extra jittered delay for lock file errors
571
+ // This happens when multiple validators try to run Claude CLI in the same workspace
572
+ const isLockError = error.message && error.message.includes('Lock file');
573
+
574
+ logTaskAttemptFailure(agent, attempt, maxRetries, error);
575
+
576
+ if (isLockError) {
577
+ await handleLockContention();
578
+ } else if (attempt < maxRetries) {
579
+ console.error(`Will retry in ${baseDelay * Math.pow(2, attempt - 1)}ms...`);
580
+ }
581
+ console.error(`${'='.repeat(80)}
582
+ `);
552
583
 
553
- agent._log(`[${agent.id}] 🔄 Starting retry attempt ${attempt + 1}/${maxRetries}`);
554
- // Continue to next iteration of for loop
555
- }
584
+ if (attempt >= maxRetries) {
585
+ await handleFinalFailure(agent, triggeringMessage, error, maxRetries);
586
+ return true;
556
587
  }
588
+
589
+ await scheduleRetry(agent, error, attempt, maxRetries, baseDelay);
590
+ return false;
557
591
  }
558
592
 
559
593
  /**
560
- * Start monitoring agent output liveness using multi-indicator stuck detection
561
- *
562
- * SAFE DETECTION: Only flags as stuck when MULTIPLE indicators agree:
563
- * - Process sleeping (state=S)
564
- * - Blocked on epoll/poll wait
565
- * - Low CPU usage (<1%)
566
- * - Low context switches (<10)
567
- * - No network data in flight
568
- *
569
- * Single-indicator detection (just output freshness) has HIGH false positive risk.
570
- * This multi-indicator approach eliminates false positives.
571
- *
594
+ * Execute claude-zeroshots with built context
595
+ * Retries disabled by default. Set agent config `maxRetries` to enable (e.g., 3).
572
596
  * @param {AgentWrapper} agent - Agent instance
597
+ * @param {Object} triggeringMessage - Message that triggered execution
573
598
  */
599
+ async function executeTask(agent, triggeringMessage) {
600
+ // Early exit if agent was stopped
601
+ if (!agent.running) {
602
+ return;
603
+ }
604
+
605
+ // Default: no retries (maxRetries=1 means 1 attempt only)
606
+ // Set agent config `maxRetries: 3` to enable exponential backoff retries
607
+ const maxRetries = agent.config.maxRetries ?? 1;
608
+ const baseDelay = 2000; // 2 seconds
609
+
610
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
611
+ // Check if agent was stopped between retries
612
+ if (!agent.running) {
613
+ return;
614
+ }
615
+
616
+ try {
617
+ await runTaskAttempt(agent, triggeringMessage);
618
+ return;
619
+ } catch (error) {
620
+ const shouldStop = await handleTaskAttemptFailure({
621
+ agent,
622
+ triggeringMessage,
623
+ error,
624
+ attempt,
625
+ maxRetries,
626
+ baseDelay,
627
+ });
628
+ if (shouldStop) {
629
+ return;
630
+ }
631
+ }
632
+ }
633
+ }
574
634
  function startLivenessCheck(agent) {
575
635
  if (agent.livenessCheckInterval) {
576
636
  clearInterval(agent.livenessCheckInterval);