crewswarm 0.9.2 → 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 (207) 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 +34 -4
  12. package/lib/bridges/gateway-ws.mjs +4 -0
  13. package/lib/crew-lead/chat-handler.mjs +34 -0
  14. package/lib/crew-lead/http-server.mjs +55 -14
  15. package/lib/crew-lead/llm-caller.mjs +24 -8
  16. package/lib/crew-lead/prompts.mjs +7 -0
  17. package/lib/crew-lead/wave-dispatcher.mjs +15 -3
  18. package/lib/crew-lead/ws-router.mjs +219 -27
  19. package/lib/engines/engine-registry.mjs +9 -0
  20. package/lib/engines/rt-envelope.mjs +1 -0
  21. package/lib/engines/runners.mjs +5 -2
  22. package/lib/runtime/paths.mjs +12 -8
  23. package/package.json +35 -15
  24. package/scripts/capture-build-flow.mjs +118 -0
  25. package/scripts/coverage-report.mjs +209 -0
  26. package/scripts/coverage-summary.mjs +47 -0
  27. package/scripts/dashboard-validation.mjs +74 -0
  28. package/scripts/dashboard.mjs +560 -70
  29. package/scripts/live-bridge-matrix.mjs +79 -0
  30. package/scripts/live-cli-matrix.mjs +166 -0
  31. package/scripts/live-crewchat-check.mjs +42 -0
  32. package/scripts/live-engine-matrix.mjs +50 -0
  33. package/scripts/live-provider-failover-matrix.mjs +107 -0
  34. package/scripts/live-provider-matrix.mjs +228 -0
  35. package/scripts/restart-all-from-repo.sh +4 -4
  36. package/scripts/smoke-dispatch.mjs +4 -1
  37. package/scripts/test-blast-radius.mjs +204 -0
  38. package/scripts/test-report-summary.mjs +88 -0
  39. package/scripts/test-reporter.mjs +651 -0
  40. package/scripts/test-rerun.mjs +136 -0
  41. package/scripts/tmux-bridge +130 -0
  42. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  43. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  44. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  45. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  46. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  47. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  48. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  49. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  50. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  51. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  52. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  53. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  54. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  56. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  60. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  62. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  64. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  65. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  66. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  67. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  68. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  69. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  70. package/apps/dashboard/dist/index.html.br +0 -0
  71. package/apps/dashboard/dist/index.html.gz +0 -0
  72. package/apps/dashboard/index.html +0 -6529
  73. package/apps/dashboard/package.json +0 -15
  74. package/apps/dashboard/src/app.js +0 -2828
  75. package/apps/dashboard/src/app.js.br +0 -0
  76. package/apps/dashboard/src/app.js.gz +0 -0
  77. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  78. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  79. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  80. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  81. package/apps/dashboard/src/cli-process.js +0 -208
  82. package/apps/dashboard/src/cli-process.js.br +0 -0
  83. package/apps/dashboard/src/cli-process.js.gz +0 -0
  84. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  85. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  86. package/apps/dashboard/src/core/api.js +0 -18
  87. package/apps/dashboard/src/core/api.js.br +0 -0
  88. package/apps/dashboard/src/core/dom.js +0 -228
  89. package/apps/dashboard/src/core/dom.js.br +0 -0
  90. package/apps/dashboard/src/core/state.js +0 -91
  91. package/apps/dashboard/src/core/state.js.br +0 -0
  92. package/apps/dashboard/src/core/task-manager.js +0 -134
  93. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  94. package/apps/dashboard/src/orchestration-status.js +0 -127
  95. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  96. package/apps/dashboard/src/setup-wizard.js +0 -562
  97. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  98. package/apps/dashboard/src/styles.css +0 -2085
  99. package/apps/dashboard/src/styles.css.br +0 -0
  100. package/apps/dashboard/src/styles.css.gz +0 -0
  101. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  102. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  103. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  104. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  105. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  106. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  107. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  108. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  109. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  110. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  111. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  112. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  113. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  114. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  115. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  116. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  117. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  118. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  119. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  120. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  121. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  122. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  123. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  124. package/apps/dashboard/src/tabs/settings-tab.js +0 -861
  125. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  126. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  127. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  128. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  129. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  130. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  131. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  132. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  133. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  134. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  135. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  136. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  137. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  138. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  139. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  140. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  141. package/apps/vibe/.crew/cost.json +0 -17
  142. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  143. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  144. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  145. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  146. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  147. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  148. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  149. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  150. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  151. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  152. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  153. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  154. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  155. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  156. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  157. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  158. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  159. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  160. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  161. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  162. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  163. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  164. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  172. package/apps/vibe/.crew/sandbox.json +0 -7
  173. package/apps/vibe/.crew/session.json +0 -330
  174. package/apps/vibe/.crew/training-data.jsonl +0 -0
  175. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  176. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  177. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  178. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  179. package/apps/vibe/ARCHITECTURE.md +0 -3393
  180. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  181. package/apps/vibe/ROADMAP.md +0 -41
  182. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  183. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  184. package/apps/vibe/capture-demo.mjs +0 -160
  185. package/apps/vibe/capture-full-demo.mjs +0 -255
  186. package/apps/vibe/capture-quickstart.mjs +0 -256
  187. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  188. package/apps/vibe/capture-vibe-video.mjs +0 -260
  189. package/apps/vibe/check-buttons.js +0 -41
  190. package/apps/vibe/diagnose.html +0 -106
  191. package/apps/vibe/fix-buttons.js +0 -103
  192. package/apps/vibe/index.html +0 -3404
  193. package/apps/vibe/package-lock.json +0 -920
  194. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  195. package/apps/vibe/src/main.js +0 -2940
  196. package/apps/vibe/src/register-all-languages.js +0 -98
  197. package/apps/vibe/start-studio.sh +0 -11
  198. package/apps/vibe/test/accessibility-tests.js +0 -77
  199. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  200. package/apps/vibe/test/performance-tests.js +0 -120
  201. package/apps/vibe/test/security-tests.js +0 -213
  202. package/apps/vibe/tests/e2e.local.mjs +0 -54
  203. package/apps/vibe/tests/server.smoke.mjs +0 -106
  204. package/apps/vibe/update_website.mjs +0 -74
  205. package/apps/vibe/vite.config.js +0 -19
  206. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  207. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -20,7 +20,7 @@ import {
20
20
  import { shouldSkipGeminiPassthroughLine } from "../gemini-cli-passthrough-noise.mjs";
21
21
  import { applyProjectDirToPipelineSteps } from "../dispatch/parsers.mjs";
22
22
  import { normalizeProjectDir } from "../runtime/project-dir.mjs";
23
- import { CREWSWARM_REPO_ROOT } from "../runtime/config.mjs";
23
+ import { CREWSWARM_REPO_ROOT, loadSwarmConfig } from "../runtime/config.mjs";
24
24
  import { resolveCursorLaunchSpec } from "../engines/cursor-launcher.mjs";
25
25
 
26
26
  let _deps = {};
@@ -3532,6 +3532,9 @@ export function createAndStartServer(PORT) {
3532
3532
  sessionId: reqSessionId,
3533
3533
  injectHistory,
3534
3534
  model: reqModel,
3535
+ permissionMode: reqPermissionMode,
3536
+ sandbox: reqSandbox,
3537
+ forceL2: reqForceL2,
3535
3538
  } = JSON.parse(body || "{}");
3536
3539
  if (!message) {
3537
3540
  json(res, 400, { ok: false, error: "message required" });
@@ -3762,7 +3765,7 @@ export function createAndStartServer(PORT) {
3762
3765
  "never",
3763
3766
  "exec",
3764
3767
  "--sandbox",
3765
- "danger-full-access",
3768
+ reqSandbox || "danger-full-access",
3766
3769
  "--skip-git-repo-check",
3767
3770
  "--color",
3768
3771
  "never",
@@ -3859,12 +3862,16 @@ export function createAndStartServer(PORT) {
3859
3862
  "-p",
3860
3863
  "--setting-sources",
3861
3864
  "user",
3862
- "--dangerously-skip-permissions",
3863
3865
  "--output-format",
3864
3866
  "stream-json",
3865
3867
  "--verbose",
3866
3868
  "--include-partial-messages",
3867
3869
  ];
3870
+ if (reqPermissionMode) {
3871
+ args.push("--permission-mode", reqPermissionMode);
3872
+ } else {
3873
+ args.push("--dangerously-skip-permissions");
3874
+ }
3868
3875
  if (projectDir) args.push("--add-dir", projectDir);
3869
3876
  if (reqModel) args.push("--model", reqModel);
3870
3877
  // Prefer --resume <session-id> for per-project isolation; fall back to --continue (most recent global)
@@ -3877,7 +3884,10 @@ export function createAndStartServer(PORT) {
3877
3884
  } else if (continueSession) {
3878
3885
  args.push("--continue");
3879
3886
  }
3880
- args.push(finalMessage);
3887
+ // Skip user MCP servers to avoid 30s+ init hangs
3888
+ args.push("--strict-mcp-config", "--mcp-config", path.join(os.homedir(), ".crewswarm", "config", "empty-mcp.json"));
3889
+ // -- separates flags from prompt (--mcp-config is variadic and eats positional args)
3890
+ args.push("--", finalMessage);
3881
3891
  }
3882
3892
 
3883
3893
  send({ type: "start", engine, message: message.slice(0, 80) });
@@ -3901,9 +3911,22 @@ export function createAndStartServer(PORT) {
3901
3911
  const spawnCwd =
3902
3912
  engine === "claude" && projectDir ? "/tmp" : projectDir;
3903
3913
 
3914
+ // Merge crewswarm.json env vars into spawn env (crew-cli L2, model overrides, etc.)
3915
+ const spawnEnv = { ...process.env };
3916
+ try {
3917
+ const _sysCfg = loadSwarmConfig();
3918
+ if (_sysCfg?.env && typeof _sysCfg.env === "object") {
3919
+ for (const [k, v] of Object.entries(_sysCfg.env)) {
3920
+ if (!spawnEnv[k] && v != null) spawnEnv[k] = String(v);
3921
+ }
3922
+ }
3923
+ } catch {}
3924
+ // Force L2 planner for crew-cli when requested (enhance-prompt planner path)
3925
+ if (reqForceL2) spawnEnv.CREW_FORCE_L2 = "true";
3926
+
3904
3927
  const proc = _spawn(bin, args, {
3905
3928
  cwd: spawnCwd,
3906
- env: process.env,
3929
+ env: spawnEnv,
3907
3930
  stdio: ["ignore", "pipe", "pipe"],
3908
3931
  });
3909
3932
  let lineBuffer = "";
@@ -3916,7 +3939,7 @@ export function createAndStartServer(PORT) {
3916
3939
  let passthroughGracefulEnd = false;
3917
3940
 
3918
3941
  const passthroughTimeoutMs = Number(
3919
- process.env.CREWSWARM_PASSTHROUGH_TIMEOUT_MS || "240000",
3942
+ process.env.CREWSWARM_PASSTHROUGH_TIMEOUT_MS || "300000",
3920
3943
  );
3921
3944
  const passthroughWatchdog = setTimeout(() => {
3922
3945
  send({
@@ -4207,15 +4230,33 @@ export function createAndStartServer(PORT) {
4207
4230
  // crew-cli special handling: extract JSON from output (skipping logs) and send only response field
4208
4231
  if (engine === "crew-cli" && fullOutput.trim()) {
4209
4232
  try {
4210
- // crew-cli may emit logs before JSON - use regex to find the JSON object
4211
- // Pattern matches: crew chat --json outputs "chat.result"
4212
- const jsonMatch = fullOutput.match(
4213
- /\{[\s\S]*"kind":\s*"chat\.result"[\s\S]*\}/,
4214
- );
4215
- if (!jsonMatch) {
4216
- throw new Error("No chat.result JSON found in output");
4233
+ // crew-cli emits logs before JSON find the JSON by scanning for the last
4234
+ // line that starts with { and contains "chat.result"
4235
+ const lines = fullOutput.split("\n");
4236
+ let jsonStart = -1;
4237
+ for (let i = lines.length - 1; i >= 0; i--) {
4238
+ if (lines[i].trimStart().startsWith("{") && fullOutput.indexOf('"kind"') >= 0) {
4239
+ jsonStart = i;
4240
+ break;
4241
+ }
4242
+ }
4243
+ if (jsonStart < 0) {
4244
+ throw new Error("No JSON object found in crew-cli output");
4245
+ }
4246
+ // Collect from jsonStart to end, parse
4247
+ const jsonCandidate = lines.slice(jsonStart).join("\n");
4248
+ let parsed;
4249
+ try {
4250
+ parsed = JSON.parse(jsonCandidate);
4251
+ } catch {
4252
+ // Try to find just the response field with regex
4253
+ const respMatch = jsonCandidate.match(/"response":\s*"((?:[^"\\]|\\.)*)"/);
4254
+ if (respMatch) {
4255
+ parsed = { response: respMatch[1].replace(/\\n/g, "\n").replace(/\\"/g, '"') };
4256
+ } else {
4257
+ throw new Error("Failed to parse crew-cli JSON output");
4258
+ }
4217
4259
  }
4218
- const parsed = JSON.parse(jsonMatch[0]);
4219
4260
  if (parsed.response) {
4220
4261
  const responseText = typeof parsed.response === 'string'
4221
4262
  ? parsed.response
@@ -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'.",
@@ -377,7 +377,17 @@ export function dispatchPipelineWave(pipelineId) {
377
377
  // this path: when Cursor CLI fails, orchestrator falls back to plain LLM text
378
378
  // with @mentions — subagents never run and the pipeline advances on garbage.
379
379
  // Direct per-agent dispatch is reliable for project pipelines.
380
- 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) {
381
391
  const waveManifest = {
382
392
  wave: currentWave + 1,
383
393
  projectDir: pipeline.projectDir || "",
@@ -702,8 +712,10 @@ export function dispatchTask(agent, task, sessionId = "owner", pipelineMeta = nu
702
712
  task = _deps.writeTaskBrief?.(agent, task, projectDir) ?? task;
703
713
  }
704
714
 
705
- const rp = typeof _deps.getRtPublish === "function" ? _deps.getRtPublish() : null;
706
- 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
+ }
707
719
  if (rp) {
708
720
  try {
709
721
  // Build extraFlags: start with global settings, override with pipeline-specific settings
@@ -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' },