crewswarm 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +22 -9
  2. package/apps/dashboard/dist/assets/{chat-core-Cx4sTxDd.js → chat-core-3KirthZA.js} +1 -1
  3. package/apps/dashboard/dist/assets/index-GSWxxEPO.js +2 -0
  4. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
  5. package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
  6. package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +1 -0
  7. package/apps/dashboard/dist/index.html +82 -11
  8. package/apps/vibe/README.md +2 -2
  9. package/apps/vibe/package.json +1 -1
  10. package/apps/vibe/server.mjs +3 -3
  11. package/crew-lead.mjs +48 -5
  12. package/lib/bridges/gateway-ws.mjs +4 -0
  13. package/lib/bridges/tmux-bridge.mjs +200 -0
  14. package/lib/cli-process-tracker.mjs +2 -1
  15. package/lib/crew-lead/chat-handler.mjs +34 -0
  16. package/lib/crew-lead/http-server.mjs +340 -14
  17. package/lib/crew-lead/llm-caller.mjs +24 -8
  18. package/lib/crew-lead/prompts.mjs +7 -0
  19. package/lib/crew-lead/wave-dispatcher.mjs +53 -3
  20. package/lib/crew-lead/ws-router.mjs +219 -27
  21. package/lib/engines/engine-registry.mjs +9 -0
  22. package/lib/engines/rt-envelope.mjs +1 -0
  23. package/lib/engines/runners.mjs +26 -2
  24. package/lib/runtime/config.mjs +7 -0
  25. package/lib/runtime/paths.mjs +12 -8
  26. package/lib/sessions/session-manager.mjs +287 -0
  27. package/package.json +35 -15
  28. package/scripts/capture-build-flow.mjs +118 -0
  29. package/scripts/coverage-report.mjs +209 -0
  30. package/scripts/coverage-summary.mjs +47 -0
  31. package/scripts/dashboard-validation.mjs +74 -0
  32. package/scripts/dashboard.mjs +560 -70
  33. package/scripts/live-bridge-matrix.mjs +79 -0
  34. package/scripts/live-cli-matrix.mjs +166 -0
  35. package/scripts/live-crewchat-check.mjs +42 -0
  36. package/scripts/live-engine-matrix.mjs +50 -0
  37. package/scripts/live-provider-failover-matrix.mjs +107 -0
  38. package/scripts/live-provider-matrix.mjs +228 -0
  39. package/scripts/restart-all-from-repo.sh +4 -4
  40. package/scripts/smoke-dispatch.mjs +4 -1
  41. package/scripts/test-blast-radius.mjs +204 -0
  42. package/scripts/test-report-summary.mjs +88 -0
  43. package/scripts/test-reporter.mjs +651 -0
  44. package/scripts/test-rerun.mjs +136 -0
  45. package/scripts/tmux-bridge +130 -0
  46. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  47. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  48. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  49. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  50. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  51. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  52. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  53. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  54. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  56. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  60. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  62. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  64. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  65. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  66. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  67. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  68. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  69. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  70. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  71. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  72. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  73. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  74. package/apps/dashboard/dist/index.html.br +0 -0
  75. package/apps/dashboard/index.html +0 -6459
  76. package/apps/dashboard/package.json +0 -15
  77. package/apps/dashboard/src/app.js +0 -2823
  78. package/apps/dashboard/src/app.js.br +0 -0
  79. package/apps/dashboard/src/app.js.gz +0 -0
  80. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  81. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  82. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  83. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  84. package/apps/dashboard/src/cli-process.js +0 -208
  85. package/apps/dashboard/src/cli-process.js.br +0 -0
  86. package/apps/dashboard/src/cli-process.js.gz +0 -0
  87. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  88. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  89. package/apps/dashboard/src/core/api.js +0 -18
  90. package/apps/dashboard/src/core/api.js.br +0 -0
  91. package/apps/dashboard/src/core/dom.js +0 -228
  92. package/apps/dashboard/src/core/dom.js.br +0 -0
  93. package/apps/dashboard/src/core/state.js +0 -91
  94. package/apps/dashboard/src/core/state.js.br +0 -0
  95. package/apps/dashboard/src/core/task-manager.js +0 -134
  96. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  97. package/apps/dashboard/src/orchestration-status.js +0 -127
  98. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  99. package/apps/dashboard/src/setup-wizard.js +0 -562
  100. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  101. package/apps/dashboard/src/styles.css +0 -2085
  102. package/apps/dashboard/src/styles.css.br +0 -0
  103. package/apps/dashboard/src/styles.css.gz +0 -0
  104. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  105. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  106. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  107. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  108. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  109. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  110. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  111. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  112. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  113. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  114. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  115. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  116. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  117. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  118. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  119. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  120. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  121. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  122. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  123. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  124. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  125. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  126. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  127. package/apps/dashboard/src/tabs/settings-tab.js +0 -803
  128. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  129. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  130. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  131. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  132. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  133. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  134. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  135. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  136. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  137. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  138. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  139. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  140. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  141. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  142. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  143. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  144. package/apps/vibe/.crew/cost.json +0 -17
  145. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  146. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  147. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  148. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  149. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  150. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  151. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  152. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  153. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  154. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  155. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  156. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  157. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  158. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  159. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  160. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  161. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  162. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  163. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  164. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  172. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  173. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  174. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  175. package/apps/vibe/.crew/sandbox.json +0 -7
  176. package/apps/vibe/.crew/session.json +0 -330
  177. package/apps/vibe/.crew/training-data.jsonl +0 -0
  178. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  179. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  180. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  181. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  182. package/apps/vibe/ARCHITECTURE.md +0 -3393
  183. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  184. package/apps/vibe/ROADMAP.md +0 -41
  185. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  186. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  187. package/apps/vibe/capture-demo.mjs +0 -160
  188. package/apps/vibe/capture-full-demo.mjs +0 -255
  189. package/apps/vibe/capture-quickstart.mjs +0 -256
  190. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  191. package/apps/vibe/capture-vibe-video.mjs +0 -260
  192. package/apps/vibe/check-buttons.js +0 -41
  193. package/apps/vibe/diagnose.html +0 -106
  194. package/apps/vibe/fix-buttons.js +0 -103
  195. package/apps/vibe/index.html +0 -3404
  196. package/apps/vibe/package-lock.json +0 -920
  197. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  198. package/apps/vibe/src/main.js +0 -2940
  199. package/apps/vibe/src/register-all-languages.js +0 -98
  200. package/apps/vibe/start-studio.sh +0 -11
  201. package/apps/vibe/test/accessibility-tests.js +0 -77
  202. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  203. package/apps/vibe/test/performance-tests.js +0 -120
  204. package/apps/vibe/test/security-tests.js +0 -213
  205. package/apps/vibe/tests/e2e.local.mjs +0 -54
  206. package/apps/vibe/tests/server.smoke.mjs +0 -106
  207. package/apps/vibe/update_website.mjs +0 -74
  208. package/apps/vibe/vite.config.js +0 -19
  209. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  210. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -21,6 +21,7 @@ import { initEngineRegistry, selectEngine as registrySelectEngine, getEngineById
21
21
  import { runCrewCLITask } from "./crew-cli.mjs";
22
22
  import { normalizeProjectDir } from "../runtime/project-dir.mjs";
23
23
  import { resolveCursorLaunchSpec } from "./cursor-launcher.mjs";
24
+ import * as tmuxBridge from "../bridges/tmux-bridge.mjs";
24
25
 
25
26
  function which(bin) {
26
27
  try { execSync(`which ${bin}`, { stdio: "ignore" }); return true; } catch { return false; }
@@ -357,6 +358,11 @@ export async function runGeminiCliTask(prompt, payload = {}) {
357
358
  stdio: ["ignore", "pipe", "pipe"],
358
359
  });
359
360
 
361
+ // Label tmux pane with agent ID for cross-agent discovery
362
+ if (payload?.tmuxSessionId && tmuxBridge.detect()) {
363
+ try { tmuxBridge.label(agentId, payload.tmuxSessionId); } catch {}
364
+ }
365
+
360
366
  let lineBuffer = "";
361
367
  let accumulatedText = "";
362
368
  let orphanStream = "";
@@ -683,6 +689,11 @@ export async function runCursorCliTask(prompt, payload = {}) {
683
689
  stdio: ["ignore", "pipe", "pipe"],
684
690
  });
685
691
 
692
+ // Label tmux pane with agent ID for cross-agent discovery
693
+ if (payload?.tmuxSessionId && tmuxBridge.detect()) {
694
+ try { tmuxBridge.label(agentId, payload.tmuxSessionId); } catch {}
695
+ }
696
+
686
697
  let lineBuffer = "";
687
698
  let accumulatedText = "";
688
699
  let lastCursorAssistantNorm = "";
@@ -915,6 +926,11 @@ export async function runCodexTask(prompt, payload = {}) {
915
926
  stdio: ["ignore", "pipe", "pipe"],
916
927
  });
917
928
 
929
+ // Label tmux pane with agent ID for cross-agent discovery
930
+ if (payload?.tmuxSessionId && tmuxBridge.detect()) {
931
+ try { tmuxBridge.label(agentId, payload.tmuxSessionId); } catch {}
932
+ }
933
+
918
934
  let lineBuffer = "";
919
935
  let accumulatedText = "";
920
936
  /** Non-JSON lines (stderr, errors, usage) — previously swallowed by catch {} */
@@ -1165,6 +1181,7 @@ export async function runClaudeCodeTask(prompt, payload = {}) {
1165
1181
  const agentPrefix = agentId ? `[${agentId}]` : "";
1166
1182
  const titledPrompt = agentPrefix ? `${agentPrefix} ${String(prompt)}` : String(prompt);
1167
1183
 
1184
+ const emptyMcpCfg = path.join(os.homedir(), ".crewswarm", "config", "empty-mcp.json");
1168
1185
  const args = [
1169
1186
  "-p",
1170
1187
  "--dangerously-skip-permissions",
@@ -1189,8 +1206,10 @@ export async function runClaudeCodeTask(prompt, payload = {}) {
1189
1206
  }
1190
1207
  }
1191
1208
 
1192
- // CRITICAL: Claude Code expects the prompt as a command-line argument, NOT via stdin
1193
- args.push(titledPrompt);
1209
+ // Skip user MCP servers to avoid 30s+ init hangs
1210
+ args.push("--strict-mcp-config", "--mcp-config", emptyMcpCfg);
1211
+ // -- separates flags from prompt (--mcp-config is variadic and eats positional args)
1212
+ args.push("--", titledPrompt);
1194
1213
 
1195
1214
  if (!which(CLAUDE_CODE_BIN)) {
1196
1215
  throw new Error(`Claude Code CLI not found: "${CLAUDE_CODE_BIN}". Install with: npm i -g @anthropic-ai/claude-code`);
@@ -1207,6 +1226,11 @@ export async function runClaudeCodeTask(prompt, payload = {}) {
1207
1226
  stdio: ["ignore", "pipe", "pipe"], // Changed from "pipe" to "ignore" for stdin since we use args
1208
1227
  });
1209
1228
 
1229
+ // Label tmux pane with agent ID for cross-agent discovery
1230
+ if (payload?.tmuxSessionId && tmuxBridge.detect()) {
1231
+ try { tmuxBridge.label(agentId, payload.tmuxSessionId); } catch {}
1232
+ }
1233
+
1210
1234
  let lineBuffer = "";
1211
1235
  let accumulatedText = "";
1212
1236
  let stderrText = "";
@@ -290,6 +290,13 @@ export function loadClaudeCodeEnabled() {
290
290
  if (typeof cfg.claudeCode === "boolean") return cfg.claudeCode;
291
291
  return false;
292
292
  }
293
+
294
+ export function loadTmuxBridgeEnabled() {
295
+ if (process.env.CREWSWARM_TMUX_BRIDGE) return /^1|true|yes$/i.test(String(process.env.CREWSWARM_TMUX_BRIDGE));
296
+ const cfg = loadSystemConfig();
297
+ if (typeof cfg.tmuxBridge === "boolean") return cfg.tmuxBridge;
298
+ return false;
299
+ }
293
300
  // ── Configuration Parsers (Migrated from registry.mjs) ───────────────────
294
301
  export function resolveConfig() {
295
302
  const paths = [CREWSWARM_CONFIG_PATH, path.join(LEGACY_STATE_DIR, "openclaw.json")];
@@ -21,14 +21,16 @@ let _stateDir = null;
21
21
  */
22
22
  export function getConfigDir() {
23
23
  if (_configDir) return _configDir;
24
-
25
- if (process.env.CREWSWARM_TEST_MODE === "true") {
24
+
25
+ if (process.env.CREWSWARM_CONFIG_DIR) {
26
+ _configDir = process.env.CREWSWARM_CONFIG_DIR;
27
+ } else if (process.env.CREWSWARM_TEST_MODE === "true") {
26
28
  // Use a consistent temp dir for the entire test process (not per-call)
27
29
  _configDir = path.join(os.tmpdir(), `crewswarm-test-${process.pid}`);
28
30
  } else {
29
- _configDir = process.env.CREWSWARM_CONFIG_DIR || path.join(os.homedir(), ".crewswarm");
31
+ _configDir = path.join(os.homedir(), ".crewswarm");
30
32
  }
31
-
33
+
32
34
  fs.mkdirSync(_configDir, { recursive: true });
33
35
  return _configDir;
34
36
  }
@@ -39,14 +41,16 @@ export function getConfigDir() {
39
41
  */
40
42
  export function getStateDir() {
41
43
  if (_stateDir) return _stateDir;
42
-
43
- if (process.env.CREWSWARM_TEST_MODE === "true") {
44
+
45
+ if (process.env.CREWSWARM_STATE_DIR) {
46
+ _stateDir = process.env.CREWSWARM_STATE_DIR;
47
+ } else if (process.env.CREWSWARM_TEST_MODE === "true") {
44
48
  // Use a consistent temp dir for the entire test process (not per-call)
45
49
  _stateDir = path.join(os.tmpdir(), `crewswarm-test-${process.pid}`);
46
50
  } else {
47
- _stateDir = process.env.CREWSWARM_STATE_DIR || path.join(os.homedir(), ".crewswarm");
51
+ _stateDir = path.join(os.homedir(), ".crewswarm");
48
52
  }
49
-
53
+
50
54
  fs.mkdirSync(_stateDir, { recursive: true });
51
55
  return _stateDir;
52
56
  }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Session Manager — persistent tmux sessions as first-class execution resources
3
+ *
4
+ * Manages session lifecycle: create, attach, exec, lock, handoff, terminate.
5
+ * One writer per session (lock enforcement). Transcripts logged for auditability.
6
+ *
7
+ * Sessions are stored as metadata files under ~/.crewswarm/state/sessions/.
8
+ * The actual tmux sessions are managed via tmux CLI.
9
+ */
10
+
11
+ import { execSync } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { randomUUID } from "node:crypto";
15
+ import { getStatePath } from "../runtime/paths.mjs";
16
+ import * as tmuxBridge from "../bridges/tmux-bridge.mjs";
17
+
18
+ const SESSION_DIR = getStatePath("sessions");
19
+ const TRANSCRIPT_DIR = getStatePath("sessions", "transcripts");
20
+
21
+ try { fs.mkdirSync(SESSION_DIR, { recursive: true }); } catch {}
22
+ try { fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true }); } catch {}
23
+
24
+ // ── Helpers ──────────────────────────────────────────────────────────────────
25
+
26
+ function sessionMetaPath(sessionId) {
27
+ return path.join(SESSION_DIR, `${sessionId}.json`);
28
+ }
29
+
30
+ function transcriptPath(sessionId) {
31
+ return path.join(TRANSCRIPT_DIR, `${sessionId}.jsonl`);
32
+ }
33
+
34
+ function loadMeta(sessionId) {
35
+ try {
36
+ return JSON.parse(fs.readFileSync(sessionMetaPath(sessionId), "utf8"));
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function saveMeta(sessionId, meta) {
43
+ try {
44
+ fs.writeFileSync(sessionMetaPath(sessionId), JSON.stringify(meta, null, 2));
45
+ } catch (e) {
46
+ console.error(`[session-manager] Failed to save meta for ${sessionId}: ${e.message}`);
47
+ }
48
+ }
49
+
50
+ function appendTranscript(sessionId, entry) {
51
+ try {
52
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...entry });
53
+ fs.appendFileSync(transcriptPath(sessionId), line + "\n");
54
+ } catch {}
55
+ }
56
+
57
+ function tmuxExec(cmd, timeout = 5000) {
58
+ try {
59
+ return execSync(cmd, { encoding: "utf8", timeout, stdio: ["ignore", "pipe", "pipe"] }).trim();
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ // ── Public API ───────────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * Create a new persistent tmux session for agent work.
69
+ * @param {object} opts
70
+ * @param {string} opts.workspaceId - Logical workspace name
71
+ * @param {string} opts.agentId - Owning agent
72
+ * @param {string} [opts.cwd] - Working directory
73
+ * @param {Record<string, string>} [opts.env] - Extra env vars
74
+ * @returns {string|null} sessionId or null on failure
75
+ */
76
+ export function create({ workspaceId, agentId, cwd, env } = {}) {
77
+ if (!tmuxBridge.detect()) return null;
78
+
79
+ const sessionId = `cs-${workspaceId}-${randomUUID().slice(0, 8)}`;
80
+ const sessionName = sessionId;
81
+
82
+ // Create a new tmux session (detached)
83
+ const envStr = env
84
+ ? Object.entries(env).map(([k, v]) => `-e ${k}=${v}`).join(" ")
85
+ : "";
86
+ const cwdFlag = cwd ? `-c "${cwd}"` : "";
87
+ const result = tmuxExec(`tmux new-session -d -s "${sessionName}" ${cwdFlag} ${envStr}`);
88
+ if (result === null) {
89
+ console.error(`[session-manager] Failed to create tmux session: ${sessionName}`);
90
+ return null;
91
+ }
92
+
93
+ // Label the session's first pane with the agent ID
94
+ const paneId = tmuxExec(`tmux list-panes -t "${sessionName}" -F "#{pane_id}" | head -1`);
95
+ if (paneId) {
96
+ tmuxBridge.label(agentId, paneId);
97
+ }
98
+
99
+ const meta = {
100
+ sessionId,
101
+ sessionName,
102
+ workspaceId,
103
+ owner: agentId,
104
+ lockedBy: agentId,
105
+ paneId: paneId || null,
106
+ cwd: cwd || null,
107
+ env: env || null,
108
+ createdAt: new Date().toISOString(),
109
+ status: "active",
110
+ };
111
+ saveMeta(sessionId, meta);
112
+ appendTranscript(sessionId, { action: "created", agent: agentId, cwd });
113
+
114
+ console.log(`[session-manager] Created session ${sessionId} for ${agentId} (pane=${paneId})`);
115
+ return sessionId;
116
+ }
117
+
118
+ /**
119
+ * Attach an agent to an existing session (for handoff or observation).
120
+ * @param {string} sessionId
121
+ * @param {string} agentId
122
+ * @returns {{ paneId: string, sessionName: string }|null}
123
+ */
124
+ export function attach(sessionId, agentId) {
125
+ const meta = loadMeta(sessionId);
126
+ if (!meta || meta.status !== "active") return null;
127
+
128
+ appendTranscript(sessionId, { action: "attached", agent: agentId });
129
+ console.log(`[session-manager] ${agentId} attached to session ${sessionId}`);
130
+
131
+ return { paneId: meta.paneId, sessionName: meta.sessionName };
132
+ }
133
+
134
+ /**
135
+ * Execute a command in a session's tmux pane.
136
+ * Only the lock owner can execute.
137
+ * @param {string} sessionId
138
+ * @param {string} command
139
+ * @param {object} [opts]
140
+ * @param {string} opts.actorId - Agent executing the command
141
+ * @param {number} [opts.timeout=30000] - Timeout in ms
142
+ * @returns {{ output: string }|null}
143
+ */
144
+ export function exec(sessionId, command, { actorId, timeout = 30000 } = {}) {
145
+ const meta = loadMeta(sessionId);
146
+ if (!meta || meta.status !== "active") return null;
147
+
148
+ // Enforce lock
149
+ if (meta.lockedBy && meta.lockedBy !== actorId) {
150
+ console.warn(`[session-manager] ${actorId} cannot exec in ${sessionId} — locked by ${meta.lockedBy}`);
151
+ return null;
152
+ }
153
+
154
+ const paneId = meta.paneId;
155
+ if (!paneId) return null;
156
+
157
+ // Send keys to the pane
158
+ tmuxExec(`tmux send-keys -t "${paneId}" "${command.replace(/"/g, '\\"')}" Enter`, timeout);
159
+ appendTranscript(sessionId, { action: "exec", agent: actorId, command: command.slice(0, 500) });
160
+
161
+ // Read back output after a short delay
162
+ const output = tmuxBridge.read(meta.owner, 50);
163
+ return { output: output || "" };
164
+ }
165
+
166
+ /**
167
+ * Lock a session for exclusive write access.
168
+ * @param {string} sessionId
169
+ * @param {string} ownerId - Agent requesting the lock
170
+ * @returns {boolean} true if lock acquired
171
+ */
172
+ export function lock(sessionId, ownerId) {
173
+ const meta = loadMeta(sessionId);
174
+ if (!meta || meta.status !== "active") return false;
175
+
176
+ if (meta.lockedBy && meta.lockedBy !== ownerId) {
177
+ console.warn(`[session-manager] Lock denied for ${ownerId} on ${sessionId} — held by ${meta.lockedBy}`);
178
+ return false;
179
+ }
180
+
181
+ meta.lockedBy = ownerId;
182
+ meta.lockedAt = new Date().toISOString();
183
+ saveMeta(sessionId, meta);
184
+ appendTranscript(sessionId, { action: "locked", agent: ownerId });
185
+ return true;
186
+ }
187
+
188
+ /**
189
+ * Unlock a session.
190
+ * @param {string} sessionId
191
+ * @param {string} ownerId - Must match current lock holder
192
+ * @returns {boolean}
193
+ */
194
+ export function unlock(sessionId, ownerId) {
195
+ const meta = loadMeta(sessionId);
196
+ if (!meta) return false;
197
+
198
+ if (meta.lockedBy && meta.lockedBy !== ownerId) {
199
+ console.warn(`[session-manager] Unlock denied for ${ownerId} on ${sessionId} — held by ${meta.lockedBy}`);
200
+ return false;
201
+ }
202
+
203
+ meta.lockedBy = null;
204
+ meta.lockedAt = null;
205
+ saveMeta(sessionId, meta);
206
+ appendTranscript(sessionId, { action: "unlocked", agent: ownerId });
207
+ return true;
208
+ }
209
+
210
+ /**
211
+ * Hand off a session from one agent to another.
212
+ * Transfers lock ownership and re-labels the pane.
213
+ * @param {string} sessionId
214
+ * @param {string} fromAgent
215
+ * @param {string} toAgent
216
+ * @returns {boolean}
217
+ */
218
+ export function handoff(sessionId, fromAgent, toAgent) {
219
+ const meta = loadMeta(sessionId);
220
+ if (!meta || meta.status !== "active") return false;
221
+
222
+ // Only the current lock holder (or unlocked session) can hand off
223
+ if (meta.lockedBy && meta.lockedBy !== fromAgent) {
224
+ console.warn(`[session-manager] Handoff denied: ${sessionId} locked by ${meta.lockedBy}, not ${fromAgent}`);
225
+ return false;
226
+ }
227
+
228
+ meta.owner = toAgent;
229
+ meta.lockedBy = toAgent;
230
+ meta.lockedAt = new Date().toISOString();
231
+ saveMeta(sessionId, meta);
232
+
233
+ // Re-label pane for the new agent
234
+ if (meta.paneId) {
235
+ tmuxBridge.label(toAgent, meta.paneId);
236
+ }
237
+
238
+ appendTranscript(sessionId, { action: "handoff", from: fromAgent, to: toAgent });
239
+ console.log(`[session-manager] Session ${sessionId} handed off: ${fromAgent} → ${toAgent}`);
240
+ return true;
241
+ }
242
+
243
+ /**
244
+ * Terminate a session and clean up its tmux pane.
245
+ * @param {string} sessionId
246
+ * @returns {boolean}
247
+ */
248
+ export function terminate(sessionId) {
249
+ const meta = loadMeta(sessionId);
250
+ if (!meta) return false;
251
+
252
+ // Kill the tmux session
253
+ if (meta.sessionName) {
254
+ tmuxExec(`tmux kill-session -t "${meta.sessionName}"`);
255
+ }
256
+
257
+ meta.status = "terminated";
258
+ meta.terminatedAt = new Date().toISOString();
259
+ saveMeta(sessionId, meta);
260
+ appendTranscript(sessionId, { action: "terminated" });
261
+ console.log(`[session-manager] Session ${sessionId} terminated`);
262
+ return true;
263
+ }
264
+
265
+ /**
266
+ * Get metadata for a session.
267
+ * @param {string} sessionId
268
+ * @returns {object|null}
269
+ */
270
+ export function getSession(sessionId) {
271
+ return loadMeta(sessionId);
272
+ }
273
+
274
+ /**
275
+ * List all active sessions.
276
+ * @returns {Array<object>}
277
+ */
278
+ export function listSessions() {
279
+ try {
280
+ const files = fs.readdirSync(SESSION_DIR).filter(f => f.endsWith(".json"));
281
+ return files
282
+ .map(f => loadMeta(f.replace(".json", "")))
283
+ .filter(m => m && m.status === "active");
284
+ } catch {
285
+ return [];
286
+ }
287
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "crewswarm",
3
- "version": "0.9.1",
4
- "description": "Local-first multi-agent orchestration platform coordinate AI coding agents, LLMs, and tools from a single dashboard",
3
+ "version": "0.9.3",
4
+ "description": "Local-first multi-agent orchestration platform \u2014 coordinate AI coding agents, LLMs, and tools from a single dashboard",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -29,10 +29,11 @@
29
29
  "engines/",
30
30
  "prompts/",
31
31
  "apps/dashboard/dist/",
32
- "apps/dashboard/src/",
33
- "apps/dashboard/index.html",
34
- "apps/dashboard/package.json",
35
- "apps/vibe/",
32
+ "apps/vibe/dist/",
33
+ "apps/vibe/package.json",
34
+ "apps/vibe/public/",
35
+ "apps/vibe/server.mjs",
36
+ "apps/vibe/watch-server.mjs",
36
37
  "contrib/openclaw-plugin/",
37
38
  "crew-lead.mjs",
38
39
  "gateway-bridge.mjs",
@@ -61,14 +62,25 @@
61
62
  "@whiskeysockets/baileys": "^7.0.0-rc.9"
62
63
  },
63
64
  "scripts": {
64
- "postinstall": "node -e \"try{require('child_process').execSync('git --version',{stdio:'pipe'})}catch{console.log('\\n⚠️ git not found. Install git for full functionality (WhatsApp bridge, crew-cli git tools).\\n macOS: xcode-select --install\\n Ubuntu: sudo apt install git\\n Alpine: apk add git\\n')}\"",
65
+ "postinstall": "node -e \"try{require('child_process').execSync('git --version',{stdio:'pipe'})}catch{console.log('\\n\u26a0\ufe0f git not found. Install git for full functionality (WhatsApp bridge, crew-cli git tools).\\n macOS: xcode-select --install\\n Ubuntu: sudo apt install git\\n Alpine: apk add git\\n')}\"",
65
66
  "start": "node scripts/start.mjs",
66
- "test": "node --test test/unit/*.test.mjs test/mention-participants.test.mjs test/project-messages-chat-protocol.test.mjs && npm --prefix crew-cli test",
67
- "test:unit": "node --test test/unit/*.test.mjs",
68
- "test:integration": "PM_LOOP_TEST_MODE=1 node --test test/integration/*.test.mjs",
67
+ "test": "node --test --test-reporter=./scripts/test-reporter.mjs test/unit/*.test.mjs test/mention-participants.test.mjs test/project-messages-chat-protocol.test.mjs && npm --prefix crew-cli test",
68
+ "test:unit": "node --test --test-reporter=./scripts/test-reporter.mjs test/unit/*.test.mjs",
69
+ "test:integration": "PM_LOOP_TEST_MODE=1 node --test --test-reporter=./scripts/test-reporter.mjs test/integration/*.test.mjs",
69
70
  "test:integration:bounded": "node scripts/run-integration-bounded.mjs",
70
- "test:e2e": "node --test test/e2e/*.test.mjs",
71
- "test:all": "PM_LOOP_TEST_MODE=1 node --test test/unit/*.test.mjs test/integration/*.test.mjs test/e2e/*.test.mjs",
71
+ "test:e2e": "node --test --test-reporter=./scripts/test-reporter.mjs test/e2e/*.test.mjs",
72
+ "test:all": "PM_LOOP_TEST_MODE=1 node --test --test-reporter=./scripts/test-reporter.mjs test/unit/*.test.mjs test/integration/*.test.mjs test/e2e/*.test.mjs",
73
+ "test:coverage:root": "node --test --experimental-test-coverage test/unit/*.test.mjs",
74
+ "test:coverage": "node scripts/coverage-report.mjs",
75
+ "test:coverage:summary": "node scripts/coverage-summary.mjs",
76
+ "test:live:providers": "node scripts/live-provider-failover-matrix.mjs",
77
+ "test:live:providers:matrix": "node scripts/live-provider-matrix.mjs",
78
+ "test:live:providers:smoke": "node scripts/live-provider-matrix.mjs --smoke",
79
+ "test:live:clis": "node scripts/live-cli-matrix.mjs",
80
+ "test:live:clis:smoke": "node scripts/live-cli-matrix.mjs --smoke",
81
+ "test:live:bridges": "node scripts/live-bridge-matrix.mjs",
82
+ "test:live:crewchat": "node scripts/live-crewchat-check.mjs",
83
+ "test:live": "node scripts/live-provider-failover-matrix.mjs && node scripts/live-bridge-matrix.mjs && node scripts/live-crewchat-check.mjs",
72
84
  "docs:api": "node scripts/generate-api-docs.mjs",
73
85
  "changelog:generate": "node scripts/generate-changelog.mjs",
74
86
  "changelog:release": "node scripts/generate-changelog.mjs --release",
@@ -86,7 +98,12 @@
86
98
  "stop-crew": "node scripts/start-crew.mjs --stop",
87
99
  "dashboard": "node scripts/dashboard.mjs",
88
100
  "vibe": "node apps/vibe/server.mjs",
89
- "vibe:watch": "node apps/vibe/watch-server.mjs",
101
+ "vibe:build": "cd apps/vibe && npm run build",
102
+ "vibe:start": "cd apps/vibe && NODE_DISABLE_COMPILE_CACHE=1 npm start",
103
+ "vibe:watch": "NODE_DISABLE_COMPILE_CACHE=1 node apps/vibe/watch-server.mjs",
104
+ "vibe:full": "bash scripts/start-studio-full.sh",
105
+ "test:e2e:vibe": "node node_modules/playwright/cli.js test --config=playwright.config.js",
106
+ "test:e2e:vibe:headed": "node node_modules/playwright/cli.js test --config=playwright.config.js --headed",
90
107
  "crew-lead": "node crew-lead.mjs",
91
108
  "mcp": "node scripts/mcp-server.mjs",
92
109
  "studio": "cd apps/vibe && npm run dev",
@@ -100,10 +117,13 @@
100
117
  "smoke:static": "bash scripts/smoke.sh",
101
118
  "health": "node scripts/health-check.mjs",
102
119
  "doctor": "node scripts/doctor.mjs",
103
- "release:check": "bash scripts/release-check.sh"
120
+ "release:check": "bash scripts/release-check.sh",
121
+ "test:report": "node scripts/test-report-summary.mjs",
122
+ "test:rerun": "node scripts/test-rerun.mjs",
123
+ "test:stale": "node scripts/test-rerun.mjs --stale"
104
124
  },
105
125
  "devDependencies": {
106
126
  "@playwright/test": "^1.58.2",
107
127
  "puppeteer-core": "^24.40.0"
108
128
  }
109
- }
129
+ }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * capture-build-flow.mjs — Puppeteer script that captures dashboard
4
+ * screenshots during a build flow for documentation / demo purposes.
5
+ *
6
+ * Usage: node scripts/capture-build-flow.mjs
7
+ * Requires: dashboard running at http://127.0.0.1:4319
8
+ */
9
+
10
+ import { mkdir } from "node:fs/promises";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, resolve } from "node:path";
13
+ import puppeteer from "puppeteer-core";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = resolve(__dirname, "..");
17
+ const OUT_DIR = resolve(ROOT, "website/screenshots/flow");
18
+ const BASE_URL = "http://127.0.0.1:4319";
19
+
20
+ const STEPS = [
21
+ { name: "step1-build-tab", hash: "build", waitMs: 2000 },
22
+ { name: "step2-requirement", action: "type" },
23
+ { name: "step3-plan", action: "plan", waitMs: 20000 },
24
+ { name: "step4-chat", hash: "chat", waitMs: 3000 },
25
+ { name: "step5-agents", hash: "swarm", waitMs: 2000 },
26
+ { name: "step6-rt-messages", hash: "rt", waitMs: 2000 },
27
+ ];
28
+
29
+ async function screenshot(page, name) {
30
+ const path = resolve(OUT_DIR, `${name}.webp`);
31
+ await page.screenshot({ path, type: "webp", quality: 85 });
32
+ }
33
+
34
+ async function sleep(ms) {
35
+ return new Promise((r) => setTimeout(r, ms));
36
+ }
37
+
38
+ async function main() {
39
+ await mkdir(OUT_DIR, { recursive: true });
40
+
41
+ const browser = await puppeteer.launch({
42
+ headless: true,
43
+ executablePath:
44
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
45
+ args: ["--no-sandbox"],
46
+ defaultViewport: { width: 1440, height: 960 },
47
+ });
48
+
49
+ const page = await browser.newPage();
50
+
51
+ // Step 1 — navigate to #build
52
+ await page.goto(`${BASE_URL}/#build`, { waitUntil: "domcontentloaded" });
53
+ await sleep(2000);
54
+ await screenshot(page, "step1-build-tab");
55
+
56
+ // Step 2 — type requirement
57
+ const REQUIREMENT = "Create a simple hello world HTML page";
58
+ // Try a few selectors for the requirement input
59
+ const textareaSelector = await page
60
+ .waitForSelector("textarea, #requirement, .requirement-input", {
61
+ timeout: 5000,
62
+ })
63
+ .catch(() => null);
64
+
65
+ if (textareaSelector) {
66
+ await textareaSelector.click();
67
+ await textareaSelector.type(REQUIREMENT, { delay: 30 });
68
+ }
69
+ await screenshot(page, "step2-requirement");
70
+
71
+ // Step 3 — click Plan and wait for result
72
+ await page.evaluate(() => {
73
+ const buttons = [...document.querySelectorAll("button")];
74
+ const planBtn = buttons.find((b) => /plan/i.test(b.textContent));
75
+ if (planBtn) planBtn.click();
76
+ });
77
+ await sleep(20000);
78
+ await screenshot(page, "step3-plan");
79
+
80
+ // Step 4 — Chat tab
81
+ await page.goto(`${BASE_URL}/#chat`, { waitUntil: "domcontentloaded" });
82
+ await sleep(3000);
83
+ await screenshot(page, "step4-chat");
84
+
85
+ // Step 5 — Swarm / agents tab
86
+ await page.goto(`${BASE_URL}/#swarm`, { waitUntil: "domcontentloaded" });
87
+ await sleep(2000);
88
+ await screenshot(page, "step5-agents");
89
+
90
+ // Step 6 — RT messages tab
91
+ await page.goto(`${BASE_URL}/#rt`, { waitUntil: "domcontentloaded" });
92
+ await sleep(2000);
93
+ await screenshot(page, "step6-rt-messages");
94
+
95
+ await browser.close();
96
+
97
+ const files = [
98
+ "step1-build-tab.webp",
99
+ "step2-requirement.webp",
100
+ "step3-plan.webp",
101
+ "step4-chat.webp",
102
+ "step5-agents.webp",
103
+ "step6-rt-messages.webp",
104
+ ];
105
+
106
+ console.log(`\nCaptured ${files.length} build flow frames:`);
107
+ for (const f of files) {
108
+ console.log(` website/screenshots/flow/${f}`);
109
+ }
110
+ console.log(
111
+ `\nTo create animated webp: convert -delay 200 website/screenshots/flow/*.webp website/screenshots/build-flow.webp`,
112
+ );
113
+ }
114
+
115
+ main().catch((err) => {
116
+ console.error("Build flow capture failed:", err.message);
117
+ process.exit(1);
118
+ });