@the-open-engine/zeroshot 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 (295) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/LICENSE +21 -0
  3. package/README.md +508 -0
  4. package/cli/commands/inspect-render.js +135 -0
  5. package/cli/commands/inspect.js +294 -0
  6. package/cli/commands/providers.js +149 -0
  7. package/cli/index.js +5431 -0
  8. package/cli/lib/first-run.js +211 -0
  9. package/cli/lib/update-checker.js +281 -0
  10. package/cli/message-formatter-utils.js +75 -0
  11. package/cli/message-formatters-normal.js +275 -0
  12. package/cli/message-formatters-watch.js +185 -0
  13. package/cluster-templates/base-templates/debug-workflow.json +422 -0
  14. package/cluster-templates/base-templates/full-workflow.json +727 -0
  15. package/cluster-templates/base-templates/heavy-validation.json +272 -0
  16. package/cluster-templates/base-templates/quick-validation.json +285 -0
  17. package/cluster-templates/base-templates/single-worker.json +71 -0
  18. package/cluster-templates/base-templates/worker-validator.json +230 -0
  19. package/cluster-templates/conductor-bootstrap.json +130 -0
  20. package/docker/zeroshot-cluster/Dockerfile +153 -0
  21. package/docker/zeroshot-cluster/pre-baked-deps.json +28 -0
  22. package/lib/agent-cli-provider/adapters/claude-parser.d.ts +3 -0
  23. package/lib/agent-cli-provider/adapters/claude-parser.d.ts.map +1 -0
  24. package/lib/agent-cli-provider/adapters/claude-parser.js +122 -0
  25. package/lib/agent-cli-provider/adapters/claude-parser.js.map +1 -0
  26. package/lib/agent-cli-provider/adapters/claude-recovery.d.ts +15 -0
  27. package/lib/agent-cli-provider/adapters/claude-recovery.d.ts.map +1 -0
  28. package/lib/agent-cli-provider/adapters/claude-recovery.js +165 -0
  29. package/lib/agent-cli-provider/adapters/claude-recovery.js.map +1 -0
  30. package/lib/agent-cli-provider/adapters/claude.d.ts +3 -0
  31. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -0
  32. package/lib/agent-cli-provider/adapters/claude.js +181 -0
  33. package/lib/agent-cli-provider/adapters/claude.js.map +1 -0
  34. package/lib/agent-cli-provider/adapters/codex-parser.d.ts +3 -0
  35. package/lib/agent-cli-provider/adapters/codex-parser.d.ts.map +1 -0
  36. package/lib/agent-cli-provider/adapters/codex-parser.js +192 -0
  37. package/lib/agent-cli-provider/adapters/codex-parser.js.map +1 -0
  38. package/lib/agent-cli-provider/adapters/codex.d.ts +3 -0
  39. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -0
  40. package/lib/agent-cli-provider/adapters/codex.js +151 -0
  41. package/lib/agent-cli-provider/adapters/codex.js.map +1 -0
  42. package/lib/agent-cli-provider/adapters/common.d.ts +28 -0
  43. package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -0
  44. package/lib/agent-cli-provider/adapters/common.js +71 -0
  45. package/lib/agent-cli-provider/adapters/common.js.map +1 -0
  46. package/lib/agent-cli-provider/adapters/gemini.d.ts +3 -0
  47. package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -0
  48. package/lib/agent-cli-provider/adapters/gemini.js +176 -0
  49. package/lib/agent-cli-provider/adapters/gemini.js.map +1 -0
  50. package/lib/agent-cli-provider/adapters/index.d.ts +9 -0
  51. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -0
  52. package/lib/agent-cli-provider/adapters/index.js +95 -0
  53. package/lib/agent-cli-provider/adapters/index.js.map +1 -0
  54. package/lib/agent-cli-provider/adapters/opencode.d.ts +3 -0
  55. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -0
  56. package/lib/agent-cli-provider/adapters/opencode.js +228 -0
  57. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -0
  58. package/lib/agent-cli-provider/contract-actions.d.ts +5 -0
  59. package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -0
  60. package/lib/agent-cli-provider/contract-actions.js +140 -0
  61. package/lib/agent-cli-provider/contract-actions.js.map +1 -0
  62. package/lib/agent-cli-provider/contract-env.d.ts +11 -0
  63. package/lib/agent-cli-provider/contract-env.d.ts.map +1 -0
  64. package/lib/agent-cli-provider/contract-env.js +113 -0
  65. package/lib/agent-cli-provider/contract-env.js.map +1 -0
  66. package/lib/agent-cli-provider/contract-envelope.d.ts +49 -0
  67. package/lib/agent-cli-provider/contract-envelope.d.ts.map +1 -0
  68. package/lib/agent-cli-provider/contract-envelope.js +52 -0
  69. package/lib/agent-cli-provider/contract-envelope.js.map +1 -0
  70. package/lib/agent-cli-provider/contract-errors.d.ts +22 -0
  71. package/lib/agent-cli-provider/contract-errors.d.ts.map +1 -0
  72. package/lib/agent-cli-provider/contract-errors.js +71 -0
  73. package/lib/agent-cli-provider/contract-errors.js.map +1 -0
  74. package/lib/agent-cli-provider/contract-fallback.d.ts +7 -0
  75. package/lib/agent-cli-provider/contract-fallback.d.ts.map +1 -0
  76. package/lib/agent-cli-provider/contract-fallback.js +102 -0
  77. package/lib/agent-cli-provider/contract-fallback.js.map +1 -0
  78. package/lib/agent-cli-provider/contract-invoke.d.ts +5 -0
  79. package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -0
  80. package/lib/agent-cli-provider/contract-invoke.js +89 -0
  81. package/lib/agent-cli-provider/contract-invoke.js.map +1 -0
  82. package/lib/agent-cli-provider/contract-options.d.ts +3 -0
  83. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -0
  84. package/lib/agent-cli-provider/contract-options.js +140 -0
  85. package/lib/agent-cli-provider/contract-options.js.map +1 -0
  86. package/lib/agent-cli-provider/contract-parse.d.ts +15 -0
  87. package/lib/agent-cli-provider/contract-parse.d.ts.map +1 -0
  88. package/lib/agent-cli-provider/contract-parse.js +81 -0
  89. package/lib/agent-cli-provider/contract-parse.js.map +1 -0
  90. package/lib/agent-cli-provider/contract-support.d.ts +18 -0
  91. package/lib/agent-cli-provider/contract-support.d.ts.map +1 -0
  92. package/lib/agent-cli-provider/contract-support.js +154 -0
  93. package/lib/agent-cli-provider/contract-support.js.map +1 -0
  94. package/lib/agent-cli-provider/contract.d.ts +13 -0
  95. package/lib/agent-cli-provider/contract.d.ts.map +1 -0
  96. package/lib/agent-cli-provider/contract.js +30 -0
  97. package/lib/agent-cli-provider/contract.js.map +1 -0
  98. package/lib/agent-cli-provider/env-safety.d.ts +11 -0
  99. package/lib/agent-cli-provider/env-safety.d.ts.map +1 -0
  100. package/lib/agent-cli-provider/env-safety.js +83 -0
  101. package/lib/agent-cli-provider/env-safety.js.map +1 -0
  102. package/lib/agent-cli-provider/errors.d.ts +5 -0
  103. package/lib/agent-cli-provider/errors.d.ts.map +1 -0
  104. package/lib/agent-cli-provider/errors.js +115 -0
  105. package/lib/agent-cli-provider/errors.js.map +1 -0
  106. package/lib/agent-cli-provider/executable.d.ts +3 -0
  107. package/lib/agent-cli-provider/executable.d.ts.map +1 -0
  108. package/lib/agent-cli-provider/executable.js +24 -0
  109. package/lib/agent-cli-provider/executable.js.map +1 -0
  110. package/lib/agent-cli-provider/index.d.ts +8 -0
  111. package/lib/agent-cli-provider/index.d.ts.map +1 -0
  112. package/lib/agent-cli-provider/index.js +31 -0
  113. package/lib/agent-cli-provider/index.js.map +1 -0
  114. package/lib/agent-cli-provider/invoke-evidence.d.ts +4 -0
  115. package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -0
  116. package/lib/agent-cli-provider/invoke-evidence.js +17 -0
  117. package/lib/agent-cli-provider/invoke-evidence.js.map +1 -0
  118. package/lib/agent-cli-provider/json.d.ts +16 -0
  119. package/lib/agent-cli-provider/json.d.ts.map +1 -0
  120. package/lib/agent-cli-provider/json.js +110 -0
  121. package/lib/agent-cli-provider/json.js.map +1 -0
  122. package/lib/agent-cli-provider/log-prefix.d.ts +2 -0
  123. package/lib/agent-cli-provider/log-prefix.d.ts.map +1 -0
  124. package/lib/agent-cli-provider/log-prefix.js +22 -0
  125. package/lib/agent-cli-provider/log-prefix.js.map +1 -0
  126. package/lib/agent-cli-provider/process-runner.d.ts +17 -0
  127. package/lib/agent-cli-provider/process-runner.d.ts.map +1 -0
  128. package/lib/agent-cli-provider/process-runner.js +89 -0
  129. package/lib/agent-cli-provider/process-runner.js.map +1 -0
  130. package/lib/agent-cli-provider/redaction.d.ts +9 -0
  131. package/lib/agent-cli-provider/redaction.d.ts.map +1 -0
  132. package/lib/agent-cli-provider/redaction.js +227 -0
  133. package/lib/agent-cli-provider/redaction.js.map +1 -0
  134. package/lib/agent-cli-provider/schema.d.ts +5 -0
  135. package/lib/agent-cli-provider/schema.d.ts.map +1 -0
  136. package/lib/agent-cli-provider/schema.js +136 -0
  137. package/lib/agent-cli-provider/schema.js.map +1 -0
  138. package/lib/agent-cli-provider/single-agent-runtime.d.ts +15 -0
  139. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -0
  140. package/lib/agent-cli-provider/single-agent-runtime.js +228 -0
  141. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -0
  142. package/lib/agent-cli-provider/types.d.ts +194 -0
  143. package/lib/agent-cli-provider/types.d.ts.map +1 -0
  144. package/lib/agent-cli-provider/types.js +12 -0
  145. package/lib/agent-cli-provider/types.js.map +1 -0
  146. package/lib/completion.js +174 -0
  147. package/lib/detached-startup.js +220 -0
  148. package/lib/docker-config.js +220 -0
  149. package/lib/git-remote-utils.js +165 -0
  150. package/lib/id-detector.js +55 -0
  151. package/lib/provider-defaults.js +62 -0
  152. package/lib/provider-detection.js +59 -0
  153. package/lib/provider-names.js +57 -0
  154. package/lib/repo-settings.js +69 -0
  155. package/lib/settings/claude-auth.js +78 -0
  156. package/lib/settings.js +542 -0
  157. package/lib/start-cluster.js +321 -0
  158. package/lib/stream-json-parser.js +67 -0
  159. package/package.json +162 -0
  160. package/scripts/fix-node-pty-permissions.js +75 -0
  161. package/scripts/record-demo.sh +279 -0
  162. package/scripts/setup-merge-queue.sh +170 -0
  163. package/scripts/test-install.sh +40 -0
  164. package/scripts/validate-templates.js +107 -0
  165. package/src/agent/agent-config.js +266 -0
  166. package/src/agent/agent-context-builder.js +189 -0
  167. package/src/agent/agent-context-sections.js +338 -0
  168. package/src/agent/agent-context-sources.js +147 -0
  169. package/src/agent/agent-hook-executor.js +721 -0
  170. package/src/agent/agent-input-injector.js +141 -0
  171. package/src/agent/agent-lifecycle.js +982 -0
  172. package/src/agent/agent-quality-gate-schema.js +93 -0
  173. package/src/agent/agent-quality-gates-context.js +51 -0
  174. package/src/agent/agent-stuck-detector.js +256 -0
  175. package/src/agent/agent-task-executor.js +2028 -0
  176. package/src/agent/agent-trigger-evaluator.js +67 -0
  177. package/src/agent/context-metrics.js +160 -0
  178. package/src/agent/context-pack-builder.js +367 -0
  179. package/src/agent/context-replay-policy.js +51 -0
  180. package/src/agent/guidance-queue.js +77 -0
  181. package/src/agent/output-extraction.js +367 -0
  182. package/src/agent/output-reformatter.js +175 -0
  183. package/src/agent/pr-verification.js +653 -0
  184. package/src/agent/rate-limit-backoff.js +82 -0
  185. package/src/agent/schema-utils.js +146 -0
  186. package/src/agent/validation-platform.js +35 -0
  187. package/src/agent-cli-provider/adapters/claude-parser.ts +133 -0
  188. package/src/agent-cli-provider/adapters/claude-recovery.ts +203 -0
  189. package/src/agent-cli-provider/adapters/claude.ts +247 -0
  190. package/src/agent-cli-provider/adapters/codex-parser.ts +211 -0
  191. package/src/agent-cli-provider/adapters/codex.ts +217 -0
  192. package/src/agent-cli-provider/adapters/common.ts +124 -0
  193. package/src/agent-cli-provider/adapters/gemini.ts +243 -0
  194. package/src/agent-cli-provider/adapters/index.ts +126 -0
  195. package/src/agent-cli-provider/adapters/opencode.ts +286 -0
  196. package/src/agent-cli-provider/contract-actions.ts +150 -0
  197. package/src/agent-cli-provider/contract-env.ts +111 -0
  198. package/src/agent-cli-provider/contract-envelope.ts +110 -0
  199. package/src/agent-cli-provider/contract-errors.ts +66 -0
  200. package/src/agent-cli-provider/contract-fallback.ts +121 -0
  201. package/src/agent-cli-provider/contract-invoke.ts +104 -0
  202. package/src/agent-cli-provider/contract-options.ts +173 -0
  203. package/src/agent-cli-provider/contract-parse.ts +94 -0
  204. package/src/agent-cli-provider/contract-support.ts +167 -0
  205. package/src/agent-cli-provider/contract.ts +56 -0
  206. package/src/agent-cli-provider/env-safety.ts +82 -0
  207. package/src/agent-cli-provider/errors.ts +122 -0
  208. package/src/agent-cli-provider/executable.ts +24 -0
  209. package/src/agent-cli-provider/index.ts +83 -0
  210. package/src/agent-cli-provider/invoke-evidence.ts +18 -0
  211. package/src/agent-cli-provider/json.ts +114 -0
  212. package/src/agent-cli-provider/log-prefix.ts +20 -0
  213. package/src/agent-cli-provider/process-runner.ts +145 -0
  214. package/src/agent-cli-provider/redaction.ts +282 -0
  215. package/src/agent-cli-provider/schema.ts +115 -0
  216. package/src/agent-cli-provider/single-agent-runtime.ts +311 -0
  217. package/src/agent-cli-provider/types.ts +237 -0
  218. package/src/agent-wrapper.js +615 -0
  219. package/src/agents/git-pusher-template.js +705 -0
  220. package/src/attach/attach-client.js +438 -0
  221. package/src/attach/attach-server.js +543 -0
  222. package/src/attach/index.js +37 -0
  223. package/src/attach/protocol.js +220 -0
  224. package/src/attach/ring-buffer.js +121 -0
  225. package/src/attach/send-input.js +88 -0
  226. package/src/attach/socket-discovery.js +267 -0
  227. package/src/claude-task-runner.js +661 -0
  228. package/src/config-router.js +89 -0
  229. package/src/config-validator.js +2202 -0
  230. package/src/copy-worker.js +43 -0
  231. package/src/guidance-topics.js +10 -0
  232. package/src/input-helpers.js +65 -0
  233. package/src/isolation-manager.js +1734 -0
  234. package/src/issue-providers/README.md +305 -0
  235. package/src/issue-providers/azure-devops-provider.js +307 -0
  236. package/src/issue-providers/base-provider.js +232 -0
  237. package/src/issue-providers/github-provider.js +210 -0
  238. package/src/issue-providers/gitlab-provider.js +262 -0
  239. package/src/issue-providers/index.js +196 -0
  240. package/src/issue-providers/jira-provider.js +260 -0
  241. package/src/ledger.js +692 -0
  242. package/src/lib/gc.js +232 -0
  243. package/src/lib/safe-exec.js +88 -0
  244. package/src/logic-engine.js +201 -0
  245. package/src/message-buffer.js +81 -0
  246. package/src/message-bus-bridge.js +144 -0
  247. package/src/message-bus.js +256 -0
  248. package/src/name-generator.js +232 -0
  249. package/src/orchestrator.js +3924 -0
  250. package/src/preflight.js +712 -0
  251. package/src/process-metrics.js +608 -0
  252. package/src/providers/anthropic/index.js +3 -0
  253. package/src/providers/base-provider.js +355 -0
  254. package/src/providers/capabilities.js +60 -0
  255. package/src/providers/google/index.js +3 -0
  256. package/src/providers/index.js +293 -0
  257. package/src/providers/openai/index.js +3 -0
  258. package/src/providers/opencode/index.js +3 -0
  259. package/src/quality-gates.js +143 -0
  260. package/src/schemas/sub-cluster.js +208 -0
  261. package/src/state-snapshot.js +398 -0
  262. package/src/state-snapshotter.js +142 -0
  263. package/src/status-footer.js +1026 -0
  264. package/src/sub-cluster-wrapper.js +693 -0
  265. package/src/task-runner.js +30 -0
  266. package/src/template-resolver.js +425 -0
  267. package/src/template-validation/index.js +338 -0
  268. package/src/template-validation/simulate-consensus-gates.js +324 -0
  269. package/src/template-validation/simulate-random-topology.js +541 -0
  270. package/src/template-validation/simulate-two-stage-validation.js +270 -0
  271. package/src/worktree-claude-config.js +135 -0
  272. package/src/worktree-tooling-env.js +150 -0
  273. package/task-lib/attachable-watcher.js +381 -0
  274. package/task-lib/commands/clean.js +50 -0
  275. package/task-lib/commands/episodes.js +105 -0
  276. package/task-lib/commands/get-log-path.js +23 -0
  277. package/task-lib/commands/kill.js +32 -0
  278. package/task-lib/commands/list.js +105 -0
  279. package/task-lib/commands/logs.js +439 -0
  280. package/task-lib/commands/resume.js +42 -0
  281. package/task-lib/commands/run.js +57 -0
  282. package/task-lib/commands/schedule.js +105 -0
  283. package/task-lib/commands/scheduler-cmd.js +96 -0
  284. package/task-lib/commands/schedules.js +148 -0
  285. package/task-lib/commands/status.js +44 -0
  286. package/task-lib/commands/unschedule.js +16 -0
  287. package/task-lib/completion.js +9 -0
  288. package/task-lib/config.js +11 -0
  289. package/task-lib/name-generator.js +230 -0
  290. package/task-lib/package.json +3 -0
  291. package/task-lib/provider-helper-runtime.js +29 -0
  292. package/task-lib/runner.js +190 -0
  293. package/task-lib/scheduler.js +252 -0
  294. package/task-lib/store.js +529 -0
  295. package/task-lib/watcher.js +305 -0
@@ -0,0 +1,982 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * AgentLifecycle - Agent state machine and lifecycle management
4
+ *
5
+ * Provides:
6
+ * - Agent startup and shutdown
7
+ * - Message handling and routing
8
+ * - Trigger action execution (execute_task, stop_cluster)
9
+ * - Task execution with retry logic
10
+ * - Liveness monitoring with multi-indicator stuck detection
11
+ *
12
+ * State machine: idle → evaluating → building_context → executing → idle
13
+ */
14
+
15
+ const { findMatchingTrigger, evaluateTrigger } = require('./agent-trigger-evaluator');
16
+ const { executeHook } = require('./agent-hook-executor');
17
+ const IsolationManager = require('../isolation-manager');
18
+ const crypto = require('crypto');
19
+ const { bufferMessage, scheduleDrain, drainBufferedMessages } = require('../message-buffer');
20
+ const {
21
+ analyzeProcessHealth,
22
+ isPlatformSupported,
23
+ STUCK_THRESHOLD,
24
+ } = require('./agent-stuck-detector');
25
+ const { normalizeProviderName } = require('../../lib/provider-names');
26
+ const { loadSettings } = require('../../lib/settings');
27
+ const { findPlatformMismatchReason } = require('./validation-platform');
28
+ const { calculateRateLimitDelay, isRateLimitError } = require('./rate-limit-backoff');
29
+
30
+ const DEFAULT_VALIDATOR_IMAGE = 'zeroshot-cluster-base';
31
+
32
+ class HookExecutionError extends Error {
33
+ constructor(message, options) {
34
+ super(message);
35
+ this.name = 'HookExecutionError';
36
+ this.hookFailure = true;
37
+ this.hookRetries = options?.hookRetries;
38
+ this.originalHookError = options?.originalHookError;
39
+ }
40
+ }
41
+
42
+ function resolveValidatorIsolationConfig(agent) {
43
+ const config = agent.config?.isolation || {};
44
+ if (config.type && config.type !== 'docker') {
45
+ return null;
46
+ }
47
+
48
+ return {
49
+ image: config.image || DEFAULT_VALIDATOR_IMAGE,
50
+ mounts: config.mounts,
51
+ noMounts: config.noMounts,
52
+ containerHome: config.containerHome,
53
+ };
54
+ }
55
+
56
+ async function createValidatorIsolation(agent, isolationConfig) {
57
+ if (!IsolationManager.isDockerAvailable()) {
58
+ agent._log(`[${agent.id}] Docker not available - cannot retry validator in isolation`);
59
+ return null;
60
+ }
61
+
62
+ const cluster = agent.cluster || {};
63
+ const workDir = agent.config?.cwd || cluster.worktree?.path || cluster.cwd || process.cwd();
64
+ const image = isolationConfig.image;
65
+ await IsolationManager.ensureImage(image);
66
+
67
+ const manager = new IsolationManager({ image });
68
+ const providerName = normalizeProviderName(
69
+ (agent._resolveProvider && agent._resolveProvider()) ||
70
+ cluster.config?.forceProvider ||
71
+ cluster.config?.defaultProvider ||
72
+ loadSettings().defaultProvider ||
73
+ 'claude'
74
+ );
75
+
76
+ const isolationClusterId = `${cluster.id}-validators`;
77
+ const containerId = await manager.createContainer(isolationClusterId, {
78
+ workDir,
79
+ image,
80
+ noMounts: isolationConfig.noMounts,
81
+ mounts: isolationConfig.mounts,
82
+ containerHome: isolationConfig.containerHome,
83
+ provider: providerName,
84
+ reuseExistingWorkspace: true,
85
+ });
86
+
87
+ const validatorIsolation = {
88
+ enabled: true,
89
+ manager,
90
+ clusterId: isolationClusterId,
91
+ containerId,
92
+ image,
93
+ workDir,
94
+ };
95
+
96
+ cluster.validatorIsolation = validatorIsolation;
97
+ return validatorIsolation;
98
+ }
99
+
100
+ async function ensureValidatorIsolation(agent) {
101
+ const cluster = agent.cluster || {};
102
+
103
+ if (agent.isolation?.enabled) {
104
+ return agent.isolation;
105
+ }
106
+
107
+ if (cluster.validatorIsolation?.enabled) {
108
+ agent.isolation = cluster.validatorIsolation;
109
+ return agent.isolation;
110
+ }
111
+
112
+ if (cluster.validatorIsolationPromise) {
113
+ const isolation = await cluster.validatorIsolationPromise;
114
+ if (isolation?.enabled) {
115
+ agent.isolation = isolation;
116
+ }
117
+ return agent.isolation || null;
118
+ }
119
+
120
+ const isolationConfig = resolveValidatorIsolationConfig(agent);
121
+ if (!isolationConfig) {
122
+ agent._log(`[${agent.id}] Validator isolation config is not docker - skipping fallback`);
123
+ return null;
124
+ }
125
+
126
+ cluster.validatorIsolationPromise = createValidatorIsolation(agent, isolationConfig);
127
+
128
+ try {
129
+ const isolation = await cluster.validatorIsolationPromise;
130
+ if (isolation?.enabled) {
131
+ agent.isolation = isolation;
132
+ return agent.isolation;
133
+ }
134
+ return null;
135
+ } finally {
136
+ cluster.validatorIsolationPromise = null;
137
+ }
138
+ }
139
+
140
+ async function maybeRetryValidatorInDocker(agent, result) {
141
+ if (agent.role !== 'validator') return null;
142
+ if (agent.isolation?.enabled) return null;
143
+ if (agent._validatorIsolationAttemptedIteration === agent.iteration) {
144
+ return null;
145
+ }
146
+
147
+ const reason = findPlatformMismatchReason(result?.result || {});
148
+ if (!reason) return null;
149
+
150
+ const isolation = await ensureValidatorIsolation(agent);
151
+ if (!isolation) {
152
+ return null;
153
+ }
154
+
155
+ agent._validatorIsolationAttemptedIteration = agent.iteration;
156
+ agent._log(`[${agent.id}] Platform mismatch detected - retrying validator in Docker isolation`);
157
+ return reason;
158
+ }
159
+
160
+ /**
161
+ * Start the agent (begin listening for triggers)
162
+ * @param {AgentWrapper} agent - Agent instance
163
+ */
164
+ function start(agent) {
165
+ if (agent.running) {
166
+ throw new Error(`Agent ${agent.id} is already running`);
167
+ }
168
+
169
+ agent.running = true;
170
+ agent.state = 'idle';
171
+
172
+ // Subscribe to all messages for this cluster
173
+ agent.unsubscribe = agent.messageBus.subscribe((message) => {
174
+ if (message.cluster_id === agent.cluster.id) {
175
+ handleMessage(agent, message).catch((error) => {
176
+ // FATAL: Message handling failed - crash loud
177
+ console.error(`\n${'='.repeat(80)}`);
178
+ console.error(`🔴 FATAL: Agent ${agent.id} message handler crashed`);
179
+ console.error(`${'='.repeat(80)}`);
180
+ console.error(`Topic: ${message.topic}`);
181
+ console.error(`Error: ${error.message}`);
182
+ console.error(`Stack: ${error.stack}`);
183
+ console.error(`${'='.repeat(80)}\n`);
184
+ // Re-throw to crash the process - DO NOT SILENTLY CONTINUE
185
+ throw error;
186
+ });
187
+ }
188
+ });
189
+
190
+ agent._log(`Agent ${agent.id} started (role: ${agent.role})`);
191
+ agent._publishLifecycle('STARTED', {
192
+ triggers: agent.config.triggers?.map((t) => t.topic) || [],
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Stop the agent
198
+ * Waits for any in-flight execution to complete before returning.
199
+ * @param {AgentWrapper} agent - Agent instance
200
+ * @returns {Promise<void>}
201
+ */
202
+ async function stop(agent) {
203
+ if (!agent.running) {
204
+ return;
205
+ }
206
+
207
+ agent.running = false;
208
+ agent.state = 'stopped';
209
+
210
+ if (agent.unsubscribe) {
211
+ agent.unsubscribe();
212
+ agent.unsubscribe = null;
213
+ }
214
+
215
+ // Kill current task if any
216
+ if (agent.currentTask) {
217
+ agent._killTask();
218
+ }
219
+
220
+ // Wait for in-flight execution to complete (up to 5 seconds)
221
+ // This prevents write-after-close race conditions
222
+ if (agent._currentExecution) {
223
+ try {
224
+ await Promise.race([
225
+ agent._currentExecution,
226
+ new Promise((resolve) => setTimeout(resolve, 5000)),
227
+ ]);
228
+ } catch {
229
+ // Ignore errors from cancelled execution
230
+ }
231
+ agent._currentExecution = null;
232
+ }
233
+
234
+ agent._log(`Agent ${agent.id} stopped`);
235
+ }
236
+
237
+ /**
238
+ * Handle incoming message
239
+ * @param {AgentWrapper} agent - Agent instance
240
+ * @param {Object} message - Incoming message
241
+ */
242
+ async function handleMessage(agent, message) {
243
+ if (!agent._bufferedMessages) {
244
+ agent._bufferedMessages = [];
245
+ }
246
+
247
+ // Check if any trigger matches FIRST (before state check)
248
+ const matchingTrigger = findMatchingTrigger({
249
+ triggers: agent.config.triggers,
250
+ message,
251
+ });
252
+
253
+ if (!matchingTrigger) {
254
+ return; // No trigger for this message type
255
+ }
256
+
257
+ // Now check state - LOG if we're dropping a message we SHOULD handle
258
+ if (!agent.running) {
259
+ console.warn(`[${agent.id}] ⚠️ DROPPING message (not running): ${message.topic}`);
260
+ return;
261
+ }
262
+ if (agent.state !== 'idle') {
263
+ // IMPORTANT: Never drop a message that matches a trigger.
264
+ // Dropping validation/coordinator signals can wedge clusters in "running" state.
265
+ bufferMessage(agent, message);
266
+ console.warn(
267
+ `[${agent.id}] ⏸️ BUFFERING message (busy, state=${agent.state}): ${message.topic}`
268
+ );
269
+ scheduleDrain(
270
+ agent,
271
+ () => drainBufferedMessages(agent, (next) => handleMessage(agent, next), { label: 'Agent' }),
272
+ { label: 'Agent' }
273
+ );
274
+ return;
275
+ }
276
+
277
+ // Evaluate trigger logic
278
+ agent.state = 'evaluating_logic';
279
+
280
+ const agentContext = {
281
+ id: agent.id,
282
+ role: agent.role,
283
+ iteration: agent.iteration,
284
+ cluster_id: agent.cluster.id,
285
+ };
286
+
287
+ const shouldExecute = evaluateTrigger({
288
+ trigger: matchingTrigger,
289
+ message,
290
+ agent: agentContext,
291
+ logicEngine: agent.logicEngine,
292
+ });
293
+
294
+ if (!shouldExecute) {
295
+ agent.state = 'idle';
296
+ return;
297
+ }
298
+
299
+ // Execute trigger action (lifecycle event published inside for execute_task)
300
+ // Track execution so stop() can wait for it
301
+ const executionPromise = executeTriggerAction(agent, matchingTrigger, message);
302
+ agent._currentExecution = executionPromise;
303
+ try {
304
+ await executionPromise;
305
+ } finally {
306
+ // Clear only if this is still our execution (not replaced by another)
307
+ if (agent._currentExecution === executionPromise) {
308
+ agent._currentExecution = null;
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Execute trigger action
315
+ * @param {AgentWrapper} agent - Agent instance
316
+ * @param {Object} trigger - Matched trigger config
317
+ * @param {Object} message - Triggering message
318
+ */
319
+ async function executeTriggerAction(agent, trigger, message) {
320
+ const action = trigger.action || 'execute_task';
321
+
322
+ if (action === 'execute_task') {
323
+ await executeTask(agent, message);
324
+ } else if (action === 'stop_cluster') {
325
+ // Publish CLUSTER_COMPLETE message to signal successful completion
326
+ agent._publish({
327
+ topic: 'CLUSTER_COMPLETE',
328
+ receiver: 'system',
329
+ content: {
330
+ text: 'All validation passed. Cluster completing successfully.',
331
+ data: {
332
+ reason: 'all_validators_approved',
333
+ timestamp: Date.now(),
334
+ },
335
+ },
336
+ });
337
+ agent.state = 'completed';
338
+ agent._log(`Agent ${agent.id}: Cluster completion triggered`);
339
+ } else {
340
+ console.warn(`Unknown action: ${action}`);
341
+ agent.state = 'idle';
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Execute task with built context
347
+ * Default: uses settings.maxRetries (default 3) for exponential backoff retries.
348
+ * Rate limit errors (429, capacity exhausted) use longer delays (30s base).
349
+ * Override via agent config `maxRetries` to change retry behavior.
350
+ * @param {AgentWrapper} agent - Agent instance
351
+ * @param {Object} triggeringMessage - Message that triggered execution
352
+ */
353
+ function handleMaxIterations(agent) {
354
+ if (agent.iteration < agent.maxIterations) {
355
+ return false;
356
+ }
357
+
358
+ agent._log(`[Agent ${agent.id}] Hit max iterations (${agent.maxIterations}), stopping cluster`);
359
+ agent._publishLifecycle('MAX_ITERATIONS_REACHED', {
360
+ iteration: agent.iteration,
361
+ maxIterations: agent.maxIterations,
362
+ });
363
+ // Publish failure message - orchestrator watches for this and auto-stops
364
+ agent._publish({
365
+ topic: 'CLUSTER_FAILED',
366
+ receiver: 'system',
367
+ content: {
368
+ text: `Agent ${agent.id} hit max iterations limit (${agent.maxIterations}). Stopping cluster.`,
369
+ data: {
370
+ reason: 'max_iterations',
371
+ iteration: agent.iteration,
372
+ maxIterations: agent.maxIterations,
373
+ },
374
+ },
375
+ });
376
+ agent.state = 'failed';
377
+ return true;
378
+ }
379
+
380
+ function logInputContext(agent, context) {
381
+ if (agent.quiet) {
382
+ return;
383
+ }
384
+
385
+ console.log(`
386
+ ${'='.repeat(80)}`);
387
+ console.log(`📥 INPUT CONTEXT - Agent: ${agent.id} (Iteration: ${agent.iteration})`);
388
+ console.log(`${'='.repeat(80)}`);
389
+ console.log(context);
390
+ console.log(`${'='.repeat(80)}
391
+ `);
392
+ }
393
+
394
+ async function applyValidatorJitter(agent) {
395
+ // LOCK CONTENTION FIX: Add random jitter for validators to prevent thundering herd
396
+ // When multiple validators wake on the same trigger (e.g., IMPLEMENTATION_READY),
397
+ // they all try to spawn Claude CLI at the same time. Claude CLI uses a lock file
398
+ // per workspace, so only one can run. Adding jitter staggers their starts.
399
+ // SKIP in testMode - tests use mocks and don't need jitter
400
+ if (agent.role !== 'validator' || agent.testMode) {
401
+ return;
402
+ }
403
+
404
+ const jitterMs = crypto.randomInt(0, 15000); // 0-15 seconds
405
+ if (!agent.quiet) {
406
+ agent._log(
407
+ `[Agent ${agent.id}] Adding ${Math.round(jitterMs / 1000)}s jitter to prevent lock contention`
408
+ );
409
+ }
410
+ await new Promise((resolve) => setTimeout(resolve, jitterMs));
411
+ }
412
+
413
+ function publishTaskStarted(agent, triggeringMessage) {
414
+ const modelSpec = agent._resolveModelSpec ? agent._resolveModelSpec() : null;
415
+ agent._publishLifecycle('TASK_STARTED', {
416
+ iteration: agent.iteration,
417
+ model: agent._selectModel(),
418
+ provider: agent._resolveProvider ? agent._resolveProvider() : 'claude',
419
+ modelSpec,
420
+ triggeredBy: triggeringMessage.topic,
421
+ triggerFrom: triggeringMessage.sender,
422
+ });
423
+ }
424
+
425
+ function attachResultMetadata(agent, result) {
426
+ // Add task ID to result for debugging and hooks
427
+ result.taskId = agent.currentTaskId;
428
+ result.agentId = agent.id;
429
+ result.iteration = agent.iteration;
430
+ }
431
+
432
+ function publishTaskCompleted(agent, result) {
433
+ agent._publishLifecycle('TASK_COMPLETED', {
434
+ iteration: agent.iteration,
435
+ success: true,
436
+ taskId: agent.currentTaskId,
437
+ tokenUsage: result.tokenUsage || null,
438
+ });
439
+ }
440
+
441
+ function publishTokenUsage(agent, result) {
442
+ // Publish TOKEN_USAGE event for aggregation and tracking
443
+ // CRITICAL: Include taskId for causal linking - allows consumers to group
444
+ // messages by task regardless of interleaved timing from async hooks
445
+ if (!result.tokenUsage) {
446
+ return;
447
+ }
448
+
449
+ agent.messageBus.publish({
450
+ cluster_id: agent.cluster.id,
451
+ topic: 'TOKEN_USAGE',
452
+ sender: agent.id,
453
+ content: {
454
+ text: `${agent.id} used ${result.tokenUsage.inputTokens} input + ${result.tokenUsage.outputTokens} output tokens`,
455
+ data: {
456
+ agentId: agent.id,
457
+ role: agent.role,
458
+ model: agent._selectModel(),
459
+ iteration: agent.iteration,
460
+ taskId: agent.currentTaskId, // Causal linking for message ordering
461
+ ...result.tokenUsage,
462
+ },
463
+ },
464
+ });
465
+ }
466
+
467
+ async function executeOnCompleteHookWithRetry(agent, triggeringMessage, result) {
468
+ // Execute onComplete hook WITH RETRY
469
+ // Hook failure shouldn't retry the entire task - just the hook
470
+ const hookMaxRetries = 3;
471
+ const hookBaseDelay = 1000;
472
+ let hookSuccess = false;
473
+
474
+ for (let hookAttempt = 1; hookAttempt <= hookMaxRetries && !hookSuccess; hookAttempt++) {
475
+ try {
476
+ await executeHook({
477
+ hook: agent.config.hooks?.onComplete,
478
+ agent: agent,
479
+ message: triggeringMessage,
480
+ result: result,
481
+ messageBus: agent.messageBus,
482
+ cluster: agent.cluster,
483
+ orchestrator: agent.orchestrator,
484
+ });
485
+ hookSuccess = true;
486
+ } catch (hookError) {
487
+ console.error(`
488
+ ${'='.repeat(80)}`);
489
+ console.error(
490
+ `🔴 HOOK EXECUTION FAILED - AGENT: ${agent.id} (Attempt ${hookAttempt}/${hookMaxRetries})`
491
+ );
492
+ console.error(`${'='.repeat(80)}`);
493
+ console.error(`Error: ${hookError.message}`);
494
+
495
+ if (hookAttempt < hookMaxRetries) {
496
+ const delay = hookBaseDelay * Math.pow(2, hookAttempt - 1);
497
+ console.error(`Will retry hook in ${delay}ms...`);
498
+ console.error(`${'='.repeat(80)}
499
+ `);
500
+ await new Promise((resolve) => setTimeout(resolve, delay));
501
+ } else {
502
+ console.error(`${'='.repeat(80)}
503
+ `);
504
+ // All hook retries exhausted - FAIL THE CLUSTER (do NOT rerun the whole task).
505
+ // Retrying the task wastes tokens and cannot fix a deterministic hook/config bug.
506
+ throw new HookExecutionError(
507
+ `Hook execution failed after ${hookMaxRetries} attempts. ` +
508
+ `Task completed successfully but hook could not publish result. ` +
509
+ `Original error: ${hookError.message}`,
510
+ { hookRetries: hookMaxRetries, originalHookError: hookError.message }
511
+ );
512
+ }
513
+ }
514
+ }
515
+ }
516
+
517
+ async function runTaskAttempt(agent, triggeringMessage) {
518
+ // Execute onStart hook
519
+ await executeHook({
520
+ hook: agent.config.hooks?.onStart,
521
+ agent: agent,
522
+ message: triggeringMessage,
523
+ result: undefined,
524
+ messageBus: agent.messageBus,
525
+ cluster: agent.cluster,
526
+ orchestrator: agent.orchestrator,
527
+ });
528
+
529
+ // Check max iterations limit BEFORE incrementing (prevents infinite rejection loops)
530
+ if (handleMaxIterations(agent)) {
531
+ return;
532
+ }
533
+
534
+ // Increment iteration BEFORE building context so worker knows current iteration
535
+ agent.iteration++;
536
+
537
+ // Build context
538
+ agent.state = 'building_context';
539
+ const context = agent._buildContext(triggeringMessage);
540
+
541
+ // Log input context (helps debug what each agent sees)
542
+ logInputContext(agent, context);
543
+
544
+ // Spawn provider task
545
+ agent.state = 'executing_task';
546
+ await applyValidatorJitter(agent);
547
+ publishTaskStarted(agent, triggeringMessage);
548
+
549
+ const result = await agent._spawnClaudeTask(context);
550
+ attachResultMetadata(agent, result);
551
+
552
+ // Check if task execution failed
553
+ if (!result.success) {
554
+ throw new Error(result.error || 'Task execution failed');
555
+ }
556
+
557
+ const fallbackReason = await maybeRetryValidatorInDocker(agent, result);
558
+ if (fallbackReason) {
559
+ throw new Error(
560
+ `Validator platform mismatch detected (${fallbackReason}). Retrying in Docker isolation.`
561
+ );
562
+ }
563
+
564
+ // Set state to idle BEFORE publishing lifecycle event
565
+ // (so lifecycle message includes correct state)
566
+ agent.state = 'idle';
567
+
568
+ // Track completion time for context filtering (used by "since: last_task_end")
569
+ agent.lastTaskEndTime = Date.now();
570
+
571
+ publishTaskCompleted(agent, result);
572
+ publishTokenUsage(agent, result);
573
+ await executeOnCompleteHookWithRetry(agent, triggeringMessage, result);
574
+ }
575
+
576
+ function logTaskAttemptFailure(agent, attempt, maxRetries, error) {
577
+ // Log attempt failure
578
+ console.error(`
579
+ ${'='.repeat(80)}`);
580
+ console.error(`🔴 TASK EXECUTION FAILED - AGENT: ${agent.id} (Attempt ${attempt}/${maxRetries})`);
581
+ console.error(`${'='.repeat(80)}`);
582
+ console.error(`Error: ${error.message}`);
583
+ }
584
+
585
+ async function handleLockContention() {
586
+ // Lock contention - add significant jittered delay
587
+ const lockDelay = 10000 + crypto.randomInt(0, 20000); // 10-30 seconds
588
+ console.error(
589
+ `⚠️ Lock contention detected - waiting ${Math.round(lockDelay / 1000)}s before retry`
590
+ );
591
+ await new Promise((resolve) => setTimeout(resolve, lockDelay));
592
+ }
593
+
594
+ async function handleFinalFailure(agent, triggeringMessage, error, maxRetries) {
595
+ console.error(`
596
+ ${'='.repeat(80)}`);
597
+ console.error(`🔴🔴🔴 MAX RETRIES EXHAUSTED - AGENT: ${agent.id} 🔴🔴🔴`);
598
+ console.error(`${'='.repeat(80)}`);
599
+ console.error(`All ${maxRetries} attempts failed`);
600
+ console.error(`Final error: ${error.message}`);
601
+ console.error(`Stack: ${error.stack}`);
602
+ console.error(`${'='.repeat(80)}
603
+ `);
604
+
605
+ // CRITICAL FIX: Validator crash = REJECTION (not auto-approval)
606
+ // Auto-approval on crash allowed broken code to be merged - unacceptable!
607
+ // If validator crashed 3x, something is fundamentally wrong - REJECT and investigate
608
+ if (agent.role === 'validator') {
609
+ console.error(`
610
+ ${'='.repeat(80)}`);
611
+ console.error(`❌ VALIDATOR CRASHED - REJECTING (NOT AUTO-APPROVING)`);
612
+ console.error(`${'='.repeat(80)}`);
613
+ console.error(`Validator ${agent.id} crashed ${maxRetries} times`);
614
+ console.error(`Error: ${error.message}`);
615
+ console.error(`REJECTING validation - broken code will NOT be merged`);
616
+ console.error(`Investigation required before retry`);
617
+ console.error(`${'='.repeat(80)}
618
+ `);
619
+
620
+ // Publish REJECTION message (NOT approval!)
621
+ const hook = agent.config.hooks?.onComplete;
622
+ if (hook && hook.action === 'publish_message') {
623
+ agent._publish({
624
+ topic: hook.config.topic,
625
+ receiver: hook.config.receiver || 'broadcast',
626
+ content: {
627
+ text: `REJECTED: Validator crashed ${maxRetries} times - ${error.message}`,
628
+ data: {
629
+ approved: false, // REJECT!
630
+ crashedAfterRetries: true,
631
+ errors: JSON.stringify([
632
+ `VALIDATOR CRASHED ${maxRetries}x: ${error.message}`,
633
+ `Validation could not be performed - REJECTING to prevent broken code merge`,
634
+ `Investigation required before retry`,
635
+ ]),
636
+ attempts: maxRetries,
637
+ requiresInvestigation: true,
638
+ },
639
+ },
640
+ });
641
+ }
642
+
643
+ agent.state = 'error';
644
+ // Don't return - fall through to publish AGENT_ERROR and save failure info
645
+ // This allows the cluster to stop and be resumed after investigation
646
+ }
647
+
648
+ // Non-validator agents: publish error and stop
649
+ agent.state = 'error';
650
+
651
+ // Hook failure: fail the whole cluster so it gets stopped + persisted (prevents deadlocked "running" clusters).
652
+ if (error?.hookFailure) {
653
+ agent._publish({
654
+ topic: 'CLUSTER_FAILED',
655
+ receiver: 'broadcast',
656
+ content: {
657
+ text: `Cluster failed: onComplete hook failed for ${agent.id} - ${error.message}`,
658
+ data: {
659
+ reason: 'on_complete_hook_failed',
660
+ agentId: agent.id,
661
+ role: agent.role,
662
+ hookRetries: error.hookRetries ?? null,
663
+ originalHookError: error.originalHookError ?? null,
664
+ error: error.message,
665
+ },
666
+ },
667
+ });
668
+ }
669
+
670
+ // Save failure info to cluster for resume capability
671
+ agent.cluster.failureInfo = {
672
+ agentId: agent.id,
673
+ taskId: agent.currentTaskId,
674
+ iteration: agent.iteration,
675
+ error: error.message,
676
+ attempts: maxRetries,
677
+ timestamp: Date.now(),
678
+ };
679
+
680
+ // Publish error to message bus for visibility in logs
681
+ agent._publish({
682
+ topic: 'AGENT_ERROR',
683
+ receiver: 'broadcast',
684
+ content: {
685
+ text: `Task execution failed after ${maxRetries} attempts: ${error.message}`,
686
+ data: {
687
+ error: error.message,
688
+ stack: error.stack,
689
+ hookFailure: error?.hookFailure === true,
690
+ hookRetries: error?.hookRetries ?? undefined,
691
+ originalHookError: error?.originalHookError ?? undefined,
692
+ agent: agent.id,
693
+ role: agent.role,
694
+ iteration: agent.iteration,
695
+ taskId: agent.currentTaskId,
696
+ attempts: maxRetries,
697
+ hookFailureContext: error.message.includes('Hook uses result')
698
+ ? {
699
+ taskId: agent.currentTaskId || 'UNKNOWN',
700
+ retrieveLogs: agent.currentTaskId
701
+ ? `zeroshot task logs ${agent.currentTaskId}`
702
+ : 'N/A',
703
+ }
704
+ : undefined,
705
+ },
706
+ },
707
+ metadata: {
708
+ triggeringTopic: triggeringMessage.topic,
709
+ },
710
+ });
711
+
712
+ // Execute onError hook
713
+ await executeHook({
714
+ hook: agent.config.hooks?.onError,
715
+ agent: agent,
716
+ message: triggeringMessage,
717
+ result: { error },
718
+ messageBus: agent.messageBus,
719
+ cluster: agent.cluster,
720
+ orchestrator: agent.orchestrator,
721
+ });
722
+
723
+ agent.state = 'idle';
724
+ }
725
+
726
+ async function scheduleRetry(agent, error, attempt, maxRetries, _baseDelay) {
727
+ // Use rate-limit-aware backoff (30s+ for 429s, 2s for others)
728
+ const settings = loadSettings();
729
+ const delay = calculateRateLimitDelay(error, attempt, settings);
730
+ const isRateLimit = isRateLimitError(error);
731
+
732
+ agent._publishLifecycle('RETRY_SCHEDULED', {
733
+ attempt,
734
+ maxRetries,
735
+ delayMs: delay,
736
+ error: error.message,
737
+ isRateLimitError: isRateLimit,
738
+ });
739
+
740
+ const prefix = isRateLimit ? '🔄 Rate limit - ' : '⚠️ ';
741
+ agent._log(
742
+ `[${agent.id}] ${prefix}Retrying in ${Math.round(delay / 1000)}s... (${attempt + 1}/${maxRetries})`
743
+ );
744
+
745
+ // Rate-limit-aware backoff
746
+ await new Promise((resolve) => setTimeout(resolve, delay));
747
+
748
+ agent._log(`[${agent.id}] 🔄 Starting retry attempt ${attempt + 1}/${maxRetries}`);
749
+ }
750
+
751
+ async function handleTaskAttemptFailure({
752
+ agent,
753
+ triggeringMessage,
754
+ error,
755
+ attempt,
756
+ maxRetries,
757
+ baseDelay,
758
+ }) {
759
+ // LOCK CONTENTION: Add extra jittered delay for lock file errors
760
+ // This happens when multiple validators try to run Claude CLI in the same workspace
761
+ const isLockError = error.message && error.message.includes('Lock file');
762
+
763
+ logTaskAttemptFailure(agent, attempt, maxRetries, error);
764
+
765
+ if (isLockError) {
766
+ await handleLockContention();
767
+ } else if (attempt < maxRetries) {
768
+ console.error(`Will retry in ${baseDelay * Math.pow(2, attempt - 1)}ms...`);
769
+ }
770
+ console.error(`${'='.repeat(80)}
771
+ `);
772
+
773
+ if (attempt >= maxRetries) {
774
+ await handleFinalFailure(agent, triggeringMessage, error, maxRetries);
775
+ return true;
776
+ }
777
+
778
+ await scheduleRetry(agent, error, attempt, maxRetries, baseDelay);
779
+ return false;
780
+ }
781
+
782
+ function maybeExtendMaxRetries({
783
+ error,
784
+ attempt,
785
+ maxRetries,
786
+ sigtermRetryGranted,
787
+ noMessagesRetryGranted,
788
+ }) {
789
+ const message = error?.message || '';
790
+ if (!message || attempt < maxRetries) {
791
+ return { maxRetries, sigtermRetryGranted, noMessagesRetryGranted };
792
+ }
793
+
794
+ if (message.includes('SIGTERM') && !sigtermRetryGranted) {
795
+ return { maxRetries: maxRetries + 1, sigtermRetryGranted: true, noMessagesRetryGranted };
796
+ }
797
+
798
+ if (message.toLowerCase().includes('no messages returned') && !noMessagesRetryGranted) {
799
+ return { maxRetries: maxRetries + 1, sigtermRetryGranted, noMessagesRetryGranted: true };
800
+ }
801
+
802
+ return { maxRetries, sigtermRetryGranted, noMessagesRetryGranted };
803
+ }
804
+
805
+ /**
806
+ * Execute claude-zeroshots with built context
807
+ * Default: uses settings.maxRetries (default 3) for exponential backoff retries.
808
+ * Override via agent config `maxRetries` to change retry behavior.
809
+ * @param {AgentWrapper} agent - Agent instance
810
+ * @param {Object} triggeringMessage - Message that triggered execution
811
+ */
812
+ async function executeTask(agent, triggeringMessage) {
813
+ // Early exit if agent was stopped
814
+ if (!agent.running) {
815
+ return;
816
+ }
817
+
818
+ // Default: uses settings.maxRetries (default 3)
819
+ // Override via agent config `maxRetries` to change retry behavior
820
+ const settings = loadSettings();
821
+ let maxRetries = agent.config.maxRetries ?? settings.maxRetries ?? 3;
822
+ const baseDelay = settings.backoffBaseMs ?? 2000;
823
+ let sigtermRetryGranted = false;
824
+ let noMessagesRetryGranted = false;
825
+
826
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
827
+ // Check if agent was stopped between retries
828
+ if (!agent.running) {
829
+ return;
830
+ }
831
+
832
+ try {
833
+ await runTaskAttempt(agent, triggeringMessage);
834
+ return;
835
+ } catch (error) {
836
+ if (!agent.running || agent.state === 'stopped') {
837
+ agent._log(`[${agent.id}] Task interrupted during shutdown; skipping retry`);
838
+ return;
839
+ }
840
+ if (error?.permanent) {
841
+ await handleFinalFailure(agent, triggeringMessage, error, attempt);
842
+ return;
843
+ }
844
+ if (error instanceof HookExecutionError) {
845
+ // Hook failures are deterministic; do not waste tokens retrying the provider task.
846
+ await handleFinalFailure(agent, triggeringMessage, error, 1);
847
+ return;
848
+ }
849
+ const updated = maybeExtendMaxRetries({
850
+ error,
851
+ attempt,
852
+ maxRetries,
853
+ sigtermRetryGranted,
854
+ noMessagesRetryGranted,
855
+ });
856
+ maxRetries = updated.maxRetries;
857
+ sigtermRetryGranted = updated.sigtermRetryGranted;
858
+ noMessagesRetryGranted = updated.noMessagesRetryGranted;
859
+ const shouldStop = await handleTaskAttemptFailure({
860
+ agent,
861
+ triggeringMessage,
862
+ error,
863
+ attempt,
864
+ maxRetries,
865
+ baseDelay,
866
+ });
867
+ if (shouldStop) {
868
+ return;
869
+ }
870
+ }
871
+ }
872
+ }
873
+ function startLivenessCheck(agent) {
874
+ if (agent.livenessCheckInterval) {
875
+ clearInterval(agent.livenessCheckInterval);
876
+ }
877
+
878
+ // Check if platform supports /proc filesystem (Linux only)
879
+ if (!isPlatformSupported()) {
880
+ agent._log(
881
+ `[${agent.id}] Liveness check disabled: /proc filesystem not available (non-Linux platform)`
882
+ );
883
+ return;
884
+ }
885
+
886
+ // Check every 60 seconds (gives time for multi-indicator analysis)
887
+ const CHECK_INTERVAL_MS = 60 * 1000;
888
+ const ANALYSIS_SAMPLE_MS = 5000; // Sample CPU/context switches over 5 seconds
889
+
890
+ agent.livenessCheckInterval = setInterval(async () => {
891
+ // Skip if no task running or no PID tracked
892
+ if (!agent.currentTask || !agent.processPid) {
893
+ return;
894
+ }
895
+
896
+ // Skip if output is recent (process is clearly active)
897
+ if (agent.lastOutputTime) {
898
+ const timeSinceLastOutput = Date.now() - agent.lastOutputTime;
899
+ if (timeSinceLastOutput < agent.staleDuration) {
900
+ return; // Output is recent, definitely not stuck
901
+ }
902
+ }
903
+
904
+ // Output is stale - run multi-indicator analysis to confirm
905
+ agent._log(
906
+ `[${agent.id}] Output stale for ${Math.round((Date.now() - (agent.lastOutputTime || 0)) / 1000)}s, running multi-indicator analysis...`
907
+ );
908
+
909
+ try {
910
+ const analysis = await analyzeProcessHealth(agent.processPid, ANALYSIS_SAMPLE_MS);
911
+
912
+ // Process died during analysis
913
+ if (analysis.isLikelyStuck === null) {
914
+ agent._log(`[${agent.id}] Process analysis inconclusive: ${analysis.reason}`);
915
+ return;
916
+ }
917
+
918
+ // Log analysis details for debugging
919
+ agent._log(
920
+ `[${agent.id}] Analysis: score=${analysis.stuckScore}/${STUCK_THRESHOLD}, ` +
921
+ `state=${analysis.state}, wchan=${analysis.wchan}, ` +
922
+ `CPU=${analysis.cpuPercent}%, ctxSwitches=${analysis.ctxSwitchesDelta}`
923
+ );
924
+
925
+ if (analysis.isLikelyStuck) {
926
+ agent._log(`⚠️ Agent ${agent.id}: CONFIRMED STUCK (confidence: ${analysis.confidence})`);
927
+ agent._log(` ${analysis.analysis}`);
928
+
929
+ // CHANGED: Stale detection is informational only - never kills tasks
930
+ // Publish stale detection event with full analysis (for logging/monitoring)
931
+ agent._publishLifecycle('AGENT_STALE_WARNING', {
932
+ timeSinceLastOutput: Date.now() - (agent.lastOutputTime || 0),
933
+ staleDuration: agent.staleDuration,
934
+ lastOutputTime: agent.lastOutputTime,
935
+ // Multi-indicator analysis results
936
+ stuckScore: analysis.stuckScore,
937
+ confidence: analysis.confidence,
938
+ processState: analysis.state,
939
+ wchan: analysis.wchan,
940
+ cpuPercent: analysis.cpuPercent,
941
+ ctxSwitchesDelta: analysis.ctxSwitchesDelta,
942
+ indicators: analysis.indicators,
943
+ analysis: analysis.analysis,
944
+ });
945
+
946
+ // Keep monitoring - do NOT stop the agent
947
+ // User can manually intervene with 'zeroshot resume' if needed
948
+ // stopLivenessCheck(agent); // REMOVED - keep monitoring
949
+ } else {
950
+ agent._log(
951
+ `[${agent.id}] Process appears WORKING despite stale output (score: ${analysis.stuckScore})`
952
+ );
953
+ agent._log(` ${analysis.analysis}`);
954
+ // Don't flag as stuck - process is legitimately working
955
+ }
956
+ } catch (err) {
957
+ agent._log(`[${agent.id}] Error during stuck analysis: ${err.message}`);
958
+ // Don't flag as stuck on analysis error
959
+ }
960
+ }, CHECK_INTERVAL_MS);
961
+ }
962
+
963
+ /**
964
+ * Stop liveness monitoring
965
+ * @param {AgentWrapper} agent - Agent instance
966
+ */
967
+ function stopLivenessCheck(agent) {
968
+ if (agent.livenessCheckInterval) {
969
+ clearInterval(agent.livenessCheckInterval);
970
+ agent.livenessCheckInterval = null;
971
+ }
972
+ }
973
+
974
+ module.exports = {
975
+ start,
976
+ stop,
977
+ handleMessage,
978
+ executeTriggerAction,
979
+ executeTask,
980
+ startLivenessCheck,
981
+ stopLivenessCheck,
982
+ };