crewswarm 0.9.0 → 0.9.2

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 (84) hide show
  1. package/README.md +2 -2
  2. package/apps/dashboard/dist/assets/{chat-core-CMoqlR6D.js → chat-core-Cx4sTxDd.js} +1 -1
  3. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  4. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  5. package/apps/dashboard/dist/assets/{components-CSUb80ze.js → components-BS9fQjE_.js} +1 -1
  6. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  7. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js +1 -0
  8. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  9. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  10. package/apps/dashboard/dist/assets/{index-DqVVQLTW.js → index-DnClJ1ee.js} +2 -2
  11. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  12. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  13. package/apps/dashboard/dist/assets/{setup-wizard-D4g5DMhW.js → setup-wizard-CA0Or47w.js} +1 -1
  14. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  15. package/apps/dashboard/dist/assets/{tab-agents-tab-BThdsdJY.js → tab-agents-tab-BgpIsjkw.js} +1 -1
  16. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  17. package/apps/dashboard/dist/assets/{tab-benchmarks-tab-DfCuAClu.js → tab-benchmarks-tab-BHjKCPm3.js} +1 -1
  18. package/apps/dashboard/dist/assets/{tab-comms-tab-eHpOSBhG.js → tab-comms-tab-kguqTIzD.js} +1 -1
  19. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  20. package/apps/dashboard/dist/assets/{tab-contacts-tab-5LHSthJM.js → tab-contacts-tab-DiOyMYth.js} +1 -1
  21. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  22. package/apps/dashboard/dist/assets/{tab-engines-tab-C3DYxTwy.js → tab-engines-tab-BsdZVvU0.js} +1 -1
  23. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  24. package/apps/dashboard/dist/assets/{tab-memory-tab-C59BYFQD.js → tab-memory-tab-Cu6u13EQ.js} +1 -1
  25. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  26. package/apps/dashboard/dist/assets/{tab-models-tab-CQzvaeVh.js → tab-models-tab-BLEjmd19.js} +1 -1
  27. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  28. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-D7mnDelU.js → tab-pm-loop-tab-Bfd449B4.js} +1 -1
  29. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  30. package/apps/dashboard/dist/assets/{tab-projects-tab-C6h2Mv1K.js → tab-projects-tab-DhNWnlzt.js} +1 -1
  31. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  32. package/apps/dashboard/dist/assets/{tab-prompts-tab-C0wZvWK3.js → tab-prompts-tab-DVkUNaJd.js} +1 -1
  33. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  34. package/apps/dashboard/dist/assets/{tab-services-tab-DBj_w3bc.js → tab-services-tab-DU_LH3uG.js} +1 -1
  35. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  36. package/apps/dashboard/dist/assets/{tab-settings-tab-ezeqAjZk.js → tab-settings-tab-Bn4nXtDe.js} +1 -1
  37. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  38. package/apps/dashboard/dist/assets/{tab-skills-tab-BYdU2whk.js → tab-skills-tab-BpY0uZHW.js} +1 -1
  39. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  40. package/apps/dashboard/dist/assets/{tab-spending-tab-Bg6w9t_p.js → tab-spending-tab-DEccQHnt.js} +1 -1
  41. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  42. package/apps/dashboard/dist/assets/{tab-swarm-chat-tab-BBV9HB2X.js → tab-swarm-chat-tab-BNrd88-r.js} +1 -1
  43. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  44. package/apps/dashboard/dist/assets/{tab-swarm-tab-ChqLlEVs.js → tab-swarm-tab-B1AcjL1W.js} +1 -1
  45. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  46. package/apps/dashboard/dist/assets/{tab-usage-tab-B2UWXenJ.js → tab-usage-tab-BIOOnB-Y.js} +1 -1
  47. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  48. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  49. package/apps/dashboard/dist/assets/{tab-workflows-tab-6QSXLJ0i.js → tab-workflows-tab-B-soSy1k.js} +1 -1
  50. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  51. package/apps/dashboard/dist/index.html +23 -23
  52. package/apps/dashboard/dist/index.html.br +0 -0
  53. package/apps/dashboard/dist/index.html.gz +0 -0
  54. package/apps/dashboard/index.html +71 -1
  55. package/apps/dashboard/src/app.js +5 -0
  56. package/apps/dashboard/src/core/dom.js +8 -0
  57. package/apps/dashboard/src/tabs/settings-tab.js +58 -0
  58. package/apps/vibe/.crew/agent-memory/pipeline.json +12 -1
  59. package/apps/vibe/.crew/cost.json +3 -3
  60. package/apps/vibe/.crew/json-parse-metrics.jsonl +1 -0
  61. package/apps/vibe/.crew/pipeline-metrics.jsonl +1 -0
  62. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +5 -0
  63. package/apps/vibe/.crew/session.json +10 -1
  64. package/apps/vibe/.studio-data/project-messages/general.jsonl +3 -0
  65. package/apps/vibe/index.html +4 -2
  66. package/apps/vibe/server.mjs +75 -3
  67. package/apps/vibe/src/main.js +126 -53
  68. package/crew-lead.mjs +14 -1
  69. package/lib/bridges/cli-executor.mjs +0 -2
  70. package/lib/bridges/tmux-bridge.mjs +200 -0
  71. package/lib/chat/unified-history.mjs +1 -1
  72. package/lib/cli-process-tracker.mjs +2 -1
  73. package/lib/crew-lead/http-server.mjs +286 -1
  74. package/lib/crew-lead/wave-dispatcher.mjs +40 -3
  75. package/lib/engines/crew-cli.mjs +3 -2
  76. package/lib/engines/llm-direct.mjs +4 -1
  77. package/lib/engines/rt-envelope.mjs +14 -5
  78. package/lib/engines/runners.mjs +30 -4
  79. package/lib/runtime/config.mjs +7 -0
  80. package/lib/sessions/session-manager.mjs +287 -0
  81. package/package.json +1 -1
  82. package/scripts/bench/performance_optimization.py +81 -0
  83. package/whatsapp-bridge.mjs +54 -10
  84. package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js +0 -1
@@ -0,0 +1,200 @@
1
+ /**
2
+ * tmux-bridge adapter — thin wrapper around smux's tmux-bridge CLI
3
+ *
4
+ * Provides agent-to-agent pane communication when running inside a tmux session
5
+ * with smux installed. All functions degrade to no-ops when unavailable.
6
+ *
7
+ * Opt-in: requires $TMUX set, `tmux-bridge` on PATH, and
8
+ * CREWSWARM_TMUX_BRIDGE=1 env var (or tmuxBridge: true in system config).
9
+ */
10
+
11
+ import { execFileSync, execSync } from "node:child_process";
12
+
13
+ function which(bin) {
14
+ try { execSync(`which ${bin}`, { stdio: "ignore" }); return true; } catch { return false; }
15
+ }
16
+
17
+ // ── Detection & caching ─────────────────────────────────────────────────────
18
+
19
+ let _available = null;
20
+ let _ownPaneId = null;
21
+ const _resolveCache = new Map(); // label → { paneId, ts }
22
+ const RESOLVE_TTL_MS = 30_000;
23
+ const TMUX_BRIDGE_BIN = process.env.SMUX_BRIDGE_BIN || "tmux-bridge";
24
+
25
+ function exec(args, { timeout = 5000 } = {}) {
26
+ try {
27
+ return execFileSync(TMUX_BRIDGE_BIN, args, {
28
+ encoding: "utf8",
29
+ timeout,
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ }).trim();
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if tmux-bridge is available and opted-in.
39
+ * Result is cached for the process lifetime.
40
+ */
41
+ export function detect() {
42
+ if (_available !== null) return _available;
43
+
44
+ // Must be inside a tmux session
45
+ if (!process.env.TMUX) {
46
+ _available = false;
47
+ return false;
48
+ }
49
+
50
+ // Must have tmux-bridge binary
51
+ if (!which(TMUX_BRIDGE_BIN)) {
52
+ _available = false;
53
+ return false;
54
+ }
55
+
56
+ // Must be opted-in via env var or config
57
+ const envFlag = process.env.CREWSWARM_TMUX_BRIDGE;
58
+ if (!envFlag || envFlag === "0" || envFlag === "false") {
59
+ _available = false;
60
+ return false;
61
+ }
62
+
63
+ _available = true;
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Get this process's own tmux pane ID.
69
+ * @returns {string|null} e.g. "%3"
70
+ */
71
+ export function id() {
72
+ if (!detect()) return null;
73
+ if (_ownPaneId) return _ownPaneId;
74
+ _ownPaneId = exec(["id"]);
75
+ return _ownPaneId;
76
+ }
77
+
78
+ /**
79
+ * Label a pane with an agent ID so other agents can discover it.
80
+ * @param {string} agentId - CrewSwarm agent ID (e.g. "crew-coder")
81
+ * @param {string} [paneId] - Target pane ID. Defaults to own pane.
82
+ * @returns {boolean} success
83
+ */
84
+ export function label(agentId, paneId) {
85
+ if (!detect()) return false;
86
+ const target = paneId || id();
87
+ if (!target) return false;
88
+ const result = exec(["name", target, agentId]);
89
+ if (result !== null) {
90
+ // Update cache
91
+ _resolveCache.set(agentId, { paneId: target, ts: Date.now() });
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ /**
98
+ * Resolve an agent ID to a tmux pane ID.
99
+ * @param {string} agentId
100
+ * @returns {string|null} pane ID or null
101
+ */
102
+ export function resolve(agentId) {
103
+ if (!detect()) return null;
104
+
105
+ // Check cache
106
+ const cached = _resolveCache.get(agentId);
107
+ if (cached && (Date.now() - cached.ts) < RESOLVE_TTL_MS) {
108
+ return cached.paneId;
109
+ }
110
+
111
+ const paneId = exec(["resolve", agentId]);
112
+ if (paneId) {
113
+ _resolveCache.set(agentId, { paneId, ts: Date.now() });
114
+ }
115
+ return paneId;
116
+ }
117
+
118
+ /**
119
+ * Read the last N lines from an agent's pane.
120
+ * Also satisfies the read-guard requirement for subsequent sends.
121
+ * @param {string} agentId
122
+ * @param {number} [lines=50]
123
+ * @returns {string|null} pane content or null
124
+ */
125
+ export function read(agentId, lines = 50) {
126
+ if (!detect()) return null;
127
+ const paneId = resolve(agentId);
128
+ if (!paneId) return null;
129
+ const output = exec(["read", paneId, String(lines)]);
130
+ return output;
131
+ }
132
+
133
+ /**
134
+ * Send text to an agent's pane followed by Enter.
135
+ * Reads the pane before each write command to satisfy tmux-bridge's read-guard
136
+ * (the guard is consumed on every type/keys call).
137
+ * @param {string} agentId
138
+ * @param {string} text
139
+ * @returns {boolean} success
140
+ */
141
+ export function send(agentId, text) {
142
+ if (!detect()) return false;
143
+ const paneId = resolve(agentId);
144
+ if (!paneId) return false;
145
+
146
+ // Read-guard is consumed on each type/keys call, so read before each one
147
+ exec(["read", paneId, "1"]);
148
+ const typeResult = exec(["type", paneId, text]);
149
+ if (typeResult === null) return false;
150
+
151
+ exec(["read", paneId, "1"]);
152
+ const keyResult = exec(["keys", paneId, "Enter"]);
153
+ return keyResult !== null;
154
+ }
155
+
156
+ /**
157
+ * List all tmux panes with their labels and metadata.
158
+ * @returns {Array<{paneId: string, label: string, raw: string}>}
159
+ */
160
+ export function list() {
161
+ if (!detect()) return [];
162
+ const raw = exec(["list"]);
163
+ if (!raw) return [];
164
+
165
+ // Parse tmux-bridge list output:
166
+ // TARGET SESSION:WIN SIZE PROCESS LABEL CWD
167
+ // %0 crewtest:0 120x29 -zsh crew-coder ~/CrewSwarm
168
+ const lines = raw.split("\n").filter(Boolean);
169
+ // Skip header row (starts with "TARGET")
170
+ return lines
171
+ .filter(line => line.trimStart().startsWith("%"))
172
+ .map(line => {
173
+ const parts = line.split(/\s+/).filter(Boolean);
174
+ return {
175
+ paneId: parts[0] || "",
176
+ session: parts[1] || "",
177
+ size: parts[2] || "",
178
+ process: parts[3] || "",
179
+ label: (parts[4] && parts[4] !== "-") ? parts[4] : "",
180
+ cwd: parts[5] || "",
181
+ raw: line,
182
+ };
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Clear the resolve cache (useful after pane layout changes).
188
+ */
189
+ export function clearCache() {
190
+ _resolveCache.clear();
191
+ }
192
+
193
+ /**
194
+ * Reset detection state (for testing).
195
+ */
196
+ export function _reset() {
197
+ _available = null;
198
+ _ownPaneId = null;
199
+ clearCache();
200
+ }
@@ -66,7 +66,7 @@ export function loadUnifiedHistory(contactId, maxMessages = 2000) {
66
66
  const files = readdirSync(telegramDir);
67
67
 
68
68
  // Find all topic files for this chat
69
- // Format: -1003624332545-topic-20.jsonl, -1003624332545-topic-94.jsonl, etc.
69
+ // Format: -100XXXXXXXXXX-topic-20.jsonl, -100XXXXXXXXXX-topic-94.jsonl, etc.
70
70
  const topicPattern = new RegExp(`^-?\\d+-topic-\\d+\\.jsonl$`);
71
71
  const topicFiles = files.filter(f => topicPattern.test(f));
72
72
 
@@ -160,7 +160,8 @@ export function getActiveProcesses() {
160
160
  idleFor: now - proc.lastActivity,
161
161
  outputLines: proc.outputLines,
162
162
  chatId: proc.chatId,
163
- sessionId: proc.sessionId
163
+ sessionId: proc.sessionId,
164
+ tmuxSessionId: proc.tmuxSessionId || null
164
165
  }));
165
166
  }
166
167
 
@@ -1087,6 +1087,7 @@ export function createAndStartServer(PORT) {
1087
1087
  bgConsciousnessIntervalMs,
1088
1088
  cursorWavesRef,
1089
1089
  claudeCodeRef,
1090
+ tmuxBridgeRef,
1090
1091
  } = _deps;
1091
1092
 
1092
1093
  // Debug: verify critical deps
@@ -1208,7 +1209,7 @@ export function createAndStartServer(PORT) {
1208
1209
  projectId = body.projectId || url.searchParams.get("projectId"); // Support query param too
1209
1210
  projectDir = body.projectDir || null;
1210
1211
  userId = body.userId || "default";
1211
- targetAgent = body.targetAgent; // Per-user routing from WhatsApp/Telegram
1212
+ targetAgent = body.targetAgent || body.agentId || null; // Per-user routing from WhatsApp/Telegram/Dashboard
1212
1213
  channelMode = body.channelMode === true;
1213
1214
  } catch (e) {
1214
1215
  json(res, 400, {
@@ -4580,6 +4581,46 @@ export function createAndStartServer(PORT) {
4580
4581
  }
4581
4582
  }
4582
4583
 
4584
+ // GET/POST /api/settings/tmux-bridge — toggle tmux-bridge session layer at runtime
4585
+ if (url.pathname === "/api/settings/tmux-bridge") {
4586
+ if (!checkBearer(req)) {
4587
+ json(res, 401, { ok: false, error: "Unauthorized" });
4588
+ return;
4589
+ }
4590
+ if (req.method === "GET") {
4591
+ json(res, 200, { ok: true, enabled: tmuxBridgeRef?.enabled ?? false });
4592
+ return;
4593
+ }
4594
+ if (req.method === "POST") {
4595
+ const body = await readBody(req);
4596
+ const enable =
4597
+ typeof body.enabled === "boolean"
4598
+ ? body.enabled
4599
+ : !(tmuxBridgeRef?.enabled ?? false);
4600
+ if (tmuxBridgeRef) tmuxBridgeRef.enabled = enable;
4601
+ try {
4602
+ const cfgPath = path.join(
4603
+ os.homedir(),
4604
+ ".crewswarm",
4605
+ "crewswarm.json",
4606
+ );
4607
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
4608
+ cfg.tmuxBridge = enable;
4609
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
4610
+ } catch (e) {
4611
+ console.warn(
4612
+ "[crew-lead] Could not persist tmuxBridge:",
4613
+ e.message,
4614
+ );
4615
+ }
4616
+ console.log(
4617
+ `[crew-lead] tmux-bridge ${enable ? "ENABLED" : "DISABLED"} via dashboard`,
4618
+ );
4619
+ json(res, 200, { ok: true, enabled: tmuxBridgeRef?.enabled ?? enable });
4620
+ return;
4621
+ }
4622
+ }
4623
+
4583
4624
  // GET/POST /api/settings/global-fallback — set/get global OpenCode fallback model
4584
4625
  if (url.pathname === "/api/settings/global-fallback") {
4585
4626
  if (!checkBearer(req)) {
@@ -4680,6 +4721,250 @@ export function createAndStartServer(PORT) {
4680
4721
  }
4681
4722
  }
4682
4723
 
4724
+ // ── Missing settings endpoints (bulk implementation) ─────────────────────
4725
+
4726
+ // Helper: read/write crewswarm.json for simple boolean/string settings
4727
+ const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
4728
+ function readCfg() {
4729
+ try { return JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch { return {}; }
4730
+ }
4731
+ function writeCfg(cfg) {
4732
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
4733
+ }
4734
+ function whichBin(bin) {
4735
+ try {
4736
+ const r = spawn("which", [bin], { stdio: "ignore" });
4737
+ // spawn is async but we can check if the binary exists via fs
4738
+ const paths = (process.env.PATH || "").split(":");
4739
+ return paths.some(p => { try { fs.accessSync(path.join(p, bin), fs.constants.X_OK); return true; } catch { return false; } });
4740
+ } catch { return false; }
4741
+ }
4742
+
4743
+ // GET/POST /api/settings/autonomous-mentions
4744
+ if (url.pathname === "/api/settings/autonomous-mentions") {
4745
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4746
+ if (req.method === "GET") {
4747
+ const cfg = readCfg();
4748
+ const enabled = cfg.settings?.autonomousMentionsEnabled !== false;
4749
+ json(res, 200, { ok: true, enabled });
4750
+ return;
4751
+ }
4752
+ if (req.method === "POST") {
4753
+ const body = await readBody(req);
4754
+ const cfg = readCfg();
4755
+ if (!cfg.settings) cfg.settings = {};
4756
+ cfg.settings.autonomousMentionsEnabled = typeof body.enabled === "boolean" ? body.enabled : true;
4757
+ writeCfg(cfg);
4758
+ console.log(`[crew-lead] Autonomous mentions ${cfg.settings.autonomousMentionsEnabled ? "ENABLED" : "DISABLED"} via dashboard`);
4759
+ json(res, 200, { ok: true, enabled: cfg.settings.autonomousMentionsEnabled });
4760
+ return;
4761
+ }
4762
+ }
4763
+
4764
+ // GET/POST /api/settings/codex
4765
+ if (url.pathname === "/api/settings/codex") {
4766
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4767
+ if (req.method === "GET") {
4768
+ const cfg = readCfg();
4769
+ const enabled = cfg.codexEnabled === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_CODEX_ENABLED || ""));
4770
+ json(res, 200, { ok: true, enabled });
4771
+ return;
4772
+ }
4773
+ if (req.method === "POST") {
4774
+ const body = await readBody(req);
4775
+ const cfg = readCfg();
4776
+ const enable = typeof body.enabled === "boolean" ? body.enabled : !cfg.codexEnabled;
4777
+ cfg.codexEnabled = enable;
4778
+ writeCfg(cfg);
4779
+ console.log(`[crew-lead] Codex executor ${enable ? "ENABLED" : "DISABLED"} via dashboard`);
4780
+ json(res, 200, { ok: true, enabled: enable });
4781
+ return;
4782
+ }
4783
+ }
4784
+
4785
+ // GET/POST /api/settings/gemini-cli
4786
+ if (url.pathname === "/api/settings/gemini-cli") {
4787
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4788
+ if (req.method === "GET") {
4789
+ const cfg = readCfg();
4790
+ const enabled = cfg.geminiCliEnabled === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_GEMINI_CLI_ENABLED || ""));
4791
+ const installed = whichBin("gemini");
4792
+ json(res, 200, { ok: true, enabled, installed });
4793
+ return;
4794
+ }
4795
+ if (req.method === "POST") {
4796
+ const body = await readBody(req);
4797
+ const cfg = readCfg();
4798
+ const enable = typeof body.enabled === "boolean" ? body.enabled : !cfg.geminiCliEnabled;
4799
+ cfg.geminiCliEnabled = enable;
4800
+ writeCfg(cfg);
4801
+ console.log(`[crew-lead] Gemini CLI ${enable ? "ENABLED" : "DISABLED"} via dashboard`);
4802
+ json(res, 200, { ok: true, enabled: enable, installed: whichBin("gemini") });
4803
+ return;
4804
+ }
4805
+ }
4806
+
4807
+ // GET/POST /api/settings/crew-cli
4808
+ if (url.pathname === "/api/settings/crew-cli") {
4809
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4810
+ if (req.method === "GET") {
4811
+ const cfg = readCfg();
4812
+ const enabled = cfg.crewCliEnabled === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_CREW_CLI_ENABLED || ""));
4813
+ json(res, 200, { ok: true, enabled });
4814
+ return;
4815
+ }
4816
+ if (req.method === "POST") {
4817
+ const body = await readBody(req);
4818
+ const cfg = readCfg();
4819
+ const enable = typeof body.enabled === "boolean" ? body.enabled : !cfg.crewCliEnabled;
4820
+ cfg.crewCliEnabled = enable;
4821
+ writeCfg(cfg);
4822
+ console.log(`[crew-lead] Crew CLI ${enable ? "ENABLED" : "DISABLED"} via dashboard`);
4823
+ json(res, 200, { ok: true, enabled: enable });
4824
+ return;
4825
+ }
4826
+ }
4827
+
4828
+ // GET/POST /api/settings/opencode
4829
+ if (url.pathname === "/api/settings/opencode") {
4830
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4831
+ if (req.method === "GET") {
4832
+ const cfg = readCfg();
4833
+ const enabled = cfg.opencodeEnabled === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_OPENCODE_ENABLED || ""));
4834
+ const installed = whichBin("opencode");
4835
+ json(res, 200, { ok: true, enabled, installed });
4836
+ return;
4837
+ }
4838
+ if (req.method === "POST") {
4839
+ const body = await readBody(req);
4840
+ const cfg = readCfg();
4841
+ const enable = typeof body.enabled === "boolean" ? body.enabled : !cfg.opencodeEnabled;
4842
+ cfg.opencodeEnabled = enable;
4843
+ writeCfg(cfg);
4844
+ console.log(`[crew-lead] OpenCode ${enable ? "ENABLED" : "DISABLED"} via dashboard`);
4845
+ json(res, 200, { ok: true, enabled: enable, installed: whichBin("opencode") });
4846
+ return;
4847
+ }
4848
+ }
4849
+
4850
+ // GET/POST /api/settings/global-oc-loop
4851
+ if (url.pathname === "/api/settings/global-oc-loop") {
4852
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4853
+ if (req.method === "GET") {
4854
+ const cfg = readCfg();
4855
+ const enabled = cfg.engineLoop === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_ENGINE_LOOP || ""));
4856
+ const maxRounds = cfg.engineLoopMaxRounds ?? parseInt(process.env.CREWSWARM_ENGINE_LOOP_MAX_ROUNDS || "10", 10);
4857
+ json(res, 200, { ok: true, enabled, maxRounds });
4858
+ return;
4859
+ }
4860
+ if (req.method === "POST") {
4861
+ const body = await readBody(req);
4862
+ const cfg = readCfg();
4863
+ if (typeof body.enabled === "boolean") cfg.engineLoop = body.enabled;
4864
+ if (body.maxRounds !== undefined) cfg.engineLoopMaxRounds = parseInt(body.maxRounds, 10) || 10;
4865
+ writeCfg(cfg);
4866
+ console.log(`[crew-lead] Engine loop: enabled=${cfg.engineLoop ?? false}, maxRounds=${cfg.engineLoopMaxRounds ?? 10}`);
4867
+ json(res, 200, { ok: true, enabled: cfg.engineLoop ?? false, maxRounds: cfg.engineLoopMaxRounds ?? 10 });
4868
+ return;
4869
+ }
4870
+ }
4871
+
4872
+ // GET/POST /api/settings/passthrough-notify
4873
+ if (url.pathname === "/api/settings/passthrough-notify") {
4874
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4875
+ if (req.method === "GET") {
4876
+ const cfg = readCfg();
4877
+ json(res, 200, { ok: true, value: cfg.passthroughNotify || "both" });
4878
+ return;
4879
+ }
4880
+ if (req.method === "POST") {
4881
+ const body = await readBody(req);
4882
+ const cfg = readCfg();
4883
+ cfg.passthroughNotify = body.value || "both";
4884
+ writeCfg(cfg);
4885
+ console.log(`[crew-lead] Passthrough notify → ${cfg.passthroughNotify}`);
4886
+ json(res, 200, { ok: true, value: cfg.passthroughNotify });
4887
+ return;
4888
+ }
4889
+ }
4890
+
4891
+ // GET/POST /api/settings/loop-brain
4892
+ if (url.pathname === "/api/settings/loop-brain") {
4893
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4894
+ if (req.method === "GET") {
4895
+ const cfg = readCfg();
4896
+ json(res, 200, { ok: true, loopBrain: cfg.loopBrain || "" });
4897
+ return;
4898
+ }
4899
+ if (req.method === "POST") {
4900
+ const body = await readBody(req);
4901
+ const cfg = readCfg();
4902
+ cfg.loopBrain = body.loopBrain || "";
4903
+ writeCfg(cfg);
4904
+ console.log(`[crew-lead] Loop brain → ${cfg.loopBrain || "(cleared)"}`);
4905
+ json(res, 200, { ok: true, loopBrain: cfg.loopBrain });
4906
+ return;
4907
+ }
4908
+ }
4909
+
4910
+ // GET /api/settings/openclaw-status
4911
+ if (url.pathname === "/api/settings/openclaw-status" && req.method === "GET") {
4912
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4913
+ const cfg = readCfg();
4914
+ const installed = !!(cfg.openClaw?.enabled || cfg.openclaw?.gatewayUrl || process.env.OPENCLAW_GATEWAY_URL);
4915
+ json(res, 200, { ok: true, installed });
4916
+ return;
4917
+ }
4918
+
4919
+ // GET/POST /api/settings/rt-token
4920
+ if (url.pathname === "/api/settings/rt-token") {
4921
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4922
+ if (req.method === "GET") {
4923
+ const cfg = readCfg();
4924
+ const token = cfg.rtToken || process.env.CREWSWARM_RT_AUTH_TOKEN || "";
4925
+ json(res, 200, { ok: true, token: token ? true : false });
4926
+ return;
4927
+ }
4928
+ if (req.method === "POST") {
4929
+ const body = await readBody(req);
4930
+ if (!body.token) { json(res, 400, { ok: false, error: "No token provided" }); return; }
4931
+ const cfg = readCfg();
4932
+ cfg.rtToken = body.token;
4933
+ writeCfg(cfg);
4934
+ console.log("[crew-lead] RT token saved via dashboard");
4935
+ json(res, 200, { ok: true, saved: true });
4936
+ return;
4937
+ }
4938
+ }
4939
+
4940
+ // GET /api/config/lock-status, POST /api/config/lock, POST /api/config/unlock
4941
+ if (url.pathname === "/api/config/lock-status" && req.method === "GET") {
4942
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4943
+ const lockFile = path.join(os.homedir(), ".crewswarm", ".config.lock");
4944
+ json(res, 200, { ok: true, locked: fs.existsSync(lockFile) });
4945
+ return;
4946
+ }
4947
+ if (url.pathname === "/api/config/lock" && req.method === "POST") {
4948
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4949
+ const lockFile = path.join(os.homedir(), ".crewswarm", ".config.lock");
4950
+ try {
4951
+ fs.writeFileSync(lockFile, new Date().toISOString());
4952
+ console.log("[crew-lead] Config LOCKED via dashboard");
4953
+ json(res, 200, { ok: true, locked: true });
4954
+ } catch (e) { json(res, 500, { ok: false, error: e.message }); }
4955
+ return;
4956
+ }
4957
+ if (url.pathname === "/api/config/unlock" && req.method === "POST") {
4958
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4959
+ const lockFile = path.join(os.homedir(), ".crewswarm", ".config.lock");
4960
+ try {
4961
+ fs.unlinkSync(lockFile);
4962
+ console.log("[crew-lead] Config UNLOCKED via dashboard");
4963
+ } catch {}
4964
+ json(res, 200, { ok: true, locked: false });
4965
+ return;
4966
+ }
4967
+
4683
4968
  // POST /api/spending/reset — reset today's spending counters
4684
4969
  if (url.pathname === "/api/spending/reset" && req.method === "POST") {
4685
4970
  if (!checkBearer(req)) {
@@ -11,6 +11,8 @@ import { randomUUID } from "node:crypto";
11
11
  import { getStatePath, getConfigPath } from "../runtime/paths.mjs";
12
12
  import { normalizeProjectDir } from "../runtime/project-dir.mjs";
13
13
  import { loadProjectMessages } from "../chat/project-messages.mjs";
14
+ import * as tmuxBridge from "../bridges/tmux-bridge.mjs";
15
+ import * as sessionManager from "../sessions/session-manager.mjs";
14
16
 
15
17
  let _deps = {};
16
18
 
@@ -397,12 +399,11 @@ export function dispatchPipelineWave(pipelineId) {
397
399
  `Fan out all ${waveSteps.length} tasks simultaneously and return combined results.`,
398
400
  ].join("\n");
399
401
 
400
- console.log(`[crew-lead] CURSOR_WAVES: routing wave ${currentWave + 1} through crew-orchestrator (${waveSteps.length} parallel tasks)`);
401
- const taskId = dispatchTask("crew-orchestrator", { task: orchestratorTask, runtime: "cursor-cli" }, sessionId, {
402
+ console.log(`[crew-lead] WAVE_DISPATCH: routing wave ${currentWave + 1} through crew-orchestrator (${waveSteps.length} parallel tasks)`);
403
+ const taskId = dispatchTask("crew-orchestrator", { task: orchestratorTask }, sessionId, {
402
404
  pipelineId,
403
405
  waveIndex: currentWave,
404
406
  projectDir: pipeline.projectDir,
405
- useCursorCli: true,
406
407
  originProjectId: pipeline.originProjectId,
407
408
  originChannel: pipeline.originChannel,
408
409
  originThreadId: pipeline.originThreadId,
@@ -426,6 +427,11 @@ export function dispatchPipelineWave(pipelineId) {
426
427
  ...(step.verify ? { verify: step.verify } : {}),
427
428
  ...(step.done ? { done: step.done } : {}),
428
429
  };
430
+ // Build session handoff metadata for tmux-bridge
431
+ const sessionMeta = {};
432
+ if (step.session) sessionMeta.session = step.session;
433
+ else if (pipeline._tmuxSessionId) sessionMeta.session = `handoff:${pipeline._tmuxSessionId}`;
434
+
429
435
  const taskId = dispatchTask(step.agent, stepSpec, sessionId, {
430
436
  pipelineId,
431
437
  waveIndex: currentWave,
@@ -435,6 +441,7 @@ export function dispatchPipelineWave(pipelineId) {
435
441
  originThreadId: pipeline.originThreadId,
436
442
  originMessageId: pipeline.originMessageId,
437
443
  triggeredBy: pipeline.triggeredBy || "pipeline",
444
+ ...sessionMeta,
438
445
  });
439
446
  if (taskId && taskId !== true) pipeline.pendingTaskIds.add(taskId);
440
447
  }
@@ -763,6 +770,36 @@ export function dispatchTask(agent, task, sessionId = "owner", pipelineMeta = nu
763
770
  if (pipelineMeta?.mentionedBy) extraFlags.mentionedBy = pipelineMeta.mentionedBy;
764
771
  if (pipelineMeta?.autonomous !== undefined) extraFlags.autonomous = pipelineMeta.autonomous;
765
772
 
773
+ // ── tmux session handoff ──────────────────────────────────────────────
774
+ // If pipelineMeta carries a tmux session, hand it off to this agent
775
+ // or create a new one if session: "persist" is set.
776
+ if (tmuxBridge.detect()) {
777
+ const sessionSpec = pipelineMeta?.session;
778
+ if (typeof sessionSpec === "string" && sessionSpec.startsWith("handoff:")) {
779
+ const existingSessionId = sessionSpec.slice("handoff:".length);
780
+ const prevOwner = sessionManager.getSession(existingSessionId)?.owner;
781
+ if (prevOwner) {
782
+ sessionManager.handoff(existingSessionId, prevOwner, agent);
783
+ }
784
+ const meta = sessionManager.getSession(existingSessionId);
785
+ if (meta?.paneId) extraFlags.tmuxSessionId = meta.paneId;
786
+ } else if (sessionSpec === "persist") {
787
+ const newSessionId = sessionManager.create({
788
+ workspaceId: pipelineMeta?.pipelineId || "default",
789
+ agentId: agent,
790
+ cwd: pipelineMeta?.projectDir || undefined,
791
+ });
792
+ if (newSessionId) {
793
+ const meta = sessionManager.getSession(newSessionId);
794
+ if (meta?.paneId) extraFlags.tmuxSessionId = meta.paneId;
795
+ // Store session ID on pipeline meta so next wave can handoff
796
+ if (pipelineMeta) pipelineMeta._tmuxSessionId = newSessionId;
797
+ }
798
+ } else if (pipelineMeta?.tmuxSessionId) {
799
+ extraFlags.tmuxSessionId = pipelineMeta.tmuxSessionId;
800
+ }
801
+ }
802
+
766
803
  // Log enrichment for verification
767
804
  const engineFlags = Object.keys(extraFlags).filter(k => k.startsWith('use') || k.includes('Model') || k === 'engine');
768
805
  if (engineFlags.length > 0) {
@@ -71,11 +71,12 @@ export async function runCrewCLITask(prompt, payload = {}) {
71
71
  ""
72
72
  );
73
73
 
74
- // Resolve project directory
75
- const projectDir =
74
+ // Resolve project directory — must be a string (Sandbox requires it)
75
+ const rawDir =
76
76
  payload?.projectDir ||
77
77
  (getOpencodeProjectDir ? getOpencodeProjectDir() : null) ||
78
78
  process.cwd();
79
+ const projectDir = typeof rawDir === "string" && rawDir.trim() ? rawDir.trim() : process.cwd();
79
80
 
80
81
  // Ensure API keys are in env
81
82
  const providers = loadProviders();
@@ -10,7 +10,10 @@ export async function callLLMDirect(prompt, ocAgentId, systemPrompt) {
10
10
  recordTokenUsage, loadProviderMap,
11
11
  } = _deps;
12
12
  const llm = loadAgentLLMConfig(ocAgentId);
13
- if (!llm) return null; // fall through to legacy gateway
13
+ if (!llm) {
14
+ console.error(`[llm-direct] loadAgentLLMConfig("${ocAgentId}") returned null. _deps set: ${!!_deps.loadAgentLLMConfig}. CREWSWARM_RT_AGENT=${process.env.CREWSWARM_RT_AGENT}`);
15
+ return null; // fall through to legacy gateway
16
+ }
14
17
 
15
18
  // ── Spending cap pre-check ─────────────────────────────────────────────────
16
19
  const capResult = checkSpendingCap(ocAgentId, llm.providerKey || llm.modelId.split("/")[0]);
@@ -908,19 +908,28 @@ export async function handleRealtimeEnvelope(envelope, client, bridge) {
908
908
  } else {
909
909
  // ── No coding engine assigned — use direct LLM (agent's configured model) ──
910
910
  engineUsed = "direct-llm";
911
- const ocAgentId = CREWSWARM_RT_AGENT;
912
- const agentSysPrompt = loadAgentPrompts()[ocAgentId] || null;
913
- const agentCfg = getAgentOpenCodeConfig(CREWSWARM_RT_AGENT);
911
+ // Use the DISPATCHED agent's ID, not the gateway's own identity.
912
+ // When a gateway process handles a task for crew-researcher, it needs
913
+ // crew-researcher's model config (perplexity/sonar), not its own.
914
+ // The target agent is in envelope.to, payload.agentId, or payload.agent.
915
+ const dispatchedAgent = String(
916
+ payload?.agentId || payload?.agent || to || ""
917
+ ).trim();
918
+ const ocAgentId = (dispatchedAgent && dispatchedAgent !== "broadcast")
919
+ ? dispatchedAgent
920
+ : CREWSWARM_RT_AGENT;
921
+ const agentSysPrompt = loadAgentPrompts()[ocAgentId] || loadAgentPrompts()[CREWSWARM_RT_AGENT] || null;
922
+ const agentCfg = getAgentOpenCodeConfig(ocAgentId);
914
923
  modelUsed = agentCfg?.model || payload?.model || "unknown";
915
924
 
916
925
  console.error(
917
926
  `[${CREWSWARM_RT_AGENT}] 🧠 Direct LLM route (no coding engine): agent=${ocAgentId}, model=${modelUsed}`,
918
927
  );
919
- progress(`Routing to direct LLM (no coding engine assigned)...`);
928
+ progress(`Routing to direct LLM (${ocAgentId} ${modelUsed})...`);
920
929
  telemetry("realtime_route_direct_llm", {
921
930
  taskId,
922
931
  incomingType,
923
- agent: CREWSWARM_RT_AGENT,
932
+ agent: ocAgentId,
924
933
  model: modelUsed,
925
934
  });
926
935