crewswarm 0.9.2 → 0.9.4

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 (228) hide show
  1. package/README.md +22 -9
  2. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
  3. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
  4. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
  5. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
  6. package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
  7. package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
  8. package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
  9. package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
  10. package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
  11. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
  12. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
  13. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
  14. package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
  15. package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
  16. package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
  17. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
  18. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
  19. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
  20. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
  21. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
  22. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
  23. package/apps/dashboard/dist/index.html +135 -15
  24. package/apps/dashboard/dist/index.html.br +0 -0
  25. package/apps/dashboard/dist/index.html.gz +0 -0
  26. package/apps/vibe/README.md +2 -2
  27. package/apps/vibe/package.json +1 -1
  28. package/apps/vibe/server.mjs +101 -56
  29. package/crew-lead.mjs +34 -4
  30. package/lib/bridges/cli-executor.mjs +1 -1
  31. package/lib/bridges/gateway-ws.mjs +4 -0
  32. package/lib/browser/passthrough-stderr.js +1 -0
  33. package/lib/chat/project-messages.mjs +3 -5
  34. package/lib/cli-process-tracker.mjs +3 -2
  35. package/lib/contacts/identity-linker.mjs +1 -0
  36. package/lib/crew-judge/judge.mjs +19 -18
  37. package/lib/crew-lead/agent-manager.mjs +1 -1
  38. package/lib/crew-lead/background.mjs +14 -1
  39. package/lib/crew-lead/chat-handler.mjs +38 -1
  40. package/lib/crew-lead/http-server.mjs +106 -57
  41. package/lib/crew-lead/llm-caller.mjs +24 -8
  42. package/lib/crew-lead/prompts.mjs +14 -1
  43. package/lib/crew-lead/tools.mjs +3 -2
  44. package/lib/crew-lead/wave-dispatcher.mjs +19 -5
  45. package/lib/crew-lead/ws-router.mjs +219 -27
  46. package/lib/engines/crew-cli.mjs +1 -1
  47. package/lib/engines/engine-registry.mjs +14 -3
  48. package/lib/engines/rt-envelope.mjs +1 -0
  49. package/lib/engines/runners.mjs +28 -4
  50. package/lib/gemini-cli-passthrough-noise.mjs +1 -1
  51. package/lib/integrations/code-search.mjs +4 -3
  52. package/lib/memory/shared-adapter.mjs +23 -10
  53. package/lib/pipeline/manager.mjs +2 -1
  54. package/lib/runtime/config.mjs +1 -1
  55. package/lib/runtime/paths.mjs +12 -8
  56. package/lib/runtime/spending.mjs +2 -1
  57. package/package.json +42 -14
  58. package/scripts/capture-build-flow.mjs +118 -0
  59. package/scripts/coverage-report.mjs +209 -0
  60. package/scripts/coverage-summary.mjs +47 -0
  61. package/scripts/dashboard-validation.mjs +76 -0
  62. package/scripts/dashboard.mjs +1667 -551
  63. package/scripts/generate-openapi.mjs +683 -277
  64. package/scripts/live-bridge-matrix.mjs +79 -0
  65. package/scripts/live-cli-matrix.mjs +166 -0
  66. package/scripts/live-crewchat-check.mjs +42 -0
  67. package/scripts/live-engine-matrix.mjs +50 -0
  68. package/scripts/live-provider-failover-matrix.mjs +107 -0
  69. package/scripts/live-provider-matrix.mjs +228 -0
  70. package/scripts/restart-all-from-repo.sh +4 -4
  71. package/scripts/restart-service.sh +12 -9
  72. package/scripts/smoke-dispatch.mjs +4 -1
  73. package/scripts/test-blast-radius.mjs +204 -0
  74. package/scripts/test-report-summary.mjs +88 -0
  75. package/scripts/test-reporter.mjs +651 -0
  76. package/scripts/test-rerun.mjs +136 -0
  77. package/scripts/tmux-bridge +130 -0
  78. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js +0 -1
  79. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  80. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
  81. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  82. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  83. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  84. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  85. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
  86. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  87. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  88. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  89. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  90. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  91. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
  92. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  93. package/apps/dashboard/index.html +0 -6529
  94. package/apps/dashboard/package.json +0 -15
  95. package/apps/dashboard/src/app.js +0 -2828
  96. package/apps/dashboard/src/app.js.br +0 -0
  97. package/apps/dashboard/src/app.js.gz +0 -0
  98. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  99. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  100. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  101. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  102. package/apps/dashboard/src/cli-process.js +0 -208
  103. package/apps/dashboard/src/cli-process.js.br +0 -0
  104. package/apps/dashboard/src/cli-process.js.gz +0 -0
  105. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  106. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  107. package/apps/dashboard/src/core/api.js +0 -18
  108. package/apps/dashboard/src/core/api.js.br +0 -0
  109. package/apps/dashboard/src/core/dom.js +0 -228
  110. package/apps/dashboard/src/core/dom.js.br +0 -0
  111. package/apps/dashboard/src/core/state.js +0 -91
  112. package/apps/dashboard/src/core/state.js.br +0 -0
  113. package/apps/dashboard/src/core/task-manager.js +0 -134
  114. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  115. package/apps/dashboard/src/orchestration-status.js +0 -127
  116. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  117. package/apps/dashboard/src/setup-wizard.js +0 -562
  118. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  119. package/apps/dashboard/src/styles.css +0 -2085
  120. package/apps/dashboard/src/styles.css.br +0 -0
  121. package/apps/dashboard/src/styles.css.gz +0 -0
  122. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  123. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  124. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  125. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  126. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  127. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  128. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  129. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  130. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  131. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  132. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  133. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  134. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  135. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  136. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  137. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  138. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  139. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  140. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  141. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  142. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  143. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  144. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  145. package/apps/dashboard/src/tabs/settings-tab.js +0 -861
  146. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  147. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  148. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  149. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  150. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  151. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  152. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  153. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  154. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  155. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  156. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  157. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  158. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  159. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  160. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  161. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  162. package/apps/vibe/.crew/cost.json +0 -17
  163. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  164. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  172. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  173. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  174. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  175. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  176. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  177. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  178. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  179. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  180. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  181. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  182. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  183. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  184. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  185. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  186. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  187. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  188. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  189. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  190. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  191. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  192. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  193. package/apps/vibe/.crew/sandbox.json +0 -7
  194. package/apps/vibe/.crew/session.json +0 -330
  195. package/apps/vibe/.crew/training-data.jsonl +0 -0
  196. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  197. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  198. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  199. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  200. package/apps/vibe/ARCHITECTURE.md +0 -3393
  201. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  202. package/apps/vibe/ROADMAP.md +0 -41
  203. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  204. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  205. package/apps/vibe/capture-demo.mjs +0 -160
  206. package/apps/vibe/capture-full-demo.mjs +0 -255
  207. package/apps/vibe/capture-quickstart.mjs +0 -256
  208. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  209. package/apps/vibe/capture-vibe-video.mjs +0 -260
  210. package/apps/vibe/check-buttons.js +0 -41
  211. package/apps/vibe/diagnose.html +0 -106
  212. package/apps/vibe/fix-buttons.js +0 -103
  213. package/apps/vibe/index.html +0 -3404
  214. package/apps/vibe/package-lock.json +0 -920
  215. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  216. package/apps/vibe/src/main.js +0 -2940
  217. package/apps/vibe/src/register-all-languages.js +0 -98
  218. package/apps/vibe/start-studio.sh +0 -11
  219. package/apps/vibe/test/accessibility-tests.js +0 -77
  220. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  221. package/apps/vibe/test/performance-tests.js +0 -120
  222. package/apps/vibe/test/security-tests.js +0 -213
  223. package/apps/vibe/tests/e2e.local.mjs +0 -54
  224. package/apps/vibe/tests/server.smoke.mjs +0 -106
  225. package/apps/vibe/update_website.mjs +0 -74
  226. package/apps/vibe/vite.config.js +0 -19
  227. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  228. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -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
 
@@ -111,7 +111,7 @@ export async function runCrewCLITask(prompt, payload = {}) {
111
111
  const engine = await getEngine();
112
112
 
113
113
  // Create sandbox pointed at the project directory
114
- const sandbox = new engine.Sandbox({ baseDir: projectDir });
114
+ const sandbox = new engine.Sandbox(projectDir);
115
115
 
116
116
  // Run the agentic executor directly — THINK→ACT→OBSERVE loop with 34 tools
117
117
  const result = await engine.runAgenticWorker(prompt, sandbox, {
@@ -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,7 +154,9 @@ 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) {
156
- // PRIORITY 1: Payload flags (from enriched task)
157
+ const prompt = payload?.prompt || payload?.task || payload?.message || "";
158
+
159
+ // PRIORITY 1: Payload flags (from enriched task) — always respected regardless of prompt content
157
160
  const explicitEngines = [
158
161
  { key: 'useCodex', id: 'codex' },
159
162
  { key: 'useCursor', id: 'cursor' },
@@ -186,7 +189,9 @@ export function selectEngine(payload, incomingType) {
186
189
  }
187
190
  }
188
191
 
189
- // PRIORITY 2.3: Per-agent engine from crewswarm.json (`engine` + use* flags), not global env alone
192
+ // PRIORITY 2.3: Per-agent engine from crewswarm.json (`engine` + use* flags) always respected.
193
+ // Agents that explicitly opt into an engine (useGeminiCli, useClaudeCode, etc.) should always
194
+ // use it, even for non-coding prompts like "reply with exactly X".
190
195
  const preferredFromCfg = resolveAgentPreferredEngineId(payload);
191
196
  if (preferredFromCfg) {
192
197
  const preferredEngine = _engines.find((e) => e.id === preferredFromCfg);
@@ -195,7 +200,13 @@ export function selectEngine(payload, incomingType) {
195
200
  }
196
201
  }
197
202
 
198
- // PRIORITY 2.5: If agent explicitly opted out of ALL CLI engines, skip fallback — use direct LLM
203
+ // PRIORITY 2.5: Chat messages and non-coding tasks always use direct LLM for fallback routing.
204
+ // Only applies when no explicit engine was configured above.
205
+ if (!isCodingTask(incomingType, prompt, payload)) {
206
+ return null;
207
+ }
208
+
209
+ // PRIORITY 2.6: If agent explicitly opted out of ALL CLI engines, skip fallback — use direct LLM
199
210
  const agentId = String(payload?.agentId || payload?.agent || "").toLowerCase();
200
211
  if (agentId && _loadAgentList) {
201
212
  try {
@@ -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
  },
@@ -27,6 +27,19 @@ function which(bin) {
27
27
  try { execSync(`which ${bin}`, { stdio: "ignore" }); return true; } catch { return false; }
28
28
  }
29
29
 
30
+ function isClaudeOauthAuthenticated() {
31
+ try {
32
+ const output = execSync(`${CLAUDE_CODE_BIN} auth status`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
33
+ .trim()
34
+ .toLowerCase();
35
+ if (!output) return false;
36
+ if (/"loggedin"\s*:\s*true/.test(output)) return true;
37
+ return output.includes("logged in");
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
30
43
  // ── Module-level deps (injected via initRunners) ───────────────────────────
31
44
  let _getAgentOpenCodeConfig = () => ({ enabled: false, useCursorCli: false, cursorCliModel: null, claudeCodeModel: null });
32
45
  let _loadAgentList = () => [];
@@ -1076,7 +1089,7 @@ export async function runDockerSandboxTask(prompt, payload = {}) {
1076
1089
 
1077
1090
  let innerArgs;
1078
1091
  if (innerEngine === "opencode") {
1079
- innerArgs = ["opencode", "run", titledPrompt, "--model", process.env.CREWSWARM_OPENCODE_MODEL || "anthropic/claude-sonnet-4-5"];
1092
+ innerArgs = ["opencode", "run", titledPrompt, "--model", process.env.CREWSWARM_OPENCODE_MODEL || "openai/gpt-5.4"];
1080
1093
  } else if (innerEngine === "codex") {
1081
1094
  innerArgs = ["codex", "exec", "--sandbox", "workspace-write", "--json", titledPrompt];
1082
1095
  } else {
@@ -1181,6 +1194,7 @@ export async function runClaudeCodeTask(prompt, payload = {}) {
1181
1194
  const agentPrefix = agentId ? `[${agentId}]` : "";
1182
1195
  const titledPrompt = agentPrefix ? `${agentPrefix} ${String(prompt)}` : String(prompt);
1183
1196
 
1197
+ const emptyMcpCfg = path.join(os.homedir(), ".crewswarm", "config", "empty-mcp.json");
1184
1198
  const args = [
1185
1199
  "-p",
1186
1200
  "--dangerously-skip-permissions",
@@ -1205,21 +1219,31 @@ export async function runClaudeCodeTask(prompt, payload = {}) {
1205
1219
  }
1206
1220
  }
1207
1221
 
1208
- // CRITICAL: Claude Code expects the prompt as a command-line argument, NOT via stdin
1209
- args.push(titledPrompt);
1222
+ // Skip user MCP servers to avoid 30s+ init hangs
1223
+ args.push("--strict-mcp-config", "--mcp-config", emptyMcpCfg);
1224
+ // -- separates flags from prompt (--mcp-config is variadic and eats positional args)
1225
+ args.push("--", titledPrompt);
1210
1226
 
1211
1227
  if (!which(CLAUDE_CODE_BIN)) {
1212
1228
  throw new Error(`Claude Code CLI not found: "${CLAUDE_CODE_BIN}". Install with: npm i -g @anthropic-ai/claude-code`);
1213
1229
  }
1230
+ if (!isClaudeOauthAuthenticated()) {
1231
+ console.error(`[ClaudeCode:${agentId}] auth preflight reported logged out; continuing and letting Claude decide at runtime.`);
1232
+ }
1214
1233
 
1215
1234
  console.error(`[ClaudeCode:${agentId}] Running: ${CLAUDE_CODE_BIN} -p --dangerously-skip-permissions (cwd=${projectDir})`);
1216
1235
 
1217
1236
  _rtClientForApprovals?.publish({ channel: "events", type: "agent_working", to: "broadcast",
1218
1237
  payload: { agent: agentId, model: model || "claude/auto", ts: Date.now() } });
1219
1238
 
1239
+ const childEnv = { ...process.env };
1240
+ delete childEnv.ANTHROPIC_API_KEY;
1241
+ delete childEnv.CLAUDECODE;
1242
+ delete childEnv.CLAUDE_CODE;
1243
+
1220
1244
  const child = spawn(CLAUDE_CODE_BIN, args, {
1221
1245
  cwd: projectDir,
1222
- env: process.env,
1246
+ env: childEnv,
1223
1247
  stdio: ["ignore", "pipe", "pipe"], // Changed from "pipe" to "ignore" for stdin since we use args
1224
1248
  });
1225
1249
 
@@ -33,5 +33,5 @@ export function filterGeminiPassthroughTextChunk(engine, text) {
33
33
  if (engine !== "gemini" && engine !== "gemini-cli") return String(text ?? "");
34
34
  const s = String(text ?? "");
35
35
  if (!s) return s;
36
- return s.split("\n").filter((ln) => !shouldSkipGeminiPassthroughLine(ln)).join("\n");
36
+ return s.split("\n").filter((ln) => !shouldSkipGeminiPassthroughLine(ln)).map((ln) => ln.replace(/\r$/, "")).join("\n");
37
37
  }
@@ -198,7 +198,8 @@ export function formatSearchResults(searchResults, maxChars = 4000) {
198
198
  */
199
199
  export async function findFiles(pattern, projectDir = process.cwd()) {
200
200
  try {
201
- const output = execSync(`rg --files | rg "${pattern}"`, {
201
+ const safePattern = pattern.replace(/["`$\\]/g, '\\$&');
202
+ const output = execSync(`rg --files | rg "${safePattern}"`, {
202
203
  cwd: projectDir,
203
204
  encoding: 'utf8',
204
205
  stdio: ['pipe', 'pipe', 'ignore']
@@ -247,7 +248,7 @@ export async function findFunctions(name, projectDir = process.cwd()) {
247
248
  // Escape special regex characters in name
248
249
  const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
249
250
  const pattern = `(function|const|let|export)\\s+${escapedName}`;
250
- return await searchPattern(pattern, projectDir, { fileTypes: ['js', 'ts', 'mjs'] });
251
+ return await searchPattern(pattern, projectDir, { fileTypes: ['js', 'ts'] });
251
252
  }
252
253
 
253
254
  /**
@@ -255,5 +256,5 @@ export async function findFunctions(name, projectDir = process.cwd()) {
255
256
  */
256
257
  export async function findClasses(name, projectDir = process.cwd()) {
257
258
  const pattern = `class\\s+${name}\\s*[{(]`;
258
- return await searchPattern(pattern, projectDir, { fileTypes: ['js', 'ts', 'mjs'] });
259
+ return await searchPattern(pattern, projectDir, { fileTypes: ['js', 'ts'] });
259
260
  }
@@ -36,6 +36,19 @@ try {
36
36
  // Shared memory root (env var or default)
37
37
  export const CREW_MEMORY_DIR = process.env.CREW_MEMORY_DIR || path.join(os.homedir(), '.crewswarm', 'shared-memory');
38
38
 
39
+ // Cache project-messages-rag module to avoid dynamic import() on every call
40
+ let _projectMessagesRag = null;
41
+ async function getProjectMessagesRag() {
42
+ if (_projectMessagesRag === null) {
43
+ try {
44
+ _projectMessagesRag = await import('../chat/project-messages-rag.mjs');
45
+ } catch {
46
+ _projectMessagesRag = false; // mark as unavailable
47
+ }
48
+ }
49
+ return _projectMessagesRag || null;
50
+ }
51
+
39
52
  /**
40
53
  * Get or create AgentKeeper instance for task memory.
41
54
  * @param {string} projectDir - Project root directory
@@ -143,8 +156,8 @@ export async function recallMemoryContext(projectDir, query, options = {}) {
143
156
  // This lets agents see past discussions about the same topic
144
157
  if (options.projectId) {
145
158
  try {
146
- const { getConversationContext } = await import('../chat/project-messages-rag.mjs');
147
- const conversationContext = getConversationContext(options.projectId, query, 3);
159
+ const ragModule = await getProjectMessagesRag();
160
+ const conversationContext = ragModule?.getConversationContext(options.projectId, query, 3);
148
161
 
149
162
  if (conversationContext) {
150
163
  memoryContext += (memoryContext ? '\n\n' : '') + conversationContext;
@@ -264,21 +277,21 @@ export async function migrateBrainToMemory(brainPath, agentId = 'crew-lead') {
264
277
 
265
278
  // Extract tags from markdown list items
266
279
  const tags = [];
267
- let content = trimmed;
280
+ let factContent = trimmed;
268
281
 
269
282
  // Extract date tags (YYYY-MM-DD)
270
- const dateMatch = content.match(/\b(\d{4}-\d{2}-\d{2})\b/);
283
+ const dateMatch = factContent.match(/\b(\d{4}-\d{2}-\d{2})\b/);
271
284
  if (dateMatch) tags.push('dated', dateMatch[1]);
272
-
285
+
273
286
  // Extract agent mentions
274
- const agentMatch = content.match(/\b(crew-\w+)\b/);
287
+ const agentMatch = factContent.match(/\b(crew-\w+)\b/);
275
288
  if (agentMatch) tags.push('agent', agentMatch[1]);
276
-
289
+
277
290
  // Determine criticality (heuristic: lines with CRITICAL, ERROR, WARNING, or !)
278
- const critical = /\b(CRITICAL|ERROR|WARNING|!)\b/i.test(content) || content.includes('MUST') || content.includes('NEVER');
279
-
291
+ const critical = /\b(CRITICAL|ERROR|WARNING|!)\b/i.test(factContent) || factContent.includes('MUST') || factContent.includes('NEVER');
292
+
280
293
  try {
281
- memory.remember(content, {
294
+ memory.remember(factContent, {
282
295
  critical,
283
296
  tags: tags.length > 0 ? tags : ['brain-migration'],
284
297
  provider: 'brain-migration'
@@ -20,7 +20,7 @@ function tryRead(p) {
20
20
 
21
21
  export const pendingProjects = new Map();
22
22
 
23
- export function initPipelineManager({ dashboard, broadcastSSE, appendHistory, handleChat, loadConfig }) {
23
+ export function initPipelineManager({ dashboard, broadcastSSE, appendHistory, handleChat, loadConfig } = {}) {
24
24
  if (dashboard !== undefined) _dashboard = dashboard;
25
25
  if (broadcastSSE) _broadcastSSE = broadcastSSE;
26
26
  if (appendHistory) _appendHistory = appendHistory;
@@ -502,6 +502,7 @@ export function parseRoadmapPhases(content) {
502
502
  }
503
503
 
504
504
  export function findNextRoadmapPhase(projectDir) {
505
+ if (!projectDir) return null;
505
506
  const roadmapPath = path.join(projectDir, "ROADMAP.md");
506
507
  if (!fs.existsSync(roadmapPath)) return null;
507
508
  try {
@@ -185,7 +185,7 @@ export const CREWSWARM_CLAUDE_CODE = (() => {
185
185
  export const CREWSWARM_OPENCODE_FORCE = process.env.CREWSWARM_OPENCODE_FORCE === "1";
186
186
  export const CREWSWARM_OPENCODE_BIN = process.env.CREWSWARM_OPENCODE_BIN || path.join(os.homedir(), ".opencode", "bin", "opencode");
187
187
  export const CREWSWARM_OPENCODE_AGENT = process.env.CREWSWARM_OPENCODE_AGENT || "admin";
188
- export const CREWSWARM_OPENCODE_MODEL = process.env.CREWSWARM_OPENCODE_MODEL || "groq/moonshotai/kimi-k2-instruct-0905";
188
+ export const CREWSWARM_OPENCODE_MODEL = process.env.CREWSWARM_OPENCODE_MODEL || "openai/gpt-5.4";
189
189
  export const CREWSWARM_OPENCODE_FALLBACK_DEFAULT = "groq/llama-3.3-70b-versatile";
190
190
  export const CREWSWARM_OPENCODE_TIMEOUT_MS = Number(process.env.CREWSWARM_OPENCODE_TIMEOUT_MS || "300000");
191
191