attocode 0.2.3 → 0.2.5

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 (787) hide show
  1. package/CHANGELOG.md +122 -1
  2. package/README.md +65 -5
  3. package/dist/src/adapters.d.ts +2 -1
  4. package/dist/src/adapters.d.ts.map +1 -1
  5. package/dist/src/adapters.js +74 -12
  6. package/dist/src/adapters.js.map +1 -1
  7. package/dist/src/agent/agent-builder.d.ts +117 -0
  8. package/dist/src/agent/agent-builder.d.ts.map +1 -0
  9. package/dist/src/agent/agent-builder.js +204 -0
  10. package/dist/src/agent/agent-builder.js.map +1 -0
  11. package/dist/src/agent/feature-initializer.d.ts +80 -0
  12. package/dist/src/agent/feature-initializer.d.ts.map +1 -0
  13. package/dist/src/agent/feature-initializer.js +677 -0
  14. package/dist/src/agent/feature-initializer.js.map +1 -0
  15. package/dist/src/agent/index.d.ts +13 -0
  16. package/dist/src/agent/index.d.ts.map +1 -0
  17. package/dist/src/agent/index.js +13 -0
  18. package/dist/src/agent/index.js.map +1 -0
  19. package/dist/src/agent/message-builder.d.ts +50 -0
  20. package/dist/src/agent/message-builder.d.ts.map +1 -0
  21. package/dist/src/agent/message-builder.js +173 -0
  22. package/dist/src/agent/message-builder.js.map +1 -0
  23. package/dist/src/agent/session-api.d.ts +94 -0
  24. package/dist/src/agent/session-api.d.ts.map +1 -0
  25. package/dist/src/agent/session-api.js +262 -0
  26. package/dist/src/agent/session-api.js.map +1 -0
  27. package/dist/src/agent-tools/lsp-file-tools.d.ts +1 -1
  28. package/dist/src/agent-tools/lsp-file-tools.d.ts.map +1 -1
  29. package/dist/src/agent.d.ts +52 -213
  30. package/dist/src/agent.d.ts.map +1 -1
  31. package/dist/src/agent.js +544 -4072
  32. package/dist/src/agent.js.map +1 -1
  33. package/dist/src/cli.d.ts.map +1 -1
  34. package/dist/src/cli.js +2 -1
  35. package/dist/src/cli.js.map +1 -1
  36. package/dist/src/commands/handler.d.ts.map +1 -1
  37. package/dist/src/commands/handler.js +18 -9
  38. package/dist/src/commands/handler.js.map +1 -1
  39. package/dist/src/commands/init-commands.d.ts.map +1 -1
  40. package/dist/src/commands/init-commands.js +16 -1
  41. package/dist/src/commands/init-commands.js.map +1 -1
  42. package/dist/src/commands/init.d.ts.map +1 -1
  43. package/dist/src/commands/init.js +31 -0
  44. package/dist/src/commands/init.js.map +1 -1
  45. package/dist/src/config/base-types.d.ts +45 -0
  46. package/dist/src/config/base-types.d.ts.map +1 -0
  47. package/dist/src/config/base-types.js +9 -0
  48. package/dist/src/config/base-types.js.map +1 -0
  49. package/dist/src/config/config-manager.d.ts +35 -0
  50. package/dist/src/config/config-manager.d.ts.map +1 -0
  51. package/dist/src/config/config-manager.js +108 -0
  52. package/dist/src/config/config-manager.js.map +1 -0
  53. package/dist/src/config/index.d.ts +4 -0
  54. package/dist/src/config/index.d.ts.map +1 -0
  55. package/dist/src/config/index.js +3 -0
  56. package/dist/src/config/index.js.map +1 -0
  57. package/dist/src/config/schema.d.ts +1546 -0
  58. package/dist/src/config/schema.d.ts.map +1 -0
  59. package/dist/src/config/schema.js +268 -0
  60. package/dist/src/config/schema.js.map +1 -0
  61. package/dist/src/config.d.ts +4 -1
  62. package/dist/src/config.d.ts.map +1 -1
  63. package/dist/src/config.js +8 -12
  64. package/dist/src/config.js.map +1 -1
  65. package/dist/src/core/agent-state-machine.d.ts +131 -0
  66. package/dist/src/core/agent-state-machine.d.ts.map +1 -0
  67. package/dist/src/core/agent-state-machine.js +302 -0
  68. package/dist/src/core/agent-state-machine.js.map +1 -0
  69. package/dist/src/core/base-manager.d.ts +79 -0
  70. package/dist/src/core/base-manager.d.ts.map +1 -0
  71. package/dist/src/core/base-manager.js +170 -0
  72. package/dist/src/core/base-manager.js.map +1 -0
  73. package/dist/src/core/completion-analyzer.d.ts +15 -0
  74. package/dist/src/core/completion-analyzer.d.ts.map +1 -0
  75. package/dist/src/core/completion-analyzer.js +53 -0
  76. package/dist/src/core/completion-analyzer.js.map +1 -0
  77. package/dist/src/core/execution-loop.d.ts +46 -0
  78. package/dist/src/core/execution-loop.d.ts.map +1 -0
  79. package/dist/src/core/execution-loop.js +1397 -0
  80. package/dist/src/core/execution-loop.js.map +1 -0
  81. package/dist/src/core/index.d.ts +7 -0
  82. package/dist/src/core/index.d.ts.map +1 -1
  83. package/dist/src/core/index.js +9 -0
  84. package/dist/src/core/index.js.map +1 -1
  85. package/dist/src/core/process-handlers.d.ts.map +1 -1
  86. package/dist/src/core/process-handlers.js +14 -0
  87. package/dist/src/core/process-handlers.js.map +1 -1
  88. package/dist/src/core/protocol/types.d.ts +12 -12
  89. package/dist/src/core/response-handler.d.ts +16 -0
  90. package/dist/src/core/response-handler.d.ts.map +1 -0
  91. package/dist/src/core/response-handler.js +235 -0
  92. package/dist/src/core/response-handler.js.map +1 -0
  93. package/dist/src/core/subagent-spawner.d.ts +43 -0
  94. package/dist/src/core/subagent-spawner.d.ts.map +1 -0
  95. package/dist/src/core/subagent-spawner.js +973 -0
  96. package/dist/src/core/subagent-spawner.js.map +1 -0
  97. package/dist/src/core/tool-executor.d.ts +59 -0
  98. package/dist/src/core/tool-executor.d.ts.map +1 -0
  99. package/dist/src/core/tool-executor.js +682 -0
  100. package/dist/src/core/tool-executor.js.map +1 -0
  101. package/dist/src/core/types.d.ts +134 -0
  102. package/dist/src/core/types.d.ts.map +1 -0
  103. package/dist/src/core/types.js +12 -0
  104. package/dist/src/core/types.js.map +1 -0
  105. package/dist/src/defaults.d.ts +2 -2
  106. package/dist/src/defaults.d.ts.map +1 -1
  107. package/dist/src/defaults.js +29 -1
  108. package/dist/src/defaults.js.map +1 -1
  109. package/dist/src/integrations/agents/agent-registry.d.ts +262 -0
  110. package/dist/src/integrations/agents/agent-registry.d.ts.map +1 -0
  111. package/dist/src/integrations/agents/agent-registry.js +686 -0
  112. package/dist/src/integrations/agents/agent-registry.js.map +1 -0
  113. package/dist/src/integrations/agents/async-subagent.d.ts +135 -0
  114. package/dist/src/integrations/agents/async-subagent.d.ts.map +1 -0
  115. package/dist/src/integrations/agents/async-subagent.js +213 -0
  116. package/dist/src/integrations/agents/async-subagent.js.map +1 -0
  117. package/dist/src/integrations/agents/complexity-classifier.d.ts +86 -0
  118. package/dist/src/integrations/agents/complexity-classifier.d.ts.map +1 -0
  119. package/dist/src/integrations/agents/complexity-classifier.js +233 -0
  120. package/dist/src/integrations/agents/complexity-classifier.js.map +1 -0
  121. package/dist/src/integrations/agents/delegation-protocol.d.ts +86 -0
  122. package/dist/src/integrations/agents/delegation-protocol.d.ts.map +1 -0
  123. package/dist/src/integrations/agents/delegation-protocol.js +127 -0
  124. package/dist/src/integrations/agents/delegation-protocol.js.map +1 -0
  125. package/dist/src/integrations/agents/multi-agent.d.ts +150 -0
  126. package/dist/src/integrations/agents/multi-agent.d.ts.map +1 -0
  127. package/dist/src/integrations/agents/multi-agent.js +306 -0
  128. package/dist/src/integrations/agents/multi-agent.js.map +1 -0
  129. package/dist/src/integrations/agents/result-synthesizer.d.ts +389 -0
  130. package/dist/src/integrations/agents/result-synthesizer.d.ts.map +1 -0
  131. package/dist/src/integrations/agents/result-synthesizer.js +951 -0
  132. package/dist/src/integrations/agents/result-synthesizer.js.map +1 -0
  133. package/dist/src/integrations/agents/shared-blackboard.d.ts +406 -0
  134. package/dist/src/integrations/agents/shared-blackboard.d.ts.map +1 -0
  135. package/dist/src/integrations/agents/shared-blackboard.js +757 -0
  136. package/dist/src/integrations/agents/shared-blackboard.js.map +1 -0
  137. package/dist/src/integrations/agents/subagent-output-store.d.ts +91 -0
  138. package/dist/src/integrations/agents/subagent-output-store.d.ts.map +1 -0
  139. package/dist/src/integrations/agents/subagent-output-store.js +257 -0
  140. package/dist/src/integrations/agents/subagent-output-store.js.map +1 -0
  141. package/dist/src/integrations/auto-compaction.d.ts.map +1 -1
  142. package/dist/src/integrations/auto-compaction.js +3 -2
  143. package/dist/src/integrations/auto-compaction.js.map +1 -1
  144. package/dist/src/integrations/budget/budget-pool.d.ts +115 -0
  145. package/dist/src/integrations/budget/budget-pool.d.ts.map +1 -0
  146. package/dist/src/integrations/budget/budget-pool.js +205 -0
  147. package/dist/src/integrations/budget/budget-pool.js.map +1 -0
  148. package/dist/src/integrations/budget/cancellation.d.ts +229 -0
  149. package/dist/src/integrations/budget/cancellation.d.ts.map +1 -0
  150. package/dist/src/integrations/budget/cancellation.js +520 -0
  151. package/dist/src/integrations/budget/cancellation.js.map +1 -0
  152. package/dist/src/integrations/budget/dynamic-budget.d.ts +81 -0
  153. package/dist/src/integrations/budget/dynamic-budget.d.ts.map +1 -0
  154. package/dist/src/integrations/budget/dynamic-budget.js +151 -0
  155. package/dist/src/integrations/budget/dynamic-budget.js.map +1 -0
  156. package/dist/src/integrations/budget/economics.d.ts +435 -0
  157. package/dist/src/integrations/budget/economics.d.ts.map +1 -0
  158. package/dist/src/integrations/budget/economics.js +1007 -0
  159. package/dist/src/integrations/budget/economics.js.map +1 -0
  160. package/dist/src/integrations/budget/injection-budget.d.ts +71 -0
  161. package/dist/src/integrations/budget/injection-budget.d.ts.map +1 -0
  162. package/dist/src/integrations/budget/injection-budget.js +137 -0
  163. package/dist/src/integrations/budget/injection-budget.js.map +1 -0
  164. package/dist/src/integrations/budget/loop-detector.d.ts +105 -0
  165. package/dist/src/integrations/budget/loop-detector.d.ts.map +1 -0
  166. package/dist/src/integrations/budget/loop-detector.js +287 -0
  167. package/dist/src/integrations/budget/loop-detector.js.map +1 -0
  168. package/dist/src/integrations/budget/phase-tracker.d.ts +114 -0
  169. package/dist/src/integrations/budget/phase-tracker.d.ts.map +1 -0
  170. package/dist/src/integrations/budget/phase-tracker.js +262 -0
  171. package/dist/src/integrations/budget/phase-tracker.js.map +1 -0
  172. package/dist/src/integrations/budget/resources.d.ts +182 -0
  173. package/dist/src/integrations/budget/resources.d.ts.map +1 -0
  174. package/dist/src/integrations/budget/resources.js +318 -0
  175. package/dist/src/integrations/budget/resources.js.map +1 -0
  176. package/dist/src/integrations/budget-pool.d.ts +7 -0
  177. package/dist/src/integrations/budget-pool.d.ts.map +1 -1
  178. package/dist/src/integrations/budget-pool.js +43 -0
  179. package/dist/src/integrations/budget-pool.js.map +1 -1
  180. package/dist/src/integrations/codebase-ast.d.ts +52 -0
  181. package/dist/src/integrations/codebase-ast.d.ts.map +1 -0
  182. package/dist/src/integrations/codebase-ast.js +457 -0
  183. package/dist/src/integrations/codebase-ast.js.map +1 -0
  184. package/dist/src/integrations/codebase-context.d.ts +18 -0
  185. package/dist/src/integrations/codebase-context.d.ts.map +1 -1
  186. package/dist/src/integrations/codebase-context.js +197 -17
  187. package/dist/src/integrations/codebase-context.js.map +1 -1
  188. package/dist/src/integrations/compaction.d.ts.map +1 -1
  189. package/dist/src/integrations/compaction.js +14 -6
  190. package/dist/src/integrations/compaction.js.map +1 -1
  191. package/dist/src/integrations/context/auto-compaction.d.ts +210 -0
  192. package/dist/src/integrations/context/auto-compaction.d.ts.map +1 -0
  193. package/dist/src/integrations/context/auto-compaction.js +477 -0
  194. package/dist/src/integrations/context/auto-compaction.js.map +1 -0
  195. package/dist/src/integrations/context/code-analyzer.d.ts +71 -0
  196. package/dist/src/integrations/context/code-analyzer.d.ts.map +1 -0
  197. package/dist/src/integrations/context/code-analyzer.js +448 -0
  198. package/dist/src/integrations/context/code-analyzer.js.map +1 -0
  199. package/dist/src/integrations/context/code-selector.d.ts +78 -0
  200. package/dist/src/integrations/context/code-selector.d.ts.map +1 -0
  201. package/dist/src/integrations/context/code-selector.js +649 -0
  202. package/dist/src/integrations/context/code-selector.js.map +1 -0
  203. package/dist/src/integrations/context/codebase-ast.d.ts +138 -0
  204. package/dist/src/integrations/context/codebase-ast.d.ts.map +1 -0
  205. package/dist/src/integrations/context/codebase-ast.js +818 -0
  206. package/dist/src/integrations/context/codebase-ast.js.map +1 -0
  207. package/dist/src/integrations/context/codebase-context.d.ts +473 -0
  208. package/dist/src/integrations/context/codebase-context.d.ts.map +1 -0
  209. package/dist/src/integrations/context/codebase-context.js +685 -0
  210. package/dist/src/integrations/context/codebase-context.js.map +1 -0
  211. package/dist/src/integrations/context/compaction.d.ts +191 -0
  212. package/dist/src/integrations/context/compaction.d.ts.map +1 -0
  213. package/dist/src/integrations/context/compaction.js +384 -0
  214. package/dist/src/integrations/context/compaction.js.map +1 -0
  215. package/dist/src/integrations/context/context-engineering.d.ts +274 -0
  216. package/dist/src/integrations/context/context-engineering.d.ts.map +1 -0
  217. package/dist/src/integrations/context/context-engineering.js +437 -0
  218. package/dist/src/integrations/context/context-engineering.js.map +1 -0
  219. package/dist/src/integrations/context/file-cache.d.ts +97 -0
  220. package/dist/src/integrations/context/file-cache.d.ts.map +1 -0
  221. package/dist/src/integrations/context/file-cache.js +218 -0
  222. package/dist/src/integrations/context/file-cache.js.map +1 -0
  223. package/dist/src/integrations/context/semantic-cache.d.ts +178 -0
  224. package/dist/src/integrations/context/semantic-cache.d.ts.map +1 -0
  225. package/dist/src/integrations/context/semantic-cache.js +372 -0
  226. package/dist/src/integrations/context/semantic-cache.js.map +1 -0
  227. package/dist/src/integrations/context-engineering.d.ts +8 -0
  228. package/dist/src/integrations/context-engineering.d.ts.map +1 -1
  229. package/dist/src/integrations/context-engineering.js +19 -0
  230. package/dist/src/integrations/context-engineering.js.map +1 -1
  231. package/dist/src/integrations/economics.d.ts +25 -1
  232. package/dist/src/integrations/economics.d.ts.map +1 -1
  233. package/dist/src/integrations/economics.js +217 -38
  234. package/dist/src/integrations/economics.js.map +1 -1
  235. package/dist/src/integrations/edit-validator.d.ts +30 -0
  236. package/dist/src/integrations/edit-validator.d.ts.map +1 -0
  237. package/dist/src/integrations/edit-validator.js +85 -0
  238. package/dist/src/integrations/edit-validator.js.map +1 -0
  239. package/dist/src/integrations/file-cache.d.ts +7 -0
  240. package/dist/src/integrations/file-cache.d.ts.map +1 -1
  241. package/dist/src/integrations/file-cache.js +54 -0
  242. package/dist/src/integrations/file-cache.js.map +1 -1
  243. package/dist/src/integrations/health-check.d.ts.map +1 -1
  244. package/dist/src/integrations/health-check.js +3 -2
  245. package/dist/src/integrations/health-check.js.map +1 -1
  246. package/dist/src/integrations/hierarchical-config.d.ts +3 -0
  247. package/dist/src/integrations/hierarchical-config.d.ts.map +1 -1
  248. package/dist/src/integrations/hierarchical-config.js +3 -0
  249. package/dist/src/integrations/hierarchical-config.js.map +1 -1
  250. package/dist/src/integrations/hooks.d.ts +2 -0
  251. package/dist/src/integrations/hooks.d.ts.map +1 -1
  252. package/dist/src/integrations/hooks.js +99 -15
  253. package/dist/src/integrations/hooks.js.map +1 -1
  254. package/dist/src/integrations/index.d.ts +77 -66
  255. package/dist/src/integrations/index.d.ts.map +1 -1
  256. package/dist/src/integrations/index.js +83 -67
  257. package/dist/src/integrations/index.js.map +1 -1
  258. package/dist/src/integrations/logger.d.ts +104 -0
  259. package/dist/src/integrations/logger.d.ts.map +1 -0
  260. package/dist/src/integrations/logger.js +219 -0
  261. package/dist/src/integrations/logger.js.map +1 -0
  262. package/dist/src/integrations/lsp/lsp.d.ts +196 -0
  263. package/dist/src/integrations/lsp/lsp.d.ts.map +1 -0
  264. package/dist/src/integrations/lsp/lsp.js +583 -0
  265. package/dist/src/integrations/lsp/lsp.js.map +1 -0
  266. package/dist/src/integrations/lsp.d.ts.map +1 -1
  267. package/dist/src/integrations/lsp.js +5 -4
  268. package/dist/src/integrations/lsp.js.map +1 -1
  269. package/dist/src/integrations/mcp/mcp-client.d.ts +279 -0
  270. package/dist/src/integrations/mcp/mcp-client.d.ts.map +1 -0
  271. package/dist/src/integrations/mcp/mcp-client.js +755 -0
  272. package/dist/src/integrations/mcp/mcp-client.js.map +1 -0
  273. package/dist/src/integrations/mcp/mcp-custom-tools.d.ts +102 -0
  274. package/dist/src/integrations/mcp/mcp-custom-tools.d.ts.map +1 -0
  275. package/dist/src/integrations/mcp/mcp-custom-tools.js +232 -0
  276. package/dist/src/integrations/mcp/mcp-custom-tools.js.map +1 -0
  277. package/dist/src/integrations/mcp/mcp-tool-search.d.ts +77 -0
  278. package/dist/src/integrations/mcp/mcp-tool-search.d.ts.map +1 -0
  279. package/dist/src/integrations/mcp/mcp-tool-search.js +220 -0
  280. package/dist/src/integrations/mcp/mcp-tool-search.js.map +1 -0
  281. package/dist/src/integrations/mcp/mcp-tool-validator.d.ts +60 -0
  282. package/dist/src/integrations/mcp/mcp-tool-validator.d.ts.map +1 -0
  283. package/dist/src/integrations/mcp/mcp-tool-validator.js +141 -0
  284. package/dist/src/integrations/mcp/mcp-tool-validator.js.map +1 -0
  285. package/dist/src/integrations/mcp-client.d.ts.map +1 -1
  286. package/dist/src/integrations/mcp-client.js +8 -7
  287. package/dist/src/integrations/mcp-client.js.map +1 -1
  288. package/dist/src/integrations/observability.d.ts.map +1 -1
  289. package/dist/src/integrations/observability.js +5 -4
  290. package/dist/src/integrations/observability.js.map +1 -1
  291. package/dist/src/integrations/openrouter-pricing.d.ts.map +1 -1
  292. package/dist/src/integrations/openrouter-pricing.js +4 -3
  293. package/dist/src/integrations/openrouter-pricing.js.map +1 -1
  294. package/dist/src/integrations/persistence/codebase-repository.d.ts +45 -0
  295. package/dist/src/integrations/persistence/codebase-repository.d.ts.map +1 -0
  296. package/dist/src/integrations/persistence/codebase-repository.js +81 -0
  297. package/dist/src/integrations/persistence/codebase-repository.js.map +1 -0
  298. package/dist/src/integrations/persistence/goal-repository.d.ts +71 -0
  299. package/dist/src/integrations/persistence/goal-repository.d.ts.map +1 -0
  300. package/dist/src/integrations/persistence/goal-repository.js +184 -0
  301. package/dist/src/integrations/persistence/goal-repository.js.map +1 -0
  302. package/dist/src/integrations/persistence/history.d.ts +72 -0
  303. package/dist/src/integrations/persistence/history.d.ts.map +1 -0
  304. package/dist/src/integrations/persistence/history.js +165 -0
  305. package/dist/src/integrations/persistence/history.js.map +1 -0
  306. package/dist/src/integrations/persistence/persistence.d.ts +49 -0
  307. package/dist/src/integrations/persistence/persistence.d.ts.map +1 -0
  308. package/dist/src/integrations/persistence/persistence.js +197 -0
  309. package/dist/src/integrations/persistence/persistence.js.map +1 -0
  310. package/dist/src/integrations/persistence/session-repository.d.ts +212 -0
  311. package/dist/src/integrations/persistence/session-repository.d.ts.map +1 -0
  312. package/dist/src/integrations/persistence/session-repository.js +770 -0
  313. package/dist/src/integrations/persistence/session-repository.js.map +1 -0
  314. package/dist/src/integrations/persistence/session-store.d.ts +184 -0
  315. package/dist/src/integrations/persistence/session-store.d.ts.map +1 -0
  316. package/dist/src/integrations/persistence/session-store.js +346 -0
  317. package/dist/src/integrations/persistence/session-store.js.map +1 -0
  318. package/dist/src/integrations/persistence/sqlite-store.d.ts +453 -0
  319. package/dist/src/integrations/persistence/sqlite-store.d.ts.map +1 -0
  320. package/dist/src/integrations/persistence/sqlite-store.js +676 -0
  321. package/dist/src/integrations/persistence/sqlite-store.js.map +1 -0
  322. package/dist/src/integrations/persistence/worker-repository.d.ts +65 -0
  323. package/dist/src/integrations/persistence/worker-repository.d.ts.map +1 -0
  324. package/dist/src/integrations/persistence/worker-repository.js +183 -0
  325. package/dist/src/integrations/persistence/worker-repository.js.map +1 -0
  326. package/dist/src/integrations/persistence.d.ts.map +1 -1
  327. package/dist/src/integrations/persistence.js +5 -4
  328. package/dist/src/integrations/persistence.js.map +1 -1
  329. package/dist/src/integrations/planning.d.ts.map +1 -1
  330. package/dist/src/integrations/planning.js +5 -4
  331. package/dist/src/integrations/planning.js.map +1 -1
  332. package/dist/src/integrations/quality/auto-checkpoint.d.ts +98 -0
  333. package/dist/src/integrations/quality/auto-checkpoint.d.ts.map +1 -0
  334. package/dist/src/integrations/quality/auto-checkpoint.js +252 -0
  335. package/dist/src/integrations/quality/auto-checkpoint.js.map +1 -0
  336. package/dist/src/integrations/quality/dead-letter-queue.d.ts +233 -0
  337. package/dist/src/integrations/quality/dead-letter-queue.d.ts.map +1 -0
  338. package/dist/src/integrations/quality/dead-letter-queue.js +543 -0
  339. package/dist/src/integrations/quality/dead-letter-queue.js.map +1 -0
  340. package/dist/src/integrations/quality/health-check.d.ts +218 -0
  341. package/dist/src/integrations/quality/health-check.d.ts.map +1 -0
  342. package/dist/src/integrations/quality/health-check.js +415 -0
  343. package/dist/src/integrations/quality/health-check.js.map +1 -0
  344. package/dist/src/integrations/quality/learning-store.d.ts +291 -0
  345. package/dist/src/integrations/quality/learning-store.d.ts.map +1 -0
  346. package/dist/src/integrations/quality/learning-store.js +646 -0
  347. package/dist/src/integrations/quality/learning-store.js.map +1 -0
  348. package/dist/src/integrations/quality/self-improvement.d.ts +90 -0
  349. package/dist/src/integrations/quality/self-improvement.d.ts.map +1 -0
  350. package/dist/src/integrations/quality/self-improvement.js +229 -0
  351. package/dist/src/integrations/quality/self-improvement.js.map +1 -0
  352. package/dist/src/integrations/quality/tool-recommendation.d.ts +61 -0
  353. package/dist/src/integrations/quality/tool-recommendation.d.ts.map +1 -0
  354. package/dist/src/integrations/quality/tool-recommendation.js +268 -0
  355. package/dist/src/integrations/quality/tool-recommendation.js.map +1 -0
  356. package/dist/src/integrations/retry.d.ts +1 -0
  357. package/dist/src/integrations/retry.d.ts.map +1 -1
  358. package/dist/src/integrations/retry.js.map +1 -1
  359. package/dist/src/integrations/routing.d.ts.map +1 -1
  360. package/dist/src/integrations/routing.js +2 -1
  361. package/dist/src/integrations/routing.js.map +1 -1
  362. package/dist/src/integrations/safety/bash-policy.d.ts +33 -0
  363. package/dist/src/integrations/safety/bash-policy.d.ts.map +1 -0
  364. package/dist/src/integrations/safety/bash-policy.js +144 -0
  365. package/dist/src/integrations/safety/bash-policy.js.map +1 -0
  366. package/dist/src/integrations/safety/edit-validator.d.ts +30 -0
  367. package/dist/src/integrations/safety/edit-validator.d.ts.map +1 -0
  368. package/dist/src/integrations/safety/edit-validator.js +87 -0
  369. package/dist/src/integrations/safety/edit-validator.js.map +1 -0
  370. package/dist/src/integrations/safety/execution-policy.d.ts +189 -0
  371. package/dist/src/integrations/safety/execution-policy.d.ts.map +1 -0
  372. package/dist/src/integrations/safety/execution-policy.js +352 -0
  373. package/dist/src/integrations/safety/execution-policy.js.map +1 -0
  374. package/dist/src/integrations/safety/policy-engine.d.ts +55 -0
  375. package/dist/src/integrations/safety/policy-engine.d.ts.map +1 -0
  376. package/dist/src/integrations/safety/policy-engine.js +247 -0
  377. package/dist/src/integrations/safety/policy-engine.js.map +1 -0
  378. package/dist/src/integrations/safety/safety.d.ts +174 -0
  379. package/dist/src/integrations/safety/safety.d.ts.map +1 -0
  380. package/dist/src/integrations/safety/safety.js +470 -0
  381. package/dist/src/integrations/safety/safety.js.map +1 -0
  382. package/dist/src/integrations/safety/sandbox/basic.d.ts +81 -0
  383. package/dist/src/integrations/safety/sandbox/basic.d.ts.map +1 -0
  384. package/dist/src/integrations/safety/sandbox/basic.js +335 -0
  385. package/dist/src/integrations/safety/sandbox/basic.js.map +1 -0
  386. package/dist/src/integrations/safety/sandbox/docker.d.ts +94 -0
  387. package/dist/src/integrations/safety/sandbox/docker.d.ts.map +1 -0
  388. package/dist/src/integrations/safety/sandbox/docker.js +294 -0
  389. package/dist/src/integrations/safety/sandbox/docker.js.map +1 -0
  390. package/dist/src/integrations/safety/sandbox/index.d.ts +188 -0
  391. package/dist/src/integrations/safety/sandbox/index.d.ts.map +1 -0
  392. package/dist/src/integrations/safety/sandbox/index.js +386 -0
  393. package/dist/src/integrations/safety/sandbox/index.js.map +1 -0
  394. package/dist/src/integrations/safety/sandbox/landlock.d.ts +59 -0
  395. package/dist/src/integrations/safety/sandbox/landlock.d.ts.map +1 -0
  396. package/dist/src/integrations/safety/sandbox/landlock.js +329 -0
  397. package/dist/src/integrations/safety/sandbox/landlock.js.map +1 -0
  398. package/dist/src/integrations/safety/sandbox/seatbelt.d.ts +68 -0
  399. package/dist/src/integrations/safety/sandbox/seatbelt.d.ts.map +1 -0
  400. package/dist/src/integrations/safety/sandbox/seatbelt.js +298 -0
  401. package/dist/src/integrations/safety/sandbox/seatbelt.js.map +1 -0
  402. package/dist/src/integrations/safety/type-checker.d.ts +53 -0
  403. package/dist/src/integrations/safety/type-checker.d.ts.map +1 -0
  404. package/dist/src/integrations/safety/type-checker.js +142 -0
  405. package/dist/src/integrations/safety/type-checker.js.map +1 -0
  406. package/dist/src/integrations/safety.d.ts.map +1 -1
  407. package/dist/src/integrations/safety.js +13 -13
  408. package/dist/src/integrations/safety.js.map +1 -1
  409. package/dist/src/integrations/sandbox/docker.d.ts.map +1 -1
  410. package/dist/src/integrations/sandbox/docker.js +2 -1
  411. package/dist/src/integrations/sandbox/docker.js.map +1 -1
  412. package/dist/src/integrations/sandbox/index.d.ts.map +1 -1
  413. package/dist/src/integrations/sandbox/index.js +5 -4
  414. package/dist/src/integrations/sandbox/index.js.map +1 -1
  415. package/dist/src/integrations/session-store.d.ts +1 -0
  416. package/dist/src/integrations/session-store.d.ts.map +1 -1
  417. package/dist/src/integrations/session-store.js +1 -0
  418. package/dist/src/integrations/session-store.js.map +1 -1
  419. package/dist/src/integrations/shared-blackboard.d.ts +3 -0
  420. package/dist/src/integrations/shared-blackboard.d.ts.map +1 -1
  421. package/dist/src/integrations/shared-blackboard.js +47 -0
  422. package/dist/src/integrations/shared-blackboard.js.map +1 -1
  423. package/dist/src/integrations/skills/skill-executor.d.ts +113 -0
  424. package/dist/src/integrations/skills/skill-executor.d.ts.map +1 -0
  425. package/dist/src/integrations/skills/skill-executor.js +270 -0
  426. package/dist/src/integrations/skills/skill-executor.js.map +1 -0
  427. package/dist/src/integrations/skills/skills.d.ts +262 -0
  428. package/dist/src/integrations/skills/skills.d.ts.map +1 -0
  429. package/dist/src/integrations/skills/skills.js +602 -0
  430. package/dist/src/integrations/skills/skills.js.map +1 -0
  431. package/dist/src/integrations/smart-decomposer.d.ts +27 -0
  432. package/dist/src/integrations/smart-decomposer.d.ts.map +1 -1
  433. package/dist/src/integrations/smart-decomposer.js +414 -30
  434. package/dist/src/integrations/smart-decomposer.js.map +1 -1
  435. package/dist/src/integrations/sqlite-store.d.ts +2 -0
  436. package/dist/src/integrations/sqlite-store.d.ts.map +1 -1
  437. package/dist/src/integrations/sqlite-store.js +18 -6
  438. package/dist/src/integrations/sqlite-store.js.map +1 -1
  439. package/dist/src/integrations/streaming/pty-shell.d.ts +169 -0
  440. package/dist/src/integrations/streaming/pty-shell.d.ts.map +1 -0
  441. package/dist/src/integrations/streaming/pty-shell.js +367 -0
  442. package/dist/src/integrations/streaming/pty-shell.js.map +1 -0
  443. package/dist/src/integrations/streaming/streaming.d.ts +102 -0
  444. package/dist/src/integrations/streaming/streaming.d.ts.map +1 -0
  445. package/dist/src/integrations/streaming/streaming.js +362 -0
  446. package/dist/src/integrations/streaming/streaming.js.map +1 -0
  447. package/dist/src/integrations/swarm/failure-classifier.d.ts +11 -0
  448. package/dist/src/integrations/swarm/failure-classifier.d.ts.map +1 -0
  449. package/dist/src/integrations/swarm/failure-classifier.js +95 -0
  450. package/dist/src/integrations/swarm/failure-classifier.js.map +1 -0
  451. package/dist/src/integrations/swarm/index.d.ts +2 -1
  452. package/dist/src/integrations/swarm/index.d.ts.map +1 -1
  453. package/dist/src/integrations/swarm/index.js +2 -0
  454. package/dist/src/integrations/swarm/index.js.map +1 -1
  455. package/dist/src/integrations/swarm/model-selector.d.ts.map +1 -1
  456. package/dist/src/integrations/swarm/model-selector.js +2 -1
  457. package/dist/src/integrations/swarm/model-selector.js.map +1 -1
  458. package/dist/src/integrations/swarm/swarm-budget.d.ts +1 -1
  459. package/dist/src/integrations/swarm/swarm-budget.d.ts.map +1 -1
  460. package/dist/src/integrations/swarm/swarm-budget.js +1 -1
  461. package/dist/src/integrations/swarm/swarm-budget.js.map +1 -1
  462. package/dist/src/integrations/swarm/swarm-config-loader.d.ts +8 -0
  463. package/dist/src/integrations/swarm/swarm-config-loader.d.ts.map +1 -1
  464. package/dist/src/integrations/swarm/swarm-config-loader.js +102 -0
  465. package/dist/src/integrations/swarm/swarm-config-loader.js.map +1 -1
  466. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts +74 -0
  467. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts.map +1 -1
  468. package/dist/src/integrations/swarm/swarm-event-bridge.js +37 -0
  469. package/dist/src/integrations/swarm/swarm-event-bridge.js.map +1 -1
  470. package/dist/src/integrations/swarm/swarm-events.d.ts +4 -1
  471. package/dist/src/integrations/swarm/swarm-events.d.ts.map +1 -1
  472. package/dist/src/integrations/swarm/swarm-events.js +1 -1
  473. package/dist/src/integrations/swarm/swarm-events.js.map +1 -1
  474. package/dist/src/integrations/swarm/swarm-execution.d.ts +27 -0
  475. package/dist/src/integrations/swarm/swarm-execution.d.ts.map +1 -0
  476. package/dist/src/integrations/swarm/swarm-execution.js +1021 -0
  477. package/dist/src/integrations/swarm/swarm-execution.js.map +1 -0
  478. package/dist/src/integrations/swarm/swarm-helpers.d.ts +26 -0
  479. package/dist/src/integrations/swarm/swarm-helpers.d.ts.map +1 -0
  480. package/dist/src/integrations/swarm/swarm-helpers.js +95 -0
  481. package/dist/src/integrations/swarm/swarm-helpers.js.map +1 -0
  482. package/dist/src/integrations/swarm/swarm-lifecycle.d.ts +100 -0
  483. package/dist/src/integrations/swarm/swarm-lifecycle.d.ts.map +1 -0
  484. package/dist/src/integrations/swarm/swarm-lifecycle.js +922 -0
  485. package/dist/src/integrations/swarm/swarm-lifecycle.js.map +1 -0
  486. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts +96 -192
  487. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts.map +1 -1
  488. package/dist/src/integrations/swarm/swarm-orchestrator.js +384 -2528
  489. package/dist/src/integrations/swarm/swarm-orchestrator.js.map +1 -1
  490. package/dist/src/integrations/swarm/swarm-quality-gate.js +1 -1
  491. package/dist/src/integrations/swarm/swarm-quality-gate.js.map +1 -1
  492. package/dist/src/integrations/swarm/swarm-recovery.d.ts +75 -0
  493. package/dist/src/integrations/swarm/swarm-recovery.d.ts.map +1 -0
  494. package/dist/src/integrations/swarm/swarm-recovery.js +550 -0
  495. package/dist/src/integrations/swarm/swarm-recovery.js.map +1 -0
  496. package/dist/src/integrations/swarm/swarm-state-store.d.ts +4 -1
  497. package/dist/src/integrations/swarm/swarm-state-store.d.ts.map +1 -1
  498. package/dist/src/integrations/swarm/swarm-state-store.js +14 -1
  499. package/dist/src/integrations/swarm/swarm-state-store.js.map +1 -1
  500. package/dist/src/integrations/swarm/task-queue.d.ts +11 -1
  501. package/dist/src/integrations/swarm/task-queue.d.ts.map +1 -1
  502. package/dist/src/integrations/swarm/task-queue.js +64 -2
  503. package/dist/src/integrations/swarm/task-queue.js.map +1 -1
  504. package/dist/src/integrations/swarm/types.d.ts +57 -4
  505. package/dist/src/integrations/swarm/types.d.ts.map +1 -1
  506. package/dist/src/integrations/swarm/types.js +9 -0
  507. package/dist/src/integrations/swarm/types.js.map +1 -1
  508. package/dist/src/integrations/swarm/worker-pool.d.ts +13 -3
  509. package/dist/src/integrations/swarm/worker-pool.d.ts.map +1 -1
  510. package/dist/src/integrations/swarm/worker-pool.js +66 -13
  511. package/dist/src/integrations/swarm/worker-pool.js.map +1 -1
  512. package/dist/src/integrations/task-manager.d.ts +33 -1
  513. package/dist/src/integrations/task-manager.d.ts.map +1 -1
  514. package/dist/src/integrations/task-manager.js +78 -4
  515. package/dist/src/integrations/task-manager.js.map +1 -1
  516. package/dist/src/integrations/tasks/dependency-analyzer.d.ts +34 -0
  517. package/dist/src/integrations/tasks/dependency-analyzer.d.ts.map +1 -0
  518. package/dist/src/integrations/tasks/dependency-analyzer.js +232 -0
  519. package/dist/src/integrations/tasks/dependency-analyzer.js.map +1 -0
  520. package/dist/src/integrations/tasks/interactive-planning.d.ts +322 -0
  521. package/dist/src/integrations/tasks/interactive-planning.d.ts.map +1 -0
  522. package/dist/src/integrations/tasks/interactive-planning.js +655 -0
  523. package/dist/src/integrations/tasks/interactive-planning.js.map +1 -0
  524. package/dist/src/integrations/tasks/pending-plan.d.ts +196 -0
  525. package/dist/src/integrations/tasks/pending-plan.d.ts.map +1 -0
  526. package/dist/src/integrations/tasks/pending-plan.js +431 -0
  527. package/dist/src/integrations/tasks/pending-plan.js.map +1 -0
  528. package/dist/src/integrations/tasks/planning.d.ts +115 -0
  529. package/dist/src/integrations/tasks/planning.d.ts.map +1 -0
  530. package/dist/src/integrations/tasks/planning.js +413 -0
  531. package/dist/src/integrations/tasks/planning.js.map +1 -0
  532. package/dist/src/integrations/tasks/smart-decomposer.d.ts +316 -0
  533. package/dist/src/integrations/tasks/smart-decomposer.d.ts.map +1 -0
  534. package/dist/src/integrations/tasks/smart-decomposer.js +661 -0
  535. package/dist/src/integrations/tasks/smart-decomposer.js.map +1 -0
  536. package/dist/src/integrations/tasks/task-manager.d.ts +164 -0
  537. package/dist/src/integrations/tasks/task-manager.d.ts.map +1 -0
  538. package/dist/src/integrations/tasks/task-manager.js +383 -0
  539. package/dist/src/integrations/tasks/task-manager.js.map +1 -0
  540. package/dist/src/integrations/tasks/task-splitter.d.ts +56 -0
  541. package/dist/src/integrations/tasks/task-splitter.d.ts.map +1 -0
  542. package/dist/src/integrations/tasks/task-splitter.js +537 -0
  543. package/dist/src/integrations/tasks/task-splitter.js.map +1 -0
  544. package/dist/src/integrations/tasks/verification-gate.d.ts +103 -0
  545. package/dist/src/integrations/tasks/verification-gate.d.ts.map +1 -0
  546. package/dist/src/integrations/tasks/verification-gate.js +193 -0
  547. package/dist/src/integrations/tasks/verification-gate.js.map +1 -0
  548. package/dist/src/integrations/tasks/work-log.d.ts +87 -0
  549. package/dist/src/integrations/tasks/work-log.d.ts.map +1 -0
  550. package/dist/src/integrations/tasks/work-log.js +275 -0
  551. package/dist/src/integrations/tasks/work-log.js.map +1 -0
  552. package/dist/src/integrations/utilities/capabilities.d.ts +160 -0
  553. package/dist/src/integrations/utilities/capabilities.d.ts.map +1 -0
  554. package/dist/src/integrations/utilities/capabilities.js +426 -0
  555. package/dist/src/integrations/utilities/capabilities.js.map +1 -0
  556. package/dist/src/integrations/utilities/diff-utils.d.ts +105 -0
  557. package/dist/src/integrations/utilities/diff-utils.d.ts.map +1 -0
  558. package/dist/src/integrations/utilities/diff-utils.js +497 -0
  559. package/dist/src/integrations/utilities/diff-utils.js.map +1 -0
  560. package/dist/src/integrations/utilities/environment-facts.d.ts +52 -0
  561. package/dist/src/integrations/utilities/environment-facts.d.ts.map +1 -0
  562. package/dist/src/integrations/utilities/environment-facts.js +84 -0
  563. package/dist/src/integrations/utilities/environment-facts.js.map +1 -0
  564. package/dist/src/integrations/utilities/file-change-tracker.d.ts +162 -0
  565. package/dist/src/integrations/utilities/file-change-tracker.d.ts.map +1 -0
  566. package/dist/src/integrations/utilities/file-change-tracker.js +538 -0
  567. package/dist/src/integrations/utilities/file-change-tracker.js.map +1 -0
  568. package/dist/src/integrations/utilities/graph-visualization.d.ts +72 -0
  569. package/dist/src/integrations/utilities/graph-visualization.d.ts.map +1 -0
  570. package/dist/src/integrations/utilities/graph-visualization.js +383 -0
  571. package/dist/src/integrations/utilities/graph-visualization.js.map +1 -0
  572. package/dist/src/integrations/utilities/hierarchical-config.d.ts +215 -0
  573. package/dist/src/integrations/utilities/hierarchical-config.d.ts.map +1 -0
  574. package/dist/src/integrations/utilities/hierarchical-config.js +504 -0
  575. package/dist/src/integrations/utilities/hierarchical-config.js.map +1 -0
  576. package/dist/src/integrations/utilities/hooks.d.ts +116 -0
  577. package/dist/src/integrations/utilities/hooks.d.ts.map +1 -0
  578. package/dist/src/integrations/utilities/hooks.js +410 -0
  579. package/dist/src/integrations/utilities/hooks.js.map +1 -0
  580. package/dist/src/integrations/utilities/ignore.d.ts +143 -0
  581. package/dist/src/integrations/utilities/ignore.d.ts.map +1 -0
  582. package/dist/src/integrations/utilities/ignore.js +417 -0
  583. package/dist/src/integrations/utilities/ignore.js.map +1 -0
  584. package/dist/src/integrations/utilities/image-renderer.d.ts +119 -0
  585. package/dist/src/integrations/utilities/image-renderer.d.ts.map +1 -0
  586. package/dist/src/integrations/utilities/image-renderer.js +306 -0
  587. package/dist/src/integrations/utilities/image-renderer.js.map +1 -0
  588. package/dist/src/integrations/utilities/logger.d.ts +104 -0
  589. package/dist/src/integrations/utilities/logger.d.ts.map +1 -0
  590. package/dist/src/integrations/utilities/logger.js +219 -0
  591. package/dist/src/integrations/utilities/logger.js.map +1 -0
  592. package/dist/src/integrations/utilities/memory.d.ts +116 -0
  593. package/dist/src/integrations/utilities/memory.d.ts.map +1 -0
  594. package/dist/src/integrations/utilities/memory.js +311 -0
  595. package/dist/src/integrations/utilities/memory.js.map +1 -0
  596. package/dist/src/integrations/utilities/observability.d.ts +162 -0
  597. package/dist/src/integrations/utilities/observability.d.ts.map +1 -0
  598. package/dist/src/integrations/utilities/observability.js +407 -0
  599. package/dist/src/integrations/utilities/observability.js.map +1 -0
  600. package/dist/src/integrations/utilities/openrouter-pricing.d.ts +67 -0
  601. package/dist/src/integrations/utilities/openrouter-pricing.d.ts.map +1 -0
  602. package/dist/src/integrations/utilities/openrouter-pricing.js +166 -0
  603. package/dist/src/integrations/utilities/openrouter-pricing.js.map +1 -0
  604. package/dist/src/integrations/utilities/react.d.ts +139 -0
  605. package/dist/src/integrations/utilities/react.d.ts.map +1 -0
  606. package/dist/src/integrations/utilities/react.js +273 -0
  607. package/dist/src/integrations/utilities/react.js.map +1 -0
  608. package/dist/src/integrations/utilities/retry.d.ts +132 -0
  609. package/dist/src/integrations/utilities/retry.d.ts.map +1 -0
  610. package/dist/src/integrations/utilities/retry.js +233 -0
  611. package/dist/src/integrations/utilities/retry.js.map +1 -0
  612. package/dist/src/integrations/utilities/routing.d.ts +118 -0
  613. package/dist/src/integrations/utilities/routing.d.ts.map +1 -0
  614. package/dist/src/integrations/utilities/routing.js +348 -0
  615. package/dist/src/integrations/utilities/routing.js.map +1 -0
  616. package/dist/src/integrations/utilities/rules.d.ts +131 -0
  617. package/dist/src/integrations/utilities/rules.d.ts.map +1 -0
  618. package/dist/src/integrations/utilities/rules.js +284 -0
  619. package/dist/src/integrations/utilities/rules.js.map +1 -0
  620. package/dist/src/integrations/utilities/sourcegraph.d.ts +169 -0
  621. package/dist/src/integrations/utilities/sourcegraph.d.ts.map +1 -0
  622. package/dist/src/integrations/utilities/sourcegraph.js +379 -0
  623. package/dist/src/integrations/utilities/sourcegraph.js.map +1 -0
  624. package/dist/src/integrations/utilities/thinking-strategy.d.ts +52 -0
  625. package/dist/src/integrations/utilities/thinking-strategy.d.ts.map +1 -0
  626. package/dist/src/integrations/utilities/thinking-strategy.js +129 -0
  627. package/dist/src/integrations/utilities/thinking-strategy.js.map +1 -0
  628. package/dist/src/integrations/utilities/thread-manager.d.ts +199 -0
  629. package/dist/src/integrations/utilities/thread-manager.d.ts.map +1 -0
  630. package/dist/src/integrations/utilities/thread-manager.js +357 -0
  631. package/dist/src/integrations/utilities/thread-manager.js.map +1 -0
  632. package/dist/src/integrations/utilities/token-estimate.d.ts +11 -0
  633. package/dist/src/integrations/utilities/token-estimate.d.ts.map +1 -0
  634. package/dist/src/integrations/utilities/token-estimate.js +14 -0
  635. package/dist/src/integrations/utilities/token-estimate.js.map +1 -0
  636. package/dist/src/main.js +92 -35
  637. package/dist/src/main.js.map +1 -1
  638. package/dist/src/modes/repl.d.ts.map +1 -1
  639. package/dist/src/modes/repl.js +60 -11
  640. package/dist/src/modes/repl.js.map +1 -1
  641. package/dist/src/modes/tui.d.ts.map +1 -1
  642. package/dist/src/modes/tui.js +57 -10
  643. package/dist/src/modes/tui.js.map +1 -1
  644. package/dist/src/modes.js +1 -1
  645. package/dist/src/modes.js.map +1 -1
  646. package/dist/src/observability/tracer.d.ts.map +1 -1
  647. package/dist/src/observability/tracer.js +2 -1
  648. package/dist/src/observability/tracer.js.map +1 -1
  649. package/dist/src/persistence/schema.d.ts +2 -0
  650. package/dist/src/persistence/schema.d.ts.map +1 -1
  651. package/dist/src/persistence/schema.js +42 -0
  652. package/dist/src/persistence/schema.js.map +1 -1
  653. package/dist/src/providers/adapters/anthropic.d.ts +6 -0
  654. package/dist/src/providers/adapters/anthropic.d.ts.map +1 -1
  655. package/dist/src/providers/adapters/anthropic.js +101 -16
  656. package/dist/src/providers/adapters/anthropic.js.map +1 -1
  657. package/dist/src/providers/adapters/azure.d.ts +74 -0
  658. package/dist/src/providers/adapters/azure.d.ts.map +1 -0
  659. package/dist/src/providers/adapters/azure.js +354 -0
  660. package/dist/src/providers/adapters/azure.js.map +1 -0
  661. package/dist/src/providers/adapters/mock.d.ts +16 -2
  662. package/dist/src/providers/adapters/mock.d.ts.map +1 -1
  663. package/dist/src/providers/adapters/mock.js +44 -3
  664. package/dist/src/providers/adapters/mock.js.map +1 -1
  665. package/dist/src/providers/adapters/openai.d.ts +6 -1
  666. package/dist/src/providers/adapters/openai.d.ts.map +1 -1
  667. package/dist/src/providers/adapters/openai.js +41 -9
  668. package/dist/src/providers/adapters/openai.js.map +1 -1
  669. package/dist/src/providers/adapters/openrouter.d.ts +6 -0
  670. package/dist/src/providers/adapters/openrouter.d.ts.map +1 -1
  671. package/dist/src/providers/adapters/openrouter.js +83 -13
  672. package/dist/src/providers/adapters/openrouter.js.map +1 -1
  673. package/dist/src/providers/circuit-breaker.d.ts +1 -0
  674. package/dist/src/providers/circuit-breaker.d.ts.map +1 -1
  675. package/dist/src/providers/circuit-breaker.js.map +1 -1
  676. package/dist/src/providers/provider.d.ts.map +1 -1
  677. package/dist/src/providers/provider.js +2 -1
  678. package/dist/src/providers/provider.js.map +1 -1
  679. package/dist/src/providers/resilient-provider.d.ts.map +1 -1
  680. package/dist/src/providers/resilient-provider.js +2 -1
  681. package/dist/src/providers/resilient-provider.js.map +1 -1
  682. package/dist/src/providers/types.d.ts +23 -2
  683. package/dist/src/providers/types.d.ts.map +1 -1
  684. package/dist/src/session-picker.d.ts +1 -1
  685. package/dist/src/session-picker.d.ts.map +1 -1
  686. package/dist/src/session-picker.js +40 -5
  687. package/dist/src/session-picker.js.map +1 -1
  688. package/dist/src/shared/budget-tracker.d.ts +65 -0
  689. package/dist/src/shared/budget-tracker.d.ts.map +1 -0
  690. package/dist/src/shared/budget-tracker.js +128 -0
  691. package/dist/src/shared/budget-tracker.js.map +1 -0
  692. package/dist/src/shared/context-engine.d.ts +64 -0
  693. package/dist/src/shared/context-engine.d.ts.map +1 -0
  694. package/dist/src/shared/context-engine.js +117 -0
  695. package/dist/src/shared/context-engine.js.map +1 -0
  696. package/dist/src/shared/index.d.ts +12 -0
  697. package/dist/src/shared/index.d.ts.map +1 -0
  698. package/dist/src/shared/index.js +12 -0
  699. package/dist/src/shared/index.js.map +1 -0
  700. package/dist/src/shared/persistence.d.ts +57 -0
  701. package/dist/src/shared/persistence.d.ts.map +1 -0
  702. package/dist/src/shared/persistence.js +168 -0
  703. package/dist/src/shared/persistence.js.map +1 -0
  704. package/dist/src/shared/shared-context-state.d.ts +89 -0
  705. package/dist/src/shared/shared-context-state.d.ts.map +1 -0
  706. package/dist/src/shared/shared-context-state.js +175 -0
  707. package/dist/src/shared/shared-context-state.js.map +1 -0
  708. package/dist/src/shared/shared-economics-state.d.ts +61 -0
  709. package/dist/src/shared/shared-economics-state.d.ts.map +1 -0
  710. package/dist/src/shared/shared-economics-state.js +100 -0
  711. package/dist/src/shared/shared-economics-state.js.map +1 -0
  712. package/dist/src/tools/agent.d.ts +1 -1
  713. package/dist/src/tools/agent.d.ts.map +1 -1
  714. package/dist/src/tools/bash.d.ts +3 -3
  715. package/dist/src/tools/bash.d.ts.map +1 -1
  716. package/dist/src/tools/bash.js +2 -1
  717. package/dist/src/tools/bash.js.map +1 -1
  718. package/dist/src/tools/file.d.ts +3 -3
  719. package/dist/src/tools/file.js +1 -1
  720. package/dist/src/tools/file.js.map +1 -1
  721. package/dist/src/tools/permission.d.ts.map +1 -1
  722. package/dist/src/tools/permission.js +7 -6
  723. package/dist/src/tools/permission.js.map +1 -1
  724. package/dist/src/tools/registry.d.ts +1 -1
  725. package/dist/src/tools/registry.d.ts.map +1 -1
  726. package/dist/src/tools/registry.js +1 -1
  727. package/dist/src/tools/registry.js.map +1 -1
  728. package/dist/src/tools/tasks.d.ts +1 -1
  729. package/dist/src/tools/tasks.d.ts.map +1 -1
  730. package/dist/src/tools/types.d.ts +1 -0
  731. package/dist/src/tools/types.d.ts.map +1 -1
  732. package/dist/src/tools/types.js.map +1 -1
  733. package/dist/src/tools/undo.d.ts +1 -1
  734. package/dist/src/tools/undo.d.ts.map +1 -1
  735. package/dist/src/tracing/cache-boundary-tracker.d.ts.map +1 -1
  736. package/dist/src/tracing/cache-boundary-tracker.js +2 -2
  737. package/dist/src/tracing/cache-boundary-tracker.js.map +1 -1
  738. package/dist/src/tracing/trace-collector.d.ts +147 -0
  739. package/dist/src/tracing/trace-collector.d.ts.map +1 -1
  740. package/dist/src/tracing/trace-collector.js +138 -7
  741. package/dist/src/tracing/trace-collector.js.map +1 -1
  742. package/dist/src/tracing/types.d.ts +112 -1
  743. package/dist/src/tracing/types.d.ts.map +1 -1
  744. package/dist/src/tracing/types.js.map +1 -1
  745. package/dist/src/tricks/failure-evidence.d.ts.map +1 -1
  746. package/dist/src/tricks/failure-evidence.js +2 -1
  747. package/dist/src/tricks/failure-evidence.js.map +1 -1
  748. package/dist/src/tricks/recitation.d.ts.map +1 -1
  749. package/dist/src/tricks/recitation.js +2 -1
  750. package/dist/src/tricks/recitation.js.map +1 -1
  751. package/dist/src/tricks/recursive-context.d.ts.map +1 -1
  752. package/dist/src/tricks/recursive-context.js +2 -2
  753. package/dist/src/tricks/recursive-context.js.map +1 -1
  754. package/dist/src/tricks/reversible-compaction.d.ts.map +1 -1
  755. package/dist/src/tricks/reversible-compaction.js +6 -2
  756. package/dist/src/tricks/reversible-compaction.js.map +1 -1
  757. package/dist/src/tui/app.d.ts +16 -3
  758. package/dist/src/tui/app.d.ts.map +1 -1
  759. package/dist/src/tui/app.js +211 -25
  760. package/dist/src/tui/app.js.map +1 -1
  761. package/dist/src/tui/components/CollapsibleDiffView.d.ts +1 -1
  762. package/dist/src/tui/components/CollapsibleDiffView.d.ts.map +1 -1
  763. package/dist/src/tui/components/DiagnosticsPanel.d.ts +24 -0
  764. package/dist/src/tui/components/DiagnosticsPanel.d.ts.map +1 -0
  765. package/dist/src/tui/components/DiagnosticsPanel.js +47 -0
  766. package/dist/src/tui/components/DiagnosticsPanel.js.map +1 -0
  767. package/dist/src/tui/components/DiffView.d.ts +1 -1
  768. package/dist/src/tui/components/DiffView.d.ts.map +1 -1
  769. package/dist/src/tui/components/ErrorBoundary.d.ts.map +1 -1
  770. package/dist/src/tui/components/ErrorBoundary.js +3 -2
  771. package/dist/src/tui/components/ErrorBoundary.js.map +1 -1
  772. package/dist/src/tui/components/TasksPanel.d.ts +1 -1
  773. package/dist/src/tui/components/TasksPanel.d.ts.map +1 -1
  774. package/dist/src/tui/event-display.d.ts.map +1 -1
  775. package/dist/src/tui/event-display.js +36 -62
  776. package/dist/src/tui/event-display.js.map +1 -1
  777. package/dist/src/tui/index.d.ts +4 -0
  778. package/dist/src/tui/index.d.ts.map +1 -1
  779. package/dist/src/tui/index.js +17 -0
  780. package/dist/src/tui/index.js.map +1 -1
  781. package/dist/src/tui/transparency-aggregator.d.ts +13 -0
  782. package/dist/src/tui/transparency-aggregator.d.ts.map +1 -1
  783. package/dist/src/tui/transparency-aggregator.js +21 -0
  784. package/dist/src/tui/transparency-aggregator.js.map +1 -1
  785. package/dist/src/types.d.ts +170 -3
  786. package/dist/src/types.d.ts.map +1 -1
  787. package/package.json +18 -3
@@ -15,77 +15,41 @@
15
15
  * - Model health tracking and failover
16
16
  * - State persistence and resume
17
17
  * - Orchestrator decision logging
18
+ *
19
+ * Phase 3a: Heavy logic extracted into:
20
+ * - swarm-lifecycle.ts — Decomposition, planning, verification, resume, synthesis, helpers
21
+ * - swarm-execution.ts — Wave dispatch loop, task completion handling
22
+ * - swarm-recovery.ts — Error recovery, resilience, circuit breaker, adaptive stagger
18
23
  */
19
- import * as fs from 'node:fs';
20
- import * as path from 'node:path';
21
- import { createSmartDecomposer, parseDecompositionResponse, validateDecomposition } from '../smart-decomposer.js';
22
- import { createResultSynthesizer } from '../result-synthesizer.js';
23
- import { taskResultToAgentOutput, DEFAULT_SWARM_CONFIG, getTaskTypeConfig } from './types.js';
24
+ import { createSmartDecomposer, parseDecompositionResponse, validateDecomposition } from '../tasks/smart-decomposer.js';
25
+ import { createResultSynthesizer } from '../agents/result-synthesizer.js';
26
+ import { DEFAULT_SWARM_CONFIG } from './types.js';
24
27
  import { createSwarmTaskQueue } from './task-queue.js';
25
28
  import { createSwarmBudgetPool } from './swarm-budget.js';
26
29
  import { createSwarmWorkerPool } from './worker-pool.js';
27
- import { evaluateWorkerOutput, runPreFlightChecks, checkArtifacts, checkArtifactsEnhanced, runConcreteChecks } from './swarm-quality-gate.js';
28
- import { ModelHealthTracker, selectAlternativeModel } from './model-selector.js';
30
+ import { ModelHealthTracker } from './model-selector.js';
29
31
  import { SwarmStateStore } from './swarm-state-store.js';
30
- // ─── Hollow Completion Detection ──────────────────────────────────────────
31
- /**
32
- * V11: Hollow completion detection — catches empty completions AND "success" with failure language.
33
- * Zero tool calls AND trivial output is always hollow.
34
- * Additionally, success=true but output containing failure admissions is also hollow —
35
- * this catches workers that report success but actually did no useful work.
36
- */
37
- const FAILURE_INDICATORS = [
38
- 'budget exhausted', 'unable to complete', 'could not complete',
39
- 'ran out of budget', 'no changes were made', 'no files were modified',
40
- 'no files were created', 'failed to complete', 'before research could begin',
41
- 'i was unable to', 'i could not', 'unfortunately i',
42
- ];
43
- const BOILERPLATE_INDICATORS = [
44
- 'task completed successfully', 'i have completed the task',
45
- 'the task has been completed', 'done', 'completed', 'finished',
46
- 'no issues found', 'everything looks good', 'all tasks completed',
47
- ];
48
- export function isHollowCompletion(spawnResult, taskType, swarmConfig) {
49
- // Timeout uses toolCalls === -1, not hollow
50
- if ((spawnResult.metrics.toolCalls ?? 0) === -1)
51
- return false;
52
- const toolCalls = spawnResult.metrics.toolCalls ?? 0;
53
- // Truly empty completions: zero tools AND trivial output
54
- // P4: Higher threshold (120 chars) + configurable via SwarmConfig
55
- const hollowThreshold = swarmConfig?.hollowOutputThreshold ?? 120;
56
- if (toolCalls === 0
57
- && (spawnResult.output?.trim().length ?? 0) < hollowThreshold) {
58
- return true;
59
- }
60
- // P4: Boilerplate detection — zero tools AND short output that's just boilerplate
61
- if (toolCalls === 0 && (spawnResult.output?.trim().length ?? 0) < 300) {
62
- const outputLower = (spawnResult.output ?? '').toLowerCase().trim();
63
- if (BOILERPLATE_INDICATORS.some(b => outputLower.includes(b))) {
64
- return true;
65
- }
66
- }
67
- // "Success" that admits failure: worker claims success but output contains failure language
68
- if (spawnResult.success) {
69
- const outputLower = (spawnResult.output ?? '').toLowerCase();
70
- if (FAILURE_INDICATORS.some(f => outputLower.includes(f))) {
71
- return true;
72
- }
73
- }
74
- // V7: Use configurable requiresToolCalls from TaskTypeConfig.
75
- // For action-oriented tasks (implement/test/refactor/etc), zero tool calls is ALWAYS hollow.
76
- if (taskType) {
77
- const typeConfig = getTaskTypeConfig(taskType, swarmConfig);
78
- if (typeConfig.requiresToolCalls && toolCalls === 0) {
79
- return true;
80
- }
81
- }
82
- return false;
83
- }
84
- // ─── Orchestrator ──────────────────────────────────────────────────────────
32
+ import { createSharedContextState } from '../../shared/shared-context-state.js';
33
+ import { createSharedEconomicsState } from '../../shared/shared-economics-state.js';
34
+ import { createSharedContextEngine } from '../../shared/context-engine.js';
35
+ import { calculateCost } from '../utilities/openrouter-pricing.js';
36
+ // ─── Extracted Module Imports ───────────────────────────────────────────
37
+ import { decomposeTask, planExecution, verifyIntegration, handleVerificationFailure, resumeExecution, synthesizeOutputs, saveCheckpoint, buildStats, buildSummary, buildErrorResult, detectFoundationTasks, buildArtifactInventory, skipRemainingTasks, } from './swarm-lifecycle.js';
38
+ import { executeWaves as executeWavesImpl, executeWave as executeWaveImpl, } from './swarm-execution.js';
39
+ import { finalRescuePass, midSwarmReplan, } from './swarm-recovery.js';
40
+ // ─── Helpers (extracted to break circular dependency) ─────────────────────
41
+ import { repoLooksUnscaffolded, } from './swarm-helpers.js';
42
+ // Re-export for backward compatibility
43
+ export { isHollowCompletion, FAILURE_INDICATORS, hasFutureIntentLanguage, BOILERPLATE_INDICATORS, repoLooksUnscaffolded } from './swarm-helpers.js';
44
+ // ─── Orchestrator ──────────────────────────────────────────────────────
85
45
  export class SwarmOrchestrator {
86
46
  config;
87
47
  provider;
88
48
  blackboard;
49
+ // Phase 3.1+3.2: Shared state for cross-worker learning
50
+ sharedContextState;
51
+ sharedEconomicsState;
52
+ sharedContextEngine;
89
53
  taskQueue;
90
54
  budgetPool;
91
55
  workerPool;
@@ -116,48 +80,56 @@ export class SwarmOrchestrator {
116
80
  healthTracker;
117
81
  stateStore;
118
82
  spawnAgentFn;
119
- // Circuit breaker: pause all dispatch after too many 429s
120
- recentRateLimits = [];
121
- circuitBreakerUntil = 0;
122
- static CIRCUIT_BREAKER_WINDOW_MS = 30_000;
123
- static CIRCUIT_BREAKER_THRESHOLD = 3;
124
- static CIRCUIT_BREAKER_PAUSE_MS = 15_000;
125
- // P3: Per-model quality gate circuit breaker (replaces global circuit breaker)
126
- perModelQualityRejections = new Map();
127
- qualityGateDisabledModels = new Set();
128
- static QUALITY_CIRCUIT_BREAKER_THRESHOLD = 5;
129
83
  // Hollow completion streak: early termination when single-model swarm produces only hollows
130
84
  hollowStreak = 0;
131
- static HOLLOW_STREAK_THRESHOLD = 3;
132
85
  // V7: Global dispatch + hollow ratio tracking for multi-model termination
133
86
  totalDispatches = 0;
134
87
  totalHollows = 0;
135
- // Hollow ratio warning (fired once, then suppressed to avoid log spam)
136
- hollowRatioWarned = false;
137
- // P7: Adaptive dispatch stagger — increases on rate limits, decreases on success
138
- adaptiveStaggerMs = 0; // Initialized from config in constructor
139
- // F25: Consecutive timeout tracking per task — early-fail after limit
140
- taskTimeoutCounts = new Map();
141
88
  // Original prompt for re-planning on resume
142
89
  originalPrompt = '';
143
90
  // Mid-swarm re-planning: only once per swarm execution
144
91
  hasReplanned = false;
92
+ // Recovery state (circuit breaker, stagger, quality gate breaker, etc.)
93
+ recoveryState;
145
94
  constructor(config, provider, agentRegistry, spawnAgentFn, blackboard) {
146
95
  this.config = { ...DEFAULT_SWARM_CONFIG, ...config };
147
96
  this.provider = provider;
148
97
  this.blackboard = blackboard;
149
98
  this.spawnAgentFn = spawnAgentFn;
150
99
  this.healthTracker = new ModelHealthTracker();
151
- this.adaptiveStaggerMs = this.getStaggerMs();
100
+ // Initialize recovery state
101
+ const initialStagger = this.config.dispatchStaggerMs ?? 500;
102
+ this.recoveryState = {
103
+ recentRateLimits: [],
104
+ circuitBreakerUntil: 0,
105
+ perModelQualityRejections: new Map(),
106
+ qualityGateDisabledModels: new Set(),
107
+ adaptiveStaggerMs: initialStagger,
108
+ taskTimeoutCounts: new Map(),
109
+ hollowRatioWarned: false,
110
+ };
111
+ // Phase 3.1+3.2: Shared context & economics for cross-worker learning
112
+ this.sharedContextState = createSharedContextState({
113
+ staticPrefix: 'You are a swarm worker agent.',
114
+ maxFailures: 100,
115
+ maxReferences: 200,
116
+ });
117
+ this.sharedEconomicsState = createSharedEconomicsState({
118
+ globalDoomLoopThreshold: 10,
119
+ });
120
+ this.sharedContextEngine = createSharedContextEngine(this.sharedContextState, {
121
+ maxFailuresInPrompt: 5,
122
+ includeInsights: true,
123
+ });
152
124
  this.taskQueue = createSwarmTaskQueue();
153
125
  this.budgetPool = createSwarmBudgetPool(this.config);
154
- this.workerPool = createSwarmWorkerPool(this.config, agentRegistry, spawnAgentFn, this.budgetPool, this.healthTracker);
126
+ this.workerPool = createSwarmWorkerPool(this.config, agentRegistry, spawnAgentFn, this.budgetPool, this.healthTracker, this.sharedContextEngine);
155
127
  // Initialize state store if persistence enabled
156
128
  if (this.config.enablePersistence) {
157
129
  this.stateStore = new SwarmStateStore(this.config.stateDir ?? '.agent/swarm-state', this.config.resumeSessionId);
158
130
  }
159
131
  // C1: Build LLM decompose function with explicit JSON schema
160
- const llmDecompose = async (task, _context) => {
132
+ const llmDecompose = async (task, context) => {
161
133
  // V7: Dynamically build the allowed type list from built-in + user-defined types
162
134
  const builtinTypes = ['research', 'analysis', 'design', 'implement', 'test', 'refactor', 'review', 'document', 'integrate', 'deploy', 'merge'];
163
135
  const customTypes = Object.keys(this.config.taskTypes ?? {}).filter(t => !builtinTypes.includes(t));
@@ -179,6 +151,26 @@ export class SwarmOrchestrator {
179
151
  }).join('\n');
180
152
  customTypeSection = `\n\nCustom task types available:\n${descriptions}\nUse these when their description matches the subtask's purpose.`;
181
153
  }
154
+ // Build codebase context section from repo map if available
155
+ let codebaseSection = '';
156
+ if (context.repoMap) {
157
+ const map = context.repoMap;
158
+ const topFiles = Array.from(map.chunks.values())
159
+ .sort((a, b) => b.importance - a.importance)
160
+ .slice(0, 30)
161
+ .map(c => ` - ${c.filePath} (${c.type}, ${c.tokenCount} tokens, importance: ${c.importance.toFixed(2)})`);
162
+ codebaseSection = `
163
+
164
+ CODEBASE STRUCTURE (${map.chunks.size} files, ${map.totalTokens} total tokens):
165
+ Entry points: ${map.entryPoints.slice(0, 5).join(', ')}
166
+ Core modules: ${map.coreModules.slice(0, 5).join(', ')}
167
+ Key files:
168
+ ${topFiles.join('\n')}
169
+
170
+ CRITICAL: Your subtasks MUST reference actual files from this codebase.
171
+ Do NOT invent new project scaffolding or create files that don't relate to the existing codebase.
172
+ Decompose the work based on what ALREADY EXISTS in the project.`;
173
+ }
182
174
  const systemPrompt = `You are a task decomposition expert. Break down the given task into well-defined subtasks with clear dependencies.
183
175
 
184
176
  CRITICAL: Dependencies MUST use zero-based integer indices referring to other subtasks in the array.
@@ -197,7 +189,7 @@ Respond with valid JSON matching this exact schema:
197
189
  ],
198
190
  "strategy": "sequential" | "parallel" | "hierarchical" | "adaptive" | "pipeline",
199
191
  "reasoning": "Brief explanation of why this decomposition was chosen"
200
- }${customTypeSection}
192
+ }${customTypeSection}${codebaseSection}
201
193
 
202
194
  EXAMPLE 1 — Research task (3 parallel research + 1 merge):
203
195
  {
@@ -234,12 +226,52 @@ Rules:
234
226
  { role: 'user', content: task },
235
227
  ], {
236
228
  model: this.config.orchestratorModel,
237
- maxTokens: 4000,
229
+ maxTokens: 16000,
238
230
  temperature: 0.3,
239
231
  });
240
232
  this.trackOrchestratorUsage(response, 'decompose');
241
233
  // Use parseDecompositionResponse which handles markdown code blocks and edge cases
242
- return parseDecompositionResponse(response.content);
234
+ const result = parseDecompositionResponse(response.content);
235
+ // If decomposition returned 0 subtasks, log diagnostics and retry with explicit JSON instruction
236
+ if (result.subtasks.length === 0) {
237
+ const snippet = response.content?.slice(0, 500) ?? '(empty response)';
238
+ const parseError = result.parseError ?? 'unknown';
239
+ this.errors.push({
240
+ phase: 'decomposition',
241
+ message: `LLM returned no subtasks. Parse error: ${parseError}. Response preview: ${snippet}`,
242
+ recovered: true,
243
+ });
244
+ this.emit({
245
+ type: 'swarm.orchestrator.decision',
246
+ decision: {
247
+ timestamp: Date.now(),
248
+ phase: 'decomposition',
249
+ decision: `Empty decomposition — retrying with explicit JSON instruction`,
250
+ reasoning: `Parse error: ${parseError}. Response preview (first 500 chars): ${snippet}`,
251
+ },
252
+ });
253
+ // Retry with explicit JSON instruction
254
+ const retryResponse = await this.provider.chat([
255
+ { role: 'system', content: systemPrompt },
256
+ { role: 'user', content: `${task}\n\nIMPORTANT: Your previous attempt was truncated or could not be parsed (${parseError}). Return ONLY a raw JSON object with NO markdown formatting, NO explanation text, NO code fences. The JSON must have a "subtasks" array with at least 2 entries matching the schema above. Keep subtask descriptions concise to avoid truncation.` },
257
+ ], {
258
+ model: this.config.orchestratorModel,
259
+ maxTokens: 16000,
260
+ temperature: 0.2,
261
+ });
262
+ this.trackOrchestratorUsage(retryResponse, 'decompose-retry');
263
+ const retryResult = parseDecompositionResponse(retryResponse.content);
264
+ if (retryResult.subtasks.length === 0) {
265
+ const retrySnippet = retryResponse.content?.slice(0, 500) ?? '(empty response)';
266
+ this.errors.push({
267
+ phase: 'decomposition',
268
+ message: `Retry also returned no subtasks. Response preview: ${retrySnippet}`,
269
+ recovered: false,
270
+ });
271
+ }
272
+ return retryResult;
273
+ }
274
+ return result;
243
275
  };
244
276
  // Configure decomposer for swarm use
245
277
  const decomposer = createSmartDecomposer({
@@ -257,6 +289,18 @@ Rules:
257
289
  getBudgetPool() {
258
290
  return this.budgetPool;
259
291
  }
292
+ /** Get shared context state for cross-worker failure learning. */
293
+ getSharedContextState() {
294
+ return this.sharedContextState;
295
+ }
296
+ /** Get shared economics state for cross-worker doom loop aggregation. */
297
+ getSharedEconomicsState() {
298
+ return this.sharedEconomicsState;
299
+ }
300
+ /** Get shared context engine for cross-worker failure learning. */
301
+ getSharedContextEngine() {
302
+ return this.sharedContextEngine;
303
+ }
260
304
  /**
261
305
  * Subscribe to swarm events.
262
306
  */
@@ -276,8 +320,11 @@ Rules:
276
320
  try {
277
321
  listener(event);
278
322
  }
279
- catch {
280
- // Don't let listener errors break the orchestrator
323
+ catch (err) {
324
+ const msg = err instanceof Error ? err.message : String(err);
325
+ if (process.env.DEBUG) {
326
+ console.error(`[SwarmOrchestrator] Listener error on ${event.type}: ${msg}`);
327
+ }
281
328
  }
282
329
  }
283
330
  }
@@ -287,8 +334,10 @@ Rules:
287
334
  trackOrchestratorUsage(response, purpose) {
288
335
  if (!response.usage)
289
336
  return;
290
- const tokens = response.usage.total_tokens ?? ((response.usage.prompt_tokens ?? 0) + (response.usage.completion_tokens ?? 0));
291
- const cost = tokens * 0.000015; // ~$15/M tokens average for orchestrator models
337
+ const input = response.usage.prompt_tokens ?? response.usage.inputTokens ?? 0;
338
+ const output = response.usage.completion_tokens ?? response.usage.outputTokens ?? 0;
339
+ const tokens = response.usage.total_tokens ?? (input + output);
340
+ const cost = response.usage.cost ?? calculateCost(this.config.orchestratorModel, input, output);
292
341
  this.orchestratorTokens += tokens;
293
342
  this.orchestratorCost += cost;
294
343
  this.orchestratorCalls++;
@@ -300,49 +349,135 @@ Rules:
300
349
  cost,
301
350
  });
302
351
  }
352
+ /**
353
+ * Build the OrchestratorInternals interface for extracted functions.
354
+ */
355
+ getInternals() {
356
+ return {
357
+ config: this.config,
358
+ provider: this.provider,
359
+ blackboard: this.blackboard,
360
+ sharedContextState: this.sharedContextState,
361
+ sharedEconomicsState: this.sharedEconomicsState,
362
+ sharedContextEngine: this.sharedContextEngine,
363
+ taskQueue: this.taskQueue,
364
+ budgetPool: this.budgetPool,
365
+ workerPool: this.workerPool,
366
+ decomposer: this._decomposer,
367
+ synthesizer: this.synthesizer,
368
+ listeners: this.listeners,
369
+ errors: this.errors,
370
+ cancelled: this.cancelled,
371
+ currentPhase: this.currentPhase,
372
+ totalTokens: this.totalTokens,
373
+ totalCost: this.totalCost,
374
+ qualityRejections: this.qualityRejections,
375
+ retries: this.retries,
376
+ startTime: this.startTime,
377
+ modelUsage: this.modelUsage,
378
+ orchestratorTokens: this.orchestratorTokens,
379
+ orchestratorCost: this.orchestratorCost,
380
+ orchestratorCalls: this.orchestratorCalls,
381
+ plan: this.plan,
382
+ waveReviews: this.waveReviews,
383
+ verificationResult: this.verificationResult,
384
+ artifactInventory: this.artifactInventory,
385
+ orchestratorDecisions: this.orchestratorDecisions,
386
+ healthTracker: this.healthTracker,
387
+ stateStore: this.stateStore,
388
+ spawnAgentFn: this.spawnAgentFn,
389
+ hollowStreak: this.hollowStreak,
390
+ totalDispatches: this.totalDispatches,
391
+ totalHollows: this.totalHollows,
392
+ originalPrompt: this.originalPrompt,
393
+ hasReplanned: this.hasReplanned,
394
+ emit: (event) => this.emit(event),
395
+ trackOrchestratorUsage: (response, purpose) => this.trackOrchestratorUsage(response, purpose),
396
+ logDecision: (phase, decision, reasoning) => this.logDecision(phase, decision, reasoning),
397
+ executeWaves: () => this.executeWavesDelegate(),
398
+ executeWave: (tasks) => this.executeWaveDelegate(tasks),
399
+ finalRescuePass: () => this.finalRescuePassDelegate(),
400
+ };
401
+ }
402
+ /**
403
+ * Sync mutable state back from internals after an extracted function call.
404
+ * The internals object holds references to mutable objects (arrays, maps),
405
+ * but primitive values need syncing back.
406
+ */
407
+ syncFromInternals(ctx) {
408
+ this.cancelled = ctx.cancelled;
409
+ this.currentPhase = ctx.currentPhase;
410
+ this.totalTokens = ctx.totalTokens;
411
+ this.totalCost = ctx.totalCost;
412
+ this.qualityRejections = ctx.qualityRejections;
413
+ this.retries = ctx.retries;
414
+ this.orchestratorTokens = ctx.orchestratorTokens;
415
+ this.orchestratorCost = ctx.orchestratorCost;
416
+ this.orchestratorCalls = ctx.orchestratorCalls;
417
+ this.plan = ctx.plan;
418
+ this.verificationResult = ctx.verificationResult;
419
+ this.artifactInventory = ctx.artifactInventory;
420
+ this.hollowStreak = ctx.hollowStreak;
421
+ this.totalDispatches = ctx.totalDispatches;
422
+ this.totalHollows = ctx.totalHollows;
423
+ this.originalPrompt = ctx.originalPrompt;
424
+ this.hasReplanned = ctx.hasReplanned;
425
+ }
303
426
  /**
304
427
  * Execute the full swarm pipeline for a task.
305
- *
306
- * V2 pipeline:
307
- * 1. Check for resume
308
- * 2. Decompose
309
- * 3. Plan (acceptance criteria + verification plan)
310
- * 4. Schedule into waves
311
- * 5. Execute waves with review
312
- * 6. Verify integration
313
- * 7. Fix-up loop if verification fails
314
- * 8. Synthesize
315
- * 9. Checkpoint (final)
316
428
  */
317
429
  async execute(task) {
318
430
  this.startTime = Date.now();
319
431
  this.originalPrompt = task;
320
432
  try {
433
+ const ctx = this.getInternals();
321
434
  // V2: Check for resume
322
435
  if (this.config.resumeSessionId && this.stateStore) {
323
- return await this.resumeExecution(task);
436
+ const resumeResult = await resumeExecution(ctx, task, () => midSwarmReplan(ctx));
437
+ this.syncFromInternals(ctx);
438
+ if (resumeResult)
439
+ return resumeResult;
440
+ // null means no checkpoint found, fall through to normal execute
324
441
  }
325
442
  // Phase 1: Decompose
326
443
  this.currentPhase = 'decomposing';
444
+ ctx.currentPhase = 'decomposing';
327
445
  this.emit({ type: 'swarm.phase.progress', phase: 'decomposing', message: 'Decomposing task into subtasks...' });
328
- let decomposition = await this.decompose(task);
329
- if (!decomposition) {
446
+ const decomposeOutcome = await decomposeTask(ctx, task);
447
+ this.syncFromInternals(ctx);
448
+ if (!decomposeOutcome.result) {
330
449
  this.currentPhase = 'failed';
331
- return this.buildErrorResult('Decomposition failed — task may be too simple for swarm mode');
450
+ return buildErrorResult(ctx, `Decomposition failed: ${decomposeOutcome.failureReason}`);
451
+ }
452
+ let decomposition = decomposeOutcome.result;
453
+ // If repository is mostly empty, force a scaffold-first dependency chain
454
+ if (repoLooksUnscaffolded(this.config.facts?.workingDirectory ?? process.cwd())) {
455
+ const scaffoldTask = decomposition.subtasks.find(st => /\b(scaffold|bootstrap|initialize|setup|set up|project scaffold)\b/i.test(st.description));
456
+ if (scaffoldTask) {
457
+ for (const subtask of decomposition.subtasks) {
458
+ if (subtask.id === scaffoldTask.id)
459
+ continue;
460
+ if (!subtask.dependencies.includes(scaffoldTask.id)) {
461
+ subtask.dependencies.push(scaffoldTask.id);
462
+ }
463
+ }
464
+ this.logDecision('scaffold-first', `Repo appears unscaffolded; enforcing scaffold task ${scaffoldTask.id} as prerequisite`, '');
465
+ }
332
466
  }
333
- // F5: Validate decomposition — check for cycles, invalid deps, granularity
467
+ // F5: Validate decomposition
334
468
  const validation = validateDecomposition(decomposition);
335
469
  if (validation.warnings.length > 0) {
336
470
  this.logDecision('decomposition-validation', `Warnings: ${validation.warnings.join('; ')}`, '');
337
471
  }
338
472
  if (!validation.valid) {
339
473
  this.logDecision('decomposition-validation', `Invalid decomposition: ${validation.issues.join('; ')}`, 'Retrying...');
340
- // Retry decomposition once with feedback
341
- decomposition = await this.decompose(`${task}\n\nIMPORTANT: Previous decomposition was invalid: ${validation.issues.join('. ')}. Fix these issues.`);
342
- if (!decomposition) {
474
+ const retryOutcome = await decomposeTask(ctx, `${task}\n\nIMPORTANT: Previous decomposition was invalid: ${validation.issues.join('. ')}. Fix these issues.`);
475
+ this.syncFromInternals(ctx);
476
+ if (!retryOutcome.result) {
343
477
  this.currentPhase = 'failed';
344
- return this.buildErrorResult(`Decomposition validation failed: ${validation.issues.join('; ')}`);
478
+ return buildErrorResult(ctx, `Decomposition validation failed: ${validation.issues.join('; ')}`);
345
479
  }
480
+ decomposition = retryOutcome.result;
346
481
  const retryValidation = validateDecomposition(decomposition);
347
482
  if (!retryValidation.valid) {
348
483
  this.logDecision('decomposition-validation', `Retry still invalid: ${retryValidation.issues.join('; ')}`, 'Proceeding anyway');
@@ -350,36 +485,30 @@ Rules:
350
485
  }
351
486
  // Phase 2: Schedule into waves
352
487
  this.currentPhase = 'scheduling';
488
+ ctx.currentPhase = 'scheduling';
353
489
  this.emit({ type: 'swarm.phase.progress', phase: 'scheduling', message: `Scheduling ${decomposition.subtasks.length} subtasks into waves...` });
354
490
  this.taskQueue.loadFromDecomposition(decomposition, this.config);
355
- // F3: Dynamic orchestrator reserve scaling based on subtask count.
356
- // More subtasks = more quality gate calls, synthesis work, and review overhead.
357
- // Formula: max(configured ratio, 5% per subtask), capped at 40%.
491
+ // F3: Dynamic orchestrator reserve scaling
358
492
  const subtaskCount = decomposition.subtasks.length;
359
493
  const dynamicReserveRatio = Math.min(0.40, Math.max(this.config.orchestratorReserveRatio, subtaskCount * 0.05));
360
494
  if (dynamicReserveRatio > this.config.orchestratorReserveRatio) {
361
495
  this.logDecision('budget-scaling', `Scaled orchestrator reserve from ${(this.config.orchestratorReserveRatio * 100).toFixed(0)}% to ${(dynamicReserveRatio * 100).toFixed(0)}% for ${subtaskCount} subtasks`, '');
362
496
  }
363
- // Foundation task detection: tasks that are the sole dependency of 3+ downstream
364
- // tasks are critical — if they fail, the entire swarm cascade-skips.
365
- // Give them extra retries and timeout scaling.
366
- this.detectFoundationTasks();
367
- // D3/F1: Probe model capability before dispatch (default: true)
497
+ // Foundation task detection
498
+ detectFoundationTasks(ctx);
499
+ // D3/F1: Probe model capability before dispatch
368
500
  if (this.config.probeModels !== false) {
369
501
  await this.probeModelCapability();
370
- // F15/F23: Handle all-models-failed probe scenario
371
- // Resolve strategy: explicit probeFailureStrategy > legacy ignoreProbeFailures > default 'warn-and-try'
372
502
  const probeStrategy = this.config.probeFailureStrategy
373
503
  ?? (this.config.ignoreProbeFailures ? 'warn-and-try' : 'warn-and-try');
374
504
  const uniqueModels = [...new Set(this.config.workers.map(w => w.model))];
375
505
  const healthyModels = this.healthTracker.getHealthy(uniqueModels);
376
506
  if (healthyModels.length === 0 && uniqueModels.length > 0) {
377
507
  if (probeStrategy === 'abort') {
378
- // Hard abort — no tasks dispatched
379
508
  const reason = `All ${uniqueModels.length} worker model(s) failed capability probes — no model can make tool calls. Aborting swarm to prevent budget waste. Fix model configuration and retry.`;
380
509
  this.logDecision('probe-abort', reason, `Models tested: ${uniqueModels.join(', ')}`);
381
510
  this.emit({ type: 'swarm.abort', reason });
382
- this.skipRemainingTasks(reason);
511
+ skipRemainingTasks(ctx, reason);
383
512
  const totalTasks = this.taskQueue.getStats().total;
384
513
  const abortStats = {
385
514
  completedTasks: 0, failedTasks: 0, skippedTasks: totalTasks,
@@ -395,27 +524,28 @@ Rules:
395
524
  };
396
525
  }
397
526
  else {
398
- // F23: warn-and-try — log warning, reset health, let real tasks prove capability
399
527
  this.logDecision('probe-warning', `All ${uniqueModels.length} model(s) failed probe — continuing anyway (strategy: warn-and-try)`, 'Will abort after first real task failure if model cannot use tools');
400
- // Reset health so dispatch doesn't skip all models
401
528
  for (const model of uniqueModels) {
402
529
  this.healthTracker.recordSuccess(model, 0);
403
530
  }
404
531
  }
405
532
  }
406
533
  }
407
- // Emit skip events when tasks are cascade-skipped due to dependency failures
534
+ // Emit skip events when tasks are cascade-skipped
408
535
  this.taskQueue.setOnCascadeSkip((skippedTaskId, reason) => {
409
536
  this.emit({ type: 'swarm.task.skipped', taskId: skippedTaskId, reason });
410
537
  });
411
538
  const stats = this.taskQueue.getStats();
412
539
  this.emit({ type: 'swarm.phase.progress', phase: 'scheduling', message: `Scheduled ${stats.total} tasks in ${this.taskQueue.getTotalWaves()} waves` });
413
- // V2: Phase 2.5: Plan execution — fire in background, don't block waves
540
+ // V2: Phase 2.5: Plan execution
414
541
  let planPromise;
415
542
  if (this.config.enablePlanning) {
416
543
  this.currentPhase = 'planning';
544
+ ctx.currentPhase = 'planning';
417
545
  this.emit({ type: 'swarm.phase.progress', phase: 'planning', message: 'Creating acceptance criteria...' });
418
- planPromise = this.planExecution(task, decomposition).catch(err => {
546
+ planPromise = planExecution(ctx, task, decomposition).then(() => {
547
+ this.syncFromInternals(ctx);
548
+ }).catch(err => {
419
549
  this.logDecision('planning', 'Planning failed (non-fatal)', err.message);
420
550
  });
421
551
  }
@@ -429,47 +559,62 @@ Rules:
429
559
  maxCost: this.config.maxCost,
430
560
  },
431
561
  });
432
- // Emit tasks AFTER swarm.start so the bridge has already initialized
433
- // (swarm.start clears tasks/edges, so loading before it would lose them)
562
+ // Emit tasks AFTER swarm.start
434
563
  this.emit({
435
564
  type: 'swarm.tasks.loaded',
436
565
  tasks: this.taskQueue.getAllTasks(),
437
566
  });
438
- // Phase 3: Execute waves (planning runs concurrently)
567
+ // Phase 3: Execute waves
439
568
  this.currentPhase = 'executing';
440
- await this.executeWaves();
441
- // V10: Final rescue pass — attempt to recover cascade-skipped tasks with lenient mode
442
- if (!this.cancelled)
443
- await this.finalRescuePass();
444
- // Ensure planning completed before verification/synthesis
569
+ ctx.currentPhase = 'executing';
570
+ await this.executeWavesDelegate();
571
+ this.syncFromInternals(ctx);
572
+ // V10: Final rescue pass
573
+ if (!this.cancelled) {
574
+ await this.finalRescuePassDelegate();
575
+ this.syncFromInternals(ctx);
576
+ }
577
+ // Ensure planning completed
445
578
  if (planPromise)
446
579
  await planPromise;
447
- // Post-wave artifact audit: scan filesystem for files created by workers
448
- this.artifactInventory = this.buildArtifactInventory();
580
+ // Post-wave artifact audit
581
+ this.artifactInventory = buildArtifactInventory(ctx);
449
582
  // V2: Phase 3.5: Verify integration
450
583
  if (this.config.enableVerification && this.plan?.integrationTestPlan) {
451
584
  this.currentPhase = 'verifying';
452
- const verification = await this.verifyIntegration(this.plan.integrationTestPlan);
585
+ ctx.currentPhase = 'verifying';
586
+ const verification = await verifyIntegration(ctx, this.plan.integrationTestPlan);
587
+ this.syncFromInternals(ctx);
453
588
  if (!verification.passed) {
454
- await this.handleVerificationFailure(verification, task);
589
+ await handleVerificationFailure(ctx, verification, task);
590
+ this.syncFromInternals(ctx);
455
591
  }
456
592
  }
457
593
  // Phase 4: Synthesize results
458
594
  this.currentPhase = 'synthesizing';
459
- const synthesisResult = await this.synthesize();
595
+ ctx.currentPhase = 'synthesizing';
596
+ const synthesisResult = await synthesizeOutputs(ctx);
597
+ this.syncFromInternals(ctx);
460
598
  this.currentPhase = 'completed';
461
- const executionStats = this.buildStats();
599
+ ctx.currentPhase = 'completed';
600
+ const executionStats = buildStats(ctx);
462
601
  // V2: Final checkpoint
463
- this.checkpoint('final');
602
+ saveCheckpoint(ctx, 'final');
464
603
  const hasArtifacts = (this.artifactInventory?.totalFiles ?? 0) > 0;
465
604
  this.emit({ type: 'swarm.complete', stats: executionStats, errors: this.errors, artifactInventory: this.artifactInventory });
605
+ // Success requires completing at least 70% of tasks
606
+ const completionRatio = executionStats.totalTasks > 0
607
+ ? executionStats.completedTasks / executionStats.totalTasks
608
+ : 0;
609
+ const isSuccess = completionRatio >= 0.7;
610
+ const isPartialSuccess = !isSuccess && executionStats.completedTasks > 0;
466
611
  return {
467
- success: executionStats.completedTasks > 0,
468
- partialSuccess: !executionStats.completedTasks && hasArtifacts,
612
+ success: isSuccess,
613
+ partialSuccess: isPartialSuccess || (!executionStats.completedTasks && hasArtifacts),
469
614
  partialFailure: executionStats.failedTasks > 0,
470
615
  synthesisResult: synthesisResult ?? undefined,
471
616
  artifactInventory: this.artifactInventory,
472
- summary: this.buildSummary(executionStats),
617
+ summary: buildSummary(ctx, executionStats),
473
618
  tasks: this.taskQueue.getAllTasks(),
474
619
  stats: executionStats,
475
620
  errors: this.errors,
@@ -484,2420 +629,131 @@ Rules:
484
629
  recovered: false,
485
630
  });
486
631
  this.emit({ type: 'swarm.error', error: message, phase: 'execution' });
487
- return this.buildErrorResult(message);
632
+ const ctx = this.getInternals();
633
+ return buildErrorResult(ctx, message);
488
634
  }
489
635
  finally {
490
636
  this.workerPool.cleanup();
491
637
  }
492
638
  }
493
639
  /**
494
- * Phase 1: Decompose the task into subtasks.
640
+ * Get live status for TUI.
495
641
  */
496
- async decompose(task) {
497
- try {
498
- const result = await this._decomposer.decompose(task);
499
- if (result.subtasks.length < 2) {
500
- // Too simple for swarm mode
501
- return null;
502
- }
503
- // Reject heuristic fallback — the generic 3-task chain is worse than aborting
504
- if (!result.metadata.llmAssisted) {
505
- this.logDecision('decomposition', 'Rejected heuristic fallback DAG', 'LLM decomposition failed after retries. Heuristic DAG is not useful.');
506
- return null;
507
- }
508
- // Flat-DAG detection: warn when all tasks land in wave 0 with no dependencies
509
- const hasAnyDependency = result.subtasks.some(s => s.dependencies.length > 0);
510
- if (!hasAnyDependency && result.subtasks.length >= 3) {
511
- this.logDecision('decomposition', `Flat DAG: ${result.subtasks.length} tasks, zero dependencies`, 'All tasks will execute in wave 0 without ordering');
512
- }
513
- return result;
514
- }
515
- catch (error) {
516
- this.errors.push({
517
- phase: 'decomposition',
518
- message: error.message,
519
- recovered: false,
520
- });
521
- this.emit({ type: 'swarm.error', error: error.message, phase: 'decomposition' });
522
- return null;
523
- }
642
+ getStatus() {
643
+ const stats = this.taskQueue.getStats();
644
+ return {
645
+ phase: this.cancelled ? 'failed' : this.currentPhase,
646
+ currentWave: this.taskQueue.getCurrentWave() + 1,
647
+ totalWaves: this.taskQueue.getTotalWaves(),
648
+ activeWorkers: this.workerPool.getActiveWorkerStatus(),
649
+ queue: stats,
650
+ budget: {
651
+ tokensUsed: this.totalTokens + this.orchestratorTokens,
652
+ tokensTotal: this.config.totalBudget,
653
+ costUsed: this.totalCost + this.orchestratorCost,
654
+ costTotal: this.config.maxCost,
655
+ },
656
+ orchestrator: {
657
+ tokens: this.orchestratorTokens,
658
+ cost: this.orchestratorCost,
659
+ calls: this.orchestratorCalls,
660
+ model: this.config.orchestratorModel,
661
+ },
662
+ };
524
663
  }
525
- // ─── V2: Planning Phase ───────────────────────────────────────────────
526
664
  /**
527
- * Create acceptance criteria and integration test plan.
528
- * Graceful: if planning fails, continues without criteria.
665
+ * Cancel the swarm execution.
529
666
  */
530
- async planExecution(task, decomposition) {
531
- try {
532
- // V3: Manager role handles planning
533
- const plannerModel = this.config.hierarchy?.manager?.model
534
- ?? this.config.plannerModel ?? this.config.orchestratorModel;
535
- this.emit({ type: 'swarm.role.action', role: 'manager', action: 'plan', model: plannerModel });
536
- this.logDecision('planning', `Creating acceptance criteria (manager: ${plannerModel})`, `Task has ${decomposition.subtasks.length} subtasks, planning to ensure quality`);
537
- const taskList = decomposition.subtasks
538
- .map(s => `- [${s.id}] (${s.type}): ${s.description}`)
539
- .join('\n');
540
- const response = await this.provider.chat([
541
- {
542
- role: 'system',
543
- content: `You are a project quality planner. Given a task and its decomposition into subtasks, create:
544
- 1. Acceptance criteria for each subtask (what "done" looks like)
545
- 2. An integration test plan (bash commands to verify the combined result works)
546
-
547
- Respond with valid JSON:
548
- {
549
- "acceptanceCriteria": [
550
- { "taskId": "st-0", "criteria": ["criterion 1", "criterion 2"] }
551
- ],
552
- "integrationTestPlan": {
553
- "description": "What this test plan verifies",
554
- "steps": [
555
- { "description": "Check if files exist", "command": "ls src/parser.js", "expectedResult": "file listed", "required": true }
556
- ],
557
- "successCriteria": "All required steps pass"
558
- },
559
- "reasoning": "Why this plan was chosen"
560
- }`,
561
- },
562
- {
563
- role: 'user',
564
- content: `Task: ${task}\n\nSubtasks:\n${taskList}`,
565
- },
566
- ], {
567
- model: plannerModel,
568
- maxTokens: 3000,
569
- temperature: 0.3,
570
- });
571
- this.trackOrchestratorUsage(response, 'plan');
572
- const parsed = this.parseJSON(response.content);
573
- if (parsed) {
574
- this.plan = {
575
- acceptanceCriteria: parsed.acceptanceCriteria ?? [],
576
- integrationTestPlan: parsed.integrationTestPlan,
577
- reasoning: parsed.reasoning ?? '',
578
- };
579
- this.emit({
580
- type: 'swarm.plan.complete',
581
- criteriaCount: this.plan.acceptanceCriteria.length,
582
- hasIntegrationPlan: !!this.plan.integrationTestPlan,
583
- });
584
- }
585
- }
586
- catch (error) {
587
- // Graceful fallback: continue without plan
588
- this.errors.push({
589
- phase: 'planning',
590
- message: `Planning failed (non-fatal): ${error.message}`,
591
- recovered: true,
592
- });
593
- }
667
+ async cancel() {
668
+ this.cancelled = true;
669
+ this.currentPhase = 'failed';
670
+ await this.workerPool.cancelAll();
594
671
  }
595
- // ─── V2: Wave Review ──────────────────────────────────────────────────
596
- /**
597
- * Review completed wave outputs against acceptance criteria.
598
- * May spawn fix-up tasks for issues found.
599
- */
600
- async reviewWave(waveIndex) {
601
- if (!this.config.enableWaveReview)
602
- return null;
603
- try {
604
- // V3: Manager role handles wave review
605
- const managerModel = this.config.hierarchy?.manager?.model
606
- ?? this.config.plannerModel ?? this.config.orchestratorModel;
607
- const managerPersona = this.config.hierarchy?.manager?.persona;
608
- this.emit({ type: 'swarm.role.action', role: 'manager', action: 'review', model: managerModel, wave: waveIndex + 1 });
609
- this.emit({ type: 'swarm.review.start', wave: waveIndex + 1 });
610
- this.logDecision('review', `Reviewing wave ${waveIndex + 1} outputs (manager: ${managerModel})`, 'Checking task outputs against acceptance criteria');
611
- const completedTasks = this.taskQueue.getAllTasks()
612
- .filter(t => t.status === 'completed' && t.wave === waveIndex);
613
- if (completedTasks.length === 0) {
614
- return { wave: waveIndex, assessment: 'good', taskAssessments: [], fixupTasks: [] };
615
- }
616
- // Build review prompt
617
- const taskSummaries = completedTasks.map(t => {
618
- const criteria = this.plan?.acceptanceCriteria.find(c => c.taskId === t.id);
619
- return `Task ${t.id}: ${t.description}
620
- Output: ${t.result?.output?.slice(0, 500) ?? 'No output'}
621
- Acceptance criteria: ${criteria?.criteria.join('; ') ?? 'None set'}`;
622
- }).join('\n\n');
623
- const reviewModel = managerModel;
624
- const reviewSystemPrompt = managerPersona
625
- ? `${managerPersona}\n\nYou are reviewing completed worker outputs. Assess each task against its acceptance criteria.\nRespond with JSON:`
626
- : `You are reviewing completed worker outputs. Assess each task against its acceptance criteria.\nRespond with JSON:`;
627
- const response = await this.provider.chat([
628
- {
629
- role: 'system',
630
- content: `${reviewSystemPrompt}
631
- {
632
- "assessment": "good" | "needs-fixes" | "critical-issues",
633
- "taskAssessments": [
634
- { "taskId": "st-0", "passed": true, "feedback": "optional feedback" }
635
- ],
636
- "fixupInstructions": [
637
- { "fixesTaskId": "st-0", "description": "What to fix", "instructions": "Specific fix instructions" }
638
- ]
639
- }`,
640
- },
641
- { role: 'user', content: `Review these wave ${waveIndex + 1} outputs:\n\n${taskSummaries}` },
642
- ], { model: reviewModel, maxTokens: 2000, temperature: 0.3 });
643
- this.trackOrchestratorUsage(response, 'review');
644
- const parsed = this.parseJSON(response.content);
645
- if (!parsed)
646
- return null;
647
- // Create fix-up tasks
648
- const fixupTasks = [];
649
- if (parsed.fixupInstructions) {
650
- for (const fix of parsed.fixupInstructions) {
651
- const fixupId = `fixup-${fix.fixesTaskId}-${Date.now()}`;
652
- const originalTask = this.taskQueue.getTask(fix.fixesTaskId);
653
- const fixupTask = {
654
- id: fixupId,
655
- description: fix.description,
656
- type: originalTask?.type ?? 'implement',
657
- dependencies: [fix.fixesTaskId],
658
- status: 'ready',
659
- complexity: 3,
660
- wave: waveIndex,
661
- attempts: 0,
662
- fixesTaskId: fix.fixesTaskId,
663
- fixInstructions: fix.instructions,
664
- };
665
- fixupTasks.push(fixupTask);
666
- this.emit({ type: 'swarm.fixup.spawned', taskId: fixupId, fixesTaskId: fix.fixesTaskId, description: fix.description });
667
- }
668
- if (fixupTasks.length > 0) {
669
- this.taskQueue.addFixupTasks(fixupTasks);
670
- // V5: Re-emit full task list so dashboard picks up fixup tasks + edges
671
- this.emit({
672
- type: 'swarm.tasks.loaded',
673
- tasks: this.taskQueue.getAllTasks(),
674
- });
675
- }
676
- }
677
- const result = {
678
- wave: waveIndex,
679
- assessment: parsed.assessment ?? 'good',
680
- taskAssessments: parsed.taskAssessments ?? [],
681
- fixupTasks,
682
- };
683
- this.waveReviews.push(result);
684
- this.emit({
685
- type: 'swarm.review.complete',
686
- wave: waveIndex + 1,
687
- assessment: result.assessment,
688
- fixupCount: fixupTasks.length,
689
- });
690
- return result;
691
- }
692
- catch (error) {
693
- // Graceful: continue without review
694
- this.errors.push({
695
- phase: 'review',
696
- message: `Wave review failed (non-fatal): ${error.message}`,
697
- recovered: true,
698
- });
699
- return null;
672
+ // ─── D3: Model Capability Probing ─────────────────────────────────────
673
+ async probeModelCapability() {
674
+ const uniqueModels = new Set(this.config.workers.map(w => w.model));
675
+ this.emit({ type: 'swarm.phase.progress', phase: 'scheduling', message: `Probing ${uniqueModels.size} model(s) for tool-calling capability...` });
676
+ const supportsTools = 'chatWithTools' in this.provider
677
+ && typeof this.provider.chatWithTools === 'function';
678
+ if (!supportsTools) {
679
+ this.logDecision('model-probe', 'Provider does not support chatWithTools — skipping probe', '');
680
+ return;
700
681
  }
701
- }
702
- // ─── V2: Verification Phase ───────────────────────────────────────────
703
- /**
704
- * Run integration verification steps.
705
- */
706
- async verifyIntegration(testPlan) {
707
- // V3: Judge role handles verification
708
- const verifyModel = this.config.hierarchy?.judge?.model
709
- ?? this.config.qualityGateModel ?? this.config.orchestratorModel;
710
- this.emit({ type: 'swarm.role.action', role: 'judge', action: 'verify', model: verifyModel });
711
- this.emit({ type: 'swarm.verify.start', stepCount: testPlan.steps.length });
712
- this.logDecision('verification', `Running ${testPlan.steps.length} verification steps (judge: ${verifyModel})`, testPlan.description);
713
- const stepResults = [];
714
- let allRequiredPassed = true;
715
- for (let i = 0; i < testPlan.steps.length; i++) {
716
- const step = testPlan.steps[i];
682
+ const providerWithTools = this.provider;
683
+ const probeTools = [{
684
+ type: 'function',
685
+ function: {
686
+ name: 'read_file',
687
+ description: 'Read a file from disk',
688
+ parameters: {
689
+ type: 'object',
690
+ properties: { path: { type: 'string', description: 'File path' } },
691
+ required: ['path'],
692
+ },
693
+ },
694
+ }];
695
+ const probeTimeout = this.config.probeTimeoutMs ?? 60_000;
696
+ for (const model of uniqueModels) {
717
697
  try {
718
- // Use spawnAgent to execute verification command safely
719
- const verifierName = `swarm-verifier-${i}`;
720
- const result = await this.spawnAgentFn(verifierName, `Run this command and report the result: ${step.command}\nExpected: ${step.expectedResult ?? 'success'}`);
721
- const passed = result.success;
722
- stepResults.push({ step, passed, output: result.output.slice(0, 500) });
723
- if (!passed && step.required) {
724
- allRequiredPassed = false;
698
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Probe timeout (${probeTimeout}ms)`)), probeTimeout));
699
+ const response = await Promise.race([
700
+ providerWithTools.chatWithTools([
701
+ { role: 'system', content: 'You are a test probe. Call the read_file tool with path "package.json".' },
702
+ { role: 'user', content: 'Read package.json.' },
703
+ ], { model, maxTokens: 200, temperature: 0, tools: probeTools, tool_choice: 'required' }),
704
+ timeoutPromise,
705
+ ]);
706
+ const hasToolCall = (response.toolCalls?.length ?? 0) > 0;
707
+ if (!hasToolCall) {
708
+ this.healthTracker.markUnhealthy(model);
709
+ this.logDecision('model-probe', `Model ${model} failed probe (no tool calls)`, 'Marked unhealthy');
725
710
  }
726
- this.emit({ type: 'swarm.verify.step', stepIndex: i, description: step.description, passed });
727
- }
728
- catch (error) {
729
- const output = `Error: ${error.message}`;
730
- stepResults.push({ step, passed: false, output });
731
- if (step.required)
732
- allRequiredPassed = false;
733
- this.emit({ type: 'swarm.verify.step', stepIndex: i, description: step.description, passed: false });
734
- }
735
- }
736
- const verificationResult = {
737
- passed: allRequiredPassed,
738
- stepResults,
739
- summary: allRequiredPassed
740
- ? `All ${stepResults.filter(r => r.passed).length}/${stepResults.length} steps passed`
741
- : `${stepResults.filter(r => !r.passed).length}/${stepResults.length} steps failed`,
742
- };
743
- this.verificationResult = verificationResult;
744
- this.emit({ type: 'swarm.verify.complete', result: verificationResult });
745
- return verificationResult;
746
- }
747
- /**
748
- * Handle verification failure: create fix-up tasks and re-verify.
749
- */
750
- async handleVerificationFailure(verification, task) {
751
- const maxRetries = this.config.maxVerificationRetries ?? 2;
752
- for (let attempt = 0; attempt < maxRetries; attempt++) {
753
- this.logDecision('verification', `Verification failed, fix-up attempt ${attempt + 1}/${maxRetries}`, `${verification.stepResults.filter(r => !r.passed).length} steps failed`);
754
- // Ask orchestrator what to fix
755
- try {
756
- const failedSteps = verification.stepResults
757
- .filter(r => !r.passed)
758
- .map(r => `- ${r.step.description}: ${r.output}`)
759
- .join('\n');
760
- const response = await this.provider.chat([
761
- {
762
- role: 'system',
763
- content: `Verification failed. Analyze the failures and create fix-up tasks.
764
- Respond with JSON: { "fixups": [{ "description": "what to fix", "type": "implement" }] }`,
765
- },
766
- { role: 'user', content: `Original task: ${task}\n\nFailed verifications:\n${failedSteps}` },
767
- ], { model: this.config.plannerModel ?? this.config.orchestratorModel, maxTokens: 1500, temperature: 0.3 });
768
- this.trackOrchestratorUsage(response, 'verification-fixup');
769
- const parsed = this.parseJSON(response.content);
770
- if (parsed?.fixups && parsed.fixups.length > 0) {
771
- const fixupTasks = parsed.fixups.map((f, i) => ({
772
- id: `verify-fix-${attempt}-${i}-${Date.now()}`,
773
- description: f.description,
774
- type: (f.type ?? 'implement'),
775
- dependencies: [],
776
- status: 'ready',
777
- complexity: 4,
778
- wave: this.taskQueue.getCurrentWave(),
779
- attempts: 0,
780
- fixesTaskId: 'verification',
781
- fixInstructions: f.description,
782
- }));
783
- this.taskQueue.addFixupTasks(fixupTasks);
784
- // V5: Re-emit full task list so dashboard picks up verification fixup tasks
785
- this.emit({
786
- type: 'swarm.tasks.loaded',
787
- tasks: this.taskQueue.getAllTasks(),
788
- });
789
- // Execute fix-up wave
790
- this.currentPhase = 'executing';
791
- await this.executeWave(fixupTasks);
792
- // Re-verify
793
- this.currentPhase = 'verifying';
794
- verification = await this.verifyIntegration(this.plan.integrationTestPlan);
795
- if (verification.passed)
796
- return;
711
+ else {
712
+ this.healthTracker.recordSuccess(model, 0);
713
+ this.logDecision('model-probe', `Model ${model} passed probe`, '');
797
714
  }
798
715
  }
799
716
  catch {
800
- // Continue to next attempt
717
+ this.healthTracker.markUnhealthy(model);
718
+ this.logDecision('model-probe', `Model ${model} probe errored`, 'Marked unhealthy');
801
719
  }
802
720
  }
803
721
  }
804
- // ─── V2: Resume ───────────────────────────────────────────────────────
805
- /**
806
- * Resume execution from a saved checkpoint.
807
- */
808
- async resumeExecution(task) {
809
- const checkpoint = SwarmStateStore.loadLatest(this.config.stateDir ?? '.agent/swarm-state', this.config.resumeSessionId);
810
- if (!checkpoint) {
811
- this.logDecision('resume', 'No checkpoint found, starting fresh', `Session: ${this.config.resumeSessionId}`);
812
- // Clear resume flag and execute normally
813
- this.config.resumeSessionId = undefined;
814
- return this.execute(task);
815
- }
816
- this.logDecision('resume', `Resuming from wave ${checkpoint.currentWave}`, `Session: ${checkpoint.sessionId}`);
817
- this.emit({ type: 'swarm.state.resume', sessionId: checkpoint.sessionId, fromWave: checkpoint.currentWave });
818
- // Restore state
819
- if (checkpoint.originalPrompt)
820
- this.originalPrompt = checkpoint.originalPrompt;
821
- if (checkpoint.plan)
822
- this.plan = checkpoint.plan;
823
- if (checkpoint.modelHealth.length > 0)
824
- this.healthTracker.restore(checkpoint.modelHealth);
825
- this.orchestratorDecisions = checkpoint.decisions ?? [];
826
- this.errors = checkpoint.errors ?? [];
827
- this.totalTokens = checkpoint.stats.totalTokens;
828
- this.totalCost = checkpoint.stats.totalCost;
829
- this.qualityRejections = checkpoint.stats.qualityRejections;
830
- this.retries = checkpoint.stats.retries;
831
- // Restore task queue
832
- this.taskQueue.restoreFromCheckpoint({
833
- taskStates: checkpoint.taskStates,
834
- waves: checkpoint.waves,
835
- currentWave: checkpoint.currentWave,
836
- });
837
- // Reset orphaned dispatched tasks — their workers died with the previous process
838
- let resetCount = 0;
839
- for (const task of this.taskQueue.getAllTasks()) {
840
- if (task.status === 'dispatched') {
841
- task.status = 'ready';
842
- // Preserve at least 1 retry attempt
843
- task.attempts = Math.min(task.attempts, Math.max(0, this.config.workerRetries - 1));
844
- resetCount++;
845
- }
846
- }
847
- if (resetCount > 0) {
848
- this.logDecision('resume', `Reset ${resetCount} orphaned dispatched tasks to ready`, 'Workers died with previous process');
849
- }
850
- // Reset skipped tasks whose dependencies are now satisfied
851
- let unskippedCount = 0;
852
- for (const task of this.taskQueue.getAllTasks()) {
853
- if (task.status === 'skipped') {
854
- const deps = task.dependencies.map(id => this.taskQueue.getTask(id));
855
- const allDepsSatisfied = deps.every(d => d && (d.status === 'completed' || d.status === 'decomposed'));
856
- if (allDepsSatisfied) {
857
- task.status = 'ready';
858
- task.attempts = 0;
859
- task.rescueContext = 'Recovered on resume — dependencies now satisfied';
860
- unskippedCount++;
861
- }
862
- }
863
- }
864
- // Also reset failed tasks that have retry budget
865
- for (const task of this.taskQueue.getAllTasks()) {
866
- if (task.status === 'failed') {
867
- task.status = 'ready';
868
- task.attempts = Math.min(task.attempts, Math.max(0, this.config.workerRetries - 1));
869
- unskippedCount++;
870
- }
871
- }
872
- if (unskippedCount > 0) {
873
- this.logDecision('resume', `Recovered ${unskippedCount} skipped/failed tasks`, 'Fresh retry on resume');
874
- }
875
- // If many tasks are still stuck after un-skip, trigger re-plan
876
- const resumeStats = this.taskQueue.getStats();
877
- const stuckCount = resumeStats.failed + resumeStats.skipped;
878
- const totalAttempted = resumeStats.completed + stuckCount;
879
- if (totalAttempted > 0 && stuckCount / totalAttempted > 0.4) {
880
- this.logDecision('resume-replan', `${stuckCount}/${totalAttempted} tasks still stuck after resume — triggering re-plan`, '');
881
- this.hasReplanned = false; // Allow re-plan on resume
882
- await this.midSwarmReplan();
883
- }
884
- // Continue from where we left off
885
- this.currentPhase = 'executing';
886
- await this.executeWaves();
887
- // V10: Final rescue pass — attempt to recover cascade-skipped tasks with lenient mode
888
- if (!this.cancelled)
889
- await this.finalRescuePass();
890
- // Post-wave artifact audit
891
- this.artifactInventory = this.buildArtifactInventory();
892
- // Continue with verification and synthesis as normal
893
- if (this.config.enableVerification && this.plan?.integrationTestPlan) {
894
- this.currentPhase = 'verifying';
895
- const verification = await this.verifyIntegration(this.plan.integrationTestPlan);
896
- if (!verification.passed) {
897
- await this.handleVerificationFailure(verification, task);
898
- }
899
- }
900
- this.currentPhase = 'synthesizing';
901
- const synthesisResult = await this.synthesize();
902
- this.currentPhase = 'completed';
903
- const executionStats = this.buildStats();
904
- this.checkpoint('final');
905
- const hasArtifacts = (this.artifactInventory?.totalFiles ?? 0) > 0;
906
- this.emit({ type: 'swarm.complete', stats: executionStats, errors: this.errors, artifactInventory: this.artifactInventory });
907
- return {
908
- success: executionStats.completedTasks > 0,
909
- partialSuccess: !executionStats.completedTasks && hasArtifacts,
910
- partialFailure: executionStats.failedTasks > 0,
911
- synthesisResult: synthesisResult ?? undefined,
912
- artifactInventory: this.artifactInventory,
913
- summary: this.buildSummary(executionStats),
914
- tasks: this.taskQueue.getAllTasks(),
915
- stats: executionStats,
916
- errors: this.errors,
722
+ // ─── V2: Decision Logging ─────────────────────────────────────────────
723
+ logDecision(phase, decision, reasoning) {
724
+ const entry = {
725
+ timestamp: Date.now(),
726
+ phase,
727
+ decision,
728
+ reasoning,
917
729
  };
730
+ this.orchestratorDecisions.push(entry);
731
+ this.emit({ type: 'swarm.orchestrator.decision', decision: entry });
918
732
  }
919
- // ─── Wave Execution ───────────────────────────────────────────────────
733
+ // ─── Delegation Methods ───────────────────────────────────────────────
920
734
  /**
921
- * Execute all waves in sequence, with review after each.
735
+ * Delegate to executeWaves in swarm-execution.ts.
922
736
  */
923
- async executeWaves() {
924
- let waveIndex = this.taskQueue.getCurrentWave();
925
- const totalWaves = this.taskQueue.getTotalWaves();
926
- while (waveIndex < totalWaves && !this.cancelled) {
927
- const readyTasks = this.taskQueue.getReadyTasks();
928
- const queueStats = this.taskQueue.getStats();
929
- // F18: Skip empty waves — if no tasks are ready and none are running,
930
- // remaining tasks are all blocked/failed/skipped. Break instead of
931
- // running useless review cycles.
932
- if (readyTasks.length === 0 && queueStats.running === 0 && queueStats.ready === 0) {
933
- this.logDecision('wave-skip', `Skipping waves ${waveIndex + 1}-${totalWaves}: no dispatchable tasks remain`, `Stats: ${queueStats.completed} completed, ${queueStats.failed} failed, ${queueStats.skipped} skipped`);
934
- break;
935
- }
936
- this.emit({
937
- type: 'swarm.wave.start',
938
- wave: waveIndex + 1,
939
- totalWaves,
940
- taskCount: readyTasks.length,
941
- });
942
- // Dispatch tasks up to concurrency limit
943
- await this.executeWave(readyTasks);
944
- // Wave complete stats
945
- const afterStats = this.taskQueue.getStats();
946
- const waveCompleted = afterStats.completed - (queueStats.completed);
947
- const waveFailed = afterStats.failed - (queueStats.failed);
948
- const waveSkipped = afterStats.skipped - (queueStats.skipped);
949
- this.emit({
950
- type: 'swarm.wave.complete',
951
- wave: waveIndex + 1,
952
- totalWaves,
953
- completed: waveCompleted,
954
- failed: waveFailed,
955
- skipped: waveSkipped,
956
- });
957
- // Wave failure recovery: if ALL tasks in a wave failed, retry with adapted context
958
- if (waveCompleted === 0 && waveFailed > 0 && readyTasks.length > 0) {
959
- this.emit({ type: 'swarm.wave.allFailed', wave: waveIndex + 1 });
960
- this.logDecision('wave-recovery', `Entire wave ${waveIndex + 1} failed (${waveFailed} tasks)`, 'Checking if budget allows retry with adapted strategy');
961
- // Re-queue failed tasks with retry context if budget allows
962
- const budgetRemaining = this.budgetPool.hasCapacity();
963
- const failedWaveTasks = readyTasks.filter(t => {
964
- const task = this.taskQueue.getTask(t.id);
965
- return task && task.status === 'failed' && task.attempts < (this.config.workerRetries + 1);
966
- });
967
- if (budgetRemaining && failedWaveTasks.length > 0) {
968
- for (const t of failedWaveTasks) {
969
- const task = this.taskQueue.getTask(t.id);
970
- if (!task)
971
- continue;
972
- task.status = 'ready';
973
- task.retryContext = {
974
- previousFeedback: 'All tasks in this batch failed. Try a fundamentally different approach — the previous strategy did not work.',
975
- previousScore: 0,
976
- attempt: task.attempts,
977
- previousModel: task.assignedModel,
978
- swarmProgress: this.getSwarmProgressSummary(),
979
- };
980
- }
981
- this.logDecision('wave-recovery', `Re-queued ${failedWaveTasks.length} tasks with adapted retry context`, 'Budget allows retry');
982
- // Re-execute the wave with adapted tasks
983
- await this.executeWave(failedWaveTasks.map(t => this.taskQueue.getTask(t.id)).filter(t => t.status === 'ready'));
984
- }
985
- }
986
- // F5: Adaptive re-decomposition — if < 50% of wave tasks succeeded,
987
- // the decomposition may be structurally flawed. Log for observability.
988
- // (Full re-decomposition of remaining work would require re-architecting the queue,
989
- // so we log the signal and let wave retry + fixup handle recovery.)
990
- const waveTotal = waveCompleted + waveFailed + waveSkipped;
991
- const waveSuccessRate = waveTotal > 0 ? waveCompleted / waveTotal : 0;
992
- if (waveSuccessRate < 0.5 && waveTotal >= 2) {
993
- this.logDecision('decomposition-quality', `Wave ${waveIndex + 1} success rate ${(waveSuccessRate * 100).toFixed(0)}% (${waveCompleted}/${waveTotal})`, 'Low success rate may indicate decomposition quality issues');
994
- }
995
- // V2: Review wave outputs
996
- const review = await this.reviewWave(waveIndex);
997
- if (review && review.fixupTasks.length > 0) {
998
- // Execute fix-up tasks immediately
999
- await this.executeWave(review.fixupTasks);
1000
- }
1001
- // Rescue cascade-skipped tasks that can still run
1002
- // (after wave review + fixup, some skipped tasks may now be viable)
1003
- const rescued = this.rescueCascadeSkipped();
1004
- if (rescued.length > 0) {
1005
- this.logDecision('cascade-rescue', `Rescued ${rescued.length} cascade-skipped tasks after wave ${waveIndex + 1}`, rescued.map(t => t.id).join(', '));
1006
- await this.executeWave(rescued);
1007
- }
1008
- // Reset quality circuit breaker at wave boundary — each wave gets a fresh chance.
1009
- // Within a wave, rejections accumulate properly so the breaker can trip.
1010
- // Between waves, we reset so each wave gets a fresh quality evaluation window.
1011
- // (The within-wave reset at quality-gate-passed is kept — that's correct.)
1012
- if (this.qualityGateDisabledModels.size > 0) {
1013
- this.qualityGateDisabledModels.clear();
1014
- this.perModelQualityRejections.clear();
1015
- this.logDecision('quality-circuit-breaker', `Re-enabled quality gates for all models at wave ${waveIndex + 1} boundary`, 'Each wave gets a fresh quality evaluation window');
1016
- }
1017
- // F3: Log budget reallocation after wave completion.
1018
- // SharedBudgetPool already returns unused tokens via release(), but we log it
1019
- // for observability so operators can see how budget flows between waves.
1020
- const budgetStats = this.budgetPool.getStats();
1021
- this.logDecision('budget-reallocation', `After wave ${waveIndex + 1}: ${budgetStats.tokensRemaining} tokens remaining (${(budgetStats.utilization * 100).toFixed(0)}% utilized)`, '');
1022
- this.budgetPool.reallocateUnused(budgetStats.tokensRemaining);
1023
- // F21: Mid-swarm situational assessment — evaluate success rate and budget health,
1024
- // optionally triage low-priority tasks to conserve budget for critical path.
1025
- await this.assessAndAdapt(waveIndex);
1026
- // V2: Checkpoint after each wave
1027
- this.checkpoint(`wave-${waveIndex}`);
1028
- // Advance to next wave
1029
- if (!this.taskQueue.advanceWave())
1030
- break;
1031
- waveIndex++;
1032
- }
737
+ async executeWavesDelegate() {
738
+ const ctx = this.getInternals();
739
+ await executeWavesImpl(ctx, this.recoveryState, () => this.getStatus());
740
+ this.syncFromInternals(ctx);
1033
741
  }
1034
742
  /**
1035
- * Execute a single wave's tasks with concurrency control.
743
+ * Delegate to executeWave in swarm-execution.ts.
1036
744
  */
1037
- async executeWave(tasks) {
1038
- // Dispatch initial batch with stagger to avoid rate limit storms
1039
- let taskIndex = 0;
1040
- while (taskIndex < tasks.length && this.workerPool.availableSlots > 0 && !this.cancelled) {
1041
- // Circuit breaker: wait if tripped
1042
- if (this.isCircuitBreakerActive()) {
1043
- const waitMs = this.circuitBreakerUntil - Date.now();
1044
- if (waitMs > 0)
1045
- await new Promise(resolve => setTimeout(resolve, waitMs));
1046
- continue; // Re-check after wait
1047
- }
1048
- const task = tasks[taskIndex];
1049
- await this.dispatchTask(task);
1050
- taskIndex++;
1051
- // Stagger dispatches to avoid rate limit storms
1052
- if (taskIndex < tasks.length && this.workerPool.availableSlots > 0) {
1053
- await new Promise(resolve => setTimeout(resolve, this.getStaggerMs()));
1054
- }
1055
- }
1056
- // Process completions and dispatch more tasks as slots open
1057
- while (this.workerPool.activeCount > 0 && !this.cancelled) {
1058
- const completed = await this.workerPool.waitForAny();
1059
- if (!completed)
1060
- break;
1061
- // H2: Use per-task startedAt for accurate duration (not orchestrator startTime)
1062
- await this.handleTaskCompletion(completed.taskId, completed.result, completed.startedAt);
1063
- // Emit budget update
1064
- this.emitBudgetUpdate();
1065
- // Emit status update
1066
- this.emitStatusUpdate();
1067
- // Dispatch more tasks if slots available and tasks remain
1068
- while (taskIndex < tasks.length && this.workerPool.availableSlots > 0 && !this.cancelled) {
1069
- const task = tasks[taskIndex];
1070
- if (task.status === 'ready') {
1071
- await this.dispatchTask(task);
1072
- // Stagger dispatches to avoid rate limit storms
1073
- if (taskIndex + 1 < tasks.length && this.workerPool.availableSlots > 0) {
1074
- await new Promise(resolve => setTimeout(resolve, this.getStaggerMs()));
1075
- }
1076
- }
1077
- taskIndex++;
1078
- }
1079
- // Also check for cross-wave ready tasks to fill slots (skip if circuit breaker active)
1080
- if (this.workerPool.availableSlots > 0 && !this.isCircuitBreakerActive()) {
1081
- const moreReady = this.taskQueue.getAllReadyTasks()
1082
- .filter(t => !this.workerPool.getActiveWorkerStatus().some(w => w.taskId === t.id));
1083
- for (let i = 0; i < moreReady.length; i++) {
1084
- if (this.workerPool.availableSlots <= 0)
1085
- break;
1086
- await this.dispatchTask(moreReady[i]);
1087
- // Stagger dispatches to avoid rate limit storms
1088
- if (i + 1 < moreReady.length && this.workerPool.availableSlots > 0) {
1089
- await new Promise(resolve => setTimeout(resolve, this.getStaggerMs()));
1090
- }
1091
- }
1092
- }
1093
- }
1094
- // F20: Re-dispatch pass — after all workers finish, budget may have been freed
1095
- // by completed tasks. Try to dispatch any still-ready tasks (e.g., those paused
1096
- // by budget exhaustion earlier).
1097
- if (!this.cancelled && this.budgetPool.hasCapacity()) {
1098
- const stillReady = this.taskQueue.getAllReadyTasks()
1099
- .filter(t => !this.workerPool.getActiveWorkerStatus().some(w => w.taskId === t.id));
1100
- if (stillReady.length > 0) {
1101
- this.logDecision('budget-redispatch', `Budget freed after wave — re-dispatching ${stillReady.length} ready task(s)`, `Budget: ${JSON.stringify(this.budgetPool.getStats())}`);
1102
- for (const task of stillReady) {
1103
- if (this.workerPool.availableSlots <= 0 || !this.budgetPool.hasCapacity())
1104
- break;
1105
- await this.dispatchTask(task);
1106
- if (this.workerPool.availableSlots > 0) {
1107
- await new Promise(resolve => setTimeout(resolve, this.getStaggerMs()));
1108
- }
1109
- }
1110
- // Wait for these re-dispatched tasks to complete
1111
- while (this.workerPool.activeCount > 0 && !this.cancelled) {
1112
- const completed = await this.workerPool.waitForAny();
1113
- if (!completed)
1114
- break;
1115
- await this.handleTaskCompletion(completed.taskId, completed.result, completed.startedAt);
1116
- this.emitBudgetUpdate();
1117
- this.emitStatusUpdate();
1118
- }
1119
- }
1120
- }
745
+ async executeWaveDelegate(tasks) {
746
+ const ctx = this.getInternals();
747
+ await executeWaveImpl(ctx, this.recoveryState, tasks, () => this.getStatus());
748
+ this.syncFromInternals(ctx);
1121
749
  }
1122
750
  /**
1123
- * Dispatch a single task to a worker.
1124
- * Selects the worker once and passes it through to avoid double-selection.
751
+ * Delegate to finalRescuePass in swarm-recovery.ts.
1125
752
  */
1126
- async dispatchTask(task) {
1127
- const worker = this.workerPool.selectWorker(task);
1128
- if (!worker) {
1129
- // M2: Emit error and mark task failed instead of silently returning
1130
- // V10: Try resilience recovery if task had previous attempts (prior worker may have produced artifacts)
1131
- this.logDecision('no-worker', `${task.id}: no worker for type ${task.type}`, '');
1132
- if (task.attempts > 0) {
1133
- const syntheticTaskResult = { success: false, output: '', tokensUsed: 0, costUsed: 0, durationMs: 0, model: 'none' };
1134
- const syntheticSpawn = { success: false, output: '', metrics: { tokens: 0, duration: 0, toolCalls: 0 } };
1135
- if (await this.tryResilienceRecovery(task, task.id, syntheticTaskResult, syntheticSpawn)) {
1136
- return;
1137
- }
1138
- }
1139
- this.taskQueue.markFailedWithoutCascade(task.id, 0);
1140
- this.taskQueue.triggerCascadeSkip(task.id);
1141
- this.emit({
1142
- type: 'swarm.task.failed',
1143
- taskId: task.id,
1144
- error: `No worker available for task type: ${task.type}`,
1145
- attempt: task.attempts,
1146
- maxAttempts: 0,
1147
- willRetry: false,
1148
- failureMode: 'error',
1149
- });
1150
- return;
1151
- }
1152
- try {
1153
- // Pre-dispatch auto-split for critical-path bottlenecks
1154
- if (this.shouldAutoSplit(task)) {
1155
- try {
1156
- const splitResult = await this.judgeSplit(task);
1157
- if (splitResult.shouldSplit && splitResult.subtasks) {
1158
- task.status = 'dispatched'; // Required for replaceWithSubtasks
1159
- this.taskQueue.replaceWithSubtasks(task.id, splitResult.subtasks);
1160
- this.emit({
1161
- type: 'swarm.task.resilience',
1162
- taskId: task.id,
1163
- strategy: 'auto-split',
1164
- succeeded: true,
1165
- reason: `Pre-dispatch split into ${splitResult.subtasks.length} parallel subtasks`,
1166
- artifactsFound: 0,
1167
- toolCalls: 0,
1168
- });
1169
- return; // Subtasks now in queue, will be dispatched this wave
1170
- }
1171
- }
1172
- catch (err) {
1173
- this.logDecision('auto-split', `${task.id}: split judge failed — ${err.message}`, '');
1174
- // Fall through to normal dispatch
1175
- }
1176
- }
1177
- this.totalDispatches++;
1178
- const dispatchedModel = task.assignedModel ?? worker.model;
1179
- this.taskQueue.markDispatched(task.id, dispatchedModel);
1180
- if (task.assignedModel && task.assignedModel !== worker.model) {
1181
- this.logDecision('failover', `Dispatching ${task.id} with failover model ${task.assignedModel} (worker default: ${worker.model})`, 'Retry model override is active');
1182
- }
1183
- // Pass the pre-selected worker to avoid double-selection in dispatch()
1184
- await this.workerPool.dispatch(task, worker);
1185
- this.emit({
1186
- type: 'swarm.task.dispatched',
1187
- taskId: task.id,
1188
- description: task.description,
1189
- model: dispatchedModel,
1190
- workerName: worker.name,
1191
- toolCount: worker.allowedTools?.length ?? -1, // -1 = all tools
1192
- tools: worker.allowedTools,
1193
- retryContext: task.retryContext,
1194
- fromModel: task.retryContext ? task.retryContext.previousModel : undefined,
1195
- attempts: task.attempts,
1196
- });
1197
- }
1198
- catch (error) {
1199
- const errorMsg = error.message;
1200
- // F20: Budget exhaustion is NOT a task failure — the task is fine, we just ran out of money.
1201
- // Reset status to ready so it can be picked up if budget becomes available
1202
- // (e.g., after tokens are released from completing tasks).
1203
- if (errorMsg.includes('Budget pool exhausted')) {
1204
- task.status = 'ready';
1205
- this.logDecision('budget-pause', `Cannot dispatch ${task.id}: budget exhausted — task kept ready for potential re-dispatch`, `Budget stats: ${JSON.stringify(this.budgetPool.getStats())}`);
1206
- return;
1207
- }
1208
- this.errors.push({
1209
- taskId: task.id,
1210
- phase: 'dispatch',
1211
- message: errorMsg,
1212
- recovered: false,
1213
- });
1214
- this.logDecision('dispatch-error', `${task.id}: dispatch failed: ${errorMsg.slice(0, 100)}`, `attempts: ${task.attempts}`);
1215
- // V10: Try resilience recovery if task had previous attempts (prior worker may have produced artifacts)
1216
- if (task.attempts > 0) {
1217
- const syntheticTaskResult = { success: false, output: '', tokensUsed: 0, costUsed: 0, durationMs: 0, model: 'none' };
1218
- const syntheticSpawn = { success: false, output: '', metrics: { tokens: 0, duration: 0, toolCalls: 0 } };
1219
- if (await this.tryResilienceRecovery(task, task.id, syntheticTaskResult, syntheticSpawn)) {
1220
- this.errors[this.errors.length - 1].recovered = true;
1221
- return;
1222
- }
1223
- }
1224
- this.taskQueue.markFailedWithoutCascade(task.id, 0);
1225
- this.taskQueue.triggerCascadeSkip(task.id);
1226
- this.emit({
1227
- type: 'swarm.task.failed',
1228
- taskId: task.id,
1229
- error: errorMsg,
1230
- attempt: task.attempts,
1231
- maxAttempts: 1 + this.config.workerRetries,
1232
- willRetry: false,
1233
- failureMode: 'error',
1234
- });
1235
- }
1236
- }
1237
- /**
1238
- * Handle a completed task: quality gate, bookkeeping, retry logic, model health, failover.
1239
- */
1240
- async handleTaskCompletion(taskId, spawnResult, startedAt) {
1241
- const task = this.taskQueue.getTask(taskId);
1242
- if (!task)
1243
- return;
1244
- // Guard: task was terminally resolved while its worker was running — ignore the result
1245
- // F4: But NOT if pendingCascadeSkip — those results are evaluated below
1246
- if ((task.status === 'skipped' || task.status === 'failed') && !task.pendingCascadeSkip)
1247
- return;
1248
- // V7: Global dispatch cap — prevent any single task from burning budget.
1249
- // Try resilience recovery (micro-decompose, degraded acceptance) before hard-failing.
1250
- const maxDispatches = this.config.maxDispatchesPerTask ?? 5;
1251
- if (task.attempts >= maxDispatches) {
1252
- const durationMs = Date.now() - startedAt;
1253
- const taskResult = this.workerPool.toTaskResult(spawnResult, task, durationMs);
1254
- this.totalTokens += taskResult.tokensUsed;
1255
- this.totalCost += taskResult.costUsed;
1256
- // Try resilience recovery before hard fail
1257
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1258
- return;
1259
- }
1260
- this.taskQueue.markFailedWithoutCascade(taskId, 0);
1261
- this.taskQueue.triggerCascadeSkip(taskId);
1262
- this.emit({
1263
- type: 'swarm.task.failed',
1264
- taskId,
1265
- error: `Dispatch cap reached (${maxDispatches} attempts)`,
1266
- attempt: task.attempts,
1267
- maxAttempts: maxDispatches,
1268
- willRetry: false,
1269
- failureMode: task.failureMode,
1270
- });
1271
- this.logDecision('dispatch-cap', `${taskId}: hard cap reached (${task.attempts}/${maxDispatches})`, 'No more retries — resilience recovery also failed');
1272
- return;
1273
- }
1274
- const durationMs = Date.now() - startedAt;
1275
- const taskResult = this.workerPool.toTaskResult(spawnResult, task, durationMs);
1276
- // Track model usage
1277
- const model = task.assignedModel ?? 'unknown';
1278
- const usage = this.modelUsage.get(model) ?? { tasks: 0, tokens: 0, cost: 0 };
1279
- usage.tasks++;
1280
- usage.tokens += taskResult.tokensUsed;
1281
- usage.cost += taskResult.costUsed;
1282
- this.modelUsage.set(model, usage);
1283
- this.totalTokens += taskResult.tokensUsed;
1284
- this.totalCost += taskResult.costUsed;
1285
- // V10: Emit per-attempt event for full decision traceability
1286
- this.emit({
1287
- type: 'swarm.task.attempt',
1288
- taskId,
1289
- attempt: task.attempts,
1290
- model,
1291
- success: spawnResult.success,
1292
- durationMs,
1293
- toolCalls: spawnResult.metrics.toolCalls ?? 0,
1294
- failureMode: !spawnResult.success ? task.failureMode : undefined,
1295
- qualityScore: taskResult.qualityScore,
1296
- output: taskResult.output.slice(0, 500),
1297
- });
1298
- if (!spawnResult.success) {
1299
- // V2: Record model health
1300
- const errorMsg = spawnResult.output.toLowerCase();
1301
- const is429 = errorMsg.includes('429') || errorMsg.includes('rate');
1302
- const is402 = errorMsg.includes('402') || errorMsg.includes('spend limit');
1303
- const isTimeout = spawnResult.metrics.toolCalls === -1;
1304
- // F25: Use 'timeout' errorType for timeouts (was 'error')
1305
- const errorType = is429 ? '429' : is402 ? '402' : isTimeout ? 'timeout' : 'error';
1306
- this.healthTracker.recordFailure(model, errorType);
1307
- this.emit({ type: 'swarm.model.health', record: { model, ...this.getModelHealthSummary(model) } });
1308
- // P6: Tag failure mode for cascade threshold awareness
1309
- task.failureMode = (is429 || is402) ? 'rate-limit' : (spawnResult.metrics.toolCalls === -1 ? 'timeout' : 'error');
1310
- // Feed circuit breaker
1311
- if (is429 || is402) {
1312
- this.recordRateLimit();
1313
- }
1314
- // F25a: Consecutive timeout tracking — early-fail after N consecutive timeouts
1315
- if (isTimeout) {
1316
- const count = (this.taskTimeoutCounts.get(taskId) ?? 0) + 1;
1317
- this.taskTimeoutCounts.set(taskId, count);
1318
- const timeoutLimit = this.config.consecutiveTimeoutLimit ?? 3;
1319
- this.logDecision('timeout-tracking', `${taskId}: consecutive timeout ${count}/${timeoutLimit}`, '');
1320
- if (count >= timeoutLimit) {
1321
- // F25b: Try model failover before giving up
1322
- let failoverSucceeded = false;
1323
- if (this.config.enableModelFailover) {
1324
- const capability = getTaskTypeConfig(task.type, this.config).capability ?? 'code';
1325
- const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
1326
- if (alternative) {
1327
- this.emit({
1328
- type: 'swarm.model.failover',
1329
- taskId,
1330
- fromModel: model,
1331
- toModel: alternative.model,
1332
- reason: 'consecutive-timeouts',
1333
- });
1334
- task.assignedModel = alternative.model;
1335
- this.taskTimeoutCounts.set(taskId, 0); // Reset counter for new model
1336
- this.logDecision('failover', `Timeout failover ${taskId}: ${model} → ${alternative.model}`, `${count} consecutive timeouts`);
1337
- failoverSucceeded = true;
1338
- }
1339
- }
1340
- if (!failoverSucceeded) {
1341
- // No alternative model — try resilience recovery before hard fail.
1342
- // Timeouts often produce artifacts (worker WAS working, just ran out of time).
1343
- task.failureMode = 'timeout';
1344
- const taskResult = this.workerPool.toTaskResult(spawnResult, task, Date.now() - startedAt);
1345
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1346
- this.taskTimeoutCounts.delete(taskId);
1347
- return;
1348
- }
1349
- this.taskQueue.markFailedWithoutCascade(taskId, 0);
1350
- this.taskQueue.triggerCascadeSkip(taskId);
1351
- this.emit({
1352
- type: 'swarm.task.failed',
1353
- taskId,
1354
- error: `${count} consecutive timeouts — no alternative model available`,
1355
- attempt: task.attempts,
1356
- maxAttempts: maxDispatches,
1357
- willRetry: false,
1358
- failureMode: 'timeout',
1359
- });
1360
- this.logDecision('timeout-early-fail', `${taskId}: ${count} consecutive timeouts, no alt model — resilience recovery also failed`, '');
1361
- this.taskTimeoutCounts.delete(taskId);
1362
- return;
1363
- }
1364
- }
1365
- }
1366
- else {
1367
- // Non-timeout failure — reset the counter
1368
- this.taskTimeoutCounts.delete(taskId);
1369
- }
1370
- // V2: Model failover on rate limits
1371
- if ((is429 || is402) && this.config.enableModelFailover) {
1372
- const capability = getTaskTypeConfig(task.type, this.config).capability ?? 'code';
1373
- const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
1374
- if (alternative) {
1375
- this.emit({
1376
- type: 'swarm.model.failover',
1377
- taskId,
1378
- fromModel: model,
1379
- toModel: alternative.model,
1380
- reason: errorType,
1381
- });
1382
- task.assignedModel = alternative.model;
1383
- this.logDecision('failover', `Switched ${taskId} from ${model} to ${alternative.model}`, `${errorType} error`);
1384
- }
1385
- }
1386
- // V5/V7: Store error context so retry gets different prompt
1387
- if (!(is429 || is402)) {
1388
- // V7: Timeout-specific feedback — the worker WAS working, just ran out of time
1389
- const timeoutSeconds = isTimeout ? Math.round(durationMs / 1000) : 0;
1390
- task.retryContext = {
1391
- previousFeedback: isTimeout
1392
- ? `Previous attempt timed out after ${timeoutSeconds}s. You must complete this task more efficiently — work faster, use fewer tool calls, and produce your result sooner.`
1393
- : spawnResult.output.slice(0, 2000),
1394
- previousScore: 0,
1395
- attempt: task.attempts,
1396
- previousModel: model,
1397
- previousFiles: taskResult.filesModified,
1398
- swarmProgress: this.getSwarmProgressSummary(),
1399
- };
1400
- }
1401
- // V7: Reset hollow streak on non-hollow failure (error is not a hollow completion)
1402
- this.hollowStreak = 0;
1403
- // Worker failed — use higher retry limit for rate limit errors.
1404
- // V7: Fixup tasks get capped retries, foundation tasks get +1.
1405
- const baseRetries = this.getEffectiveRetries(task);
1406
- const retryLimit = (is429 || is402)
1407
- ? Math.min(this.config.rateLimitRetries ?? 3, baseRetries + 1)
1408
- : baseRetries;
1409
- const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, retryLimit);
1410
- if (canRetry) {
1411
- this.retries++;
1412
- // Non-blocking cooldown: set retryAfter timestamp instead of blocking
1413
- if (is429 || is402) {
1414
- const baseDelay = this.config.retryBaseDelayMs ?? 5000;
1415
- const cooldownMs = Math.min(baseDelay * Math.pow(2, task.attempts - 1), 30000);
1416
- this.taskQueue.setRetryAfter(taskId, cooldownMs);
1417
- this.logDecision('rate-limit-cooldown', `${taskId}: ${errorType} cooldown ${cooldownMs}ms, model ${model}`, '');
1418
- }
1419
- }
1420
- else if (!(is429 || is402)) {
1421
- // Resilience recovery for non-rate-limit errors (micro-decompose + degraded acceptance)
1422
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1423
- return;
1424
- }
1425
- // Recovery failed — NOW trigger cascade
1426
- this.taskQueue.triggerCascadeSkip(taskId);
1427
- }
1428
- else {
1429
- // Rate-limit exhaustion — trigger cascade
1430
- this.taskQueue.triggerCascadeSkip(taskId);
1431
- }
1432
- this.emit({
1433
- type: 'swarm.task.failed',
1434
- taskId,
1435
- error: spawnResult.output.slice(0, 200),
1436
- attempt: task.attempts,
1437
- maxAttempts: 1 + this.config.workerRetries,
1438
- willRetry: canRetry,
1439
- toolCalls: spawnResult.metrics.toolCalls,
1440
- failoverModel: task.assignedModel !== model ? task.assignedModel : undefined,
1441
- failureMode: task.failureMode,
1442
- });
1443
- return;
1444
- }
1445
- // V6: Hollow completion detection — workers that "succeed" without doing any work
1446
- // Must check BEFORE recording success, otherwise hollow completions inflate health scores
1447
- if (isHollowCompletion(spawnResult, task.type, this.config)) {
1448
- // F4: Hollow result + pendingCascadeSkip — honor the skip immediately, no retry
1449
- if (task.pendingCascadeSkip) {
1450
- task.pendingCascadeSkip = undefined;
1451
- task.status = 'skipped';
1452
- this.totalHollows++;
1453
- this.logDecision('cascade-skip', `${taskId}: pending cascade skip honored (hollow completion)`, '');
1454
- this.emit({ type: 'swarm.task.skipped', taskId, reason: 'cascade skip honored — hollow completion' });
1455
- return;
1456
- }
1457
- // P6: Tag failure mode for cascade threshold awareness
1458
- task.failureMode = 'hollow';
1459
- // Record hollow completion so hollow-prone models accumulate hollow-specific records
1460
- // and get deprioritized by the model selector (also records generic failure internally)
1461
- this.healthTracker.recordHollow(model);
1462
- const admitsFailure = spawnResult.success && FAILURE_INDICATORS.some(f => (spawnResult.output ?? '').toLowerCase().includes(f));
1463
- task.retryContext = {
1464
- previousFeedback: admitsFailure
1465
- ? 'Previous attempt reported success but admitted failure (e.g., "budget exhausted", "unable to complete"). You MUST execute tool calls and produce concrete output this time.'
1466
- : 'Previous attempt produced no meaningful output. Try again with a concrete approach.',
1467
- previousScore: 1,
1468
- attempt: task.attempts,
1469
- previousModel: model,
1470
- previousFiles: taskResult.filesModified,
1471
- swarmProgress: this.getSwarmProgressSummary(),
1472
- };
1473
- // Model failover for hollow completions — same pattern as quality failover
1474
- if (this.config.enableModelFailover) {
1475
- const capability = getTaskTypeConfig(task.type, this.config).capability ?? 'code';
1476
- const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
1477
- if (alternative) {
1478
- this.emit({
1479
- type: 'swarm.model.failover',
1480
- taskId,
1481
- fromModel: model,
1482
- toModel: alternative.model,
1483
- reason: 'hollow-completion',
1484
- });
1485
- task.assignedModel = alternative.model;
1486
- this.logDecision('failover', `Hollow failover ${taskId}: ${model} → ${alternative.model}`, 'Model produced hollow completion');
1487
- }
1488
- }
1489
- const hollowRetries = this.getEffectiveRetries(task);
1490
- const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, hollowRetries);
1491
- if (canRetry) {
1492
- this.retries++;
1493
- }
1494
- else {
1495
- // Retries exhausted — try shared resilience recovery (micro-decompose, degraded acceptance)
1496
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1497
- return;
1498
- }
1499
- // Recovery failed — NOW trigger cascade
1500
- this.taskQueue.triggerCascadeSkip(taskId);
1501
- }
1502
- this.emit({
1503
- type: 'swarm.task.failed',
1504
- taskId,
1505
- error: 'Hollow completion: worker used no tools',
1506
- attempt: task.attempts,
1507
- maxAttempts: 1 + this.config.workerRetries,
1508
- willRetry: canRetry,
1509
- toolCalls: spawnResult.metrics.toolCalls,
1510
- failoverModel: task.assignedModel !== model ? task.assignedModel : undefined,
1511
- failureMode: 'hollow',
1512
- });
1513
- this.hollowStreak++;
1514
- this.totalHollows++;
1515
- this.logDecision('hollow-completion', `${taskId}: worker completed with 0 tool calls (streak: ${this.hollowStreak}, total hollows: ${this.totalHollows}/${this.totalDispatches})`, canRetry ? 'Marking as failed for retry' : 'Retries exhausted — hard fail');
1516
- // B2: Hollow streak handling — only terminate if enableHollowTermination is explicitly on
1517
- if (this.hollowStreak >= SwarmOrchestrator.HOLLOW_STREAK_THRESHOLD) {
1518
- const uniqueModels = new Set(this.config.workers.map(w => w.model));
1519
- const singleModel = uniqueModels.size === 1;
1520
- const onlyModel = [...uniqueModels][0];
1521
- const modelUnhealthy = singleModel && !this.healthTracker.getAllRecords().find(r => r.model === onlyModel)?.healthy;
1522
- if (singleModel && modelUnhealthy) {
1523
- if (this.config.enableHollowTermination) {
1524
- this.logDecision('early-termination', `Terminating swarm: ${this.hollowStreak} consecutive hollow completions on sole model ${onlyModel}`, 'Single-model swarm with unhealthy model — enableHollowTermination is on');
1525
- this.skipRemainingTasks(`Single-model hollow streak (${this.hollowStreak}x on ${onlyModel})`);
1526
- }
1527
- else {
1528
- this.logDecision('stall-mode', `${this.hollowStreak} consecutive hollows on sole model ${onlyModel} — entering stall mode`, 'Will attempt model failover or simplified retry on next dispatch');
1529
- // Reset streak to allow more attempts with adjusted strategy
1530
- this.hollowStreak = 0;
1531
- }
1532
- }
1533
- }
1534
- // V7: Multi-model hollow ratio — warn but don't terminate unless opt-in
1535
- const minDispatches = this.config.hollowTerminationMinDispatches ?? 8;
1536
- const threshold = this.config.hollowTerminationRatio ?? 0.55;
1537
- if (this.totalDispatches >= minDispatches) {
1538
- const ratio = this.totalHollows / this.totalDispatches;
1539
- if (ratio > threshold) {
1540
- if (this.config.enableHollowTermination) {
1541
- this.logDecision('early-termination', `Terminating swarm: hollow ratio ${(ratio * 100).toFixed(0)}% (${this.totalHollows}/${this.totalDispatches})`, `Exceeds threshold ${(threshold * 100).toFixed(0)}% after ${minDispatches}+ dispatches — enableHollowTermination is on`);
1542
- this.skipRemainingTasks(`Hollow ratio ${(ratio * 100).toFixed(0)}% — models cannot execute tasks`);
1543
- }
1544
- else if (!this.hollowRatioWarned) {
1545
- this.hollowRatioWarned = true;
1546
- this.logDecision('stall-warning', `Hollow ratio ${(ratio * 100).toFixed(0)}% (${this.totalHollows}/${this.totalDispatches})`, 'High hollow rate but continuing — tasks may still recover via resilience');
1547
- }
1548
- }
1549
- }
1550
- return;
1551
- }
1552
- // F4: Task had pendingCascadeSkip but produced non-hollow results.
1553
- // Run pre-flight checks — if the output is good, accept it instead of skipping.
1554
- if (task.pendingCascadeSkip) {
1555
- const cachedReport = checkArtifacts(task);
1556
- const preFlight = runPreFlightChecks(task, taskResult, this.config, cachedReport);
1557
- if (preFlight && !preFlight.passed) {
1558
- // Output is garbage — honor the cascade skip
1559
- task.pendingCascadeSkip = undefined;
1560
- task.status = 'skipped';
1561
- this.logDecision('cascade-skip', `${taskId}: pending cascade skip honored (pre-flight failed: ${preFlight.feedback})`, '');
1562
- this.emit({ type: 'swarm.task.skipped', taskId, reason: `cascade skip honored — output failed pre-flight: ${preFlight.feedback}` });
1563
- return;
1564
- }
1565
- // Output is good — clear the flag and accept the result
1566
- task.pendingCascadeSkip = undefined;
1567
- task.status = 'dispatched'; // Reset so markCompleted works
1568
- this.logDecision('cascade-skip', `${taskId}: pending cascade skip overridden — worker produced valid output`, '');
1569
- }
1570
- // Record model health on success (only for non-hollow completions)
1571
- this.healthTracker.recordSuccess(model, durationMs);
1572
- this.decreaseStagger(); // P7: Speed up on success
1573
- // Run quality gate if enabled — skip under API pressure, skip if circuit breaker tripped,
1574
- // and let the final attempt through without quality gate (so tasks produce *something*)
1575
- // Foundation tasks get +1 retry to reduce cascade failure risk.
1576
- const effectiveRetries = this.getEffectiveRetries(task);
1577
- const recentRLCount = this.recentRateLimits.filter(t => t > Date.now() - 30_000).length;
1578
- const isLastAttempt = task.attempts >= (effectiveRetries + 1);
1579
- const shouldRunQualityGate = this.config.qualityGates
1580
- && !this.qualityGateDisabledModels.has(model)
1581
- && !isLastAttempt
1582
- && Date.now() >= this.circuitBreakerUntil
1583
- && recentRLCount < 2;
1584
- // C1: Pre-compute artifact report once — shared by quality gate and pre-flight checks
1585
- const cachedArtifactReport = checkArtifacts(task);
1586
- if (shouldRunQualityGate) {
1587
- // V3: Judge role handles quality gates
1588
- const judgeModel = this.config.hierarchy?.judge?.model
1589
- ?? this.config.qualityGateModel ?? this.config.orchestratorModel;
1590
- const judgeConfig = {
1591
- model: judgeModel,
1592
- persona: this.config.hierarchy?.judge?.persona,
1593
- };
1594
- this.emit({ type: 'swarm.role.action', role: 'judge', action: 'quality-gate', model: judgeModel, taskId });
1595
- // Extract file artifacts from worker output for quality gate visibility.
1596
- // When workers create files via write_file/edit_file, the judge needs to see
1597
- // the actual content — not just the worker's text claims about what was created.
1598
- const fileArtifacts = this.extractFileArtifacts(task, taskResult);
1599
- // Foundation tasks get a relaxed quality threshold (threshold - 1, min 2)
1600
- // to reduce the chance of cascade-skipping the entire swarm.
1601
- const baseThreshold = this.config.qualityThreshold ?? 3;
1602
- const qualityThreshold = task.isFoundation ? Math.max(2, baseThreshold - 1) : baseThreshold;
1603
- const quality = await evaluateWorkerOutput(this.provider, judgeModel, task, taskResult, judgeConfig, qualityThreshold, (resp, purpose) => this.trackOrchestratorUsage(resp, purpose), fileArtifacts, this.config, cachedArtifactReport);
1604
- taskResult.qualityScore = quality.score;
1605
- taskResult.qualityFeedback = quality.feedback;
1606
- // F11: Foundation tasks that barely pass the relaxed threshold get concrete validation.
1607
- // A 2/5 foundation task with truncated output will cascade-poison all dependents.
1608
- if (quality.passed && task.isFoundation && quality.score <= baseThreshold - 1) {
1609
- const concreteResult = runConcreteChecks(task, taskResult);
1610
- if (!concreteResult.passed) {
1611
- quality.passed = false;
1612
- quality.feedback += ` [F11: foundation task barely passed (${quality.score}/${baseThreshold}) but concrete validation failed: ${concreteResult.issues.join('; ')}]`;
1613
- this.logDecision('foundation-concrete-gate', `${taskId}: foundation task scored ${quality.score} (relaxed threshold ${qualityThreshold}) but concrete checks failed — rejecting`, concreteResult.issues.join('; '));
1614
- }
1615
- }
1616
- if (!quality.passed) {
1617
- // F7: Gate error fallback — when LLM judge fails, use concrete validation
1618
- // If concrete checks pass, tentatively accept the result instead of rejecting.
1619
- if (quality.gateError && (this.config.enableConcreteValidation !== false)) {
1620
- const concreteResult = runConcreteChecks(task, taskResult);
1621
- if (concreteResult.passed) {
1622
- // Concrete validation passed — tentatively accept despite gate error
1623
- this.logDecision('gate-error-fallback', `${taskId}: gate error but concrete checks passed — tentatively accepting`, quality.gateErrorMessage ?? 'unknown');
1624
- taskResult.qualityScore = quality.score;
1625
- taskResult.qualityFeedback = `${quality.feedback} [concrete validation passed — tentative accept]`;
1626
- // Fall through to success path (don't return)
1627
- }
1628
- else {
1629
- // Both gate and concrete failed — reject
1630
- this.logDecision('gate-error-fallback', `${taskId}: gate error AND concrete checks failed — rejecting`, `Concrete issues: ${concreteResult.issues.join('; ')}`);
1631
- // Fall through to normal rejection below
1632
- }
1633
- // If concrete passed, skip the rejection path
1634
- if (concreteResult.passed) {
1635
- this.perModelQualityRejections.delete(model);
1636
- // Jump to success path below
1637
- }
1638
- else {
1639
- // Proceed with normal rejection
1640
- this.qualityRejections++;
1641
- task.failureMode = 'quality';
1642
- this.healthTracker.recordQualityRejection(model, quality.score);
1643
- this.emit({ type: 'swarm.model.health', record: { model, ...this.getModelHealthSummary(model) } });
1644
- this.hollowStreak = 0;
1645
- task.retryContext = {
1646
- previousFeedback: `Gate error + concrete validation failed: ${concreteResult.issues.join('; ')}`,
1647
- previousScore: quality.score,
1648
- attempt: task.attempts,
1649
- previousModel: model,
1650
- previousFiles: taskResult.filesModified,
1651
- swarmProgress: this.getSwarmProgressSummary(),
1652
- };
1653
- const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
1654
- if (canRetry) {
1655
- this.retries++;
1656
- }
1657
- else {
1658
- // Retries exhausted — try resilience recovery before cascade-skip
1659
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1660
- return;
1661
- }
1662
- // Recovery failed — NOW trigger cascade
1663
- this.taskQueue.triggerCascadeSkip(taskId);
1664
- }
1665
- this.emit({
1666
- type: 'swarm.quality.rejected',
1667
- taskId,
1668
- score: quality.score,
1669
- feedback: quality.feedback,
1670
- artifactCount: fileArtifacts.length,
1671
- outputLength: taskResult.output.length,
1672
- preFlightReject: false,
1673
- filesOnDisk: checkArtifactsEnhanced(task, taskResult).files.filter(f => f.exists && f.sizeBytes > 0).length,
1674
- });
1675
- return;
1676
- }
1677
- }
1678
- else if (!quality.gateError) {
1679
- // Normal quality rejection (LLM judge rejected, no gate error)
1680
- this.qualityRejections++;
1681
- // P6: Tag failure mode for cascade threshold awareness
1682
- task.failureMode = 'quality';
1683
- // P1: Quality rejections update model health — undo premature recordSuccess
1684
- this.healthTracker.recordQualityRejection(model, quality.score);
1685
- this.emit({ type: 'swarm.model.health', record: { model, ...this.getModelHealthSummary(model) } });
1686
- // V7: Quality rejection is NOT hollow — worker did work, just poorly
1687
- this.hollowStreak = 0;
1688
- // F7: Per-model circuit breaker → "pre-flight only mode" instead of fully disabling gates.
1689
- // After threshold rejections, skip LLM judge but keep pre-flight mandatory.
1690
- if (!quality.preFlightReject) {
1691
- const modelRejections = (this.perModelQualityRejections.get(model) ?? 0) + 1;
1692
- this.perModelQualityRejections.set(model, modelRejections);
1693
- if (modelRejections >= SwarmOrchestrator.QUALITY_CIRCUIT_BREAKER_THRESHOLD) {
1694
- this.qualityGateDisabledModels.add(model);
1695
- this.logDecision('quality-circuit-breaker', `Switched model ${model} to pre-flight-only mode after ${modelRejections} rejections`, 'Skipping LLM judge but keeping pre-flight checks mandatory');
1696
- }
1697
- }
1698
- // V5: Attach feedback so retry prompt includes it
1699
- task.retryContext = {
1700
- previousFeedback: quality.feedback,
1701
- previousScore: quality.score,
1702
- attempt: task.attempts,
1703
- previousModel: model,
1704
- previousFiles: taskResult.filesModified,
1705
- swarmProgress: this.getSwarmProgressSummary(),
1706
- };
1707
- // V5: Model failover on quality rejection — but NOT on artifact auto-fails
1708
- // P1: Widened from score<=1 to score<threshold so failover triggers on any rejection
1709
- if (quality.score < qualityThreshold && this.config.enableModelFailover && !quality.artifactAutoFail) {
1710
- const capability = getTaskTypeConfig(task.type, this.config).capability ?? 'code';
1711
- const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
1712
- if (alternative) {
1713
- this.emit({
1714
- type: 'swarm.model.failover',
1715
- taskId,
1716
- fromModel: model,
1717
- toModel: alternative.model,
1718
- reason: `quality-score-${quality.score}`,
1719
- });
1720
- task.assignedModel = alternative.model;
1721
- this.logDecision('failover', `Quality failover ${taskId}: ${model} → ${alternative.model}`, `Score ${quality.score}/5`);
1722
- }
1723
- }
1724
- const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
1725
- if (canRetry) {
1726
- this.retries++;
1727
- }
1728
- else {
1729
- // Retries exhausted — try resilience recovery before cascade-skip
1730
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1731
- return;
1732
- }
1733
- // Recovery failed — NOW trigger cascade
1734
- this.taskQueue.triggerCascadeSkip(taskId);
1735
- }
1736
- // M1: Only emit quality.rejected (not duplicate task.failed)
1737
- this.emit({
1738
- type: 'swarm.quality.rejected',
1739
- taskId,
1740
- score: quality.score,
1741
- feedback: quality.feedback,
1742
- artifactCount: fileArtifacts.length,
1743
- outputLength: taskResult.output.length,
1744
- preFlightReject: quality.preFlightReject,
1745
- filesOnDisk: checkArtifactsEnhanced(task, taskResult).files.filter(f => f.exists && f.sizeBytes > 0).length,
1746
- });
1747
- return;
1748
- }
1749
- else {
1750
- // gateError=true but concrete validation disabled — reject
1751
- this.qualityRejections++;
1752
- task.failureMode = 'quality';
1753
- this.hollowStreak = 0;
1754
- task.retryContext = {
1755
- previousFeedback: quality.feedback,
1756
- previousScore: quality.score,
1757
- attempt: task.attempts,
1758
- previousModel: model,
1759
- previousFiles: taskResult.filesModified,
1760
- swarmProgress: this.getSwarmProgressSummary(),
1761
- };
1762
- const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
1763
- if (canRetry) {
1764
- this.retries++;
1765
- }
1766
- else {
1767
- // Retries exhausted — try resilience recovery before cascade-skip
1768
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1769
- return;
1770
- }
1771
- // Recovery failed — NOW trigger cascade
1772
- this.taskQueue.triggerCascadeSkip(taskId);
1773
- }
1774
- this.emit({
1775
- type: 'swarm.quality.rejected',
1776
- taskId,
1777
- score: quality.score,
1778
- feedback: quality.feedback,
1779
- artifactCount: fileArtifacts.length,
1780
- outputLength: taskResult.output.length,
1781
- preFlightReject: false,
1782
- filesOnDisk: checkArtifactsEnhanced(task, taskResult).files.filter(f => f.exists && f.sizeBytes > 0).length,
1783
- });
1784
- return;
1785
- }
1786
- }
1787
- // Quality passed — reset per-model rejection counter
1788
- this.perModelQualityRejections.delete(model);
1789
- }
1790
- // F7: When quality gate was skipped (last attempt, pre-flight-only mode, API pressure),
1791
- // still run pre-flight + concrete checks so obviously broken outputs don't slip through.
1792
- // C1: Use cached artifact report to avoid double filesystem scan.
1793
- if (!shouldRunQualityGate && this.config.qualityGates) {
1794
- const preFlight = runPreFlightChecks(task, taskResult, this.config, cachedArtifactReport);
1795
- if (preFlight && !preFlight.passed) {
1796
- taskResult.qualityScore = preFlight.score;
1797
- taskResult.qualityFeedback = preFlight.feedback;
1798
- this.qualityRejections++;
1799
- const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
1800
- if (canRetry) {
1801
- this.retries++;
1802
- }
1803
- else {
1804
- // Retries exhausted — try resilience recovery before cascade-skip
1805
- this.logDecision('preflight-reject', `${taskId}: pre-flight failed: ${preFlight.feedback}`, '');
1806
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1807
- return;
1808
- }
1809
- // Recovery failed — NOW trigger cascade
1810
- this.taskQueue.triggerCascadeSkip(taskId);
1811
- }
1812
- this.emit({
1813
- type: 'swarm.quality.rejected',
1814
- taskId,
1815
- score: preFlight.score,
1816
- feedback: preFlight.feedback,
1817
- artifactCount: 0,
1818
- outputLength: taskResult.output.length,
1819
- preFlightReject: true,
1820
- });
1821
- return;
1822
- }
1823
- // F2: Run concrete validation when pre-flight passes but gate was skipped
1824
- if (this.config.enableConcreteValidation !== false) {
1825
- const concreteResult = runConcreteChecks(task, taskResult);
1826
- if (!concreteResult.passed) {
1827
- taskResult.qualityScore = 2;
1828
- taskResult.qualityFeedback = `Concrete validation failed: ${concreteResult.issues.join('; ')}`;
1829
- this.qualityRejections++;
1830
- const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
1831
- if (canRetry) {
1832
- this.retries++;
1833
- }
1834
- else {
1835
- // Retries exhausted — try resilience recovery before cascade-skip
1836
- this.logDecision('concrete-reject', `${taskId}: concrete validation failed: ${concreteResult.issues.join('; ')}`, '');
1837
- if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
1838
- return;
1839
- }
1840
- // Recovery failed — NOW trigger cascade
1841
- this.taskQueue.triggerCascadeSkip(taskId);
1842
- }
1843
- this.emit({
1844
- type: 'swarm.quality.rejected',
1845
- taskId,
1846
- score: 2,
1847
- feedback: taskResult.qualityFeedback,
1848
- artifactCount: 0,
1849
- outputLength: taskResult.output.length,
1850
- preFlightReject: false,
1851
- });
1852
- return;
1853
- }
1854
- }
1855
- }
1856
- // Task passed — mark completed
1857
- this.taskQueue.markCompleted(taskId, taskResult);
1858
- this.hollowStreak = 0;
1859
- // F25: Clear timeout counter on success
1860
- this.taskTimeoutCounts.delete(taskId);
1861
- // H6: Post findings to blackboard with error handling
1862
- if (this.blackboard && taskResult.findings) {
1863
- try {
1864
- for (const finding of taskResult.findings) {
1865
- this.blackboard.post(`swarm-worker-${taskId}`, {
1866
- topic: `swarm.task.${task.type}`,
1867
- content: finding,
1868
- type: 'progress',
1869
- confidence: (taskResult.qualityScore ?? 3) / 5,
1870
- tags: ['swarm', task.type],
1871
- relatedFiles: task.targetFiles,
1872
- });
1873
- }
1874
- }
1875
- catch {
1876
- // Don't crash orchestrator on blackboard failures
1877
- this.errors.push({
1878
- taskId,
1879
- phase: 'execution',
1880
- message: 'Failed to post findings to blackboard',
1881
- recovered: true,
1882
- });
1883
- }
1884
- }
1885
- this.emit({
1886
- type: 'swarm.task.completed',
1887
- taskId,
1888
- success: true,
1889
- tokensUsed: taskResult.tokensUsed,
1890
- costUsed: taskResult.costUsed,
1891
- durationMs: taskResult.durationMs,
1892
- qualityScore: taskResult.qualityScore,
1893
- qualityFeedback: taskResult.qualityFeedback,
1894
- output: taskResult.output,
1895
- closureReport: taskResult.closureReport,
1896
- toolCalls: spawnResult.metrics.toolCalls,
1897
- });
1898
- }
1899
- /**
1900
- * Phase 4: Synthesize all completed task outputs.
1901
- */
1902
- async synthesize() {
1903
- const tasks = this.taskQueue.getAllTasks();
1904
- const outputs = tasks
1905
- .filter(t => t.status === 'completed')
1906
- .map(t => taskResultToAgentOutput(t, this.config))
1907
- .filter((o) => o !== null);
1908
- if (outputs.length === 0)
1909
- return null;
1910
- try {
1911
- return await this.synthesizer.synthesize(outputs);
1912
- }
1913
- catch (error) {
1914
- this.errors.push({
1915
- phase: 'synthesis',
1916
- message: error.message,
1917
- recovered: true,
1918
- });
1919
- // Fallback: concatenate outputs
1920
- return this.synthesizer.synthesizeFindings(outputs);
1921
- }
1922
- }
1923
- /**
1924
- * Get live status for TUI.
1925
- */
1926
- // M5: Use explicit phase tracking instead of inferring from queue state
1927
- getStatus() {
1928
- const stats = this.taskQueue.getStats();
1929
- return {
1930
- phase: this.cancelled ? 'failed' : this.currentPhase,
1931
- currentWave: this.taskQueue.getCurrentWave() + 1,
1932
- totalWaves: this.taskQueue.getTotalWaves(),
1933
- activeWorkers: this.workerPool.getActiveWorkerStatus(),
1934
- queue: stats,
1935
- budget: {
1936
- tokensUsed: this.totalTokens + this.orchestratorTokens,
1937
- tokensTotal: this.config.totalBudget,
1938
- costUsed: this.totalCost + this.orchestratorCost,
1939
- costTotal: this.config.maxCost,
1940
- },
1941
- orchestrator: {
1942
- tokens: this.orchestratorTokens,
1943
- cost: this.orchestratorCost,
1944
- calls: this.orchestratorCalls,
1945
- model: this.config.orchestratorModel,
1946
- },
1947
- };
1948
- }
1949
- /**
1950
- * Cancel the swarm execution.
1951
- * M6: Wait for active workers before cleanup.
1952
- */
1953
- async cancel() {
1954
- this.cancelled = true;
1955
- this.currentPhase = 'failed';
1956
- await this.workerPool.cancelAll();
1957
- }
1958
- // ─── D3: Model Capability Probing ─────────────────────────────────────
1959
- /**
1960
- * D3/F23: Probe each unique model to verify it can make tool calls.
1961
- * Models that fail the probe are marked unhealthy so they're skipped in dispatch.
1962
- *
1963
- * F23 fix: Uses chatWithTools() with actual tool definitions instead of
1964
- * plain chat() which never included tools in the API request.
1965
- */
1966
- async probeModelCapability() {
1967
- const uniqueModels = new Set(this.config.workers.map(w => w.model));
1968
- this.emit({ type: 'swarm.phase.progress', phase: 'scheduling', message: `Probing ${uniqueModels.size} model(s) for tool-calling capability...` });
1969
- // F23: Check if provider supports native tool calling
1970
- const supportsTools = 'chatWithTools' in this.provider
1971
- && typeof this.provider.chatWithTools === 'function';
1972
- if (!supportsTools) {
1973
- // Provider doesn't support chatWithTools — skip probe entirely.
1974
- // Workers will rely on text-based tool parsing fallback.
1975
- this.logDecision('model-probe', 'Provider does not support chatWithTools — skipping probe', '');
1976
- return;
1977
- }
1978
- const providerWithTools = this.provider;
1979
- const probeTools = [{
1980
- type: 'function',
1981
- function: {
1982
- name: 'read_file',
1983
- description: 'Read a file from disk',
1984
- parameters: {
1985
- type: 'object',
1986
- properties: { path: { type: 'string', description: 'File path' } },
1987
- required: ['path'],
1988
- },
1989
- },
1990
- }];
1991
- // F24: Configurable probe timeout — generous default for slow models/connections
1992
- const probeTimeout = this.config.probeTimeoutMs ?? 60_000;
1993
- for (const model of uniqueModels) {
1994
- try {
1995
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Probe timeout (${probeTimeout}ms)`)), probeTimeout));
1996
- const response = await Promise.race([
1997
- providerWithTools.chatWithTools([
1998
- { role: 'system', content: 'You are a test probe. Call the read_file tool with path "package.json".' },
1999
- { role: 'user', content: 'Read package.json.' },
2000
- ], { model, maxTokens: 200, temperature: 0, tools: probeTools, tool_choice: 'required' }),
2001
- timeoutPromise,
2002
- ]);
2003
- const hasToolCall = (response.toolCalls?.length ?? 0) > 0;
2004
- if (!hasToolCall) {
2005
- // F19: Directly mark unhealthy — probe failure is definitive evidence
2006
- this.healthTracker.markUnhealthy(model);
2007
- this.logDecision('model-probe', `Model ${model} failed probe (no tool calls)`, 'Marked unhealthy');
2008
- }
2009
- else {
2010
- this.healthTracker.recordSuccess(model, 0);
2011
- this.logDecision('model-probe', `Model ${model} passed probe`, '');
2012
- }
2013
- }
2014
- catch {
2015
- // F19: Directly mark unhealthy on probe error (includes timeout)
2016
- this.healthTracker.markUnhealthy(model);
2017
- this.logDecision('model-probe', `Model ${model} probe errored`, 'Marked unhealthy');
2018
- }
2019
- }
2020
- }
2021
- // ─── Circuit Breaker ────────────────────────────────────────────────
2022
- /**
2023
- * Record a rate limit hit and check if the circuit breaker should trip.
2024
- */
2025
- recordRateLimit() {
2026
- const now = Date.now();
2027
- this.recentRateLimits.push(now);
2028
- this.increaseStagger(); // P7: Back off on rate limits
2029
- // Prune entries older than the window
2030
- const cutoff = now - SwarmOrchestrator.CIRCUIT_BREAKER_WINDOW_MS;
2031
- this.recentRateLimits = this.recentRateLimits.filter(t => t > cutoff);
2032
- if (this.recentRateLimits.length >= SwarmOrchestrator.CIRCUIT_BREAKER_THRESHOLD) {
2033
- this.circuitBreakerUntil = now + SwarmOrchestrator.CIRCUIT_BREAKER_PAUSE_MS;
2034
- this.emit({
2035
- type: 'swarm.circuit.open',
2036
- recentCount: this.recentRateLimits.length,
2037
- pauseMs: SwarmOrchestrator.CIRCUIT_BREAKER_PAUSE_MS,
2038
- });
2039
- this.logDecision('circuit-breaker', 'Tripped — pausing all dispatch', `${this.recentRateLimits.length} rate limits in ${SwarmOrchestrator.CIRCUIT_BREAKER_WINDOW_MS / 1000}s window`);
2040
- }
2041
- }
2042
- /**
2043
- * Check if the circuit breaker is currently active.
2044
- * Returns true if dispatch should be paused.
2045
- */
2046
- isCircuitBreakerActive() {
2047
- if (Date.now() < this.circuitBreakerUntil)
2048
- return true;
2049
- if (this.circuitBreakerUntil > 0) {
2050
- // Circuit just closed
2051
- this.circuitBreakerUntil = 0;
2052
- this.emit({ type: 'swarm.circuit.closed' });
2053
- }
2054
- return false;
2055
- }
2056
- // ─── P7: Adaptive Stagger ────────────────────────────────────────────
2057
- /** P7: Get current stagger delay (adapts based on rate limit / success signals). */
2058
- getStaggerMs() {
2059
- return this.adaptiveStaggerMs;
2060
- }
2061
- /** P7: Increase stagger on rate limit (×1.5, capped at 10s). */
2062
- increaseStagger() {
2063
- this.adaptiveStaggerMs = Math.min(this.adaptiveStaggerMs * 1.5, 10_000);
2064
- }
2065
- /** P7: Decrease stagger on success (×0.9, floor at 200ms). */
2066
- decreaseStagger() {
2067
- this.adaptiveStaggerMs = Math.max(this.adaptiveStaggerMs * 0.9, 200);
2068
- }
2069
- // ─── V2: Decision Logging ─────────────────────────────────────────────
2070
- logDecision(phase, decision, reasoning) {
2071
- const entry = {
2072
- timestamp: Date.now(),
2073
- phase,
2074
- decision,
2075
- reasoning,
2076
- };
2077
- this.orchestratorDecisions.push(entry);
2078
- this.emit({ type: 'swarm.orchestrator.decision', decision: entry });
2079
- }
2080
- // ─── V2: Persistence ──────────────────────────────────────────────────
2081
- checkpoint(_label) {
2082
- if (!this.config.enablePersistence || !this.stateStore)
2083
- return;
2084
- try {
2085
- const queueState = this.taskQueue.getCheckpointState();
2086
- this.stateStore.saveCheckpoint({
2087
- sessionId: this.stateStore.id,
2088
- timestamp: Date.now(),
2089
- phase: this.currentPhase,
2090
- plan: this.plan,
2091
- taskStates: queueState.taskStates,
2092
- waves: queueState.waves,
2093
- currentWave: queueState.currentWave,
2094
- stats: {
2095
- totalTokens: this.totalTokens + this.orchestratorTokens,
2096
- totalCost: this.totalCost + this.orchestratorCost,
2097
- qualityRejections: this.qualityRejections,
2098
- retries: this.retries,
2099
- },
2100
- modelHealth: this.healthTracker.getAllRecords(),
2101
- decisions: this.orchestratorDecisions,
2102
- errors: this.errors,
2103
- originalPrompt: this.originalPrompt,
2104
- });
2105
- this.emit({
2106
- type: 'swarm.state.checkpoint',
2107
- sessionId: this.stateStore.id,
2108
- wave: this.taskQueue.getCurrentWave(),
2109
- });
2110
- }
2111
- catch (error) {
2112
- this.errors.push({
2113
- phase: 'persistence',
2114
- message: `Checkpoint failed (non-fatal): ${error.message}`,
2115
- recovered: true,
2116
- });
2117
- }
2118
- }
2119
- // ─── Private Helpers ───────────────────────────────────────────────────
2120
- emitBudgetUpdate() {
2121
- this.emit({
2122
- type: 'swarm.budget.update',
2123
- tokensUsed: this.totalTokens + this.orchestratorTokens,
2124
- tokensTotal: this.config.totalBudget,
2125
- costUsed: this.totalCost + this.orchestratorCost,
2126
- costTotal: this.config.maxCost,
2127
- });
2128
- }
2129
- emitStatusUpdate() {
2130
- this.emit({ type: 'swarm.status', status: this.getStatus() });
2131
- }
2132
- buildStats() {
2133
- const queueStats = this.taskQueue.getStats();
2134
- return {
2135
- totalTasks: queueStats.total,
2136
- completedTasks: queueStats.completed,
2137
- failedTasks: queueStats.failed,
2138
- skippedTasks: queueStats.skipped,
2139
- totalWaves: this.taskQueue.getTotalWaves(),
2140
- totalTokens: this.totalTokens + this.orchestratorTokens,
2141
- totalCost: this.totalCost + this.orchestratorCost,
2142
- totalDurationMs: Date.now() - this.startTime,
2143
- qualityRejections: this.qualityRejections,
2144
- retries: this.retries,
2145
- modelUsage: this.modelUsage,
2146
- };
2147
- }
2148
- buildSummary(stats) {
2149
- const parts = [
2150
- `Swarm execution complete:`,
2151
- ` Tasks: ${stats.completedTasks}/${stats.totalTasks} completed, ${stats.failedTasks} failed, ${stats.skippedTasks} skipped`,
2152
- ` Waves: ${stats.totalWaves}`,
2153
- ` Tokens: ${(stats.totalTokens / 1000).toFixed(0)}k`,
2154
- ` Cost: $${stats.totalCost.toFixed(4)}`,
2155
- ` Duration: ${(stats.totalDurationMs / 1000).toFixed(1)}s`,
2156
- ];
2157
- if (stats.qualityRejections > 0) {
2158
- parts.push(` Quality rejections: ${stats.qualityRejections}`);
2159
- }
2160
- if (stats.retries > 0) {
2161
- parts.push(` Retries: ${stats.retries}`);
2162
- }
2163
- if (this.verificationResult) {
2164
- parts.push(` Verification: ${this.verificationResult.passed ? 'PASSED' : 'FAILED'}`);
2165
- }
2166
- // Artifact inventory: show what files actually exist on disk regardless of task status
2167
- if (this.artifactInventory && this.artifactInventory.totalFiles > 0) {
2168
- parts.push(` Files on disk: ${this.artifactInventory.totalFiles} files (${(this.artifactInventory.totalBytes / 1024).toFixed(1)}KB)`);
2169
- for (const f of this.artifactInventory.files.slice(0, 15)) {
2170
- parts.push(` ${f.path}: ${f.sizeBytes}B`);
2171
- }
2172
- if (this.artifactInventory.files.length > 15) {
2173
- parts.push(` ... and ${this.artifactInventory.files.length - 15} more`);
2174
- }
2175
- }
2176
- return parts.join('\n');
2177
- }
2178
- buildErrorResult(message) {
2179
- return {
2180
- success: false,
2181
- summary: `Swarm failed: ${message}`,
2182
- tasks: this.taskQueue.getAllTasks(),
2183
- stats: this.buildStats(),
2184
- errors: this.errors,
2185
- };
2186
- }
2187
- /** Parse JSON from LLM response, handling markdown code blocks. */
2188
- parseJSON(content) {
2189
- try {
2190
- // Strip markdown code blocks if present
2191
- let json = content;
2192
- const codeBlockMatch = content.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
2193
- if (codeBlockMatch) {
2194
- json = codeBlockMatch[1];
2195
- }
2196
- return JSON.parse(json);
2197
- }
2198
- catch {
2199
- return null;
2200
- }
2201
- }
2202
- /**
2203
- * Detect foundation tasks: tasks that are a dependency of 2+ downstream tasks.
2204
- * These are critical single-points-of-failure — mark them for extra resilience.
2205
- */
2206
- detectFoundationTasks() {
2207
- const allTasks = this.taskQueue.getAllTasks();
2208
- const dependentCounts = new Map();
2209
- for (const task of allTasks) {
2210
- for (const depId of task.dependencies) {
2211
- dependentCounts.set(depId, (dependentCounts.get(depId) ?? 0) + 1);
2212
- }
2213
- }
2214
- for (const task of allTasks) {
2215
- const dependentCount = dependentCounts.get(task.id) ?? 0;
2216
- if (dependentCount >= 2) {
2217
- task.isFoundation = true;
2218
- this.logDecision('scheduling', `Foundation task: ${task.id} (${dependentCount} dependents)`, 'Extra retries and relaxed quality threshold applied');
2219
- }
2220
- }
2221
- }
2222
- /**
2223
- * Extract file artifacts from a worker's output for quality gate visibility.
2224
- * Reads actual file content from disk so the judge can verify real work,
2225
- * not just text claims about what was created.
2226
- */
2227
- extractFileArtifacts(task, taskResult) {
2228
- const artifacts = [];
2229
- const seen = new Set();
2230
- // Collect file paths from multiple sources
2231
- const candidatePaths = [];
2232
- // 1. filesModified from structured closure report
2233
- if (taskResult.filesModified) {
2234
- candidatePaths.push(...taskResult.filesModified);
2235
- }
2236
- // 2. targetFiles from task definition
2237
- if (task.targetFiles) {
2238
- candidatePaths.push(...task.targetFiles);
2239
- }
2240
- // 3. Extract file paths mentioned in worker output (e.g., "Created src/foo.ts")
2241
- const filePathPattern = /(?:created|wrote|modified|edited|updated)\s+["`']?([^\s"`',]+\.\w+)/gi;
2242
- let match;
2243
- while ((match = filePathPattern.exec(taskResult.output)) !== null) {
2244
- candidatePaths.push(match[1]);
2245
- }
2246
- // Resolve against the target project directory, not CWD
2247
- const baseDir = this.config.facts?.workingDirectory ?? process.cwd();
2248
- // Read previews from disk
2249
- for (const filePath of candidatePaths) {
2250
- if (seen.has(filePath))
2251
- continue;
2252
- seen.add(filePath);
2253
- try {
2254
- const resolved = path.resolve(baseDir, filePath);
2255
- if (fs.existsSync(resolved)) {
2256
- const content = fs.readFileSync(resolved, 'utf-8');
2257
- if (content.length > 0) {
2258
- artifacts.push({ path: filePath, preview: content.slice(0, 2000) });
2259
- }
2260
- }
2261
- }
2262
- catch {
2263
- // Skip unreadable files
2264
- }
2265
- // Limit to 10 files to keep prompt size reasonable
2266
- if (artifacts.length >= 10)
2267
- break;
2268
- }
2269
- return artifacts;
2270
- }
2271
- /**
2272
- * Build an inventory of filesystem artifacts produced during swarm execution.
2273
- * Scans all tasks' targetFiles and readFiles to check what actually exists on disk.
2274
- * This reveals work done by workers even when tasks "failed" (timeout, quality gate, etc.).
2275
- */
2276
- buildArtifactInventory() {
2277
- const allFiles = new Set();
2278
- for (const task of this.taskQueue.getAllTasks()) {
2279
- for (const f of (task.targetFiles ?? []))
2280
- allFiles.add(f);
2281
- for (const f of (task.readFiles ?? []))
2282
- allFiles.add(f);
2283
- }
2284
- const baseDir = this.config.facts?.workingDirectory ?? process.cwd();
2285
- const artifacts = [];
2286
- for (const filePath of allFiles) {
2287
- try {
2288
- const resolved = path.resolve(baseDir, filePath);
2289
- if (fs.existsSync(resolved)) {
2290
- const stats = fs.statSync(resolved);
2291
- if (stats.isFile() && stats.size > 0) {
2292
- artifacts.push({ path: filePath, sizeBytes: stats.size, exists: true });
2293
- }
2294
- }
2295
- }
2296
- catch { /* skip unreadable files */ }
2297
- }
2298
- return {
2299
- files: artifacts,
2300
- totalFiles: artifacts.length,
2301
- totalBytes: artifacts.reduce((s, a) => s + a.sizeBytes, 0),
2302
- };
2303
- }
2304
- /**
2305
- * Skip all remaining pending/ready tasks (used for early termination).
2306
- */
2307
- skipRemainingTasks(reason) {
2308
- for (const task of this.taskQueue.getAllTasks()) {
2309
- if (task.status === 'pending' || task.status === 'ready') {
2310
- task.status = 'skipped';
2311
- this.emit({ type: 'swarm.task.skipped', taskId: task.id, reason });
2312
- }
2313
- }
2314
- }
2315
- /**
2316
- * F21: Mid-swarm situational assessment after each wave.
2317
- * Evaluates success rate and budget health, triages low-priority tasks when budget is tight.
2318
- * Also detects stalled progress and triggers mid-swarm re-planning.
2319
- */
2320
- async assessAndAdapt(waveIndex) {
2321
- const stats = this.taskQueue.getStats();
2322
- const budgetStats = this.budgetPool.getStats();
2323
- // 1. Calculate success rate for this swarm run
2324
- const successRate = stats.completed / Math.max(1, stats.completed + stats.failed + stats.skipped);
2325
- // 2. Budget efficiency: tokens spent per completed task
2326
- const tokensPerTask = stats.completed > 0
2327
- ? (this.totalTokens / stats.completed)
2328
- : Infinity;
2329
- // 3. Remaining budget vs remaining tasks
2330
- const remainingTasks = stats.total - stats.completed - stats.failed - stats.skipped;
2331
- const estimatedTokensNeeded = remainingTasks * tokensPerTask;
2332
- const budgetSufficient = budgetStats.tokensRemaining > estimatedTokensNeeded * 0.5;
2333
- // Log the assessment for observability
2334
- this.logDecision('mid-swarm-assessment', `After wave ${waveIndex + 1}: ${stats.completed}/${stats.total} completed (${(successRate * 100).toFixed(0)}%), ` +
2335
- `${remainingTasks} remaining, ${budgetStats.tokensRemaining} tokens left`, budgetSufficient ? 'Budget looks sufficient' : 'Budget may be insufficient for remaining tasks');
2336
- // 4. If budget is tight, prioritize: skip low-value remaining tasks
2337
- // Only triage if we have actual data (at least one completion to estimate from)
2338
- if (!budgetSufficient && remainingTasks > 1 && stats.completed > 0) {
2339
- // Prefer pausing over skipping: if workers are still running, wait for budget release
2340
- const runningCount = stats.running ?? 0;
2341
- if (runningCount > 0) {
2342
- this.logDecision('budget-wait', 'Budget tight but workers still running — waiting for budget release', `${runningCount} workers active, ${budgetStats.tokensRemaining} tokens remaining`);
2343
- return;
2344
- }
2345
- const expendableTasks = this.findExpendableTasks();
2346
- // Hard cap: never skip more than 20% of remaining tasks in one triage pass
2347
- const maxSkips = Math.max(1, Math.floor(remainingTasks * 0.2));
2348
- if (expendableTasks.length > 0) {
2349
- let currentEstimate = estimatedTokensNeeded;
2350
- let skipped = 0;
2351
- for (const task of expendableTasks) {
2352
- if (skipped >= maxSkips)
2353
- break;
2354
- // Stop trimming once we're within budget
2355
- if (currentEstimate * 0.7 <= budgetStats.tokensRemaining)
2356
- break;
2357
- task.status = 'skipped';
2358
- skipped++;
2359
- this.emit({ type: 'swarm.task.skipped', taskId: task.id,
2360
- reason: 'Budget conservation: skipping low-priority task to protect critical path' });
2361
- this.logDecision('budget-triage', `Skipping ${task.id} (${task.type}, complexity ${task.complexity}) to conserve budget`, `${remainingTasks} tasks remain, ${budgetStats.tokensRemaining} tokens`);
2362
- currentEstimate -= tokensPerTask;
2363
- }
2364
- }
2365
- }
2366
- // 5. Stall detection: if progress ratio is too low, trigger re-plan
2367
- const attemptedTasks = stats.completed + stats.failed + stats.skipped;
2368
- if (attemptedTasks >= 5) {
2369
- const progressRatio = stats.completed / Math.max(1, attemptedTasks);
2370
- if (progressRatio < 0.4) {
2371
- this.logDecision('stall-detected', `Progress stalled: ${stats.completed}/${attemptedTasks} tasks succeeded (${(progressRatio * 100).toFixed(0)}%)`, 'Triggering mid-swarm re-plan');
2372
- this.emit({
2373
- type: 'swarm.stall',
2374
- progressRatio,
2375
- attempted: attemptedTasks,
2376
- completed: stats.completed,
2377
- });
2378
- await this.midSwarmReplan();
2379
- }
2380
- }
2381
- }
2382
- /**
2383
- * F21: Find expendable tasks — leaf tasks (no dependents) with lowest complexity.
2384
- * These are the safest to skip when budget is tight.
2385
- * Only tasks with complexity <= 2 are considered expendable.
2386
- */
2387
- findExpendableTasks() {
2388
- const allTasks = this.taskQueue.getAllTasks();
2389
- // Build reverse dependency map: which tasks depend on each task?
2390
- const dependentCounts = new Map();
2391
- for (const task of allTasks) {
2392
- for (const depId of task.dependencies) {
2393
- dependentCounts.set(depId, (dependentCounts.get(depId) ?? 0) + 1);
2394
- }
2395
- }
2396
- // Expendable = pending/ready, never attempted, no dependents, not foundation,
2397
- // complexity <= 2 (simple leaf tasks only), lowest complexity first
2398
- return allTasks
2399
- .filter(t => (t.status === 'pending' || t.status === 'ready') &&
2400
- t.attempts === 0 &&
2401
- !t.isFoundation &&
2402
- (t.complexity ?? 5) <= 2 &&
2403
- (dependentCounts.get(t.id) ?? 0) === 0)
2404
- .sort((a, b) => (a.complexity ?? 5) - (b.complexity ?? 5));
2405
- }
2406
- /**
2407
- * Mid-swarm re-planning: when progress stalls, ask LLM to re-plan remaining work.
2408
- * Creates simpler replacement tasks for stuck/failed work, building on what's already done.
2409
- * Only triggers once per swarm execution to avoid infinite re-planning loops.
2410
- */
2411
- async midSwarmReplan() {
2412
- if (this.hasReplanned)
2413
- return;
2414
- this.hasReplanned = true;
2415
- const allTasks = this.taskQueue.getAllTasks();
2416
- const completed = allTasks.filter(t => t.status === 'completed' || t.status === 'decomposed');
2417
- const stuck = allTasks.filter(t => t.status === 'failed' || t.status === 'skipped');
2418
- if (stuck.length === 0)
2419
- return;
2420
- const completedSummary = completed.map(t => `- ${t.description} [${t.type}] → completed${t.degraded ? ' (degraded)' : ''}`).join('\n') || '(none)';
2421
- const stuckSummary = stuck.map(t => `- ${t.description} [${t.type}] → ${t.status} (${t.failureMode ?? 'unknown'})`).join('\n');
2422
- const artifactInventory = this.buildArtifactInventory();
2423
- const artifactSummary = artifactInventory.files.map(f => `- ${f.path} (${f.sizeBytes}B)`).join('\n') || '(none)';
2424
- const replanPrompt = `The swarm is stalled. Here's the situation:
2425
-
2426
- COMPLETED WORK:
2427
- ${completedSummary}
2428
-
2429
- FILES ON DISK:
2430
- ${artifactSummary}
2431
-
2432
- STUCK TASKS (failed or skipped):
2433
- ${stuckSummary}
2434
-
2435
- Re-plan the remaining work. Create new subtasks that:
2436
- 1. Build on what's already completed (don't redo work)
2437
- 2. Are more focused in scope (but assign realistic complexity for the work involved — don't underestimate)
2438
- 3. Can succeed independently (minimize dependencies)
2439
-
2440
- Return JSON: { "subtasks": [{ "description": "...", "type": "implement|test|research|review|document|refactor", "complexity": 1-5, "dependencies": [], "relevantFiles": [] }] }
2441
- Return ONLY the JSON object, no other text.`;
2442
- try {
2443
- const response = await this.provider.chat([{ role: 'user', content: replanPrompt }]);
2444
- this.trackOrchestratorUsage(response, 'mid-swarm-replan');
2445
- const content = response.content ?? '';
2446
- const jsonMatch = content.match(/\{[\s\S]*"subtasks"[\s\S]*\}/);
2447
- if (!jsonMatch) {
2448
- this.logDecision('replan-failed', 'LLM produced no parseable re-plan JSON', content.slice(0, 200));
2449
- return;
2450
- }
2451
- const parsed = JSON.parse(jsonMatch[0]);
2452
- if (!parsed.subtasks || parsed.subtasks.length === 0) {
2453
- this.logDecision('replan-failed', 'LLM produced empty subtask list', '');
2454
- return;
2455
- }
2456
- // Add new tasks from re-plan into current wave
2457
- const newTasks = this.taskQueue.addReplanTasks(parsed.subtasks, this.taskQueue.getCurrentWave());
2458
- this.logDecision('replan-success', `Re-planned ${stuck.length} stuck tasks into ${newTasks.length} new tasks`, newTasks.map(t => t.description).join('; '));
2459
- this.emit({
2460
- type: 'swarm.replan',
2461
- stuckCount: stuck.length,
2462
- newTaskCount: newTasks.length,
2463
- });
2464
- this.emit({
2465
- type: 'swarm.orchestrator.decision',
2466
- decision: {
2467
- timestamp: Date.now(),
2468
- phase: 'replan',
2469
- decision: `Re-planned ${stuck.length} stuck tasks into ${newTasks.length} new tasks`,
2470
- reasoning: newTasks.map(t => `${t.id}: ${t.description}`).join('; '),
2471
- },
2472
- });
2473
- }
2474
- catch (error) {
2475
- this.logDecision('replan-failed', `Re-plan LLM call failed: ${error.message}`, '');
2476
- }
2477
- }
2478
- /**
2479
- * Rescue cascade-skipped tasks that can still run.
2480
- * After cascade-skip fires, assess whether skipped tasks can still be attempted:
2481
- * - If all OTHER dependencies completed and the failed dep's artifacts exist on disk → un-skip
2482
- * - If the task has no strict data dependency on the failed task (different file targets) → un-skip with warning
2483
- */
2484
- rescueCascadeSkipped(lenient = false) {
2485
- const skippedTasks = this.taskQueue.getSkippedTasks();
2486
- const rescued = [];
2487
- for (const task of skippedTasks) {
2488
- if (task.dependencies.length === 0)
2489
- continue;
2490
- let completedDeps = 0;
2491
- let failedDepsWithArtifacts = 0;
2492
- let failedDepsWithoutArtifacts = 0;
2493
- let skippedDepsBlockedBySkipped = 0;
2494
- let totalDeps = 0;
2495
- const failedDepDescriptions = [];
2496
- for (const depId of task.dependencies) {
2497
- const dep = this.taskQueue.getTask(depId);
2498
- if (!dep)
2499
- continue;
2500
- totalDeps++;
2501
- if (dep.status === 'completed' || dep.status === 'decomposed') {
2502
- completedDeps++;
2503
- }
2504
- else if (dep.status === 'failed' || dep.status === 'skipped') {
2505
- // V10: In lenient mode, use checkArtifactsEnhanced for broader detection
2506
- const artifactReport = lenient ? checkArtifactsEnhanced(dep) : checkArtifacts(dep);
2507
- if (artifactReport && artifactReport.files.filter(f => f.exists && f.sizeBytes > 0).length > 0) {
2508
- failedDepsWithArtifacts++;
2509
- failedDepDescriptions.push(`${dep.description} (failed but ${artifactReport.files.filter(f => f.exists && f.sizeBytes > 0).length} artifacts exist)`);
2510
- }
2511
- else {
2512
- // Check if this dep's target files exist on disk (may have been created by earlier attempt)
2513
- const targetFiles = dep.targetFiles ?? [];
2514
- const existingFiles = targetFiles.filter(f => {
2515
- try {
2516
- const resolved = path.resolve(this.config.facts?.workingDirectory ?? process.cwd(), f);
2517
- return fs.statSync(resolved).size > 0;
2518
- }
2519
- catch {
2520
- return false;
2521
- }
2522
- });
2523
- if (existingFiles.length > 0) {
2524
- failedDepsWithArtifacts++;
2525
- failedDepDescriptions.push(`${dep.description} (failed but ${existingFiles.length}/${targetFiles.length} target files exist)`);
2526
- }
2527
- else {
2528
- // Check if skipped task's targets don't overlap with the failed dep's targets
2529
- const taskTargets = new Set(task.targetFiles ?? []);
2530
- const depTargets = new Set(dep.targetFiles ?? []);
2531
- const hasOverlap = [...taskTargets].some(f => depTargets.has(f));
2532
- if (!hasOverlap && taskTargets.size > 0) {
2533
- // Different file targets — task probably doesn't need the failed dep's output
2534
- failedDepsWithArtifacts++;
2535
- failedDepDescriptions.push(`${dep.description} (failed, no file overlap — likely independent)`);
2536
- }
2537
- else if (lenient && dep.status === 'skipped') {
2538
- // V10: In lenient mode, count skipped-by-skipped deps separately
2539
- // (transitive cascade — the dep itself was a victim, not truly broken)
2540
- skippedDepsBlockedBySkipped++;
2541
- failedDepDescriptions.push(`${dep.description} (skipped — transitive cascade victim)`);
2542
- }
2543
- else {
2544
- failedDepsWithoutArtifacts++;
2545
- }
2546
- }
2547
- }
2548
- }
2549
- }
2550
- // Rescue condition:
2551
- // Normal: all failed deps have artifacts or are independent, AND at least some deps completed
2552
- // Lenient: tolerate up to 1 truly-missing dep, and count transitive cascade victims as recoverable
2553
- const effectiveWithout = failedDepsWithoutArtifacts;
2554
- const maxMissing = lenient ? 1 : 0;
2555
- const hasEnoughContext = lenient ? (completedDeps + failedDepsWithArtifacts + skippedDepsBlockedBySkipped > 0) : (completedDeps > 0);
2556
- if (totalDeps > 0 && effectiveWithout <= maxMissing && hasEnoughContext) {
2557
- const rescueContext = `Rescued from cascade-skip${lenient ? ' (lenient)' : ''}: ${completedDeps}/${totalDeps} deps completed, ` +
2558
- `${failedDepsWithArtifacts} failed deps have artifacts${skippedDepsBlockedBySkipped > 0 ? `, ${skippedDepsBlockedBySkipped} transitive cascade victims` : ''}. ${failedDepDescriptions.join('; ')}`;
2559
- this.taskQueue.rescueTask(task.id, rescueContext);
2560
- rescued.push(task);
2561
- this.logDecision('cascade-rescue', `${task.id}: rescued from cascade-skip${lenient ? ' (lenient)' : ''}`, rescueContext);
2562
- }
2563
- }
2564
- return rescued;
2565
- }
2566
- /**
2567
- * Final rescue pass — runs after executeWaves() finishes.
2568
- * Uses lenient mode to rescue cascade-skipped tasks that have partial context.
2569
- * Re-dispatches rescued tasks in a final wave.
2570
- */
2571
- async finalRescuePass() {
2572
- const skipped = this.taskQueue.getSkippedTasks();
2573
- if (skipped.length === 0)
2574
- return;
2575
- this.logDecision('final-rescue', `${skipped.length} skipped tasks — running final rescue pass`, '');
2576
- const rescued = this.rescueCascadeSkipped(true); // lenient=true
2577
- if (rescued.length > 0) {
2578
- this.logDecision('final-rescue', `Rescued ${rescued.length} tasks`, rescued.map(t => t.id).join(', '));
2579
- await this.executeWave(rescued);
2580
- }
2581
- }
2582
- /**
2583
- * Try resilience recovery strategies before hard-failing a task.
2584
- * Called from dispatch-cap, timeout, hollow, and error paths to avoid bypassing resilience.
2585
- *
2586
- * Strategies (in order):
2587
- * 1. Micro-decomposition — break complex failing tasks into subtasks
2588
- * 2. Degraded acceptance — accept partial work if artifacts exist on disk
2589
- *
2590
- * Returns true if recovery succeeded (caller should return), false if hard-fail should proceed.
2591
- */
2592
- async tryResilienceRecovery(task, taskId, taskResult, spawnResult) {
2593
- // Strategy 1: Micro-decompose complex tasks into smaller subtasks
2594
- // V10: Lowered threshold from >= 6 to >= 4 so moderately complex tasks can be recovered
2595
- if ((task.complexity ?? 0) >= 4 && task.attempts >= 2 && this.budgetPool.hasCapacity()) {
2596
- const subtasks = await this.microDecompose(task);
2597
- if (subtasks && subtasks.length >= 2) {
2598
- // Reset task status so replaceWithSubtasks can mark it as decomposed
2599
- task.status = 'dispatched';
2600
- this.taskQueue.replaceWithSubtasks(taskId, subtasks);
2601
- this.logDecision('micro-decompose', `${taskId}: decomposed into ${subtasks.length} subtasks after ${task.attempts} failures`, subtasks.map(s => `${s.id}: ${s.description.slice(0, 60)}`).join('; '));
2602
- this.emit({
2603
- type: 'swarm.task.failed',
2604
- taskId,
2605
- error: `Micro-decomposed into ${subtasks.length} subtasks`,
2606
- attempt: task.attempts,
2607
- maxAttempts: this.config.maxDispatchesPerTask ?? 5,
2608
- willRetry: false,
2609
- toolCalls: spawnResult.metrics.toolCalls,
2610
- failureMode: task.failureMode,
2611
- });
2612
- this.emit({
2613
- type: 'swarm.task.resilience',
2614
- taskId,
2615
- strategy: 'micro-decompose',
2616
- succeeded: true,
2617
- reason: `Decomposed into ${subtasks.length} subtasks after ${task.attempts} failures`,
2618
- artifactsFound: 0,
2619
- toolCalls: spawnResult.metrics.toolCalls ?? 0,
2620
- });
2621
- return true;
2622
- }
2623
- // Micro-decompose was attempted but didn't produce usable subtasks
2624
- if ((task.complexity ?? 0) < 4) {
2625
- this.logDecision('resilience-skip', `${taskId}: skipped micro-decompose — complexity ${task.complexity} < 4`, '');
2626
- }
2627
- }
2628
- // Strategy 2: Degraded acceptance — check if any attempt produced files on disk.
2629
- // V10: Use checkArtifactsEnhanced for broader detection (filesModified, closureReport, output)
2630
- const artifactReport = checkArtifactsEnhanced(task, taskResult);
2631
- const existingArtifacts = artifactReport.files.filter(f => f.exists && f.sizeBytes > 0);
2632
- const hasArtifacts = existingArtifacts.length > 0;
2633
- // V10: Fix timeout detection — toolCalls=-1 means timeout (worker WAS working)
2634
- const toolCalls = spawnResult.metrics.toolCalls ?? 0;
2635
- const hadToolCalls = toolCalls > 0 || toolCalls === -1
2636
- || (taskResult.filesModified && taskResult.filesModified.length > 0);
2637
- if (hasArtifacts || hadToolCalls) {
2638
- // Accept with degraded flag — prevents cascade-skip of dependents
2639
- taskResult.success = true;
2640
- taskResult.degraded = true;
2641
- taskResult.qualityScore = 2; // Capped at low quality
2642
- taskResult.qualityFeedback = 'Degraded acceptance: retries exhausted but filesystem artifacts exist';
2643
- task.degraded = true;
2644
- // Reset status so markCompleted works (markFailed may have set it to 'failed')
2645
- task.status = 'dispatched';
2646
- this.taskQueue.markCompleted(taskId, taskResult);
2647
- this.hollowStreak = 0;
2648
- this.logDecision('degraded-acceptance', `${taskId}: accepted as degraded — ${existingArtifacts.length} artifacts on disk, ${toolCalls} tool calls`, 'Prevents cascade-skip of dependent tasks');
2649
- this.emit({
2650
- type: 'swarm.task.completed',
2651
- taskId,
2652
- success: true,
2653
- tokensUsed: taskResult.tokensUsed,
2654
- costUsed: taskResult.costUsed,
2655
- durationMs: taskResult.durationMs,
2656
- qualityScore: 2,
2657
- qualityFeedback: 'Degraded acceptance',
2658
- output: taskResult.output,
2659
- toolCalls: spawnResult.metrics.toolCalls,
2660
- });
2661
- this.emit({
2662
- type: 'swarm.task.resilience',
2663
- taskId,
2664
- strategy: 'degraded-acceptance',
2665
- succeeded: true,
2666
- reason: `${existingArtifacts.length} artifacts on disk, ${toolCalls} tool calls`,
2667
- artifactsFound: existingArtifacts.length,
2668
- toolCalls,
2669
- });
2670
- return true;
2671
- }
2672
- // Both strategies failed — log exhaustion for traceability
2673
- this.logDecision('resilience-exhausted', `${taskId}: no recovery — artifacts: ${existingArtifacts.length}, toolCalls: ${toolCalls}, filesModified: ${taskResult.filesModified?.length ?? 0}`, '');
2674
- this.emit({
2675
- type: 'swarm.task.resilience',
2676
- taskId,
2677
- strategy: 'none',
2678
- succeeded: false,
2679
- reason: `No artifacts found, toolCalls=${toolCalls}, filesModified=${taskResult.filesModified?.length ?? 0}`,
2680
- artifactsFound: existingArtifacts.length,
2681
- toolCalls,
2682
- });
2683
- return false;
2684
- }
2685
- /**
2686
- * Micro-decompose a complex task into 2-3 smaller subtasks using the LLM.
2687
- * Called when a complex task (complexity >= 6) fails 2+ times with the same failure mode.
2688
- * Returns null if decomposition doesn't make sense or LLM can't produce valid subtasks.
2689
- */
2690
- async microDecompose(task) {
2691
- if ((task.complexity ?? 0) < 4)
2692
- return null;
2693
- try {
2694
- const prompt = `Task "${task.description}" failed ${task.attempts} times on model ${task.assignedModel ?? 'unknown'}.
2695
- The task has complexity ${task.complexity}/10 and type "${task.type}".
2696
- ${task.targetFiles?.length ? `Target files: ${task.targetFiles.join(', ')}` : ''}
2697
-
2698
- Break this task into 2-3 smaller, independent subtasks that each handle a portion of the work.
2699
- Each subtask MUST be simpler (complexity <= ${Math.ceil(task.complexity / 2)}).
2700
- Each subtask should be self-contained and produce concrete file changes.
2701
-
2702
- Return JSON ONLY (no markdown, no explanation):
2703
- {
2704
- "subtasks": [
2705
- { "description": "...", "type": "${task.type}", "targetFiles": ["..."], "complexity": <number> }
2706
- ]
2707
- }`;
2708
- const response = await this.provider.chat([
2709
- { role: 'system', content: 'You are a task decomposition assistant. Return only valid JSON.' },
2710
- { role: 'user', content: prompt },
2711
- ], {
2712
- model: this.config.orchestratorModel,
2713
- maxTokens: 2000,
2714
- temperature: 0.3,
2715
- });
2716
- this.trackOrchestratorUsage(response, 'micro-decompose');
2717
- // Parse response — handle markdown code blocks
2718
- let jsonStr = response.content.trim();
2719
- const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
2720
- if (codeBlockMatch)
2721
- jsonStr = codeBlockMatch[1].trim();
2722
- const parsed = JSON.parse(jsonStr);
2723
- if (!parsed.subtasks || !Array.isArray(parsed.subtasks) || parsed.subtasks.length < 2) {
2724
- return null;
2725
- }
2726
- const subtasks = parsed.subtasks.map((sub, idx) => ({
2727
- id: `${task.id}-sub${idx + 1}`,
2728
- description: sub.description,
2729
- type: sub.type ?? task.type,
2730
- dependencies: [], // Will be set by replaceWithSubtasks
2731
- status: 'ready',
2732
- complexity: Math.min(sub.complexity ?? Math.ceil(task.complexity / 2), task.complexity - 1),
2733
- wave: task.wave,
2734
- targetFiles: sub.targetFiles ?? [],
2735
- readFiles: task.readFiles,
2736
- attempts: 0,
2737
- }));
2738
- return subtasks;
2739
- }
2740
- catch (error) {
2741
- this.logDecision('micro-decompose', `${task.id}: micro-decomposition failed — ${error.message}`, 'Falling through to normal failure path');
2742
- return null;
2743
- }
2744
- }
2745
- // ─── Pre-Dispatch Auto-Split ──────────────────────────────────────────────
2746
- /**
2747
- * Heuristic pre-filter: should this task be considered for auto-split?
2748
- * Cheap check — no LLM call. Returns true if all conditions are met.
2749
- */
2750
- shouldAutoSplit(task) {
2751
- const cfg = this.config.autoSplit;
2752
- if (cfg?.enabled === false)
2753
- return false;
2754
- const floor = cfg?.complexityFloor ?? 6;
2755
- const splittable = cfg?.splittableTypes ?? ['implement', 'refactor', 'test'];
2756
- // Only first attempts — retries use micro-decompose
2757
- if (task.attempts > 0)
2758
- return false;
2759
- // Complexity check
2760
- if ((task.complexity ?? 0) < floor)
2761
- return false;
2762
- // Type check
2763
- if (!splittable.includes(task.type))
2764
- return false;
2765
- // Must be on critical path (foundation task)
2766
- if (!task.isFoundation)
2767
- return false;
2768
- // Budget capacity check
2769
- if (!this.budgetPool.hasCapacity())
2770
- return false;
2771
- return true;
2772
- }
2773
- /**
2774
- * LLM judge call: ask the orchestrator model whether and how to split a task.
2775
- * Returns { shouldSplit: false } or { shouldSplit: true, subtasks: [...] }.
2776
- */
2777
- async judgeSplit(task) {
2778
- const maxSubs = this.config.autoSplit?.maxSubtasks ?? 4;
2779
- const prompt = `You are evaluating whether a task should be split into parallel subtasks before dispatch.
2780
-
2781
- TASK: "${task.description}"
2782
- TYPE: ${task.type}
2783
- COMPLEXITY: ${task.complexity}/10
2784
- TARGET FILES: ${task.targetFiles?.join(', ') || 'none specified'}
2785
- DOWNSTREAM DEPENDENTS: This is a foundation task — other tasks are waiting on it.
2786
-
2787
- Should this task be split into 2-${maxSubs} parallel subtasks that different workers can execute simultaneously?
2788
-
2789
- SPLIT if:
2790
- - The task involves multiple independent pieces of work (e.g., different files, different functions, different concerns)
2791
- - Parallel execution would meaningfully reduce wall-clock time
2792
- - The subtasks can produce useful output independently
2793
-
2794
- DO NOT SPLIT if:
2795
- - The work is conceptually atomic (one function, one algorithm, tightly coupled logic)
2796
- - The subtasks would need to coordinate on the same files/functions
2797
- - Splitting would add more overhead than it saves
2798
-
2799
- Return JSON ONLY:
2800
- {
2801
- "shouldSplit": true/false,
2802
- "reason": "brief explanation",
2803
- "subtasks": [
2804
- { "description": "...", "type": "${task.type}", "targetFiles": ["..."], "complexity": <number 1-10> }
2805
- ]
2806
- }
2807
- If shouldSplit is false, omit subtasks.`;
2808
- const response = await this.provider.chat([
2809
- { role: 'system', content: 'You are a task planning judge. Return only valid JSON.' },
2810
- { role: 'user', content: prompt },
2811
- ], {
2812
- model: this.config.orchestratorModel,
2813
- maxTokens: 1500,
2814
- temperature: 0.2,
2815
- });
2816
- this.trackOrchestratorUsage(response, 'auto-split-judge');
2817
- // Parse response — reuse markdown code block stripping from microDecompose
2818
- let jsonStr = response.content.trim();
2819
- const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
2820
- if (codeBlockMatch)
2821
- jsonStr = codeBlockMatch[1].trim();
2822
- const parsed = JSON.parse(jsonStr);
2823
- if (!parsed.shouldSplit) {
2824
- this.logDecision('auto-split', `${task.id}: judge says no split — ${parsed.reason}`, '');
2825
- return { shouldSplit: false };
2826
- }
2827
- if (!parsed.subtasks || !Array.isArray(parsed.subtasks) || parsed.subtasks.length < 2) {
2828
- return { shouldSplit: false };
2829
- }
2830
- // Build SwarmTask[] from judge output (same pattern as microDecompose)
2831
- const subtasks = parsed.subtasks.slice(0, maxSubs).map((sub, idx) => ({
2832
- id: `${task.id}-split${idx + 1}`,
2833
- description: sub.description,
2834
- type: sub.type ?? task.type,
2835
- dependencies: [],
2836
- status: 'ready',
2837
- complexity: Math.max(3, Math.min(sub.complexity ?? Math.ceil(task.complexity / 2), task.complexity - 1)),
2838
- wave: task.wave,
2839
- targetFiles: sub.targetFiles ?? [],
2840
- readFiles: task.readFiles,
2841
- attempts: 0,
2842
- rescueContext: `Auto-split from ${task.id} (original complexity ${task.complexity})`,
2843
- }));
2844
- this.logDecision('auto-split', `${task.id}: split into ${subtasks.length} subtasks — ${parsed.reason}`, subtasks.map(s => `${s.id}: ${s.description.slice(0, 60)}`).join('; '));
2845
- return { shouldSplit: true, subtasks };
2846
- }
2847
- /**
2848
- * V7: Compute effective retry limit for a task.
2849
- * F10: Fixup tasks get max 2 retries (3 attempts total) — one full model-failover cycle.
2850
- * Foundation tasks get +1 retry to reduce cascade failure risk.
2851
- */
2852
- getEffectiveRetries(task) {
2853
- const isFixup = 'fixesTaskId' in task;
2854
- if (isFixup)
2855
- return 2; // Fixup tasks: 2 retries max (3 attempts total)
2856
- return task.isFoundation ? this.config.workerRetries + 1 : this.config.workerRetries;
2857
- }
2858
- /**
2859
- * F22: Build a brief summary of swarm progress for retry context.
2860
- * Helps retrying workers understand what the swarm has already accomplished.
2861
- */
2862
- getSwarmProgressSummary() {
2863
- const allTasks = this.taskQueue.getAllTasks();
2864
- const completed = allTasks.filter(t => t.status === 'completed');
2865
- if (completed.length === 0)
2866
- return '';
2867
- const lines = [];
2868
- for (const task of completed) {
2869
- const score = task.result?.qualityScore ? ` (${task.result.qualityScore}/5)` : '';
2870
- lines.push(`- ${task.id}: ${task.description.slice(0, 80)}${score}`);
2871
- }
2872
- // Collect files created by completed tasks
2873
- const files = new Set();
2874
- const baseDir = this.config.facts?.workingDirectory ?? process.cwd();
2875
- for (const task of completed) {
2876
- for (const f of (task.result?.filesModified ?? []))
2877
- files.add(f);
2878
- for (const f of (task.targetFiles ?? [])) {
2879
- try {
2880
- const resolved = path.resolve(baseDir, f);
2881
- if (fs.existsSync(resolved))
2882
- files.add(f);
2883
- }
2884
- catch { /* skip */ }
2885
- }
2886
- }
2887
- const parts = [`The following tasks have completed successfully:\n${lines.join('\n')}`];
2888
- if (files.size > 0) {
2889
- parts.push(`Files already created/modified: ${[...files].slice(0, 20).join(', ')}`);
2890
- parts.push('You can build on these existing files.');
2891
- }
2892
- return parts.join('\n');
2893
- }
2894
- /** Get a model health summary for emitting events. */
2895
- getModelHealthSummary(model) {
2896
- const records = this.healthTracker.getAllRecords();
2897
- const record = records.find(r => r.model === model);
2898
- return record
2899
- ? { successes: record.successes, failures: record.failures, rateLimits: record.rateLimits, lastRateLimit: record.lastRateLimit, averageLatencyMs: record.averageLatencyMs, healthy: record.healthy }
2900
- : { successes: 0, failures: 0, rateLimits: 0, averageLatencyMs: 0, healthy: true };
753
+ async finalRescuePassDelegate() {
754
+ const ctx = this.getInternals();
755
+ await finalRescuePass(ctx, (tasks) => this.executeWaveDelegate(tasks));
756
+ this.syncFromInternals(ctx);
2901
757
  }
2902
758
  }
2903
759
  /**