crewswarm 0.9.3 → 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 (73) hide show
  1. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
  2. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
  3. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
  4. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
  5. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  6. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  7. package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
  8. package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
  9. package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
  10. package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
  11. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  12. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  13. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  14. package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
  15. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  16. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  17. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  18. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  19. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
  20. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
  21. package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
  22. package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
  23. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  24. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  25. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
  26. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
  27. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
  28. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
  29. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  30. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  31. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  32. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
  33. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
  34. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  35. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  36. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  37. package/apps/dashboard/dist/index.html +56 -7
  38. package/apps/dashboard/dist/index.html.br +0 -0
  39. package/apps/dashboard/dist/index.html.gz +0 -0
  40. package/apps/vibe/server.mjs +98 -53
  41. package/lib/bridges/cli-executor.mjs +1 -1
  42. package/lib/browser/passthrough-stderr.js +1 -0
  43. package/lib/chat/project-messages.mjs +3 -5
  44. package/lib/cli-process-tracker.mjs +3 -2
  45. package/lib/contacts/identity-linker.mjs +1 -0
  46. package/lib/crew-judge/judge.mjs +19 -18
  47. package/lib/crew-lead/agent-manager.mjs +1 -1
  48. package/lib/crew-lead/background.mjs +14 -1
  49. package/lib/crew-lead/chat-handler.mjs +4 -1
  50. package/lib/crew-lead/http-server.mjs +73 -65
  51. package/lib/crew-lead/prompts.mjs +7 -1
  52. package/lib/crew-lead/tools.mjs +3 -2
  53. package/lib/crew-lead/wave-dispatcher.mjs +4 -2
  54. package/lib/engines/crew-cli.mjs +1 -1
  55. package/lib/engines/engine-registry.mjs +11 -9
  56. package/lib/engines/runners.mjs +23 -2
  57. package/lib/gemini-cli-passthrough-noise.mjs +1 -1
  58. package/lib/integrations/code-search.mjs +4 -3
  59. package/lib/memory/shared-adapter.mjs +23 -10
  60. package/lib/pipeline/manager.mjs +2 -1
  61. package/lib/runtime/config.mjs +1 -1
  62. package/lib/runtime/spending.mjs +2 -1
  63. package/package.json +17 -9
  64. package/scripts/dashboard-validation.mjs +2 -0
  65. package/scripts/dashboard.mjs +1110 -484
  66. package/scripts/generate-openapi.mjs +683 -277
  67. package/scripts/restart-service.sh +12 -9
  68. package/apps/dashboard/dist/assets/chat-core-3KirthZA.js +0 -1
  69. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
  70. package/apps/dashboard/dist/assets/index-GSWxxEPO.js +0 -2
  71. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
  72. package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +0 -1
  73. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
@@ -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(
@@ -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";
@@ -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) &&
@@ -3733,7 +3753,7 @@ export function createAndStartServer(PORT) {
3733
3753
  const ocModel =
3734
3754
  reqModel ||
3735
3755
  process.env.CREWSWARM_OPENCODE_MODEL ||
3736
- "groq/moonshotai/kimi-k2-instruct-0905";
3756
+ "openai/gpt-5.4";
3737
3757
  args = ["run", finalMessage, "--model", ocModel];
3738
3758
  // Add workspace directory context
3739
3759
  if (projectDir) args.push("--dir", projectDir);
@@ -3756,7 +3776,7 @@ export function createAndStartServer(PORT) {
3756
3776
 
3757
3777
  if (priorCodexThread) {
3758
3778
  // `codex resume <thread_id> "prompt"` continues the prior session
3759
- args = ["resume", priorCodexThread, finalMessage, "--json"];
3779
+ args = ["resume", priorCodexThread, finalMessage];
3760
3780
  if (codexModel) args.push("--model", codexModel);
3761
3781
  if (projectDir) args.push("-C", projectDir);
3762
3782
  } else {
@@ -3806,10 +3826,10 @@ export function createAndStartServer(PORT) {
3806
3826
  const crewCliModel =
3807
3827
  reqModel || process.env.CREWSWARM_CREW_CLI_MODEL || null;
3808
3828
  if (bin.endsWith(".js")) {
3809
- args = [bin, "chat", finalMessage, "--direct", "--json"];
3829
+ args = [bin, "chat", finalMessage, "--direct", "--json", "--apply"];
3810
3830
  bin = process.execPath;
3811
3831
  } else {
3812
- args = ["chat", finalMessage, "--direct", "--json"];
3832
+ args = ["chat", finalMessage, "--direct", "--json", "--apply"];
3813
3833
  }
3814
3834
  if (crewCliModel) args.push("--model", crewCliModel);
3815
3835
  if (projectDir && projectDir !== process.cwd())
@@ -3921,6 +3941,25 @@ export function createAndStartServer(PORT) {
3921
3941
  }
3922
3942
  }
3923
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
+ }
3924
3963
  // Force L2 planner for crew-cli when requested (enhance-prompt planner path)
3925
3964
  if (reqForceL2) spawnEnv.CREW_FORCE_L2 = "true";
3926
3965
 
@@ -4229,56 +4268,39 @@ export function createAndStartServer(PORT) {
4229
4268
 
4230
4269
  // crew-cli special handling: extract JSON from output (skipping logs) and send only response field
4231
4270
  if (engine === "crew-cli" && fullOutput.trim()) {
4271
+ let extracted = false;
4232
4272
  try {
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;
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;
4241
4285
  }
4242
4286
  }
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;
4287
+ } catch { /* fall through */ }
4288
+ if (!extracted) {
4289
+ // Strategy 2: regex extract just the response field
4249
4290
  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*"((?:[^"\\]|\\.)*)"/);
4291
+ const respMatch = fullOutput.match(/"response":\s*"((?:[^"\\]|\\.)*)"/);
4254
4292
  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");
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;
4258
4298
  }
4259
- }
4260
- if (parsed.response) {
4261
- const responseText = typeof parsed.response === 'string'
4262
- ? parsed.response
4263
- : JSON.stringify(parsed.response, null, 2);
4264
- console.error(
4265
- `[crew-cli] ✅ Extracted response: ${responseText.slice(0, 80)}...`,
4266
- );
4267
- // NOW send the actual response as a chunk
4268
- send({ type: "chunk", text: responseText });
4269
- fullOutput = responseText; // Replace fullOutput for notifications
4270
- } else {
4271
- console.error(`[crew-cli] ⚠️ No response field in JSON`);
4272
- send({ type: "chunk", text: fullOutput }); // Send raw output as fallback
4273
- }
4274
- } catch (err) {
4275
- console.error(
4276
- `[crew-cli] ❌ Failed to extract JSON: ${err.message}`,
4277
- );
4278
- console.error(
4279
- `[crew-cli] First 200 chars: ${fullOutput.slice(0, 200)}`,
4280
- );
4281
- // Send raw output as fallback
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)}`);
4282
4304
  send({ type: "chunk", text: fullOutput });
4283
4305
  }
4284
4306
  }
@@ -4536,22 +4558,8 @@ export function createAndStartServer(PORT) {
4536
4558
  );
4537
4559
  enabled = cfg.claudeCode === true;
4538
4560
  } catch { }
4539
- const hasKey = !!(
4540
- process.env.ANTHROPIC_API_KEY ||
4541
- (() => {
4542
- try {
4543
- return JSON.parse(
4544
- fs.readFileSync(
4545
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
4546
- "utf8",
4547
- ),
4548
- )?.providers?.anthropic?.apiKey;
4549
- } catch {
4550
- return null;
4551
- }
4552
- })()
4553
- );
4554
- json(res, 200, { ok: true, enabled, hasKey });
4561
+ const hasAuth = isClaudeOauthAuthenticated();
4562
+ json(res, 200, { ok: true, enabled, hasKey: hasAuth, hasAuth });
4555
4563
  return;
4556
4564
  }
4557
4565
  if (req.method === "POST") {
@@ -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.",
@@ -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
  }
@@ -535,10 +535,12 @@ export function checkWaveQualityGate(pipeline, pipelineId) {
535
535
  const hasWriteFile = /@@WRITE_FILE/.test(result);
536
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);
537
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);
538
540
  const explicitDone = /Done\.\s+(?:Created|Updated|Enhanced|Implemented|The\s+(?:file|prototype|component))/i.test(result);
539
541
  // OpenCode agents write silently — check if any src files changed in last 20 min as fallback
540
542
  let opencodeFilesChanged = false;
541
- if (!hasWriteFile && !wroteFile && !opencodeWrote && !explicitDone) {
543
+ if (!hasWriteFile && !wroteFile && !opencodeWrote && !cursorInPlace && !explicitDone) {
542
544
  try {
543
545
  const cutoff = Date.now() - 20 * 60 * 1000;
544
546
  // Use dynamic project directory from pipeline metadata or environment
@@ -565,7 +567,7 @@ export function checkWaveQualityGate(pipeline, pipelineId) {
565
567
  opencodeFilesChanged = dirs.some(checkDir);
566
568
  } catch {}
567
569
  }
568
- if (!hasWriteFile && !wroteFile && !opencodeWrote && !explicitDone && !opencodeFilesChanged) {
570
+ if (!hasWriteFile && !wroteFile && !opencodeWrote && !cursorInPlace && !explicitDone && !opencodeFilesChanged) {
569
571
  issues.push(`${agent} (builder) did not write any files`);
570
572
  }
571
573
  }
@@ -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, {
@@ -154,15 +154,9 @@ function evaluateShouldUse(engineDef, payload, incomingType) {
154
154
  * Select engine: payload flags → payload runtime → per-agent crewswarm.json → fallback rules
155
155
  */
156
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
157
  const prompt = payload?.prompt || payload?.task || payload?.message || "";
161
- if (!isCodingTask(incomingType, prompt, payload)) {
162
- return null;
163
- }
164
158
 
165
- // PRIORITY 1: Payload flags (from enriched task)
159
+ // PRIORITY 1: Payload flags (from enriched task) — always respected regardless of prompt content
166
160
  const explicitEngines = [
167
161
  { key: 'useCodex', id: 'codex' },
168
162
  { key: 'useCursor', id: 'cursor' },
@@ -195,7 +189,9 @@ export function selectEngine(payload, incomingType) {
195
189
  }
196
190
  }
197
191
 
198
- // 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".
199
195
  const preferredFromCfg = resolveAgentPreferredEngineId(payload);
200
196
  if (preferredFromCfg) {
201
197
  const preferredEngine = _engines.find((e) => e.id === preferredFromCfg);
@@ -204,7 +200,13 @@ export function selectEngine(payload, incomingType) {
204
200
  }
205
201
  }
206
202
 
207
- // 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
208
210
  const agentId = String(payload?.agentId || payload?.agent || "").toLowerCase();
209
211
  if (agentId && _loadAgentList) {
210
212
  try {
@@ -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 {
@@ -1214,15 +1227,23 @@ export async function runClaudeCodeTask(prompt, payload = {}) {
1214
1227
  if (!which(CLAUDE_CODE_BIN)) {
1215
1228
  throw new Error(`Claude Code CLI not found: "${CLAUDE_CODE_BIN}". Install with: npm i -g @anthropic-ai/claude-code`);
1216
1229
  }
1230
+ if (!isClaudeOauthAuthenticated()) {
1231
+ console.error(`[ClaudeCode:${agentId}] auth preflight reported logged out; continuing and letting Claude decide at runtime.`);
1232
+ }
1217
1233
 
1218
1234
  console.error(`[ClaudeCode:${agentId}] Running: ${CLAUDE_CODE_BIN} -p --dangerously-skip-permissions (cwd=${projectDir})`);
1219
1235
 
1220
1236
  _rtClientForApprovals?.publish({ channel: "events", type: "agent_working", to: "broadcast",
1221
1237
  payload: { agent: agentId, model: model || "claude/auto", ts: Date.now() } });
1222
1238
 
1239
+ const childEnv = { ...process.env };
1240
+ delete childEnv.ANTHROPIC_API_KEY;
1241
+ delete childEnv.CLAUDECODE;
1242
+ delete childEnv.CLAUDE_CODE;
1243
+
1223
1244
  const child = spawn(CLAUDE_CODE_BIN, args, {
1224
1245
  cwd: projectDir,
1225
- env: process.env,
1246
+ env: childEnv,
1226
1247
  stdio: ["ignore", "pipe", "pipe"], // Changed from "pipe" to "ignore" for stdin since we use args
1227
1248
  });
1228
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
 
@@ -6,6 +6,7 @@
6
6
  import fs from "fs";
7
7
  import path from "path";
8
8
  import os from "os";
9
+ import { getConfigPath } from "./paths.mjs";
9
10
 
10
11
  const SPENDING_FILE = path.join(os.homedir(), ".crewswarm", "spending.json");
11
12
  const TOKEN_USAGE_FILE = path.join(os.homedir(), ".crewswarm", "token-usage.json");
@@ -86,7 +87,7 @@ export function addAgentSpend(agentId, tokens, costUSD) {
86
87
 
87
88
  export function checkSpendingCap(agentId, providerKey) {
88
89
  try {
89
- const csw = JSON.parse(fs.readFileSync(getCrewswarmConfigPath(), "utf8"));
90
+ const csw = JSON.parse(fs.readFileSync(getConfigPath("crewswarm.json"), "utf8"));
90
91
  const s = loadSpending();
91
92
  const gl = csw.globalSpendingCaps || {};
92
93
  if (gl.dailyTokenLimit && s.global.tokens >= gl.dailyTokenLimit)