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
@@ -185,6 +185,7 @@ export function unlinkIdentity(contactId) {
185
185
  SET platform_links = NULL
186
186
  WHERE contact_id = ?
187
187
  `).run(contactId);
188
+ return true;
188
189
  }
189
190
 
190
191
  /**
@@ -150,25 +150,26 @@ function getJudgeModel() {
150
150
 
151
151
  async function callJudgeModel(model, prompt) {
152
152
  // Import LLM caller from crew-lead
153
- const { callLLMForText } = await import('../crew-lead/llm-caller.mjs');
154
-
153
+ const { callLLM } = await import('../crew-lead/llm-caller.mjs');
154
+ const { loadProviderMap } = await import('../runtime/config.mjs');
155
+
155
156
  try {
156
- const response = await callLLMForText({
157
- provider: model.split('/')[0],
158
- model: model.split('/').slice(1).join('/'),
159
- messages: [
160
- {
161
- role: 'system',
162
- content: 'You are crew-judge. Return ONLY valid JSON: {"decision": "CONTINUE"|"SHIP"|"RESET", "reasoning": "...", "confidence": 0.0-1.0}'
163
- },
164
- {
165
- role: 'user',
166
- content: prompt
167
- }
168
- ],
169
- temperature: 0.3, // Lower temperature for more consistent decisions
170
- maxTokens: 500
171
- });
157
+ const providerKey = model.split('/')[0];
158
+ const modelId = model.split('/').slice(1).join('/');
159
+ const providerMap = loadProviderMap();
160
+ const provider = providerMap[providerKey] || null;
161
+
162
+ const messages = [
163
+ {
164
+ role: 'system',
165
+ content: 'You are crew-judge. Return ONLY valid JSON: {"decision": "CONTINUE"|"SHIP"|"RESET", "reasoning": "...", "confidence": 0.0-1.0}'
166
+ },
167
+ {
168
+ role: 'user',
169
+ content: prompt
170
+ }
171
+ ];
172
+ const { reply: response } = await callLLM(messages, { provider, modelId, providerKey }, { temperature: 0.3, maxTokens: 500 });
172
173
 
173
174
  // Parse JSON response
174
175
  const jsonMatch = response.match(/\{[\s\S]*\}/);
@@ -79,7 +79,7 @@ export function createAgent({ id, role, displayName, prompt, description, model
79
79
  const defaultOcModel = (() => {
80
80
  const existingCoder = swarm.agents.find(a => a.opencodeModel && a.useOpenCode);
81
81
  if (existingCoder) return existingCoder.opencodeModel;
82
- return process.env.CREWSWARM_OPENCODE_MODEL || "openai/gpt-5.3-codex";
82
+ return process.env.CREWSWARM_OPENCODE_MODEL || "openai/gpt-5.4";
83
83
  })();
84
84
 
85
85
  const agentEntry = {
@@ -28,6 +28,19 @@ const _timeoutLog = [];
28
28
  let _lastBgConsciousnessAt = 0;
29
29
  let _bgLoopInterval = null;
30
30
 
31
+ /** Reset internal state for test isolation. */
32
+ export function resetForTesting() {
33
+ _agentTimeoutCounts.clear();
34
+ _timeoutLog.length = 0;
35
+ _lastBgConsciousnessAt = 0;
36
+ if (_bgLoopInterval) { clearInterval(_bgLoopInterval); _bgLoopInterval = null; }
37
+ }
38
+
39
+ /** Stop the background loop interval (prevents process from hanging in tests). */
40
+ export function stopBackgroundLoop() {
41
+ if (_bgLoopInterval) { clearInterval(_bgLoopInterval); _bgLoopInterval = null; }
42
+ }
43
+
31
44
  export function initBackground({
32
45
  broadcastSSE,
33
46
  appendHistory,
@@ -267,4 +280,4 @@ export function getRateLimitFallback(agentId) {
267
280
  return "crew-main";
268
281
  }
269
282
 
270
- export const RATE_LIMIT_PATTERN = /429|rate\s*limit|throttl|quota\s*exceeded|too\s*many\s*requests|resource_exhausted|overloaded/i;
283
+ export const RATE_LIMIT_PATTERN = /429|rate[\s_]*limit|throttl|quota[\s_]*exceeded|too[\s_]*many[\s_]*requests|resource_exhausted|overloaded/i;
@@ -750,7 +750,10 @@ Reply with your answers and I'll turn this into a concrete build plan with file
750
750
  // IMPORTANT: Do not gate on keywords only. Casual replies ("yeah no shit", "ok cool")
751
751
  // used to skip the snapshot while long contradictory assistant history stayed in context,
752
752
  // so the LLM invented swarm-wide offline/online flips. Fetch for substantive messages.
753
- const needsHealth = message.trim().length >= 6;
753
+ // Custom personas (displayName set to something other than default "crew-lead") are
754
+ // personality/chat personas — they don't need operational system state, skip snapshot.
755
+ const isCustomPersona = cfg.displayName && cfg.displayName.toLowerCase() !== "crew-lead";
756
+ const needsHealth = !isCustomPersona && message.trim().length >= 6;
754
757
  const needsBenchmarkCatalog =
755
758
  message.length > 4 &&
756
759
  /benchmark|zeroeval|leaderboard|llm-stats|swe-bench|livecodebench|mmlu|gpqa|humaneval|gsm8k|what\.tests?|which\.tests?|available\.tests?/i.test(
@@ -2577,6 +2580,40 @@ Reply with your answers and I'll turn this into a concrete build plan with file
2577
2580
  ...(usedFallback ? { fallbackReason } : {}),
2578
2581
  },
2579
2582
  });
2583
+
2584
+ // Route @mentions in assistant replies — crew-lead can autonomously dispatch
2585
+ if (replyMentions.length && channelMode) {
2586
+ try {
2587
+ const {
2588
+ handleAutonomousMentions: routeMentions,
2589
+ detectMentionTargets,
2590
+ } = await import("../chat/autonomous-mentions.mjs");
2591
+ const mentionTargets = detectMentionTargets(historyReply);
2592
+ const mentionRoute = classifySharedChatMention(historyReply);
2593
+ if (mentionTargets.length && mentionRoute.mode === "dispatch") {
2594
+ void routeMentions({
2595
+ message: { content: historyReply },
2596
+ sender: "crew-lead",
2597
+ channel: sharedChannel,
2598
+ projectId: sharedChannel,
2599
+ sessionId,
2600
+ projectDir: explicitProjectDir || activeProjectOutputDir || null,
2601
+ originMessageId: assistantMessageId,
2602
+ originThreadId: sharedThreadId,
2603
+ chatHistory: loadProjectMessages(sharedChannel, {
2604
+ limit: 10,
2605
+ threadId: sharedThreadId,
2606
+ excludeDirect: true,
2607
+ }),
2608
+ broadcastSSE: _deps.broadcastSSE,
2609
+ }).catch((err) => {
2610
+ console.warn(`[chat-handler] Assistant mention routing failed: ${err.message}`);
2611
+ });
2612
+ }
2613
+ } catch (mentionErr) {
2614
+ console.warn(`[chat-handler] Assistant mention dispatch error: ${mentionErr.message}`);
2615
+ }
2616
+ }
2580
2617
  } catch (e) {
2581
2618
  console.warn(
2582
2619
  `[chat-handler] Failed to save assistant message to project store: ${e.message}`,
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
  import crypto from "node:crypto";
9
9
  import path from "node:path";
10
10
  import os from "node:os";
11
- import { spawn } from "node:child_process";
11
+ import { spawn, execFileSync } from "node:child_process";
12
12
  import { executeCLI } from "../bridges/cli-executor.mjs";
13
13
  import { applySharedChatPromptOverlay } from "../chat/shared-chat-prompt-overlay.mjs";
14
14
  import { classifySharedChatMention } from "../chat/mention-routing-intent.mjs";
@@ -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 = {};
@@ -60,6 +60,25 @@ function resolveCliBinary(configured, candidates = []) {
60
60
  return configured;
61
61
  }
62
62
 
63
+ function isClaudeOauthAuthenticated() {
64
+ try {
65
+ const bin = resolveCliBinary("claude", [
66
+ path.join(os.homedir(), ".local", "bin", "claude"),
67
+ "/usr/local/bin/claude",
68
+ "/opt/homebrew/bin/claude",
69
+ ]);
70
+ const output = execFileSync(bin, ["auth", "status"], {
71
+ encoding: "utf8",
72
+ stdio: ["ignore", "pipe", "ignore"],
73
+ }).trim().toLowerCase();
74
+ if (!output) return false;
75
+ if (/"loggedin"\s*:\s*true/.test(output)) return true;
76
+ return output.includes("logged in");
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
63
82
  /** Strip SGR / OSC ANSI sequences (OpenCode and CLIs often emit colored TTY junk on stdout). */
64
83
  function stripAnsiPassthrough(text) {
65
84
  return String(text || "")
@@ -94,6 +113,7 @@ function shouldDropPassthroughStderrLine(engine, line) {
94
113
  if (/rmcp::/i.test(l)) return true;
95
114
  if (/error decoding response body.*initialized notification/i.test(l))
96
115
  return true;
116
+ if (/\[Executor\]\s+\w+\s+\((OAuth|API)\)/i.test(l)) return true;
97
117
  if (
98
118
  engine === "codex" &&
99
119
  /worker quit with fatal/i.test(l) &&
@@ -3532,6 +3552,9 @@ export function createAndStartServer(PORT) {
3532
3552
  sessionId: reqSessionId,
3533
3553
  injectHistory,
3534
3554
  model: reqModel,
3555
+ permissionMode: reqPermissionMode,
3556
+ sandbox: reqSandbox,
3557
+ forceL2: reqForceL2,
3535
3558
  } = JSON.parse(body || "{}");
3536
3559
  if (!message) {
3537
3560
  json(res, 400, { ok: false, error: "message required" });
@@ -3730,7 +3753,7 @@ export function createAndStartServer(PORT) {
3730
3753
  const ocModel =
3731
3754
  reqModel ||
3732
3755
  process.env.CREWSWARM_OPENCODE_MODEL ||
3733
- "groq/moonshotai/kimi-k2-instruct-0905";
3756
+ "openai/gpt-5.4";
3734
3757
  args = ["run", finalMessage, "--model", ocModel];
3735
3758
  // Add workspace directory context
3736
3759
  if (projectDir) args.push("--dir", projectDir);
@@ -3753,7 +3776,7 @@ export function createAndStartServer(PORT) {
3753
3776
 
3754
3777
  if (priorCodexThread) {
3755
3778
  // `codex resume <thread_id> "prompt"` continues the prior session
3756
- args = ["resume", priorCodexThread, finalMessage, "--json"];
3779
+ args = ["resume", priorCodexThread, finalMessage];
3757
3780
  if (codexModel) args.push("--model", codexModel);
3758
3781
  if (projectDir) args.push("-C", projectDir);
3759
3782
  } else {
@@ -3762,7 +3785,7 @@ export function createAndStartServer(PORT) {
3762
3785
  "never",
3763
3786
  "exec",
3764
3787
  "--sandbox",
3765
- "danger-full-access",
3788
+ reqSandbox || "danger-full-access",
3766
3789
  "--skip-git-repo-check",
3767
3790
  "--color",
3768
3791
  "never",
@@ -3803,10 +3826,10 @@ export function createAndStartServer(PORT) {
3803
3826
  const crewCliModel =
3804
3827
  reqModel || process.env.CREWSWARM_CREW_CLI_MODEL || null;
3805
3828
  if (bin.endsWith(".js")) {
3806
- args = [bin, "chat", finalMessage, "--direct", "--json"];
3829
+ args = [bin, "chat", finalMessage, "--direct", "--json", "--apply"];
3807
3830
  bin = process.execPath;
3808
3831
  } else {
3809
- args = ["chat", finalMessage, "--direct", "--json"];
3832
+ args = ["chat", finalMessage, "--direct", "--json", "--apply"];
3810
3833
  }
3811
3834
  if (crewCliModel) args.push("--model", crewCliModel);
3812
3835
  if (projectDir && projectDir !== process.cwd())
@@ -3859,12 +3882,16 @@ export function createAndStartServer(PORT) {
3859
3882
  "-p",
3860
3883
  "--setting-sources",
3861
3884
  "user",
3862
- "--dangerously-skip-permissions",
3863
3885
  "--output-format",
3864
3886
  "stream-json",
3865
3887
  "--verbose",
3866
3888
  "--include-partial-messages",
3867
3889
  ];
3890
+ if (reqPermissionMode) {
3891
+ args.push("--permission-mode", reqPermissionMode);
3892
+ } else {
3893
+ args.push("--dangerously-skip-permissions");
3894
+ }
3868
3895
  if (projectDir) args.push("--add-dir", projectDir);
3869
3896
  if (reqModel) args.push("--model", reqModel);
3870
3897
  // Prefer --resume <session-id> for per-project isolation; fall back to --continue (most recent global)
@@ -3877,7 +3904,10 @@ export function createAndStartServer(PORT) {
3877
3904
  } else if (continueSession) {
3878
3905
  args.push("--continue");
3879
3906
  }
3880
- args.push(finalMessage);
3907
+ // Skip user MCP servers to avoid 30s+ init hangs
3908
+ args.push("--strict-mcp-config", "--mcp-config", path.join(os.homedir(), ".crewswarm", "config", "empty-mcp.json"));
3909
+ // -- separates flags from prompt (--mcp-config is variadic and eats positional args)
3910
+ args.push("--", finalMessage);
3881
3911
  }
3882
3912
 
3883
3913
  send({ type: "start", engine, message: message.slice(0, 80) });
@@ -3901,9 +3931,41 @@ export function createAndStartServer(PORT) {
3901
3931
  const spawnCwd =
3902
3932
  engine === "claude" && projectDir ? "/tmp" : projectDir;
3903
3933
 
3934
+ // Merge crewswarm.json env vars into spawn env (crew-cli L2, model overrides, etc.)
3935
+ const spawnEnv = { ...process.env };
3936
+ try {
3937
+ const _sysCfg = loadSwarmConfig();
3938
+ if (_sysCfg?.env && typeof _sysCfg.env === "object") {
3939
+ for (const [k, v] of Object.entries(_sysCfg.env)) {
3940
+ if (!spawnEnv[k] && v != null) spawnEnv[k] = String(v);
3941
+ }
3942
+ }
3943
+ } catch {}
3944
+ // Strip API keys AFTER config merge so CLI tools use OAuth (free) not API credits
3945
+ if (engine === "claude") {
3946
+ delete spawnEnv.ANTHROPIC_API_KEY;
3947
+ delete spawnEnv.CLAUDECODE;
3948
+ delete spawnEnv.CLAUDE_CODE;
3949
+ delete spawnEnv.CREWSWARM_CLAUDE_CODE;
3950
+ }
3951
+ if (engine === "codex") {
3952
+ delete spawnEnv.OPENAI_API_KEY;
3953
+ }
3954
+ if (engine === "gemini" || engine === "gemini-cli") {
3955
+ delete spawnEnv.GEMINI_API_KEY;
3956
+ delete spawnEnv.GOOGLE_API_KEY;
3957
+ }
3958
+ if (engine === "opencode" || engine === "antigravity") {
3959
+ delete spawnEnv.OPENAI_API_KEY;
3960
+ delete spawnEnv.ANTHROPIC_API_KEY;
3961
+ delete spawnEnv.GEMINI_API_KEY;
3962
+ }
3963
+ // Force L2 planner for crew-cli when requested (enhance-prompt planner path)
3964
+ if (reqForceL2) spawnEnv.CREW_FORCE_L2 = "true";
3965
+
3904
3966
  const proc = _spawn(bin, args, {
3905
3967
  cwd: spawnCwd,
3906
- env: process.env,
3968
+ env: spawnEnv,
3907
3969
  stdio: ["ignore", "pipe", "pipe"],
3908
3970
  });
3909
3971
  let lineBuffer = "";
@@ -3916,7 +3978,7 @@ export function createAndStartServer(PORT) {
3916
3978
  let passthroughGracefulEnd = false;
3917
3979
 
3918
3980
  const passthroughTimeoutMs = Number(
3919
- process.env.CREWSWARM_PASSTHROUGH_TIMEOUT_MS || "240000",
3981
+ process.env.CREWSWARM_PASSTHROUGH_TIMEOUT_MS || "300000",
3920
3982
  );
3921
3983
  const passthroughWatchdog = setTimeout(() => {
3922
3984
  send({
@@ -4206,38 +4268,39 @@ export function createAndStartServer(PORT) {
4206
4268
 
4207
4269
  // crew-cli special handling: extract JSON from output (skipping logs) and send only response field
4208
4270
  if (engine === "crew-cli" && fullOutput.trim()) {
4271
+ let extracted = false;
4209
4272
  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");
4217
- }
4218
- const parsed = JSON.parse(jsonMatch[0]);
4219
- if (parsed.response) {
4220
- const responseText = typeof parsed.response === 'string'
4221
- ? parsed.response
4222
- : JSON.stringify(parsed.response, null, 2);
4223
- console.error(
4224
- `[crew-cli] ✅ Extracted response: ${responseText.slice(0, 80)}...`,
4225
- );
4226
- // NOW send the actual response as a chunk
4227
- send({ type: "chunk", text: responseText });
4228
- fullOutput = responseText; // Replace fullOutput for notifications
4229
- } else {
4230
- console.error(`[crew-cli] ⚠️ No response field in JSON`);
4231
- send({ type: "chunk", text: fullOutput }); // Send raw output as fallback
4273
+ // Strategy 1: cross-line regex to find JSON envelope
4274
+ const jsonMatch = fullOutput.match(/\{[\s\S]*"kind":\s*"[^"]+\.result"[\s\S]*\}/);
4275
+ if (jsonMatch) {
4276
+ const parsed = JSON.parse(jsonMatch[0]);
4277
+ if (parsed.response) {
4278
+ const responseText = typeof parsed.response === "string"
4279
+ ? parsed.response
4280
+ : JSON.stringify(parsed.response, null, 2);
4281
+ console.log(`[crew-cli] Extracted response: ${responseText.slice(0, 80)}...`);
4282
+ send({ type: "chunk", text: responseText });
4283
+ fullOutput = responseText;
4284
+ extracted = true;
4285
+ }
4232
4286
  }
4233
- } catch (err) {
4234
- console.error(
4235
- `[crew-cli] Failed to extract JSON: ${err.message}`,
4236
- );
4237
- console.error(
4238
- `[crew-cli] First 200 chars: ${fullOutput.slice(0, 200)}`,
4239
- );
4240
- // Send raw output as fallback
4287
+ } catch { /* fall through */ }
4288
+ if (!extracted) {
4289
+ // Strategy 2: regex extract just the response field
4290
+ try {
4291
+ const respMatch = fullOutput.match(/"response":\s*"((?:[^"\\]|\\.)*)"/);
4292
+ if (respMatch) {
4293
+ const responseText = respMatch[1].replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\t/g, "\t");
4294
+ console.log(`[crew-cli] Extracted response (regex): ${responseText.slice(0, 80)}...`);
4295
+ send({ type: "chunk", text: responseText });
4296
+ fullOutput = responseText;
4297
+ extracted = true;
4298
+ }
4299
+ } catch { /* fall through */ }
4300
+ }
4301
+ if (!extracted) {
4302
+ console.error(`[crew-cli] ❌ Failed to extract response from crew-cli output`);
4303
+ console.error(`[crew-cli] First 200 chars: ${fullOutput.slice(0, 200)}`);
4241
4304
  send({ type: "chunk", text: fullOutput });
4242
4305
  }
4243
4306
  }
@@ -4495,22 +4558,8 @@ export function createAndStartServer(PORT) {
4495
4558
  );
4496
4559
  enabled = cfg.claudeCode === true;
4497
4560
  } catch { }
4498
- const hasKey = !!(
4499
- process.env.ANTHROPIC_API_KEY ||
4500
- (() => {
4501
- try {
4502
- return JSON.parse(
4503
- fs.readFileSync(
4504
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
4505
- "utf8",
4506
- ),
4507
- )?.providers?.anthropic?.apiKey;
4508
- } catch {
4509
- return null;
4510
- }
4511
- })()
4512
- );
4513
- json(res, 200, { ok: true, enabled, hasKey });
4561
+ const hasAuth = isClaudeOauthAuthenticated();
4562
+ json(res, 200, { ok: true, enabled, hasKey: hasAuth, hasAuth });
4514
4563
  return;
4515
4564
  }
4516
4565
  if (req.method === "POST") {
@@ -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);
@@ -41,6 +41,12 @@ export function initPrompts({
41
41
  let _sysPromptCache = null;
42
42
  let _sysPromptKey = "";
43
43
 
44
+ /** Clear the system prompt memoization cache (for test isolation). */
45
+ export function resetPromptCache() {
46
+ _sysPromptCache = null;
47
+ _sysPromptKey = "";
48
+ }
49
+
44
50
  export function buildSystemPrompt(cfg) {
45
51
  // Memoize — only rebuild when config files or agent prompts change
46
52
  const keyParts = [cfg.providerKey, cfg.modelId, cfg.displayName];
@@ -137,7 +143,7 @@ export function buildSystemPrompt(cfg) {
137
143
  "DEFAULT: CHAT. You are a conversational assistant first.",
138
144
  "- Questions, explanations, status, clarifications, follow-ups, 'how does X work', 'what is X', 'can you', 'show me' → ANSWER. Never dispatch.",
139
145
  "- Short messages under ~8 words ('i mean X', 'no, X', 'about X') → corrections, not directives. ANSWER.",
140
- "- Greetings ('hi', 'yo', 'hey') → reply briefly. No health dump unless asked.",
146
+ "- Greetings ('hi', 'yo', 'hey', 'hello', 'sup', 'what's up') → reply with 1-2 SHORT sentences. Do NOT report system status, agent counts, RT bus, skills, or health data. Just be conversational. Only report health/status when the user EXPLICITLY asks ('status', 'health', 'how is the system', 'what's running').",
141
147
  "- Self-audit requests (find bugs, review, inspect) → DO IT YOURSELF with @@READ_FILE / @@RUN_CMD. No asking permission. Only dispatch to crew-qa/crew-coder if user explicitly says 'have X do it'.",
142
148
  "",
143
149
  "DISPATCH: when user gives explicit action language.",
@@ -150,6 +156,13 @@ export function buildSystemPrompt(cfg) {
150
156
  `- 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
157
  "- You MUST emit the @@DISPATCH line. Describing a dispatch in prose without the line = NOTHING happens.",
152
158
  "",
159
+ "ENGINE ROUTING: tasks are routed to CLI engines or direct-llm based on keyword matching.",
160
+ "- 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).",
161
+ "- Tasks WITHOUT coding keywords → direct-llm (LLM API call; can still write files via @@WRITE_FILE markers, but no shell or repo context).",
162
+ "- CLI engines are better for complex multi-file coding. direct-llm is fine for research reports, single-file writes, and simple tasks.",
163
+ "- When dispatching build tasks, use verbs like 'Create', 'Build', 'Implement', 'Fix' to ensure CLI routing.",
164
+ "- This is automatic keyword matching — no LLM call. See docs/ORCHESTRATION-PROTOCOL.md for the full keyword list.",
165
+ "",
153
166
  "PIPELINE: when user wants multi-agent coordinated work.",
154
167
  "- Triggers: 'build me X', 'create X', 'kick off', 'rally the crew', 'dispatch the crew'.",
155
168
  "- 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'.",
@@ -154,10 +154,11 @@ export async function execCrewLeadTools(reply) {
154
154
  } catch { return null; }
155
155
  })();
156
156
 
157
- // Fallback: check search-tools.json
157
+ // Fallback: check search-tools.json (same dir as config file)
158
158
  if (!braveKey) {
159
159
  try {
160
- const searchTools = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".crewswarm", "search-tools.json"), "utf8"));
160
+ const cfgDir = _crewswarmCfgFile ? path.dirname(_crewswarmCfgFile) : path.join(os.homedir(), ".crewswarm");
161
+ const searchTools = JSON.parse(fs.readFileSync(path.join(cfgDir, "search-tools.json"), "utf8"));
161
162
  braveKey = searchTools.brave?.apiKey;
162
163
  } catch {}
163
164
  }
@@ -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 || "",
@@ -525,10 +535,12 @@ export function checkWaveQualityGate(pipeline, pipelineId) {
525
535
  const hasWriteFile = /@@WRITE_FILE/.test(result);
526
536
  const wroteFile = /(?:wrote|created|saved|written|updated|enhanced|implemented|added)\s+(?:to\s+)?(?:\/\S+|[a-zA-Z][\w/.-]+\.(?:html|css|js|ts|py|json|md|txt|sh|yaml|yml))/i.test(result);
527
537
  const opencodeWrote = /(?:←\s*Write|Wrote file|Write\s+\.\.[\w/.-]+\.(?:html|css|js|ts|py|json|md)|Created\s+`\/)/i.test(result);
538
+ // Cursor outputs "`filename` is in place at:" or "Created the file at:\n`/path`"
539
+ const cursorInPlace = /`[\w/.-]+`\s+is\s+in\s+place\s+at|Created\s+the\s+file\s+at:/i.test(result);
528
540
  const explicitDone = /Done\.\s+(?:Created|Updated|Enhanced|Implemented|The\s+(?:file|prototype|component))/i.test(result);
529
541
  // OpenCode agents write silently — check if any src files changed in last 20 min as fallback
530
542
  let opencodeFilesChanged = false;
531
- if (!hasWriteFile && !wroteFile && !opencodeWrote && !explicitDone) {
543
+ if (!hasWriteFile && !wroteFile && !opencodeWrote && !cursorInPlace && !explicitDone) {
532
544
  try {
533
545
  const cutoff = Date.now() - 20 * 60 * 1000;
534
546
  // Use dynamic project directory from pipeline metadata or environment
@@ -555,7 +567,7 @@ export function checkWaveQualityGate(pipeline, pipelineId) {
555
567
  opencodeFilesChanged = dirs.some(checkDir);
556
568
  } catch {}
557
569
  }
558
- if (!hasWriteFile && !wroteFile && !opencodeWrote && !explicitDone && !opencodeFilesChanged) {
570
+ if (!hasWriteFile && !wroteFile && !opencodeWrote && !cursorInPlace && !explicitDone && !opencodeFilesChanged) {
559
571
  issues.push(`${agent} (builder) did not write any files`);
560
572
  }
561
573
  }
@@ -702,8 +714,10 @@ export function dispatchTask(agent, task, sessionId = "owner", pipelineMeta = nu
702
714
  task = _deps.writeTaskBrief?.(agent, task, projectDir) ?? task;
703
715
  }
704
716
 
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}`);
717
+ let rp = typeof _deps.getRtPublish === "function" ? _deps.getRtPublish() : null;
718
+ if (!rp && typeof _deps.getRtPublish === "function") {
719
+ console.warn(`[wave-dispatcher] RT publish not available for ${agent} — will use fallback`);
720
+ }
707
721
  if (rp) {
708
722
  try {
709
723
  // Build extraFlags: start with global settings, override with pipeline-specific settings