crewswarm 0.9.1 → 0.9.3

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 (210) hide show
  1. package/README.md +22 -9
  2. package/apps/dashboard/dist/assets/{chat-core-Cx4sTxDd.js → chat-core-3KirthZA.js} +1 -1
  3. package/apps/dashboard/dist/assets/index-GSWxxEPO.js +2 -0
  4. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
  5. package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
  6. package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +1 -0
  7. package/apps/dashboard/dist/index.html +82 -11
  8. package/apps/vibe/README.md +2 -2
  9. package/apps/vibe/package.json +1 -1
  10. package/apps/vibe/server.mjs +3 -3
  11. package/crew-lead.mjs +48 -5
  12. package/lib/bridges/gateway-ws.mjs +4 -0
  13. package/lib/bridges/tmux-bridge.mjs +200 -0
  14. package/lib/cli-process-tracker.mjs +2 -1
  15. package/lib/crew-lead/chat-handler.mjs +34 -0
  16. package/lib/crew-lead/http-server.mjs +340 -14
  17. package/lib/crew-lead/llm-caller.mjs +24 -8
  18. package/lib/crew-lead/prompts.mjs +7 -0
  19. package/lib/crew-lead/wave-dispatcher.mjs +53 -3
  20. package/lib/crew-lead/ws-router.mjs +219 -27
  21. package/lib/engines/engine-registry.mjs +9 -0
  22. package/lib/engines/rt-envelope.mjs +1 -0
  23. package/lib/engines/runners.mjs +26 -2
  24. package/lib/runtime/config.mjs +7 -0
  25. package/lib/runtime/paths.mjs +12 -8
  26. package/lib/sessions/session-manager.mjs +287 -0
  27. package/package.json +35 -15
  28. package/scripts/capture-build-flow.mjs +118 -0
  29. package/scripts/coverage-report.mjs +209 -0
  30. package/scripts/coverage-summary.mjs +47 -0
  31. package/scripts/dashboard-validation.mjs +74 -0
  32. package/scripts/dashboard.mjs +560 -70
  33. package/scripts/live-bridge-matrix.mjs +79 -0
  34. package/scripts/live-cli-matrix.mjs +166 -0
  35. package/scripts/live-crewchat-check.mjs +42 -0
  36. package/scripts/live-engine-matrix.mjs +50 -0
  37. package/scripts/live-provider-failover-matrix.mjs +107 -0
  38. package/scripts/live-provider-matrix.mjs +228 -0
  39. package/scripts/restart-all-from-repo.sh +4 -4
  40. package/scripts/smoke-dispatch.mjs +4 -1
  41. package/scripts/test-blast-radius.mjs +204 -0
  42. package/scripts/test-report-summary.mjs +88 -0
  43. package/scripts/test-reporter.mjs +651 -0
  44. package/scripts/test-rerun.mjs +136 -0
  45. package/scripts/tmux-bridge +130 -0
  46. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  47. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  48. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  49. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  50. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  51. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  52. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  53. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  54. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  56. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  60. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  62. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  64. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  65. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  66. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  67. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  68. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  69. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  70. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  71. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  72. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  73. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  74. package/apps/dashboard/dist/index.html.br +0 -0
  75. package/apps/dashboard/index.html +0 -6459
  76. package/apps/dashboard/package.json +0 -15
  77. package/apps/dashboard/src/app.js +0 -2823
  78. package/apps/dashboard/src/app.js.br +0 -0
  79. package/apps/dashboard/src/app.js.gz +0 -0
  80. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  81. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  82. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  83. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  84. package/apps/dashboard/src/cli-process.js +0 -208
  85. package/apps/dashboard/src/cli-process.js.br +0 -0
  86. package/apps/dashboard/src/cli-process.js.gz +0 -0
  87. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  88. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  89. package/apps/dashboard/src/core/api.js +0 -18
  90. package/apps/dashboard/src/core/api.js.br +0 -0
  91. package/apps/dashboard/src/core/dom.js +0 -228
  92. package/apps/dashboard/src/core/dom.js.br +0 -0
  93. package/apps/dashboard/src/core/state.js +0 -91
  94. package/apps/dashboard/src/core/state.js.br +0 -0
  95. package/apps/dashboard/src/core/task-manager.js +0 -134
  96. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  97. package/apps/dashboard/src/orchestration-status.js +0 -127
  98. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  99. package/apps/dashboard/src/setup-wizard.js +0 -562
  100. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  101. package/apps/dashboard/src/styles.css +0 -2085
  102. package/apps/dashboard/src/styles.css.br +0 -0
  103. package/apps/dashboard/src/styles.css.gz +0 -0
  104. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  105. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  106. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  107. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  108. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  109. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  110. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  111. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  112. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  113. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  114. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  115. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  116. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  117. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  118. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  119. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  120. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  121. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  122. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  123. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  124. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  125. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  126. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  127. package/apps/dashboard/src/tabs/settings-tab.js +0 -803
  128. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  129. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  130. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  131. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  132. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  133. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  134. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  135. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  136. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  137. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  138. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  139. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  140. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  141. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  142. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  143. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  144. package/apps/vibe/.crew/cost.json +0 -17
  145. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  146. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  147. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  148. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  149. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  150. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  151. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  152. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  153. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  154. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  155. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  156. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  157. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  158. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  159. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  160. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  161. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  162. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  163. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  164. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  172. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  173. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  174. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  175. package/apps/vibe/.crew/sandbox.json +0 -7
  176. package/apps/vibe/.crew/session.json +0 -330
  177. package/apps/vibe/.crew/training-data.jsonl +0 -0
  178. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  179. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  180. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  181. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  182. package/apps/vibe/ARCHITECTURE.md +0 -3393
  183. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  184. package/apps/vibe/ROADMAP.md +0 -41
  185. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  186. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  187. package/apps/vibe/capture-demo.mjs +0 -160
  188. package/apps/vibe/capture-full-demo.mjs +0 -255
  189. package/apps/vibe/capture-quickstart.mjs +0 -256
  190. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  191. package/apps/vibe/capture-vibe-video.mjs +0 -260
  192. package/apps/vibe/check-buttons.js +0 -41
  193. package/apps/vibe/diagnose.html +0 -106
  194. package/apps/vibe/fix-buttons.js +0 -103
  195. package/apps/vibe/index.html +0 -3404
  196. package/apps/vibe/package-lock.json +0 -920
  197. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  198. package/apps/vibe/src/main.js +0 -2940
  199. package/apps/vibe/src/register-all-languages.js +0 -98
  200. package/apps/vibe/start-studio.sh +0 -11
  201. package/apps/vibe/test/accessibility-tests.js +0 -77
  202. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  203. package/apps/vibe/test/performance-tests.js +0 -120
  204. package/apps/vibe/test/security-tests.js +0 -213
  205. package/apps/vibe/tests/e2e.local.mjs +0 -54
  206. package/apps/vibe/tests/server.smoke.mjs +0 -106
  207. package/apps/vibe/update_website.mjs +0 -74
  208. package/apps/vibe/vite.config.js +0 -19
  209. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  210. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -128,14 +128,11 @@ async function callGeminiCliForChat(messages) {
128
128
  }
129
129
 
130
130
  export async function _callLLMOnce(baseUrl, apiKey, modelId, providerKey, messages, options = {}) {
131
- // OpenRouter requires full model ID (e.g. openrouter/hunter-alpha), not bare "hunter-alpha"
132
- const isOpenRouter = providerKey === "openrouter" || baseUrl.includes("openrouter.ai");
133
- if (isOpenRouter && modelId && !modelId.startsWith("openrouter/")) {
134
- modelId = "openrouter/" + modelId;
135
- }
131
+ modelId = normalizeExternalModelId(modelId, providerKey, baseUrl);
136
132
  const isAnthropic = providerKey === "anthropic" || baseUrl.includes("anthropic.com");
137
133
  const enableStreaming = options.stream || false;
138
134
  const onStreamToken = options.onStreamToken || null;
135
+ const requestTimeoutMs = Number(options.timeoutMs || _LLM_TIMEOUT) || _LLM_TIMEOUT;
139
136
 
140
137
  const headers = { "content-type": "application/json" };
141
138
  if (isAnthropic) {
@@ -150,9 +147,10 @@ export async function _callLLMOnce(baseUrl, apiKey, modelId, providerKey, messag
150
147
  const isOpenAI = providerKey === "openai" || baseUrl.includes("openai.com");
151
148
 
152
149
  // Gemini 2.5 Flash: input 1,048,576 tokens / output 65,536 tokens
153
- // Anthropic Claude: output typically capped at 8,192 tokens
150
+ // Anthropic Claude varies by model; Haiku is stricter at 4,096 output tokens
154
151
  // All others: 4,096 safe default
155
- const maxOutputTokens = isGemini ? 16384 : isAnthropic ? 8192 : 4096;
152
+ const anthropicMaxOutputTokens = /haiku/i.test(modelId) ? 4096 : 8192;
153
+ const maxOutputTokens = isGemini ? 16384 : isAnthropic ? anthropicMaxOutputTokens : 4096;
156
154
 
157
155
  let body, endpoint;
158
156
 
@@ -230,7 +228,7 @@ export async function _callLLMOnce(baseUrl, apiKey, modelId, providerKey, messag
230
228
  method: "POST",
231
229
  headers,
232
230
  body: JSON.stringify(body),
233
- signal: AbortSignal.timeout(_LLM_TIMEOUT),
231
+ signal: AbortSignal.timeout(requestTimeoutMs),
234
232
  });
235
233
 
236
234
  if (!res.ok) {
@@ -321,6 +319,24 @@ export async function _callLLMOnce(baseUrl, apiKey, modelId, providerKey, messag
321
319
  return reply;
322
320
  }
323
321
 
322
+ export function normalizeExternalModelId(modelId, providerKey, baseUrl = "") {
323
+ let normalized = String(modelId || "").trim();
324
+ if (!normalized) return normalized;
325
+
326
+ const isOpenRouter = providerKey === "openrouter" || baseUrl.includes("openrouter.ai");
327
+ const isPerplexity = providerKey === "perplexity" || baseUrl.includes("api.perplexity.ai");
328
+
329
+ if (isOpenRouter) {
330
+ normalized = normalized.replace(/^openrouter\//i, "");
331
+ }
332
+
333
+ if (isPerplexity) {
334
+ normalized = normalized.replace(/^perplexity\//i, "");
335
+ }
336
+
337
+ return normalized;
338
+ }
339
+
324
340
  function _recordCrewLeadTokens(modelId, providerKey, usage) {
325
341
  if (!usage) return;
326
342
  const p = Number(usage.prompt_tokens || usage.input_tokens || 0);
@@ -150,6 +150,13 @@ export function buildSystemPrompt(cfg) {
150
150
  `- With acceptance criteria: @@DISPATCH {"agent":"crew-coder","task":"Write JWT auth middleware","verify":"@@READ_FILE src/auth.ts","done":"exports verifyToken, returns 401 on invalid"}`,
151
151
  "- You MUST emit the @@DISPATCH line. Describing a dispatch in prose without the line = NOTHING happens.",
152
152
  "",
153
+ "ENGINE ROUTING: tasks are routed to CLI engines or direct-llm based on keyword matching.",
154
+ "- Tasks with coding keywords (create, build, implement, fix, refactor, add, update, modify, code, function, class, component, api, endpoint, route, test, bug, error, issue, file, script, module, package) → CLI engine (native file I/O, shell, full repo context).",
155
+ "- Tasks WITHOUT coding keywords → direct-llm (LLM API call; can still write files via @@WRITE_FILE markers, but no shell or repo context).",
156
+ "- CLI engines are better for complex multi-file coding. direct-llm is fine for research reports, single-file writes, and simple tasks.",
157
+ "- When dispatching build tasks, use verbs like 'Create', 'Build', 'Implement', 'Fix' to ensure CLI routing.",
158
+ "- This is automatic keyword matching — no LLM call. See docs/ORCHESTRATION-PROTOCOL.md for the full keyword list.",
159
+ "",
153
160
  "PIPELINE: when user wants multi-agent coordinated work.",
154
161
  "- Triggers: 'build me X', 'create X', 'kick off', 'rally the crew', 'dispatch the crew'.",
155
162
  "- Before firing for complex tasks (3+ agents / 2+ waves): show plan in 1-3 lines, ask 'Fire it?'. Skip confirmation for single-agent or explicit 'go build'.",
@@ -11,6 +11,8 @@ import { randomUUID } from "node:crypto";
11
11
  import { getStatePath, getConfigPath } from "../runtime/paths.mjs";
12
12
  import { normalizeProjectDir } from "../runtime/project-dir.mjs";
13
13
  import { loadProjectMessages } from "../chat/project-messages.mjs";
14
+ import * as tmuxBridge from "../bridges/tmux-bridge.mjs";
15
+ import * as sessionManager from "../sessions/session-manager.mjs";
14
16
 
15
17
  let _deps = {};
16
18
 
@@ -375,7 +377,17 @@ export function dispatchPipelineWave(pipelineId) {
375
377
  // this path: when Cursor CLI fails, orchestrator falls back to plain LLM text
376
378
  // with @mentions — subagents never run and the pipeline advances on garbage.
377
379
  // Direct per-agent dispatch is reliable for project pipelines.
378
- if (_deps._cursorWavesEnabled && waveSteps.length > 1 && !pipeline.projectDir) {
380
+ // Only route through orchestrator if it has a CLI engine that can actually dispatch.
381
+ // Without a CLI engine, the orchestrator just describes dispatching in text — useless.
382
+ const orchestratorHasCliEngine = (() => {
383
+ try {
384
+ const agents = (_deps.loadAgentList || (() => []))();
385
+ const orch = agents.find(a => a.id === "crew-orchestrator");
386
+ if (!orch) return false;
387
+ return !!(orch.engine || orch.useClaudeCode || orch.useCursorCli || orch.useCursor || orch.useCodex || orch.useCrewCLI || orch.useGeminiCli || orch.useOpenCode);
388
+ } catch { return false; }
389
+ })();
390
+ if (_deps._cursorWavesEnabled && orchestratorHasCliEngine && waveSteps.length > 1 && !pipeline.projectDir) {
379
391
  const waveManifest = {
380
392
  wave: currentWave + 1,
381
393
  projectDir: pipeline.projectDir || "",
@@ -425,6 +437,11 @@ export function dispatchPipelineWave(pipelineId) {
425
437
  ...(step.verify ? { verify: step.verify } : {}),
426
438
  ...(step.done ? { done: step.done } : {}),
427
439
  };
440
+ // Build session handoff metadata for tmux-bridge
441
+ const sessionMeta = {};
442
+ if (step.session) sessionMeta.session = step.session;
443
+ else if (pipeline._tmuxSessionId) sessionMeta.session = `handoff:${pipeline._tmuxSessionId}`;
444
+
428
445
  const taskId = dispatchTask(step.agent, stepSpec, sessionId, {
429
446
  pipelineId,
430
447
  waveIndex: currentWave,
@@ -434,6 +451,7 @@ export function dispatchPipelineWave(pipelineId) {
434
451
  originThreadId: pipeline.originThreadId,
435
452
  originMessageId: pipeline.originMessageId,
436
453
  triggeredBy: pipeline.triggeredBy || "pipeline",
454
+ ...sessionMeta,
437
455
  });
438
456
  if (taskId && taskId !== true) pipeline.pendingTaskIds.add(taskId);
439
457
  }
@@ -694,8 +712,10 @@ export function dispatchTask(agent, task, sessionId = "owner", pipelineMeta = nu
694
712
  task = _deps.writeTaskBrief?.(agent, task, projectDir) ?? task;
695
713
  }
696
714
 
697
- const rp = typeof _deps.getRtPublish === "function" ? _deps.getRtPublish() : null;
698
- console.log(`[wave-dispatcher] getRtPublish exists: ${typeof _deps.getRtPublish === "function"}, rp is: ${typeof rp}`);
715
+ let rp = typeof _deps.getRtPublish === "function" ? _deps.getRtPublish() : null;
716
+ if (!rp && typeof _deps.getRtPublish === "function") {
717
+ console.warn(`[wave-dispatcher] RT publish not available for ${agent} — will use fallback`);
718
+ }
699
719
  if (rp) {
700
720
  try {
701
721
  // Build extraFlags: start with global settings, override with pipeline-specific settings
@@ -762,6 +782,36 @@ export function dispatchTask(agent, task, sessionId = "owner", pipelineMeta = nu
762
782
  if (pipelineMeta?.mentionedBy) extraFlags.mentionedBy = pipelineMeta.mentionedBy;
763
783
  if (pipelineMeta?.autonomous !== undefined) extraFlags.autonomous = pipelineMeta.autonomous;
764
784
 
785
+ // ── tmux session handoff ──────────────────────────────────────────────
786
+ // If pipelineMeta carries a tmux session, hand it off to this agent
787
+ // or create a new one if session: "persist" is set.
788
+ if (tmuxBridge.detect()) {
789
+ const sessionSpec = pipelineMeta?.session;
790
+ if (typeof sessionSpec === "string" && sessionSpec.startsWith("handoff:")) {
791
+ const existingSessionId = sessionSpec.slice("handoff:".length);
792
+ const prevOwner = sessionManager.getSession(existingSessionId)?.owner;
793
+ if (prevOwner) {
794
+ sessionManager.handoff(existingSessionId, prevOwner, agent);
795
+ }
796
+ const meta = sessionManager.getSession(existingSessionId);
797
+ if (meta?.paneId) extraFlags.tmuxSessionId = meta.paneId;
798
+ } else if (sessionSpec === "persist") {
799
+ const newSessionId = sessionManager.create({
800
+ workspaceId: pipelineMeta?.pipelineId || "default",
801
+ agentId: agent,
802
+ cwd: pipelineMeta?.projectDir || undefined,
803
+ });
804
+ if (newSessionId) {
805
+ const meta = sessionManager.getSession(newSessionId);
806
+ if (meta?.paneId) extraFlags.tmuxSessionId = meta.paneId;
807
+ // Store session ID on pipeline meta so next wave can handoff
808
+ if (pipelineMeta) pipelineMeta._tmuxSessionId = newSessionId;
809
+ }
810
+ } else if (pipelineMeta?.tmuxSessionId) {
811
+ extraFlags.tmuxSessionId = pipelineMeta.tmuxSessionId;
812
+ }
813
+ }
814
+
765
815
  // Log enrichment for verification
766
816
  const engineFlags = Object.keys(extraFlags).filter(k => k.startsWith('use') || k.includes('Model') || k === 'engine');
767
817
  if (engineFlags.length > 0) {
@@ -7,6 +7,60 @@ import { applyProjectDirToPipelineSteps } from "../dispatch/parsers.mjs";
7
7
  let reconnectTimer = null;
8
8
  let isConnecting = false;
9
9
  let crewLeadHeartbeat = null;
10
+ let currentWs = null; // Global ref to current WebSocket — prevents stale closures
11
+ let connectionId = 0; // Monotonic ID to detect stale connections
12
+ let reconnectAttempts = 0; // For exponential backoff
13
+
14
+ const CODER_AGENT_RE = /crew-coder|crew-frontend|crew-fixer|crew-ml|crew-coder-back|crew-coder-front/;
15
+
16
+ function normalizeEngineId(value) {
17
+ const raw = String(value || "").trim().toLowerCase();
18
+ if (!raw) return null;
19
+ if (raw === "claude" || raw === "claude-code" || raw.includes("claude code")) return "claude";
20
+ if (raw === "codex" || raw === "codex-cli" || raw.includes("codex")) return "codex";
21
+ if (raw === "cursor" || raw === "cursor-cli" || raw.includes("cursor")) return "cursor";
22
+ return null;
23
+ }
24
+
25
+ export function inferDispatchEngine(dispatch = null, message = "") {
26
+ const explicit =
27
+ normalizeEngineId(dispatch?.engineUsed)
28
+ || normalizeEngineId(dispatch?.runtime)
29
+ || (dispatch?.useCodex === true ? "codex" : null)
30
+ || (dispatch?.useCursorCli === true ? "cursor" : null)
31
+ || (dispatch?.useClaudeCode === true ? "claude" : null);
32
+ if (explicit) return explicit;
33
+
34
+ const text = String(message || "");
35
+ if (/claude\s*code|anthropic|sonnet|opus/i.test(text)) return "claude";
36
+ if (/codex|gpt-5(\.\d+)?-codex|openai/i.test(text)) return "codex";
37
+ if (/cursor/i.test(text)) return "cursor";
38
+ return null;
39
+ }
40
+
41
+ export function getNextCoderEngine(currentEngine) {
42
+ const current = normalizeEngineId(currentEngine);
43
+ if (current === "claude") return "codex";
44
+ if (current === "codex") return "claude";
45
+ if (current === "cursor") return null;
46
+ return "codex";
47
+ }
48
+
49
+ export function buildEngineFallbackMeta(dispatch = null, currentEngine = null, trigger = "rate-limit-fallback") {
50
+ const nextEngine = getNextCoderEngine(currentEngine);
51
+ if (!nextEngine) return null;
52
+
53
+ return {
54
+ ...(dispatch || {}),
55
+ useClaudeCode: nextEngine === "claude",
56
+ useCodex: nextEngine === "codex",
57
+ useCursorCli: nextEngine === "cursor",
58
+ runtime: nextEngine,
59
+ engineFallbackFrom: normalizeEngineId(currentEngine),
60
+ engineFallbackTo: nextEngine,
61
+ triggeredBy: trigger,
62
+ };
63
+ }
10
64
 
11
65
  export function initWsRouter(deps) {
12
66
  const {
@@ -29,6 +83,7 @@ export function initWsRouter(deps) {
29
83
  appendHistory,
30
84
  pendingPipelines,
31
85
  handleAutonomousMentions,
86
+ saveProjectMessage,
32
87
  checkWaveQualityGate,
33
88
  failPipelineOnQualityGate,
34
89
  savePipelineState,
@@ -40,6 +95,25 @@ export function initWsRouter(deps) {
40
95
  autonomousPmLoopSessions
41
96
  } = deps;
42
97
 
98
+ function scheduleReconnect() {
99
+ if (reconnectTimer) clearTimeout(reconnectTimer);
100
+ // Exponential backoff: 1s, 2s, 4s, 8s, ... capped at 30s
101
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
102
+ reconnectAttempts++;
103
+ console.log(`[crew-lead] RT reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
104
+ reconnectTimer = setTimeout(connectRT, delay);
105
+ }
106
+
107
+ function cleanupConnection() {
108
+ if (crewLeadHeartbeat) { clearInterval(crewLeadHeartbeat); crewLeadHeartbeat = null; }
109
+ setRtPublish(null);
110
+ // Close old WebSocket and remove all listeners to prevent leaks
111
+ if (currentWs) {
112
+ try { currentWs.removeAllListeners(); currentWs.close(); } catch {}
113
+ currentWs = null;
114
+ }
115
+ }
116
+
43
117
  function connectRT() {
44
118
  if (isConnecting) {
45
119
  console.log("[crew-lead] Already connecting to RT, skipping duplicate call");
@@ -51,17 +125,27 @@ export function initWsRouter(deps) {
51
125
  reconnectTimer = null;
52
126
  }
53
127
 
128
+ // Clean up any previous connection before creating a new one (#1, #8)
129
+ cleanupConnection();
130
+
54
131
  isConnecting = true;
132
+ const thisConnId = ++connectionId;
55
133
  const ws = new WebSocket(RT_URL);
134
+ currentWs = ws;
56
135
 
57
136
  ws.on("open", () => {
137
+ if (thisConnId !== connectionId) return; // stale (#7)
58
138
  console.log("[crew-lead] RT socket open");
59
139
  isConnecting = false;
60
140
  });
61
141
 
62
142
  ws.on("message", (raw) => {
143
+ if (thisConnId !== connectionId) return; // stale (#7)
63
144
  let p;
64
- try { p = JSON.parse(raw.toString()); } catch { return; }
145
+ try { p = JSON.parse(raw.toString()); } catch (parseErr) {
146
+ console.warn(`[crew-lead] RT message parse failed: ${parseErr.message} (${raw.toString().slice(0, 80)})`); // #15
147
+ return;
148
+ }
65
149
 
66
150
  if (p.type === "server.hello") {
67
151
  ws.send(JSON.stringify({ type: "hello", agent: "crew-lead", token: RT_TOKEN }));
@@ -69,10 +153,27 @@ export function initWsRouter(deps) {
69
153
  }
70
154
  if (p.type === "hello.ack") {
71
155
  ws.send(JSON.stringify({ type: "subscribe", channels: ["done", "events", "command", "issues", "status"] }));
156
+ reconnectAttempts = 0; // Reset backoff on successful connection (#12)
72
157
 
158
+ // rtPublish always uses currentWs, not a captured closure (#6)
73
159
  setRtPublish(({ channel, type, to, payload }) => {
74
160
  const taskId = crypto.randomUUID();
75
- ws.send(JSON.stringify({ type: "publish", channel, messageType: type, to, taskId, priority: "high", payload }));
161
+ const activeWs = currentWs;
162
+ if (!activeWs || activeWs.readyState !== WebSocket.OPEN) {
163
+ console.error(`[crew-lead] RT ws not open (state=${activeWs?.readyState}) — triggering reconnect`);
164
+ setRtPublish(null);
165
+ scheduleReconnect();
166
+ return null;
167
+ }
168
+ try {
169
+ activeWs.send(JSON.stringify({ type: "publish", channel, messageType: type, to, taskId, priority: "high", payload }));
170
+ } catch (sendErr) {
171
+ console.error(`[crew-lead] RT ws.send failed (${sendErr.message}) — triggering reconnect`);
172
+ setRtPublish(null);
173
+ try { activeWs.close(); } catch {}
174
+ scheduleReconnect();
175
+ return null;
176
+ }
76
177
  return taskId;
77
178
  });
78
179
 
@@ -80,16 +181,42 @@ export function initWsRouter(deps) {
80
181
  setTimeout(resumePipelines, 2000);
81
182
  startBackgroundLoop();
82
183
 
184
+ // Heartbeat with failure detection (#3, #5, #10)
83
185
  if (crewLeadHeartbeat) clearInterval(crewLeadHeartbeat);
186
+ let missedHeartbeats = 0;
84
187
  crewLeadHeartbeat = setInterval(() => {
188
+ if (thisConnId !== connectionId) {
189
+ clearInterval(crewLeadHeartbeat);
190
+ crewLeadHeartbeat = null;
191
+ return;
192
+ }
85
193
  try {
194
+ if (!currentWs || currentWs.readyState !== WebSocket.OPEN) {
195
+ missedHeartbeats++;
196
+ console.warn(`[crew-lead] Heartbeat skipped — ws not open (missed=${missedHeartbeats})`);
197
+ if (missedHeartbeats >= 3) {
198
+ console.error(`[crew-lead] 3 missed heartbeats — triggering reconnect`);
199
+ cleanupConnection();
200
+ scheduleReconnect();
201
+ }
202
+ return;
203
+ }
204
+ missedHeartbeats = 0;
86
205
  const taskId = crypto.randomUUID();
87
- ws.send(JSON.stringify({
206
+ currentWs.send(JSON.stringify({
88
207
  type: "publish", channel: "status", messageType: "agent.heartbeat",
89
208
  to: "broadcast", taskId, priority: "low",
90
209
  payload: { agent: "crew-lead", ts: new Date().toISOString() },
91
210
  }));
92
- } catch { }
211
+ } catch (hbErr) {
212
+ console.warn(`[crew-lead] Heartbeat send failed: ${hbErr.message}`);
213
+ missedHeartbeats++;
214
+ if (missedHeartbeats >= 3) {
215
+ console.error(`[crew-lead] 3 heartbeat failures — triggering reconnect`);
216
+ cleanupConnection();
217
+ scheduleReconnect();
218
+ }
219
+ }
93
220
  }, 30000);
94
221
  return;
95
222
  }
@@ -103,7 +230,10 @@ export function initWsRouter(deps) {
103
230
 
104
231
  if (p.type === "message" && p.envelope) {
105
232
  const env = p.envelope;
106
- if (env.id) ws.send(JSON.stringify({ type: "ack", messageId: env.id, status: "received" }));
233
+ if (env.id) {
234
+ try { ws.send(JSON.stringify({ type: "ack", messageId: env.id, status: "received" })); }
235
+ catch (ackErr) { console.warn(`[crew-lead] Ack send failed for ${env.id}: ${ackErr.message}`); }
236
+ }
107
237
 
108
238
  const from = env.from || env.sender_agent_id || env.payload?.source || "";
109
239
  const msgType = env.messageType || env.type || "";
@@ -154,8 +284,22 @@ export function initWsRouter(deps) {
154
284
  emitTaskLifecycle("failed", { taskId: failedTaskId, agentId: failedAgent, taskType: "task", error: { message: errMsg } });
155
285
  const dispatch = pendingDispatches.get(failedTaskId);
156
286
  if (dispatch && RATE_LIMIT_PATTERN.test(errMsg)) {
157
- const fallback = getRateLimitFallback(failedAgent);
158
287
  const targetSession = dispatch.sessionId || "owner";
288
+ const currentEngine = inferDispatchEngine({ ...dispatch, engineUsed: env.payload?.engineUsed || dispatch.engineUsed }, errMsg);
289
+ const engineFallbackMeta = CODER_AGENT_RE.test(failedAgent)
290
+ ? buildEngineFallbackMeta(dispatch, currentEngine, "rate-limit-engine-fallback")
291
+ : null;
292
+ if (engineFallbackMeta) {
293
+ pendingDispatches.delete(failedTaskId);
294
+ const newTaskId = dispatchTask(failedAgent, dispatch.task, targetSession, engineFallbackMeta);
295
+ if (newTaskId) {
296
+ appendHistory("default", targetSession, "system", `[crew-lead] ${failedAgent} hit rate limit on ${currentEngine || "current engine"} (${errMsg.slice(0, 80)}). Re-dispatched same task on ${engineFallbackMeta.engineFallbackTo}.`);
297
+ broadcastSSE({ type: "agent_reply", from: "crew-lead", content: `Rate limit: retried ${failedAgent} on ${engineFallbackMeta.engineFallbackTo}.`, sessionId: targetSession, taskId: failedTaskId, ts: Date.now() });
298
+ console.log(`[crew-lead] Rate limit engine fallback: ${failedAgent} ${currentEngine || "unknown"} → ${engineFallbackMeta.engineFallbackTo}`);
299
+ return;
300
+ }
301
+ }
302
+ const fallback = getRateLimitFallback(failedAgent);
159
303
  if (fallback !== failedAgent) {
160
304
  pendingDispatches.delete(failedTaskId);
161
305
  const newTaskId = dispatchTask(fallback, dispatch.task, targetSession, { ...dispatch, pipelineId: dispatch.pipelineId, waveIndex: dispatch.waveIndex });
@@ -183,12 +327,10 @@ export function initWsRouter(deps) {
183
327
  setTimeout(() => pendingDispatches.delete(taskId), 600_000);
184
328
  }
185
329
 
186
- const _autoRetryKey = `_question_retried_${taskId}`;
187
330
  const _askedQuestion = /(?:would you like|shall i|should i|do you want|want me to|may i|can i proceed|would it help|do you need|is that correct|shall we|ready to proceed|would you prefer|let me know|please (?:confirm|clarify|specify|advise))\??/i.test(content);
188
331
  const _didWork = /@@WRITE_FILE|@@RUN_CMD|wrote|created|updated|fixed|patched|done\.|complete/i.test(content);
189
- if (_askedQuestion && !_didWork && !pendingPipelines.has(dispatch?.pipelineId) && !global[_autoRetryKey]) {
190
- global[_autoRetryKey] = true;
191
- setTimeout(() => { delete global[_autoRetryKey]; }, 600_000);
332
+ if (_askedQuestion && !_didWork && !pendingPipelines.has(dispatch?.pipelineId) && !dispatch?._questionRetried) {
333
+ if (dispatch) dispatch._questionRetried = true;
192
334
  const _originalTask = dispatch?.task || "";
193
335
  const _retryTask = (_originalTask.slice(0, 2000) || content.slice(0, 500)) +
194
336
  "\n\nDo NOT ask for permission or confirmation. Proceed immediately with your best judgment. Just do it.";
@@ -204,15 +346,13 @@ export function initWsRouter(deps) {
204
346
  return;
205
347
  }
206
348
 
207
- const _planRetryKey = `_plan_retried_${taskId}`;
208
- const _isCoderAgent = /crew-coder|crew-frontend|crew-fixer|crew-ml|crew-coder-back|crew-coder-front/.test(from);
349
+ const _isCoderAgent = CODER_AGENT_RE.test(from);
209
350
  const _returnedPlan = !_didWork && content.length > 300 && (
210
351
  /##\s+(component|feature|file structure|design|breakdown|overview|plan|approach|implementation plan|technical spec)/i.test(content) ||
211
352
  /here'?s? (?:the|my|a|what|how)/i.test(content.slice(0, 200))
212
353
  );
213
- if (_isCoderAgent && _returnedPlan && !global[_planRetryKey]) {
214
- global[_planRetryKey] = true;
215
- setTimeout(() => { delete global[_planRetryKey]; }, 600_000);
354
+ if (_isCoderAgent && _returnedPlan && !dispatch?._planRetried) {
355
+ if (dispatch) dispatch._planRetried = true;
216
356
  const _originalTask = dispatch?.task || "";
217
357
  const _retryTask = `STOP PLANNING. Your last response was a plan/analysis with no code written.\n\nOriginal task: ${_originalTask.slice(0, 1500)}\n\nNow WRITE THE CODE. Use @@WRITE_FILE for every file. Do not describe what you will do — do it.`;
218
358
  console.log(`[crew-lead] Agent ${from} returned a plan instead of code — auto-retrying`);
@@ -229,17 +369,20 @@ export function initWsRouter(deps) {
229
369
  return;
230
370
  }
231
371
 
232
- const _bailRetryKey = `_bail_retried_${taskId}`;
233
372
  const _bailed = /couldn'?t complete|could not complete|i'?m sorry[,.]? but|i was unable to|i'?m unable to|session (?:limit|ended|expired)|ran out of|context (?:limit|window)|i (?:apologize|regret)|partial(?:ly)? complete|not (?:all|every|fully) (?:changes?|tasks?|items?|fixes?)/i.test(content);
234
- if (_bailed && !global[_bailRetryKey]) {
235
- global[_bailRetryKey] = true;
236
- setTimeout(() => { delete global[_bailRetryKey]; }, 600_000);
373
+ if (_bailed && !dispatch?._bailRetried) {
374
+ if (dispatch) dispatch._bailRetried = true;
237
375
  const _originalTask = dispatch?.task || "";
238
- const fallbackAgent = _isCoderAgent ? from : (getRateLimitFallback(from) || from);
376
+ const currentEngine = inferDispatchEngine(dispatch, content);
377
+ const engineFallbackMeta = _isCoderAgent
378
+ ? buildEngineFallbackMeta(dispatch, currentEngine, "auto-retry-bail")
379
+ : null;
380
+ const fallbackAgent = engineFallbackMeta ? from : (_isCoderAgent ? from : (getRateLimitFallback(from) || from));
239
381
  const _retryTask = `Your previous attempt at this task was incomplete. You said you couldn't finish.\n\nOriginal task:\n${_originalTask.slice(0, 2000)}\n\nDo not apologize. Do not explain why you couldn't finish. Just complete the remaining work now. Use @@WRITE_FILE for every file you change. If the task is too large, complete the most critical items first.`;
240
- console.log(`[crew-lead] Agent ${from} bailed out mid-task — auto-retrying with ${fallbackAgent}`);
241
- appendHistory("default", targetSession, "system", `${from} bailed mid-task — auto-retrying with ${fallbackAgent}.`);
382
+ console.log(`[crew-lead] Agent ${from} bailed out mid-task — auto-retrying with ${engineFallbackMeta?.engineFallbackTo || fallbackAgent}`);
383
+ appendHistory("default", targetSession, "system", `${from} bailed mid-task — auto-retrying with ${engineFallbackMeta?.engineFallbackTo || fallbackAgent}.`);
242
384
  dispatchTask(fallbackAgent, _retryTask, targetSession, {
385
+ ...(engineFallbackMeta || {}),
243
386
  ...(dispatch?.pipelineId ? { pipelineId: dispatch.pipelineId } : {}),
244
387
  ...(dispatch?.projectDir ? { projectDir: dispatch.projectDir } : {}),
245
388
  originProjectId: dispatch?.originProjectId,
@@ -283,6 +426,31 @@ export function initWsRouter(deps) {
283
426
  }
284
427
 
285
428
  const originChannel = dispatch?.originChannel || dispatch?.originProjectId || null;
429
+
430
+ // Persist agent result to project messages so swarm chat history is complete
431
+ if (originChannel && saveProjectMessage) {
432
+ try {
433
+ saveProjectMessage(originChannel, {
434
+ source: "agent",
435
+ role: "assistant",
436
+ content: content.slice(0, 8000),
437
+ agent: from,
438
+ threadId: dispatch?.originThreadId || `${originChannel}:${targetSession}`,
439
+ parentId: dispatch?.originMessageId || null,
440
+ metadata: {
441
+ agentName: from,
442
+ autonomous: true,
443
+ engineUsed: env.payload?.engineUsed || null,
444
+ durationMs: dispatch?.ts ? Date.now() - dispatch.ts : null,
445
+ triggeredBy: dispatch?.triggeredBy || "dispatch",
446
+ taskId,
447
+ },
448
+ });
449
+ } catch (e) {
450
+ console.warn(`[crew-lead] Failed to save agent result to project messages: ${e.message}`);
451
+ }
452
+ }
453
+
286
454
  if (originChannel) {
287
455
  void handleAutonomousMentions({
288
456
  message: { content },
@@ -293,6 +461,9 @@ export function initWsRouter(deps) {
293
461
  projectDir: dispatch?.projectDir || null,
294
462
  originMessageId: dispatch?.originMessageId || null,
295
463
  originThreadId: dispatch?.originThreadId || `${originChannel}:${targetSession}`,
464
+ appendToChatHistory: (entry) => {
465
+ appendHistory("default", targetSession, "system", entry.content || String(entry));
466
+ },
296
467
  broadcastSSE,
297
468
  }).catch((err) => {
298
469
  console.warn(`[crew-lead] Autonomous mention routing failed for ${from}: ${err.message}`);
@@ -325,6 +496,27 @@ export function initWsRouter(deps) {
325
496
  }
326
497
  }
327
498
 
499
+ // Parse @@DISPATCH markers from any agent result (not just crew-pm).
500
+ // This lets crew-orchestrator (and others) fan out tasks via @@DISPATCH
501
+ // even when running on direct-llm without CLI tools.
502
+ if (from !== "crew-pm" && content.includes("@@DISPATCH")) {
503
+ const agentDispatches = parseDispatches(content);
504
+ for (const d of agentDispatches) {
505
+ const ok = dispatchTask(d.agent, d, targetSession, {
506
+ originProjectId: dispatch?.originProjectId || dispatch?.projectId || "general",
507
+ originChannel: dispatch?.originChannel || dispatch?.projectId || "general",
508
+ originThreadId: dispatch?.originThreadId || `${dispatch?.originProjectId || dispatch?.projectId || "general"}:${targetSession}`,
509
+ originMessageId: dispatch?.originMessageId || null,
510
+ projectDir: d.projectDir || dispatch?.projectDir || null,
511
+ triggeredBy: `${from}-dispatch`,
512
+ });
513
+ if (ok) {
514
+ console.log(`[crew-lead] ${from} dispatched to ${d.agent}: "${(d.task || "").slice(0, 120)}"`);
515
+ appendHistory("default", targetSession, "system", `${from} dispatched to ${d.agent}: "${(d.task || "").slice(0, 120)}".`);
516
+ }
517
+ }
518
+ }
519
+
328
520
  if (from === "crew-pm") {
329
521
  const pipelineSpec = parsePipeline(content);
330
522
  if (pipelineSpec) {
@@ -410,17 +602,17 @@ export function initWsRouter(deps) {
410
602
  });
411
603
 
412
604
  ws.on("close", () => {
413
- setRtPublish(null);
605
+ if (thisConnId !== connectionId) return; // stale close event
606
+ cleanupConnection();
414
607
  isConnecting = false;
415
- if (crewLeadHeartbeat) { clearInterval(crewLeadHeartbeat); crewLeadHeartbeat = null; }
416
- console.log("[crew-lead] RT disconnected — reconnecting in 5s");
417
- if (reconnectTimer) clearTimeout(reconnectTimer);
418
- reconnectTimer = setTimeout(connectRT, 5000);
608
+ console.log("[crew-lead] RT disconnected");
609
+ scheduleReconnect();
419
610
  });
420
611
 
421
612
  ws.on("error", (e) => {
422
613
  console.error("[crew-lead] RT socket error:", e.message);
423
614
  isConnecting = false;
615
+ // close event will fire after error — reconnect happens there
424
616
  });
425
617
  }
426
618
 
@@ -7,6 +7,7 @@ import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import os from "node:os";
9
9
  import { loadAgentList as _defaultLoadAgentList } from "../runtime/config.mjs";
10
+ import { isCodingTask } from "../agents/dispatch.mjs";
10
11
 
11
12
  const ENGINES_BUNDLED_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "engines");
12
13
  const ENGINES_USER_DIR = path.join(os.homedir(), ".crewswarm", "engines");
@@ -153,6 +154,14 @@ function evaluateShouldUse(engineDef, payload, incomingType) {
153
154
  * Select engine: payload flags → payload runtime → per-agent crewswarm.json → fallback rules
154
155
  */
155
156
  export function selectEngine(payload, incomingType) {
157
+ // PRIORITY 0: Chat messages always use direct LLM, not CLI engines.
158
+ // CLI engines (claude-code, cursor, codex, etc.) are only for coding tasks.
159
+ // The agent's configured model handles conversational replies directly.
160
+ const prompt = payload?.prompt || payload?.task || payload?.message || "";
161
+ if (!isCodingTask(incomingType, prompt, payload)) {
162
+ return null;
163
+ }
164
+
156
165
  // PRIORITY 1: Payload flags (from enriched task)
157
166
  const explicitEngines = [
158
167
  { key: 'useCodex', id: 'codex' },
@@ -1612,6 +1612,7 @@ export async function handleRealtimeEnvelope(envelope, client, bridge) {
1612
1612
  payload: {
1613
1613
  source: CREWSWARM_RT_AGENT,
1614
1614
  error: message,
1615
+ engineUsed: typeof engineUsed !== "undefined" ? engineUsed : null,
1615
1616
  idempotencyKey: dispatchKey,
1616
1617
  attempt: dispatchAttempt,
1617
1618
  },