crewswarm 0.9.2 → 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 (207) 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 +34 -4
  12. package/lib/bridges/gateway-ws.mjs +4 -0
  13. package/lib/crew-lead/chat-handler.mjs +34 -0
  14. package/lib/crew-lead/http-server.mjs +55 -14
  15. package/lib/crew-lead/llm-caller.mjs +24 -8
  16. package/lib/crew-lead/prompts.mjs +7 -0
  17. package/lib/crew-lead/wave-dispatcher.mjs +15 -3
  18. package/lib/crew-lead/ws-router.mjs +219 -27
  19. package/lib/engines/engine-registry.mjs +9 -0
  20. package/lib/engines/rt-envelope.mjs +1 -0
  21. package/lib/engines/runners.mjs +5 -2
  22. package/lib/runtime/paths.mjs +12 -8
  23. package/package.json +35 -15
  24. package/scripts/capture-build-flow.mjs +118 -0
  25. package/scripts/coverage-report.mjs +209 -0
  26. package/scripts/coverage-summary.mjs +47 -0
  27. package/scripts/dashboard-validation.mjs +74 -0
  28. package/scripts/dashboard.mjs +560 -70
  29. package/scripts/live-bridge-matrix.mjs +79 -0
  30. package/scripts/live-cli-matrix.mjs +166 -0
  31. package/scripts/live-crewchat-check.mjs +42 -0
  32. package/scripts/live-engine-matrix.mjs +50 -0
  33. package/scripts/live-provider-failover-matrix.mjs +107 -0
  34. package/scripts/live-provider-matrix.mjs +228 -0
  35. package/scripts/restart-all-from-repo.sh +4 -4
  36. package/scripts/smoke-dispatch.mjs +4 -1
  37. package/scripts/test-blast-radius.mjs +204 -0
  38. package/scripts/test-report-summary.mjs +88 -0
  39. package/scripts/test-reporter.mjs +651 -0
  40. package/scripts/test-rerun.mjs +136 -0
  41. package/scripts/tmux-bridge +130 -0
  42. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  43. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  44. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  45. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  46. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  47. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  48. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  49. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  50. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  51. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  52. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  53. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  54. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  56. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  60. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  62. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  64. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  65. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  66. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  67. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  68. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  69. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  70. package/apps/dashboard/dist/index.html.br +0 -0
  71. package/apps/dashboard/dist/index.html.gz +0 -0
  72. package/apps/dashboard/index.html +0 -6529
  73. package/apps/dashboard/package.json +0 -15
  74. package/apps/dashboard/src/app.js +0 -2828
  75. package/apps/dashboard/src/app.js.br +0 -0
  76. package/apps/dashboard/src/app.js.gz +0 -0
  77. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  78. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  79. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  80. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  81. package/apps/dashboard/src/cli-process.js +0 -208
  82. package/apps/dashboard/src/cli-process.js.br +0 -0
  83. package/apps/dashboard/src/cli-process.js.gz +0 -0
  84. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  85. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  86. package/apps/dashboard/src/core/api.js +0 -18
  87. package/apps/dashboard/src/core/api.js.br +0 -0
  88. package/apps/dashboard/src/core/dom.js +0 -228
  89. package/apps/dashboard/src/core/dom.js.br +0 -0
  90. package/apps/dashboard/src/core/state.js +0 -91
  91. package/apps/dashboard/src/core/state.js.br +0 -0
  92. package/apps/dashboard/src/core/task-manager.js +0 -134
  93. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  94. package/apps/dashboard/src/orchestration-status.js +0 -127
  95. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  96. package/apps/dashboard/src/setup-wizard.js +0 -562
  97. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  98. package/apps/dashboard/src/styles.css +0 -2085
  99. package/apps/dashboard/src/styles.css.br +0 -0
  100. package/apps/dashboard/src/styles.css.gz +0 -0
  101. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  102. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  103. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  104. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  105. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  106. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  107. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  108. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  109. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  110. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  111. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  112. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  113. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  114. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  115. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  116. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  117. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  118. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  119. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  120. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  121. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  122. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  123. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  124. package/apps/dashboard/src/tabs/settings-tab.js +0 -861
  125. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  126. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  127. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  128. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  129. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  130. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  131. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  132. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  133. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  134. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  135. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  136. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  137. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  138. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  139. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  140. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  141. package/apps/vibe/.crew/cost.json +0 -17
  142. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  143. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  144. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  145. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  146. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  147. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  148. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  149. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  150. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  151. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  152. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  153. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  154. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  155. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  156. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  157. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  158. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  159. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  160. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  161. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  162. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  163. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  164. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  172. package/apps/vibe/.crew/sandbox.json +0 -7
  173. package/apps/vibe/.crew/session.json +0 -330
  174. package/apps/vibe/.crew/training-data.jsonl +0 -0
  175. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  176. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  177. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  178. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  179. package/apps/vibe/ARCHITECTURE.md +0 -3393
  180. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  181. package/apps/vibe/ROADMAP.md +0 -41
  182. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  183. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  184. package/apps/vibe/capture-demo.mjs +0 -160
  185. package/apps/vibe/capture-full-demo.mjs +0 -255
  186. package/apps/vibe/capture-quickstart.mjs +0 -256
  187. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  188. package/apps/vibe/capture-vibe-video.mjs +0 -260
  189. package/apps/vibe/check-buttons.js +0 -41
  190. package/apps/vibe/diagnose.html +0 -106
  191. package/apps/vibe/fix-buttons.js +0 -103
  192. package/apps/vibe/index.html +0 -3404
  193. package/apps/vibe/package-lock.json +0 -920
  194. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  195. package/apps/vibe/src/main.js +0 -2940
  196. package/apps/vibe/src/register-all-languages.js +0 -98
  197. package/apps/vibe/start-studio.sh +0 -11
  198. package/apps/vibe/test/accessibility-tests.js +0 -77
  199. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  200. package/apps/vibe/test/performance-tests.js +0 -120
  201. package/apps/vibe/test/security-tests.js +0 -213
  202. package/apps/vibe/tests/e2e.local.mjs +0 -54
  203. package/apps/vibe/tests/server.smoke.mjs +0 -106
  204. package/apps/vibe/update_website.mjs +0 -74
  205. package/apps/vibe/vite.config.js +0 -19
  206. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  207. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -30,9 +30,25 @@ import {
30
30
  } from "../lib/agents/tool-instructions.mjs";
31
31
  import {
32
32
  StartBuildSchema,
33
+ EnhancePromptSchema,
33
34
  StartPMLoopSchema,
34
35
  ServiceActionSchema,
35
36
  ImportSkillSchema,
37
+ AgentConfigCreateSchema,
38
+ AgentConfigDeleteSchema,
39
+ AgentResetSessionSchema,
40
+ ProviderAddSchema,
41
+ ProviderSaveSchema,
42
+ ProviderTestSchema,
43
+ ProviderBuiltinTestSchema,
44
+ ContinuousBuildSchema,
45
+ ReplayDLQSchema,
46
+ DeleteProjectSchema,
47
+ UpdateProjectSchema,
48
+ RoadmapWriteSchema,
49
+ RoadmapRetryFailedSchema,
50
+ ContactDeleteSchema,
51
+ ContactSendSchema,
36
52
  validate,
37
53
  } from "./dashboard-validation.mjs";
38
54
  import { execCrewLeadTools } from "../lib/crew-lead/tools.mjs";
@@ -960,6 +976,233 @@ Keep the same intent; make it specific enough for a PM to break into small tasks
960
976
  return content;
961
977
  }
962
978
 
979
+ function getRtAuthToken() {
980
+ try {
981
+ return readSwarmConfigSafe()?.rt?.authToken || "";
982
+ } catch {
983
+ return "";
984
+ }
985
+ }
986
+
987
+ function resolvePlannerEngine(preferredEngine = null, preferredModel = null) {
988
+ if (preferredEngine) {
989
+ return {
990
+ engine: preferredEngine,
991
+ model: preferredModel || null,
992
+ permissionMode: null,
993
+ sandbox: preferredEngine === "codex" ? "read-only" : null,
994
+ source: "request",
995
+ };
996
+ }
997
+
998
+ const cfg = readSwarmConfigSafe();
999
+ const agents = Array.isArray(cfg?.agents) ? cfg.agents : [];
1000
+ const pm = agents.find((agent) => agent?.id === "crew-pm") || {};
1001
+
1002
+ if (pm.useClaudeCode) {
1003
+ return {
1004
+ engine: "claude",
1005
+ model: pm.claudeCodeModel || null,
1006
+ // Claude's plan mode can exit 0 with no streamed text for this planner path.
1007
+ // Use the normal direct lane here; the build-planner prompt already forbids edits.
1008
+ permissionMode: null,
1009
+ sandbox: null,
1010
+ source: "crew-pm",
1011
+ };
1012
+ }
1013
+ if (pm.useCodex) {
1014
+ return {
1015
+ engine: "codex",
1016
+ model: pm.codexModel || null,
1017
+ permissionMode: null,
1018
+ sandbox: "read-only",
1019
+ source: "crew-pm",
1020
+ };
1021
+ }
1022
+ if (pm.useCursorCli) {
1023
+ return {
1024
+ engine: "cursor",
1025
+ model: pm.cursorCliModel || null,
1026
+ permissionMode: null,
1027
+ sandbox: null,
1028
+ source: "crew-pm",
1029
+ };
1030
+ }
1031
+ if (pm.useGeminiCli) {
1032
+ return {
1033
+ engine: "gemini",
1034
+ model: pm.geminiCliModel || null,
1035
+ permissionMode: null,
1036
+ sandbox: null,
1037
+ source: "crew-pm",
1038
+ };
1039
+ }
1040
+ if (pm.useCrewCLI) {
1041
+ return {
1042
+ engine: "crew-cli",
1043
+ model: pm.crewCliModel || null,
1044
+ permissionMode: null,
1045
+ sandbox: null,
1046
+ source: "crew-pm",
1047
+ };
1048
+ }
1049
+ if (pm.useOpenCode) {
1050
+ return {
1051
+ engine: "opencode",
1052
+ model: pm.opencodeModel || null,
1053
+ permissionMode: null,
1054
+ sandbox: null,
1055
+ source: "crew-pm",
1056
+ };
1057
+ }
1058
+
1059
+ const fallbacks = [
1060
+ commandExists(process.env.CLAUDE_CODE_BIN || "claude") && { engine: "claude", model: process.env.CREWSWARM_CLAUDE_CODE_MODEL || null, permissionMode: null, sandbox: null, source: "fallback" },
1061
+ commandExists(process.env.CODEX_CLI_BIN || "codex") && { engine: "codex", model: process.env.CREWSWARM_CODEX_MODEL || null, permissionMode: null, sandbox: "read-only", source: "fallback" },
1062
+ commandExists(process.env.CURSOR_CLI_BIN || path.join(os.homedir(), ".local", "bin", "agent"), [path.join(os.homedir(), ".local", "bin", "agent")]) && { engine: "cursor", model: process.env.CREWSWARM_CURSOR_MODEL || process.env.CURSOR_DEFAULT_MODEL || null, permissionMode: null, sandbox: null, source: "fallback" },
1063
+ commandExists(process.env.GEMINI_CLI_BIN || "gemini") && { engine: "gemini", model: process.env.CREWSWARM_GEMINI_CLI_MODEL || null, permissionMode: null, sandbox: null, source: "fallback" },
1064
+ commandExists(process.env.CREWSWARM_OPENCODE_BIN || "opencode") && { engine: "opencode", model: process.env.CREWSWARM_OPENCODE_MODEL || null, permissionMode: null, sandbox: null, source: "fallback" },
1065
+ ].filter(Boolean);
1066
+
1067
+ return fallbacks[0] || null;
1068
+ }
1069
+
1070
+ function buildRequirementPlanningPrompt(userText, projectDir = null) {
1071
+ return [
1072
+ "You are the planning stage for CrewSwarm's Build tab.",
1073
+ "Transform the user's rough build idea into a concrete build brief that crew-pm can execute.",
1074
+ "If repository context is relevant, inspect the workspace before answering. Do not edit files.",
1075
+ "",
1076
+ "Output format:",
1077
+ "## Build Brief",
1078
+ "A 1-2 paragraph concrete requirement with explicit scope and deliverables.",
1079
+ "",
1080
+ "## Acceptance Criteria",
1081
+ "- 3 to 7 flat bullets",
1082
+ "",
1083
+ "## Constraints / Assumptions",
1084
+ "- Flat bullets only when needed",
1085
+ "",
1086
+ "Rules:",
1087
+ "- Preserve the user's intent; do not invent a different product.",
1088
+ "- Make it specific enough for PM decomposition and agent dispatch.",
1089
+ "- Mention likely subsystems or files only if you have evidence from the repo.",
1090
+ "- Keep it concise and actionable.",
1091
+ "- Do not include implementation code, shell commands, or extra commentary.",
1092
+ projectDir ? `- Current project directory: ${projectDir}` : "",
1093
+ "",
1094
+ "User idea:",
1095
+ userText.trim(),
1096
+ ].filter(Boolean).join("\n");
1097
+ }
1098
+
1099
+ async function collectClaudePlannerTextDirect({ message, projectDir, model = null }) {
1100
+ const claudeBin = resolveCommandPath(process.env.CLAUDE_CODE_BIN || "claude", [
1101
+ path.join(os.homedir(), ".local", "bin", "claude"),
1102
+ "/usr/local/bin/claude",
1103
+ "/opt/homebrew/bin/claude",
1104
+ ]) || (process.env.CLAUDE_CODE_BIN || "claude");
1105
+ const { execFile } = await import("node:child_process");
1106
+ const { promisify } = await import("node:util");
1107
+ const execFileAsync = promisify(execFile);
1108
+ const args = ["-p"];
1109
+ if (projectDir) args.push("--add-dir", projectDir);
1110
+ if (model) args.push("--model", model);
1111
+ // Match the fixed engine-passthrough Claude invocation:
1112
+ // skip user MCP startup and terminate option parsing before the prompt.
1113
+ args.push(
1114
+ "--strict-mcp-config",
1115
+ "--mcp-config",
1116
+ path.join(os.homedir(), ".crewswarm", "config", "empty-mcp.json"),
1117
+ "--",
1118
+ message,
1119
+ );
1120
+ const { stdout, stderr } = await execFileAsync(claudeBin, args, {
1121
+ cwd: projectDir || process.cwd(),
1122
+ env: process.env,
1123
+ timeout: Number(process.env.CREWSWARM_PLANNER_TIMEOUT_MS || 300000),
1124
+ maxBuffer: 2 * 1024 * 1024,
1125
+ });
1126
+ const trimmed = String(stdout || "").trim();
1127
+ if (trimmed) return trimmed;
1128
+ const stderrText = String(stderr || "").trim();
1129
+ if (stderrText) throw new Error(stderrText);
1130
+ throw new Error("planner produced no output");
1131
+ }
1132
+
1133
+ async function collectPassthroughText({
1134
+ engine,
1135
+ message,
1136
+ projectDir,
1137
+ model = null,
1138
+ permissionMode = null,
1139
+ sandbox = null,
1140
+ forceL2 = false,
1141
+ }) {
1142
+ if (engine === "claude") {
1143
+ return collectClaudePlannerTextDirect({ message, projectDir, model });
1144
+ }
1145
+ const token = getRtAuthToken();
1146
+ const crewLeadPort = process.env.CREW_LEAD_PORT || "5010";
1147
+ let upstream;
1148
+ try {
1149
+ upstream = await fetch(`http://127.0.0.1:${crewLeadPort}/api/engine-passthrough`, {
1150
+ method: "POST",
1151
+ headers: {
1152
+ "content-type": "application/json",
1153
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
1154
+ "x-passthrough-continue": "false",
1155
+ },
1156
+ body: JSON.stringify({
1157
+ engine,
1158
+ message,
1159
+ projectDir: projectDir || process.cwd(),
1160
+ sessionId: "build-planner",
1161
+ ...(model ? { model } : {}),
1162
+ ...(permissionMode ? { permissionMode } : {}),
1163
+ ...(sandbox ? { sandbox } : {}),
1164
+ ...(forceL2 ? { forceL2: true } : {}),
1165
+ }),
1166
+ signal: AbortSignal.timeout(300_000),
1167
+ });
1168
+ } catch (fetchErr) {
1169
+ throw new Error(`planner fetch failed for ${engine}: ${fetchErr.message}`);
1170
+ }
1171
+
1172
+ if (!upstream.ok) {
1173
+ throw new Error(`planner upstream ${upstream.status}`);
1174
+ }
1175
+
1176
+ let rawSSE;
1177
+ try {
1178
+ rawSSE = await upstream.text();
1179
+ } catch (readErr) {
1180
+ throw new Error(`planner SSE read failed for ${engine}: ${readErr.message}`);
1181
+ }
1182
+ let text = "";
1183
+ let stderr = "";
1184
+ let exitCode = 0;
1185
+
1186
+ for (const line of rawSSE.split("\n")) {
1187
+ if (!line.startsWith("data: ")) continue;
1188
+ try {
1189
+ const ev = JSON.parse(line.slice(6));
1190
+ if (ev.type === "chunk" && ev.text) text += ev.text;
1191
+ else if (ev.type === "stderr" && ev.text) stderr += ev.text;
1192
+ else if (ev.type === "done") exitCode = ev.exitCode ?? 0;
1193
+ } catch {}
1194
+ }
1195
+
1196
+ const trimmed = text.trim();
1197
+ if (!trimmed && stderr.trim()) {
1198
+ throw new Error(stderr.trim());
1199
+ }
1200
+ if (!trimmed) {
1201
+ throw new Error(`planner produced no output${exitCode ? ` (exit ${exitCode})` : ""}`);
1202
+ }
1203
+ return trimmed;
1204
+ }
1205
+
963
1206
  async function getPhasedProgress(limit = 80) {
964
1207
  const { readFile } = await import("node:fs/promises");
965
1208
  const { existsSync } = await import("node:fs");
@@ -1908,7 +2151,7 @@ const server = http.createServer(async (req, res) => {
1908
2151
  return;
1909
2152
  }
1910
2153
 
1911
- // ── Auth Token (for Studio) ───────────────────────────────────────────────────
2154
+ // ── Auth Token (for Vibe) ─────────────────────────────────────────────────────
1912
2155
  if (url.pathname === "/api/auth/token") {
1913
2156
  res.writeHead(200, {
1914
2157
  "content-type": "application/json",
@@ -2335,24 +2578,76 @@ const server = http.createServer(async (req, res) => {
2335
2578
  if (url.pathname === "/api/enhance-prompt" && req.method === "POST") {
2336
2579
  let body = "";
2337
2580
  for await (const chunk of req) body += chunk;
2338
- const { text } = JSON.parse(body || "{}");
2339
- if (!text || typeof text !== "string") {
2581
+ let parsed;
2582
+ try { parsed = JSON.parse(body || "{}"); } catch {
2583
+ res.writeHead(400, { "content-type": "application/json" });
2584
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
2585
+ return;
2586
+ }
2587
+ const vr = validate(EnhancePromptSchema, parsed);
2588
+ if (!vr.ok) {
2340
2589
  res.writeHead(400, { "content-type": "application/json" });
2341
- res.end(JSON.stringify({ error: "missing text" }));
2590
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
2342
2591
  return;
2343
2592
  }
2593
+ const { text, projectId, engine: requestedEngine, model: requestedModel } = vr.data;
2344
2594
  try {
2345
- const enhanced = await enhancePromptWithGroq(text);
2595
+ // Default to cwd for planner context — engines need repo access to produce aware briefs.
2596
+ // Claude/Cursor/Codex use --add-dir for safe read access; crew-cli uses --project.
2597
+ // Fall back to temp dir only if no project context is available.
2598
+ let projectDir = process.cwd();
2599
+ if (projectId) {
2600
+ const regPath = path.join(CFG_DIR, "projects.json");
2601
+ if (fs.existsSync(regPath)) {
2602
+ const reg = JSON.parse(fs.readFileSync(regPath, "utf8") || "{}");
2603
+ const proj = reg[projectId];
2604
+ if (proj?.outputDir) projectDir = proj.outputDir;
2605
+ }
2606
+ }
2607
+ fs.mkdirSync(projectDir, { recursive: true });
2608
+
2609
+ const planner = resolvePlannerEngine(requestedEngine, requestedModel);
2610
+ if (!planner) throw new Error("No planning engine is configured or installed");
2611
+
2612
+ const planned = await collectPassthroughText({
2613
+ engine: planner.engine,
2614
+ message: buildRequirementPlanningPrompt(text, projectDir),
2615
+ projectDir,
2616
+ model: planner.model,
2617
+ permissionMode: planner.permissionMode,
2618
+ sandbox: planner.sandbox,
2619
+ forceL2: planner.engine === "crew-cli",
2620
+ });
2621
+
2346
2622
  res.writeHead(200, { "content-type": "application/json" });
2347
- res.end(JSON.stringify({ enhanced }));
2623
+ res.end(JSON.stringify({
2624
+ enhanced: planned,
2625
+ engine: planner.engine,
2626
+ model: planner.model,
2627
+ mode: planner.permissionMode || planner.sandbox || "prompt",
2628
+ source: planner.source,
2629
+ }));
2348
2630
  } catch (err) {
2349
- res.writeHead(200, { "content-type": "application/json" });
2350
- res.end(
2351
- JSON.stringify({
2352
- error: err?.message || String(err),
2353
- enhanced: null,
2354
- }),
2355
- );
2631
+ try {
2632
+ const enhanced = await enhancePromptWithGroq(text);
2633
+ res.writeHead(200, { "content-type": "application/json" });
2634
+ res.end(JSON.stringify({
2635
+ enhanced,
2636
+ engine: "groq",
2637
+ model: "llama-3.3-70b-versatile",
2638
+ mode: "fallback-rewrite",
2639
+ source: "fallback",
2640
+ warning: err?.message || String(err),
2641
+ }));
2642
+ } catch (fallbackErr) {
2643
+ res.writeHead(200, { "content-type": "application/json" });
2644
+ res.end(
2645
+ JSON.stringify({
2646
+ error: fallbackErr?.message || err?.message || String(fallbackErr || err),
2647
+ enhanced: null,
2648
+ }),
2649
+ );
2650
+ }
2356
2651
  }
2357
2652
  return;
2358
2653
  }
@@ -2457,9 +2752,19 @@ const server = http.createServer(async (req, res) => {
2457
2752
  if (url.pathname === "/api/continuous-build" && req.method === "POST") {
2458
2753
  let body = "";
2459
2754
  for await (const chunk of req) body += chunk;
2460
- const { requirement, projectId } = JSON.parse(body || "{}");
2461
- if (!requirement || typeof requirement !== "string")
2462
- throw new Error("missing requirement");
2755
+ let parsed;
2756
+ try { parsed = JSON.parse(body || "{}"); } catch {
2757
+ res.writeHead(400, { "content-type": "application/json" });
2758
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
2759
+ return;
2760
+ }
2761
+ const vr = validate(ContinuousBuildSchema, parsed);
2762
+ if (!vr.ok) {
2763
+ res.writeHead(400, { "content-type": "application/json" });
2764
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
2765
+ return;
2766
+ }
2767
+ const { requirement, projectId } = vr.data;
2463
2768
  let projectEnv = {};
2464
2769
  if (projectId) {
2465
2770
  const { existsSync: ex } = await import("node:fs");
@@ -3097,8 +3402,19 @@ const server = http.createServer(async (req, res) => {
3097
3402
  if (url.pathname === "/api/projects/delete" && req.method === "POST") {
3098
3403
  let body = "";
3099
3404
  for await (const chunk of req) body += chunk;
3100
- const { projectId } = JSON.parse(body || "{}");
3101
- if (!projectId) throw new Error("projectId required");
3405
+ let parsed;
3406
+ try { parsed = JSON.parse(body || "{}"); } catch {
3407
+ res.writeHead(400, { "content-type": "application/json" });
3408
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
3409
+ return;
3410
+ }
3411
+ const vr = validate(DeleteProjectSchema, parsed);
3412
+ if (!vr.ok) {
3413
+ res.writeHead(400, { "content-type": "application/json" });
3414
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
3415
+ return;
3416
+ }
3417
+ const { projectId } = vr.data;
3102
3418
  const registryFile = path.join(CFG_DIR, "projects.json");
3103
3419
  const { existsSync, rmSync, writeFileSync, unlinkSync } = await import("node:fs");
3104
3420
  const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
@@ -3146,9 +3462,19 @@ const server = http.createServer(async (req, res) => {
3146
3462
  if (url.pathname === "/api/projects/update" && req.method === "POST") {
3147
3463
  let body = "";
3148
3464
  for await (const chunk of req) body += chunk;
3149
- const { projectId, autoAdvance, name, description, outputDir } =
3150
- JSON.parse(body || "{}");
3151
- if (!projectId) throw new Error("projectId required");
3465
+ let parsed;
3466
+ try { parsed = JSON.parse(body || "{}"); } catch {
3467
+ res.writeHead(400, { "content-type": "application/json" });
3468
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
3469
+ return;
3470
+ }
3471
+ const vr = validate(UpdateProjectSchema, parsed);
3472
+ if (!vr.ok) {
3473
+ res.writeHead(400, { "content-type": "application/json" });
3474
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
3475
+ return;
3476
+ }
3477
+ const { projectId, autoAdvance, name, description, outputDir } = vr.data;
3152
3478
  const registryFile = path.join(CFG_DIR, "projects.json");
3153
3479
  const { existsSync } = await import("node:fs");
3154
3480
  const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
@@ -3423,8 +3749,19 @@ const server = http.createServer(async (req, res) => {
3423
3749
  if (url.pathname === "/api/dlq/replay" && req.method === "POST") {
3424
3750
  let body = "";
3425
3751
  for await (const chunk of req) body += chunk;
3426
- const { key } = JSON.parse(body);
3427
- if (!key) throw new Error("missing key");
3752
+ let parsed;
3753
+ try { parsed = JSON.parse(body); } catch {
3754
+ res.writeHead(400, { "content-type": "application/json" });
3755
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
3756
+ return;
3757
+ }
3758
+ const vr = validate(ReplayDLQSchema, parsed);
3759
+ if (!vr.ok) {
3760
+ res.writeHead(400, { "content-type": "application/json" });
3761
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
3762
+ return;
3763
+ }
3764
+ const { key } = vr.data;
3428
3765
  const { execSync } = await import("node:child_process");
3429
3766
  execSync(`"${ctlPath}" dlq-replay "${key}"`, {
3430
3767
  encoding: "utf8",
@@ -3779,7 +4116,19 @@ const server = http.createServer(async (req, res) => {
3779
4116
  ) {
3780
4117
  let body = "";
3781
4118
  for await (const chunk of req) body += chunk;
3782
- const { providerId } = JSON.parse(body);
4119
+ let parsed;
4120
+ try { parsed = JSON.parse(body); } catch {
4121
+ res.writeHead(400, { "content-type": "application/json" });
4122
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
4123
+ return;
4124
+ }
4125
+ const vr = validate(ProviderBuiltinTestSchema, parsed);
4126
+ if (!vr.ok) {
4127
+ res.writeHead(400, { "content-type": "application/json" });
4128
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
4129
+ return;
4130
+ }
4131
+ const { providerId } = vr.data;
3783
4132
  const apiKey = getBuiltinKey(providerId);
3784
4133
  const baseUrl = BUILTIN_URLS[providerId] || "";
3785
4134
  if (providerId === "ollama") {
@@ -4690,7 +5039,7 @@ const server = http.createServer(async (req, res) => {
4690
5039
  return;
4691
5040
  }
4692
5041
 
4693
- // ── Models API (list available models for CrewChat dropdown) ─────────────
5042
+ // ── Models API (list available models for crewchat dropdown) ─────────────
4694
5043
  if (url.pathname === "/api/models" && req.method === "GET") {
4695
5044
  try {
4696
5045
  const csSwarm = JSON.parse(
@@ -4780,7 +5129,7 @@ const server = http.createServer(async (req, res) => {
4780
5129
  return;
4781
5130
  }
4782
5131
 
4783
- // ── CLI Chat API (CrewChat CLI mode passthrough) ──────────────────────
5132
+ // ── CLI Chat API (crewchat CLI mode passthrough) ──────────────────────
4784
5133
  if (url.pathname === "/api/cli/chat" && req.method === "POST") {
4785
5134
  let body = "";
4786
5135
  for await (const chunk of req) body += chunk;
@@ -5020,7 +5369,7 @@ const server = http.createServer(async (req, res) => {
5020
5369
  return;
5021
5370
  }
5022
5371
 
5023
- // ── Chat Agent API (CrewChat direct agent chat) ──────────────────────────
5372
+ // ── Chat Agent API (crewchat direct agent chat) ──────────────────────────
5024
5373
  if (url.pathname === "/api/chat-agent" && req.method === "POST") {
5025
5374
  let body = "";
5026
5375
  for await (const chunk of req) body += chunk;
@@ -5068,7 +5417,7 @@ const server = http.createServer(async (req, res) => {
5068
5417
  return;
5069
5418
  }
5070
5419
 
5071
- // ── Dispatch API (CrewChat agent direct mode) ─────────────────────────────
5420
+ // ── Dispatch API (crewchat agent direct mode) ─────────────────────────────
5072
5421
  if (url.pathname === "/api/dispatch" && req.method === "POST") {
5073
5422
  let body = "";
5074
5423
  for await (const chunk of req) body += chunk;
@@ -5139,7 +5488,7 @@ const server = http.createServer(async (req, res) => {
5139
5488
  }
5140
5489
 
5141
5490
  if (url.pathname === "/api/transcribe-audio" && req.method === "POST") {
5142
- // Expects multipart/form-data with audio file (CrewChat: m4a, Dashboard: webm)
5491
+ // Expects multipart/form-data with audio file (crewchat: m4a, Dashboard: webm)
5143
5492
  // Per Groq docs: https://console.groq.com/docs/speech-to-text — file, model required
5144
5493
  const sendJson = (status, body) => {
5145
5494
  if (res.headersSent) return;
@@ -5149,7 +5498,7 @@ const server = http.createServer(async (req, res) => {
5149
5498
  try {
5150
5499
  const busboy = await import("busboy");
5151
5500
  const chunks = [];
5152
- let mimeType = "audio/m4a"; // CrewChat default
5501
+ let mimeType = "audio/m4a"; // crewchat default
5153
5502
  let resolved = false;
5154
5503
  const resolveOnce = () => {
5155
5504
  if (resolved) return;
@@ -5371,7 +5720,7 @@ const server = http.createServer(async (req, res) => {
5371
5720
  }
5372
5721
 
5373
5722
  if (wantAgentSSE && upstream.body) {
5374
- // SSE streaming path (Vibe, Studio)
5723
+ // SSE streaming path (Dashboard or Vibe)
5375
5724
  res.writeHead(200, {
5376
5725
  "content-type": "text/event-stream",
5377
5726
  "cache-control": "no-cache",
@@ -5471,7 +5820,7 @@ const server = http.createServer(async (req, res) => {
5471
5820
  }
5472
5821
 
5473
5822
  if (wantSSE && upstream.body) {
5474
- // SSE streaming path (Vibe, Studio)
5823
+ // SSE streaming path (Dashboard or Vibe)
5475
5824
  res.writeHead(200, {
5476
5825
  "content-type": "text/event-stream",
5477
5826
  "cache-control": "no-cache",
@@ -5820,9 +6169,19 @@ const server = http.createServer(async (req, res) => {
5820
6169
  const { readFile, writeFile } = await import("node:fs/promises");
5821
6170
  let body = "";
5822
6171
  for await (const chunk of req) body += chunk;
5823
- const { providerId, apiKey } = JSON.parse(body);
5824
- if (!providerId || !apiKey)
5825
- throw new Error("providerId and apiKey required");
6172
+ let parsed;
6173
+ try { parsed = JSON.parse(body); } catch {
6174
+ res.writeHead(400, { "content-type": "application/json" });
6175
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
6176
+ return;
6177
+ }
6178
+ const vr = validate(ProviderSaveSchema, parsed);
6179
+ if (!vr.ok) {
6180
+ res.writeHead(400, { "content-type": "application/json" });
6181
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
6182
+ return;
6183
+ }
6184
+ const { providerId, apiKey } = vr.data;
5826
6185
  const cfgPath = CFG_FILE;
5827
6186
  const cfg = JSON.parse(await readFile(cfgPath, "utf8"));
5828
6187
  const fromModels = cfg?.models?.providers?.[providerId];
@@ -5870,8 +6229,19 @@ const server = http.createServer(async (req, res) => {
5870
6229
  const { readFile, writeFile } = await import("node:fs/promises");
5871
6230
  let body = "";
5872
6231
  for await (const chunk of req) body += chunk;
5873
- const { id, baseUrl, apiKey, api } = JSON.parse(body);
5874
- if (!id || !baseUrl) throw new Error("id and baseUrl required");
6232
+ let parsed;
6233
+ try { parsed = JSON.parse(body); } catch {
6234
+ res.writeHead(400, { "content-type": "application/json" });
6235
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
6236
+ return;
6237
+ }
6238
+ const vr = validate(ProviderAddSchema, parsed);
6239
+ if (!vr.ok) {
6240
+ res.writeHead(400, { "content-type": "application/json" });
6241
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
6242
+ return;
6243
+ }
6244
+ const { id, baseUrl, apiKey, api } = vr.data;
5875
6245
  const cfgPath = CFG_FILE;
5876
6246
  const cfg = JSON.parse(await readFile(cfgPath, "utf8"));
5877
6247
  if (!cfg.models) cfg.models = {};
@@ -6191,7 +6561,19 @@ const server = http.createServer(async (req, res) => {
6191
6561
  if (url.pathname === "/api/providers/test" && req.method === "POST") {
6192
6562
  let body = "";
6193
6563
  for await (const chunk of req) body += chunk;
6194
- const { providerId } = JSON.parse(body);
6564
+ let parsed;
6565
+ try { parsed = JSON.parse(body); } catch {
6566
+ res.writeHead(400, { "content-type": "application/json" });
6567
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
6568
+ return;
6569
+ }
6570
+ const vr = validate(ProviderTestSchema, parsed);
6571
+ if (!vr.ok) {
6572
+ res.writeHead(400, { "content-type": "application/json" });
6573
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
6574
+ return;
6575
+ }
6576
+ const { providerId } = vr.data;
6195
6577
  const { readFile } = await import("node:fs/promises");
6196
6578
  const cfgPath = CFG_FILE;
6197
6579
  const cfg = JSON.parse(await readFile(cfgPath, "utf8").catch(() => "{}"));
@@ -7204,6 +7586,18 @@ ORDER BY day DESC, cost DESC;`;
7204
7586
  const { readFile, writeFile } = await import("node:fs/promises");
7205
7587
  let body = "";
7206
7588
  for await (const chunk of req) body += chunk;
7589
+ let parsed;
7590
+ try { parsed = JSON.parse(body); } catch {
7591
+ res.writeHead(400, { "content-type": "application/json" });
7592
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
7593
+ return;
7594
+ }
7595
+ const vr = validate(AgentConfigCreateSchema, parsed);
7596
+ if (!vr.ok) {
7597
+ res.writeHead(400, { "content-type": "application/json" });
7598
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
7599
+ return;
7600
+ }
7207
7601
  const {
7208
7602
  id,
7209
7603
  model,
@@ -7212,7 +7606,7 @@ ORDER BY day DESC, cost DESC;`;
7212
7606
  theme,
7213
7607
  systemPrompt,
7214
7608
  alsoAllow: reqAlsoAllow,
7215
- } = JSON.parse(body);
7609
+ } = vr.data;
7216
7610
  const rawId = String(id || "");
7217
7611
  const normalizedId =
7218
7612
  rawId && !rawId.startsWith("crew-")
@@ -7302,8 +7696,19 @@ ORDER BY day DESC, cost DESC;`;
7302
7696
  const { readFile, writeFile } = await import("node:fs/promises");
7303
7697
  let body = "";
7304
7698
  for await (const chunk of req) body += chunk;
7305
- const { agentId } = JSON.parse(body);
7306
- if (!agentId) throw new Error("agentId required");
7699
+ let parsed;
7700
+ try { parsed = JSON.parse(body); } catch {
7701
+ res.writeHead(400, { "content-type": "application/json" });
7702
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
7703
+ return;
7704
+ }
7705
+ const vr = validate(AgentConfigDeleteSchema, parsed);
7706
+ if (!vr.ok) {
7707
+ res.writeHead(400, { "content-type": "application/json" });
7708
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
7709
+ return;
7710
+ }
7711
+ const { agentId } = vr.data;
7307
7712
  const cfgPath = CFG_FILE;
7308
7713
  const promptsPath = path.join(CFG_DIR, "agent-prompts.json");
7309
7714
  const cfg = JSON.parse(await readFile(cfgPath, "utf8"));
@@ -7351,12 +7756,19 @@ ORDER BY day DESC, cost DESC;`;
7351
7756
  ) {
7352
7757
  let body = "";
7353
7758
  for await (const chunk of req) body += chunk;
7354
- const { agentId } = JSON.parse(body || "{}");
7355
- if (!agentId) {
7356
- res.writeHead(400);
7357
- res.end(JSON.stringify({ error: "agentId required" }));
7759
+ let parsed;
7760
+ try { parsed = JSON.parse(body || "{}"); } catch {
7761
+ res.writeHead(400, { "content-type": "application/json" });
7762
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
7763
+ return;
7764
+ }
7765
+ const vr = validate(AgentResetSessionSchema, parsed);
7766
+ if (!vr.ok) {
7767
+ res.writeHead(400, { "content-type": "application/json" });
7768
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
7358
7769
  return;
7359
7770
  }
7771
+ const { agentId } = vr.data;
7360
7772
  const { execFile } = await import("node:child_process");
7361
7773
  const bridgePath = path.join(CREWSWARM_DIR, "gateway-bridge.mjs");
7362
7774
  // 1. Reset the agent session via gateway-bridge --reset-session
@@ -7434,9 +7846,19 @@ ORDER BY day DESC, cost DESC;`;
7434
7846
  const { writeFile } = await import("node:fs/promises");
7435
7847
  let body = "";
7436
7848
  for await (const chunk of req) body += chunk;
7437
- const { roadmapFile, content } = JSON.parse(body);
7438
- if (!roadmapFile || content === undefined)
7439
- throw new Error("roadmapFile and content required");
7849
+ let parsed;
7850
+ try { parsed = JSON.parse(body); } catch {
7851
+ res.writeHead(400, { "content-type": "application/json" });
7852
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
7853
+ return;
7854
+ }
7855
+ const vr = validate(RoadmapWriteSchema, parsed);
7856
+ if (!vr.ok) {
7857
+ res.writeHead(400, { "content-type": "application/json" });
7858
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
7859
+ return;
7860
+ }
7861
+ const { roadmapFile, content } = vr.data;
7440
7862
  await writeFile(roadmapFile, content, "utf8");
7441
7863
  res.writeHead(200, { "content-type": "application/json" });
7442
7864
  res.end(JSON.stringify({ ok: true }));
@@ -7448,8 +7870,19 @@ ORDER BY day DESC, cost DESC;`;
7448
7870
  const { readFile, writeFile } = await import("node:fs/promises");
7449
7871
  let body = "";
7450
7872
  for await (const chunk of req) body += chunk;
7451
- const { roadmapFile } = JSON.parse(body);
7452
- if (!roadmapFile) throw new Error("roadmapFile required");
7873
+ let parsed;
7874
+ try { parsed = JSON.parse(body); } catch {
7875
+ res.writeHead(400, { "content-type": "application/json" });
7876
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
7877
+ return;
7878
+ }
7879
+ const vr = validate(RoadmapRetryFailedSchema, parsed);
7880
+ if (!vr.ok) {
7881
+ res.writeHead(400, { "content-type": "application/json" });
7882
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
7883
+ return;
7884
+ }
7885
+ const { roadmapFile } = vr.data;
7453
7886
  const content = await readFile(roadmapFile, "utf8");
7454
7887
  // Strip [!] markers back to [ ] and remove failure timestamps
7455
7888
  const reset = content
@@ -8117,11 +8550,22 @@ ORDER BY day DESC, cost DESC;`;
8117
8550
  }
8118
8551
 
8119
8552
  if (url.pathname === "/api/contacts/delete" && req.method === "POST") {
8553
+ let raw = "";
8554
+ for await (const chunk of req) raw += chunk;
8555
+ let parsed;
8556
+ try { parsed = JSON.parse(raw || "{}"); } catch {
8557
+ res.writeHead(400, { "content-type": "application/json" });
8558
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
8559
+ return;
8560
+ }
8561
+ const vr = validate(ContactDeleteSchema, parsed);
8562
+ if (!vr.ok) {
8563
+ res.writeHead(400, { "content-type": "application/json" });
8564
+ res.end(JSON.stringify({ ok: false, error: vr.error }));
8565
+ return;
8566
+ }
8567
+ const { contactId } = vr.data;
8120
8568
  try {
8121
- let raw = "";
8122
- for await (const chunk of req) raw += chunk;
8123
- const { contactId } = JSON.parse(raw || "{}");
8124
- if (!contactId) throw new Error("contactId required");
8125
8569
  const { deleteContact } = await import("../lib/contacts/index.mjs");
8126
8570
  deleteContact(contactId);
8127
8571
  res.writeHead(200, { "content-type": "application/json" });
@@ -8134,12 +8578,22 @@ ORDER BY day DESC, cost DESC;`;
8134
8578
  }
8135
8579
 
8136
8580
  if (url.pathname === "/api/contacts/send" && req.method === "POST") {
8581
+ let raw = "";
8582
+ for await (const chunk of req) raw += chunk;
8583
+ let parsedSend;
8584
+ try { parsedSend = JSON.parse(raw || "{}"); } catch {
8585
+ res.writeHead(400, { "content-type": "application/json" });
8586
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
8587
+ return;
8588
+ }
8589
+ const vrSend = validate(ContactSendSchema, parsedSend);
8590
+ if (!vrSend.ok) {
8591
+ res.writeHead(400, { "content-type": "application/json" });
8592
+ res.end(JSON.stringify({ ok: false, error: vrSend.error }));
8593
+ return;
8594
+ }
8595
+ const { contactId, platform, message } = vrSend.data;
8137
8596
  try {
8138
- let raw = "";
8139
- for await (const chunk of req) raw += chunk;
8140
- const { contactId, platform, message } = JSON.parse(raw || "{}");
8141
- if (!contactId || !message)
8142
- throw new Error("contactId and message required");
8143
8597
 
8144
8598
  const { getContact } = await import("../lib/contacts/index.mjs");
8145
8599
  const contact = getContact(contactId);
@@ -8155,12 +8609,19 @@ ORDER BY day DESC, cost DESC;`;
8155
8609
  : platformLinks.whatsapp;
8156
8610
  if (waJid) {
8157
8611
  // Call WhatsApp bridge's sendMessage function
8158
- const waUrl = `http://127.0.0.1:${process.env.WA_HTTP_PORT || 3000}/send`;
8159
- await fetch(waUrl, {
8612
+ const waUrl = `http://127.0.0.1:${process.env.WA_HTTP_PORT || 5015}/send`;
8613
+ const waRes = await fetch(waUrl, {
8160
8614
  method: "POST",
8161
8615
  headers: { "content-type": "application/json" },
8162
- body: JSON.stringify({ jid: waJid, message }),
8616
+ body: JSON.stringify({ jid: waJid, text: message }),
8163
8617
  });
8618
+ const waData = await waRes.json().catch(() => ({}));
8619
+ if (!waRes.ok || !waData.ok) {
8620
+ throw new Error(
8621
+ waData.error ||
8622
+ `WhatsApp send failed (${waRes.status})`,
8623
+ );
8624
+ }
8164
8625
  }
8165
8626
  }
8166
8627
 
@@ -8179,7 +8640,7 @@ ORDER BY day DESC, cost DESC;`;
8179
8640
  const tgCfg = JSON.parse(fs.readFileSync(TG_CONFIG_PATH, "utf8"));
8180
8641
  const botToken = tgCfg.token;
8181
8642
  if (botToken) {
8182
- await fetch(
8643
+ const tgRes = await fetch(
8183
8644
  `https://api.telegram.org/bot${botToken}/sendMessage`,
8184
8645
  {
8185
8646
  method: "POST",
@@ -8187,6 +8648,14 @@ ORDER BY day DESC, cost DESC;`;
8187
8648
  body: JSON.stringify({ chat_id: tgChatId, text: message }),
8188
8649
  },
8189
8650
  );
8651
+ const tgData = await tgRes.json().catch(() => ({}));
8652
+ if (!tgRes.ok || !tgData.ok) {
8653
+ throw new Error(
8654
+ tgData.description ||
8655
+ tgData.error ||
8656
+ `Telegram send failed (${tgRes.status})`,
8657
+ );
8658
+ }
8190
8659
  }
8191
8660
  }
8192
8661
  }
@@ -8458,7 +8927,7 @@ ORDER BY day DESC, cost DESC;`;
8458
8927
  {
8459
8928
  id: "crew-lead",
8460
8929
  label: "crew-lead",
8461
- description: "Chat commander — dashboard chat, CrewChat, Telegram",
8930
+ description: "Chat commander — dashboard chat, crewchat, Telegram",
8462
8931
  port: crewLeadPort,
8463
8932
  running: crewLeadUp,
8464
8933
  canRestart: true,
@@ -8590,7 +9059,7 @@ ORDER BY day DESC, cost DESC;`;
8590
9059
  label: "Vibe UI",
8591
9060
  description: studioUp
8592
9061
  ? "Monaco editor + agent chat — Cursor-like IDE for crewswarm"
8593
- : "Run: npm run studio:start (port 3333)",
9062
+ : "Run: npm run vibe:start (port 3333)",
8594
9063
  port: 3333,
8595
9064
  running: studioUp,
8596
9065
  canRestart: true,
@@ -8602,7 +9071,7 @@ ORDER BY day DESC, cost DESC;`;
8602
9071
  label: "Vibe Watch Server",
8603
9072
  description: watchUp
8604
9073
  ? "CLI → Vibe live reload WebSocket relay (port 3334)"
8605
- : "Run: npm run studio:watch — enables live file reload in Vibe",
9074
+ : "Run: npm run vibe:watch — enables live file reload in Vibe",
8606
9075
  port: 3334,
8607
9076
  running: watchUp,
8608
9077
  canRestart: true,
@@ -9683,14 +10152,35 @@ if (process.argv.includes("--print-html")) {
9683
10152
  }
9684
10153
 
9685
10154
  process.on("uncaughtException", (err) => {
10155
+ const msg = String(err?.message || err || "");
9686
10156
  console.error(
9687
10157
  "[dashboard] uncaughtException:",
9688
- err?.stack || err?.message || err,
10158
+ err?.stack || msg,
9689
10159
  );
9690
10160
 
9691
- // Always exit on uncaught exceptions process is in undefined state
9692
- console.error("[dashboard] FATAL — exiting due to uncaught exception");
9693
- process.exit(1);
10161
+ // Benign errors from engine passthrough / SSE streams keep alive
10162
+ if (
10163
+ msg === "terminated" ||
10164
+ msg === "aborted" ||
10165
+ /client.*disconnect/i.test(msg) ||
10166
+ /socket hang up/i.test(msg) ||
10167
+ /ECONNRESET/i.test(msg) ||
10168
+ /EPIPE/i.test(msg) ||
10169
+ /fetch failed/i.test(msg) ||
10170
+ /UND_ERR/i.test(msg)
10171
+ ) {
10172
+ console.error("[dashboard] Non-fatal uncaughtException — keeping alive");
10173
+ return;
10174
+ }
10175
+
10176
+ // Fatal errors: port conflicts, permissions, OOM — must exit
10177
+ if (/EADDRINUSE|EACCES|out of memory|cannot allocate/i.test(msg)) {
10178
+ console.error("[dashboard] FATAL — exiting due to uncaught exception");
10179
+ process.exit(1);
10180
+ }
10181
+
10182
+ // Default: log but keep alive — engine passthrough errors shouldn't kill the dashboard
10183
+ console.error("[dashboard] Unexpected uncaughtException — keeping alive (not fatal)");
9694
10184
  });
9695
10185
 
9696
10186
  process.on("unhandledRejection", (reason) => {