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.
- package/README.md +22 -9
- package/apps/dashboard/dist/assets/{chat-core-Cx4sTxDd.js → chat-core-3KirthZA.js} +1 -1
- package/apps/dashboard/dist/assets/index-GSWxxEPO.js +2 -0
- package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
- package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +1 -0
- package/apps/dashboard/dist/index.html +82 -11
- package/apps/vibe/README.md +2 -2
- package/apps/vibe/package.json +1 -1
- package/apps/vibe/server.mjs +3 -3
- package/crew-lead.mjs +34 -4
- package/lib/bridges/gateway-ws.mjs +4 -0
- package/lib/crew-lead/chat-handler.mjs +34 -0
- package/lib/crew-lead/http-server.mjs +55 -14
- package/lib/crew-lead/llm-caller.mjs +24 -8
- package/lib/crew-lead/prompts.mjs +7 -0
- package/lib/crew-lead/wave-dispatcher.mjs +15 -3
- package/lib/crew-lead/ws-router.mjs +219 -27
- package/lib/engines/engine-registry.mjs +9 -0
- package/lib/engines/rt-envelope.mjs +1 -0
- package/lib/engines/runners.mjs +5 -2
- package/lib/runtime/paths.mjs +12 -8
- package/package.json +35 -15
- package/scripts/capture-build-flow.mjs +118 -0
- package/scripts/coverage-report.mjs +209 -0
- package/scripts/coverage-summary.mjs +47 -0
- package/scripts/dashboard-validation.mjs +74 -0
- package/scripts/dashboard.mjs +560 -70
- package/scripts/live-bridge-matrix.mjs +79 -0
- package/scripts/live-cli-matrix.mjs +166 -0
- package/scripts/live-crewchat-check.mjs +42 -0
- package/scripts/live-engine-matrix.mjs +50 -0
- package/scripts/live-provider-failover-matrix.mjs +107 -0
- package/scripts/live-provider-matrix.mjs +228 -0
- package/scripts/restart-all-from-repo.sh +4 -4
- package/scripts/smoke-dispatch.mjs +4 -1
- package/scripts/test-blast-radius.mjs +204 -0
- package/scripts/test-report-summary.mjs +88 -0
- package/scripts/test-reporter.mjs +651 -0
- package/scripts/test-rerun.mjs +136 -0
- package/scripts/tmux-bridge +130 -0
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
- package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
- package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
- package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
- package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
- package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/dashboard/index.html +0 -6529
- package/apps/dashboard/package.json +0 -15
- package/apps/dashboard/src/app.js +0 -2828
- package/apps/dashboard/src/app.js.br +0 -0
- package/apps/dashboard/src/app.js.gz +0 -0
- package/apps/dashboard/src/chat/chat-actions.js +0 -1847
- package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
- package/apps/dashboard/src/chat/unified-messages.js +0 -327
- package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
- package/apps/dashboard/src/cli-process.js +0 -208
- package/apps/dashboard/src/cli-process.js.br +0 -0
- package/apps/dashboard/src/cli-process.js.gz +0 -0
- package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
- package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
- package/apps/dashboard/src/core/api.js +0 -18
- package/apps/dashboard/src/core/api.js.br +0 -0
- package/apps/dashboard/src/core/dom.js +0 -228
- package/apps/dashboard/src/core/dom.js.br +0 -0
- package/apps/dashboard/src/core/state.js +0 -91
- package/apps/dashboard/src/core/state.js.br +0 -0
- package/apps/dashboard/src/core/task-manager.js +0 -134
- package/apps/dashboard/src/core/task-manager.js.br +0 -0
- package/apps/dashboard/src/orchestration-status.js +0 -127
- package/apps/dashboard/src/orchestration-status.js.br +0 -0
- package/apps/dashboard/src/setup-wizard.js +0 -562
- package/apps/dashboard/src/setup-wizard.js.br +0 -0
- package/apps/dashboard/src/styles.css +0 -2085
- package/apps/dashboard/src/styles.css.br +0 -0
- package/apps/dashboard/src/styles.css.gz +0 -0
- package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
- package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
- package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/comms-tab.js +0 -955
- package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
- package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/engines-tab.js +0 -175
- package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/memory-tab.js +0 -182
- package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/models-tab.js +0 -450
- package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
- package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js +0 -663
- package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
- package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
- package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/services-tab.js +0 -202
- package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/settings-tab.js +0 -861
- package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/skills-tab.js +0 -284
- package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/spending-tab.js +0 -173
- package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
- package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
- package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/usage-tab.js +0 -390
- package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/waves-tab.js +0 -238
- package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
- package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
- package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
- package/apps/vibe/.crew/cost.json +0 -17
- package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
- package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
- package/apps/vibe/.crew/sandbox.json +0 -7
- package/apps/vibe/.crew/session.json +0 -330
- package/apps/vibe/.crew/training-data.jsonl +0 -0
- package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
- package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
- package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
- package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
- package/apps/vibe/ARCHITECTURE.md +0 -3393
- package/apps/vibe/QUICK-REFERENCE.md +0 -211
- package/apps/vibe/ROADMAP.md +0 -41
- package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
- package/apps/vibe/VISUAL-GUIDE.md +0 -378
- package/apps/vibe/capture-demo.mjs +0 -160
- package/apps/vibe/capture-full-demo.mjs +0 -255
- package/apps/vibe/capture-quickstart.mjs +0 -256
- package/apps/vibe/capture-vibe-assets.mjs +0 -71
- package/apps/vibe/capture-vibe-video.mjs +0 -260
- package/apps/vibe/check-buttons.js +0 -41
- package/apps/vibe/diagnose.html +0 -106
- package/apps/vibe/fix-buttons.js +0 -103
- package/apps/vibe/index.html +0 -3404
- package/apps/vibe/package-lock.json +0 -920
- package/apps/vibe/scripts/studio-pty-host.py +0 -117
- package/apps/vibe/src/main.js +0 -2940
- package/apps/vibe/src/register-all-languages.js +0 -98
- package/apps/vibe/start-studio.sh +0 -11
- package/apps/vibe/test/accessibility-tests.js +0 -77
- package/apps/vibe/test/browser-performance-audit.mjs +0 -205
- package/apps/vibe/test/performance-tests.js +0 -120
- package/apps/vibe/test/security-tests.js +0 -213
- package/apps/vibe/tests/e2e.local.mjs +0 -54
- package/apps/vibe/tests/server.smoke.mjs +0 -106
- package/apps/vibe/update_website.mjs +0 -74
- package/apps/vibe/vite.config.js +0 -19
- package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
- package/lib/engines/rt-envelope.mjs.backup-current +0 -870
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { shouldSkipGeminiPassthroughLine } from "../gemini-cli-passthrough-noise.mjs";
|
|
21
21
|
import { applyProjectDirToPipelineSteps } from "../dispatch/parsers.mjs";
|
|
22
22
|
import { normalizeProjectDir } from "../runtime/project-dir.mjs";
|
|
23
|
-
import { CREWSWARM_REPO_ROOT } from "../runtime/config.mjs";
|
|
23
|
+
import { CREWSWARM_REPO_ROOT, loadSwarmConfig } from "../runtime/config.mjs";
|
|
24
24
|
import { resolveCursorLaunchSpec } from "../engines/cursor-launcher.mjs";
|
|
25
25
|
|
|
26
26
|
let _deps = {};
|
|
@@ -3532,6 +3532,9 @@ export function createAndStartServer(PORT) {
|
|
|
3532
3532
|
sessionId: reqSessionId,
|
|
3533
3533
|
injectHistory,
|
|
3534
3534
|
model: reqModel,
|
|
3535
|
+
permissionMode: reqPermissionMode,
|
|
3536
|
+
sandbox: reqSandbox,
|
|
3537
|
+
forceL2: reqForceL2,
|
|
3535
3538
|
} = JSON.parse(body || "{}");
|
|
3536
3539
|
if (!message) {
|
|
3537
3540
|
json(res, 400, { ok: false, error: "message required" });
|
|
@@ -3762,7 +3765,7 @@ export function createAndStartServer(PORT) {
|
|
|
3762
3765
|
"never",
|
|
3763
3766
|
"exec",
|
|
3764
3767
|
"--sandbox",
|
|
3765
|
-
"danger-full-access",
|
|
3768
|
+
reqSandbox || "danger-full-access",
|
|
3766
3769
|
"--skip-git-repo-check",
|
|
3767
3770
|
"--color",
|
|
3768
3771
|
"never",
|
|
@@ -3859,12 +3862,16 @@ export function createAndStartServer(PORT) {
|
|
|
3859
3862
|
"-p",
|
|
3860
3863
|
"--setting-sources",
|
|
3861
3864
|
"user",
|
|
3862
|
-
"--dangerously-skip-permissions",
|
|
3863
3865
|
"--output-format",
|
|
3864
3866
|
"stream-json",
|
|
3865
3867
|
"--verbose",
|
|
3866
3868
|
"--include-partial-messages",
|
|
3867
3869
|
];
|
|
3870
|
+
if (reqPermissionMode) {
|
|
3871
|
+
args.push("--permission-mode", reqPermissionMode);
|
|
3872
|
+
} else {
|
|
3873
|
+
args.push("--dangerously-skip-permissions");
|
|
3874
|
+
}
|
|
3868
3875
|
if (projectDir) args.push("--add-dir", projectDir);
|
|
3869
3876
|
if (reqModel) args.push("--model", reqModel);
|
|
3870
3877
|
// Prefer --resume <session-id> for per-project isolation; fall back to --continue (most recent global)
|
|
@@ -3877,7 +3884,10 @@ export function createAndStartServer(PORT) {
|
|
|
3877
3884
|
} else if (continueSession) {
|
|
3878
3885
|
args.push("--continue");
|
|
3879
3886
|
}
|
|
3880
|
-
|
|
3887
|
+
// Skip user MCP servers to avoid 30s+ init hangs
|
|
3888
|
+
args.push("--strict-mcp-config", "--mcp-config", path.join(os.homedir(), ".crewswarm", "config", "empty-mcp.json"));
|
|
3889
|
+
// -- separates flags from prompt (--mcp-config is variadic and eats positional args)
|
|
3890
|
+
args.push("--", finalMessage);
|
|
3881
3891
|
}
|
|
3882
3892
|
|
|
3883
3893
|
send({ type: "start", engine, message: message.slice(0, 80) });
|
|
@@ -3901,9 +3911,22 @@ export function createAndStartServer(PORT) {
|
|
|
3901
3911
|
const spawnCwd =
|
|
3902
3912
|
engine === "claude" && projectDir ? "/tmp" : projectDir;
|
|
3903
3913
|
|
|
3914
|
+
// Merge crewswarm.json env vars into spawn env (crew-cli L2, model overrides, etc.)
|
|
3915
|
+
const spawnEnv = { ...process.env };
|
|
3916
|
+
try {
|
|
3917
|
+
const _sysCfg = loadSwarmConfig();
|
|
3918
|
+
if (_sysCfg?.env && typeof _sysCfg.env === "object") {
|
|
3919
|
+
for (const [k, v] of Object.entries(_sysCfg.env)) {
|
|
3920
|
+
if (!spawnEnv[k] && v != null) spawnEnv[k] = String(v);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
} catch {}
|
|
3924
|
+
// Force L2 planner for crew-cli when requested (enhance-prompt planner path)
|
|
3925
|
+
if (reqForceL2) spawnEnv.CREW_FORCE_L2 = "true";
|
|
3926
|
+
|
|
3904
3927
|
const proc = _spawn(bin, args, {
|
|
3905
3928
|
cwd: spawnCwd,
|
|
3906
|
-
env:
|
|
3929
|
+
env: spawnEnv,
|
|
3907
3930
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3908
3931
|
});
|
|
3909
3932
|
let lineBuffer = "";
|
|
@@ -3916,7 +3939,7 @@ export function createAndStartServer(PORT) {
|
|
|
3916
3939
|
let passthroughGracefulEnd = false;
|
|
3917
3940
|
|
|
3918
3941
|
const passthroughTimeoutMs = Number(
|
|
3919
|
-
process.env.CREWSWARM_PASSTHROUGH_TIMEOUT_MS || "
|
|
3942
|
+
process.env.CREWSWARM_PASSTHROUGH_TIMEOUT_MS || "300000",
|
|
3920
3943
|
);
|
|
3921
3944
|
const passthroughWatchdog = setTimeout(() => {
|
|
3922
3945
|
send({
|
|
@@ -4207,15 +4230,33 @@ export function createAndStartServer(PORT) {
|
|
|
4207
4230
|
// crew-cli special handling: extract JSON from output (skipping logs) and send only response field
|
|
4208
4231
|
if (engine === "crew-cli" && fullOutput.trim()) {
|
|
4209
4232
|
try {
|
|
4210
|
-
// crew-cli
|
|
4211
|
-
//
|
|
4212
|
-
const
|
|
4213
|
-
|
|
4214
|
-
)
|
|
4215
|
-
|
|
4216
|
-
|
|
4233
|
+
// crew-cli emits logs before JSON — find the JSON by scanning for the last
|
|
4234
|
+
// line that starts with { and contains "chat.result"
|
|
4235
|
+
const lines = fullOutput.split("\n");
|
|
4236
|
+
let jsonStart = -1;
|
|
4237
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
4238
|
+
if (lines[i].trimStart().startsWith("{") && fullOutput.indexOf('"kind"') >= 0) {
|
|
4239
|
+
jsonStart = i;
|
|
4240
|
+
break;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
if (jsonStart < 0) {
|
|
4244
|
+
throw new Error("No JSON object found in crew-cli output");
|
|
4245
|
+
}
|
|
4246
|
+
// Collect from jsonStart to end, parse
|
|
4247
|
+
const jsonCandidate = lines.slice(jsonStart).join("\n");
|
|
4248
|
+
let parsed;
|
|
4249
|
+
try {
|
|
4250
|
+
parsed = JSON.parse(jsonCandidate);
|
|
4251
|
+
} catch {
|
|
4252
|
+
// Try to find just the response field with regex
|
|
4253
|
+
const respMatch = jsonCandidate.match(/"response":\s*"((?:[^"\\]|\\.)*)"/);
|
|
4254
|
+
if (respMatch) {
|
|
4255
|
+
parsed = { response: respMatch[1].replace(/\\n/g, "\n").replace(/\\"/g, '"') };
|
|
4256
|
+
} else {
|
|
4257
|
+
throw new Error("Failed to parse crew-cli JSON output");
|
|
4258
|
+
}
|
|
4217
4259
|
}
|
|
4218
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
4219
4260
|
if (parsed.response) {
|
|
4220
4261
|
const responseText = typeof parsed.response === 'string'
|
|
4221
4262
|
? parsed.response
|
|
@@ -128,14 +128,11 @@ async function callGeminiCliForChat(messages) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
export async function _callLLMOnce(baseUrl, apiKey, modelId, providerKey, messages, options = {}) {
|
|
131
|
-
|
|
132
|
-
const isOpenRouter = providerKey === "openrouter" || baseUrl.includes("openrouter.ai");
|
|
133
|
-
if (isOpenRouter && modelId && !modelId.startsWith("openrouter/")) {
|
|
134
|
-
modelId = "openrouter/" + modelId;
|
|
135
|
-
}
|
|
131
|
+
modelId = normalizeExternalModelId(modelId, providerKey, baseUrl);
|
|
136
132
|
const isAnthropic = providerKey === "anthropic" || baseUrl.includes("anthropic.com");
|
|
137
133
|
const enableStreaming = options.stream || false;
|
|
138
134
|
const onStreamToken = options.onStreamToken || null;
|
|
135
|
+
const requestTimeoutMs = Number(options.timeoutMs || _LLM_TIMEOUT) || _LLM_TIMEOUT;
|
|
139
136
|
|
|
140
137
|
const headers = { "content-type": "application/json" };
|
|
141
138
|
if (isAnthropic) {
|
|
@@ -150,9 +147,10 @@ export async function _callLLMOnce(baseUrl, apiKey, modelId, providerKey, messag
|
|
|
150
147
|
const isOpenAI = providerKey === "openai" || baseUrl.includes("openai.com");
|
|
151
148
|
|
|
152
149
|
// Gemini 2.5 Flash: input 1,048,576 tokens / output 65,536 tokens
|
|
153
|
-
// Anthropic Claude
|
|
150
|
+
// Anthropic Claude varies by model; Haiku is stricter at 4,096 output tokens
|
|
154
151
|
// All others: 4,096 safe default
|
|
155
|
-
const
|
|
152
|
+
const anthropicMaxOutputTokens = /haiku/i.test(modelId) ? 4096 : 8192;
|
|
153
|
+
const maxOutputTokens = isGemini ? 16384 : isAnthropic ? anthropicMaxOutputTokens : 4096;
|
|
156
154
|
|
|
157
155
|
let body, endpoint;
|
|
158
156
|
|
|
@@ -230,7 +228,7 @@ export async function _callLLMOnce(baseUrl, apiKey, modelId, providerKey, messag
|
|
|
230
228
|
method: "POST",
|
|
231
229
|
headers,
|
|
232
230
|
body: JSON.stringify(body),
|
|
233
|
-
signal: AbortSignal.timeout(
|
|
231
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
234
232
|
});
|
|
235
233
|
|
|
236
234
|
if (!res.ok) {
|
|
@@ -321,6 +319,24 @@ export async function _callLLMOnce(baseUrl, apiKey, modelId, providerKey, messag
|
|
|
321
319
|
return reply;
|
|
322
320
|
}
|
|
323
321
|
|
|
322
|
+
export function normalizeExternalModelId(modelId, providerKey, baseUrl = "") {
|
|
323
|
+
let normalized = String(modelId || "").trim();
|
|
324
|
+
if (!normalized) return normalized;
|
|
325
|
+
|
|
326
|
+
const isOpenRouter = providerKey === "openrouter" || baseUrl.includes("openrouter.ai");
|
|
327
|
+
const isPerplexity = providerKey === "perplexity" || baseUrl.includes("api.perplexity.ai");
|
|
328
|
+
|
|
329
|
+
if (isOpenRouter) {
|
|
330
|
+
normalized = normalized.replace(/^openrouter\//i, "");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (isPerplexity) {
|
|
334
|
+
normalized = normalized.replace(/^perplexity\//i, "");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return normalized;
|
|
338
|
+
}
|
|
339
|
+
|
|
324
340
|
function _recordCrewLeadTokens(modelId, providerKey, usage) {
|
|
325
341
|
if (!usage) return;
|
|
326
342
|
const p = Number(usage.prompt_tokens || usage.input_tokens || 0);
|
|
@@ -150,6 +150,13 @@ export function buildSystemPrompt(cfg) {
|
|
|
150
150
|
`- With acceptance criteria: @@DISPATCH {"agent":"crew-coder","task":"Write JWT auth middleware","verify":"@@READ_FILE src/auth.ts","done":"exports verifyToken, returns 401 on invalid"}`,
|
|
151
151
|
"- You MUST emit the @@DISPATCH line. Describing a dispatch in prose without the line = NOTHING happens.",
|
|
152
152
|
"",
|
|
153
|
+
"ENGINE ROUTING: tasks are routed to CLI engines or direct-llm based on keyword matching.",
|
|
154
|
+
"- Tasks with coding keywords (create, build, implement, fix, refactor, add, update, modify, code, function, class, component, api, endpoint, route, test, bug, error, issue, file, script, module, package) → CLI engine (native file I/O, shell, full repo context).",
|
|
155
|
+
"- Tasks WITHOUT coding keywords → direct-llm (LLM API call; can still write files via @@WRITE_FILE markers, but no shell or repo context).",
|
|
156
|
+
"- CLI engines are better for complex multi-file coding. direct-llm is fine for research reports, single-file writes, and simple tasks.",
|
|
157
|
+
"- When dispatching build tasks, use verbs like 'Create', 'Build', 'Implement', 'Fix' to ensure CLI routing.",
|
|
158
|
+
"- This is automatic keyword matching — no LLM call. See docs/ORCHESTRATION-PROTOCOL.md for the full keyword list.",
|
|
159
|
+
"",
|
|
153
160
|
"PIPELINE: when user wants multi-agent coordinated work.",
|
|
154
161
|
"- Triggers: 'build me X', 'create X', 'kick off', 'rally the crew', 'dispatch the crew'.",
|
|
155
162
|
"- Before firing for complex tasks (3+ agents / 2+ waves): show plan in 1-3 lines, ask 'Fire it?'. Skip confirmation for single-agent or explicit 'go build'.",
|
|
@@ -377,7 +377,17 @@ export function dispatchPipelineWave(pipelineId) {
|
|
|
377
377
|
// this path: when Cursor CLI fails, orchestrator falls back to plain LLM text
|
|
378
378
|
// with @mentions — subagents never run and the pipeline advances on garbage.
|
|
379
379
|
// Direct per-agent dispatch is reliable for project pipelines.
|
|
380
|
-
if
|
|
380
|
+
// Only route through orchestrator if it has a CLI engine that can actually dispatch.
|
|
381
|
+
// Without a CLI engine, the orchestrator just describes dispatching in text — useless.
|
|
382
|
+
const orchestratorHasCliEngine = (() => {
|
|
383
|
+
try {
|
|
384
|
+
const agents = (_deps.loadAgentList || (() => []))();
|
|
385
|
+
const orch = agents.find(a => a.id === "crew-orchestrator");
|
|
386
|
+
if (!orch) return false;
|
|
387
|
+
return !!(orch.engine || orch.useClaudeCode || orch.useCursorCli || orch.useCursor || orch.useCodex || orch.useCrewCLI || orch.useGeminiCli || orch.useOpenCode);
|
|
388
|
+
} catch { return false; }
|
|
389
|
+
})();
|
|
390
|
+
if (_deps._cursorWavesEnabled && orchestratorHasCliEngine && waveSteps.length > 1 && !pipeline.projectDir) {
|
|
381
391
|
const waveManifest = {
|
|
382
392
|
wave: currentWave + 1,
|
|
383
393
|
projectDir: pipeline.projectDir || "",
|
|
@@ -702,8 +712,10 @@ export function dispatchTask(agent, task, sessionId = "owner", pipelineMeta = nu
|
|
|
702
712
|
task = _deps.writeTaskBrief?.(agent, task, projectDir) ?? task;
|
|
703
713
|
}
|
|
704
714
|
|
|
705
|
-
|
|
706
|
-
|
|
715
|
+
let rp = typeof _deps.getRtPublish === "function" ? _deps.getRtPublish() : null;
|
|
716
|
+
if (!rp && typeof _deps.getRtPublish === "function") {
|
|
717
|
+
console.warn(`[wave-dispatcher] RT publish not available for ${agent} — will use fallback`);
|
|
718
|
+
}
|
|
707
719
|
if (rp) {
|
|
708
720
|
try {
|
|
709
721
|
// Build extraFlags: start with global settings, override with pipeline-specific settings
|
|
@@ -7,6 +7,60 @@ import { applyProjectDirToPipelineSteps } from "../dispatch/parsers.mjs";
|
|
|
7
7
|
let reconnectTimer = null;
|
|
8
8
|
let isConnecting = false;
|
|
9
9
|
let crewLeadHeartbeat = null;
|
|
10
|
+
let currentWs = null; // Global ref to current WebSocket — prevents stale closures
|
|
11
|
+
let connectionId = 0; // Monotonic ID to detect stale connections
|
|
12
|
+
let reconnectAttempts = 0; // For exponential backoff
|
|
13
|
+
|
|
14
|
+
const CODER_AGENT_RE = /crew-coder|crew-frontend|crew-fixer|crew-ml|crew-coder-back|crew-coder-front/;
|
|
15
|
+
|
|
16
|
+
function normalizeEngineId(value) {
|
|
17
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
18
|
+
if (!raw) return null;
|
|
19
|
+
if (raw === "claude" || raw === "claude-code" || raw.includes("claude code")) return "claude";
|
|
20
|
+
if (raw === "codex" || raw === "codex-cli" || raw.includes("codex")) return "codex";
|
|
21
|
+
if (raw === "cursor" || raw === "cursor-cli" || raw.includes("cursor")) return "cursor";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function inferDispatchEngine(dispatch = null, message = "") {
|
|
26
|
+
const explicit =
|
|
27
|
+
normalizeEngineId(dispatch?.engineUsed)
|
|
28
|
+
|| normalizeEngineId(dispatch?.runtime)
|
|
29
|
+
|| (dispatch?.useCodex === true ? "codex" : null)
|
|
30
|
+
|| (dispatch?.useCursorCli === true ? "cursor" : null)
|
|
31
|
+
|| (dispatch?.useClaudeCode === true ? "claude" : null);
|
|
32
|
+
if (explicit) return explicit;
|
|
33
|
+
|
|
34
|
+
const text = String(message || "");
|
|
35
|
+
if (/claude\s*code|anthropic|sonnet|opus/i.test(text)) return "claude";
|
|
36
|
+
if (/codex|gpt-5(\.\d+)?-codex|openai/i.test(text)) return "codex";
|
|
37
|
+
if (/cursor/i.test(text)) return "cursor";
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getNextCoderEngine(currentEngine) {
|
|
42
|
+
const current = normalizeEngineId(currentEngine);
|
|
43
|
+
if (current === "claude") return "codex";
|
|
44
|
+
if (current === "codex") return "claude";
|
|
45
|
+
if (current === "cursor") return null;
|
|
46
|
+
return "codex";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildEngineFallbackMeta(dispatch = null, currentEngine = null, trigger = "rate-limit-fallback") {
|
|
50
|
+
const nextEngine = getNextCoderEngine(currentEngine);
|
|
51
|
+
if (!nextEngine) return null;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...(dispatch || {}),
|
|
55
|
+
useClaudeCode: nextEngine === "claude",
|
|
56
|
+
useCodex: nextEngine === "codex",
|
|
57
|
+
useCursorCli: nextEngine === "cursor",
|
|
58
|
+
runtime: nextEngine,
|
|
59
|
+
engineFallbackFrom: normalizeEngineId(currentEngine),
|
|
60
|
+
engineFallbackTo: nextEngine,
|
|
61
|
+
triggeredBy: trigger,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
10
64
|
|
|
11
65
|
export function initWsRouter(deps) {
|
|
12
66
|
const {
|
|
@@ -29,6 +83,7 @@ export function initWsRouter(deps) {
|
|
|
29
83
|
appendHistory,
|
|
30
84
|
pendingPipelines,
|
|
31
85
|
handleAutonomousMentions,
|
|
86
|
+
saveProjectMessage,
|
|
32
87
|
checkWaveQualityGate,
|
|
33
88
|
failPipelineOnQualityGate,
|
|
34
89
|
savePipelineState,
|
|
@@ -40,6 +95,25 @@ export function initWsRouter(deps) {
|
|
|
40
95
|
autonomousPmLoopSessions
|
|
41
96
|
} = deps;
|
|
42
97
|
|
|
98
|
+
function scheduleReconnect() {
|
|
99
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
100
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, ... capped at 30s
|
|
101
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
102
|
+
reconnectAttempts++;
|
|
103
|
+
console.log(`[crew-lead] RT reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
|
|
104
|
+
reconnectTimer = setTimeout(connectRT, delay);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cleanupConnection() {
|
|
108
|
+
if (crewLeadHeartbeat) { clearInterval(crewLeadHeartbeat); crewLeadHeartbeat = null; }
|
|
109
|
+
setRtPublish(null);
|
|
110
|
+
// Close old WebSocket and remove all listeners to prevent leaks
|
|
111
|
+
if (currentWs) {
|
|
112
|
+
try { currentWs.removeAllListeners(); currentWs.close(); } catch {}
|
|
113
|
+
currentWs = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
43
117
|
function connectRT() {
|
|
44
118
|
if (isConnecting) {
|
|
45
119
|
console.log("[crew-lead] Already connecting to RT, skipping duplicate call");
|
|
@@ -51,17 +125,27 @@ export function initWsRouter(deps) {
|
|
|
51
125
|
reconnectTimer = null;
|
|
52
126
|
}
|
|
53
127
|
|
|
128
|
+
// Clean up any previous connection before creating a new one (#1, #8)
|
|
129
|
+
cleanupConnection();
|
|
130
|
+
|
|
54
131
|
isConnecting = true;
|
|
132
|
+
const thisConnId = ++connectionId;
|
|
55
133
|
const ws = new WebSocket(RT_URL);
|
|
134
|
+
currentWs = ws;
|
|
56
135
|
|
|
57
136
|
ws.on("open", () => {
|
|
137
|
+
if (thisConnId !== connectionId) return; // stale (#7)
|
|
58
138
|
console.log("[crew-lead] RT socket open");
|
|
59
139
|
isConnecting = false;
|
|
60
140
|
});
|
|
61
141
|
|
|
62
142
|
ws.on("message", (raw) => {
|
|
143
|
+
if (thisConnId !== connectionId) return; // stale (#7)
|
|
63
144
|
let p;
|
|
64
|
-
try { p = JSON.parse(raw.toString()); } catch {
|
|
145
|
+
try { p = JSON.parse(raw.toString()); } catch (parseErr) {
|
|
146
|
+
console.warn(`[crew-lead] RT message parse failed: ${parseErr.message} (${raw.toString().slice(0, 80)})`); // #15
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
65
149
|
|
|
66
150
|
if (p.type === "server.hello") {
|
|
67
151
|
ws.send(JSON.stringify({ type: "hello", agent: "crew-lead", token: RT_TOKEN }));
|
|
@@ -69,10 +153,27 @@ export function initWsRouter(deps) {
|
|
|
69
153
|
}
|
|
70
154
|
if (p.type === "hello.ack") {
|
|
71
155
|
ws.send(JSON.stringify({ type: "subscribe", channels: ["done", "events", "command", "issues", "status"] }));
|
|
156
|
+
reconnectAttempts = 0; // Reset backoff on successful connection (#12)
|
|
72
157
|
|
|
158
|
+
// rtPublish always uses currentWs, not a captured closure (#6)
|
|
73
159
|
setRtPublish(({ channel, type, to, payload }) => {
|
|
74
160
|
const taskId = crypto.randomUUID();
|
|
75
|
-
|
|
161
|
+
const activeWs = currentWs;
|
|
162
|
+
if (!activeWs || activeWs.readyState !== WebSocket.OPEN) {
|
|
163
|
+
console.error(`[crew-lead] RT ws not open (state=${activeWs?.readyState}) — triggering reconnect`);
|
|
164
|
+
setRtPublish(null);
|
|
165
|
+
scheduleReconnect();
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
activeWs.send(JSON.stringify({ type: "publish", channel, messageType: type, to, taskId, priority: "high", payload }));
|
|
170
|
+
} catch (sendErr) {
|
|
171
|
+
console.error(`[crew-lead] RT ws.send failed (${sendErr.message}) — triggering reconnect`);
|
|
172
|
+
setRtPublish(null);
|
|
173
|
+
try { activeWs.close(); } catch {}
|
|
174
|
+
scheduleReconnect();
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
76
177
|
return taskId;
|
|
77
178
|
});
|
|
78
179
|
|
|
@@ -80,16 +181,42 @@ export function initWsRouter(deps) {
|
|
|
80
181
|
setTimeout(resumePipelines, 2000);
|
|
81
182
|
startBackgroundLoop();
|
|
82
183
|
|
|
184
|
+
// Heartbeat with failure detection (#3, #5, #10)
|
|
83
185
|
if (crewLeadHeartbeat) clearInterval(crewLeadHeartbeat);
|
|
186
|
+
let missedHeartbeats = 0;
|
|
84
187
|
crewLeadHeartbeat = setInterval(() => {
|
|
188
|
+
if (thisConnId !== connectionId) {
|
|
189
|
+
clearInterval(crewLeadHeartbeat);
|
|
190
|
+
crewLeadHeartbeat = null;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
85
193
|
try {
|
|
194
|
+
if (!currentWs || currentWs.readyState !== WebSocket.OPEN) {
|
|
195
|
+
missedHeartbeats++;
|
|
196
|
+
console.warn(`[crew-lead] Heartbeat skipped — ws not open (missed=${missedHeartbeats})`);
|
|
197
|
+
if (missedHeartbeats >= 3) {
|
|
198
|
+
console.error(`[crew-lead] 3 missed heartbeats — triggering reconnect`);
|
|
199
|
+
cleanupConnection();
|
|
200
|
+
scheduleReconnect();
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
missedHeartbeats = 0;
|
|
86
205
|
const taskId = crypto.randomUUID();
|
|
87
|
-
|
|
206
|
+
currentWs.send(JSON.stringify({
|
|
88
207
|
type: "publish", channel: "status", messageType: "agent.heartbeat",
|
|
89
208
|
to: "broadcast", taskId, priority: "low",
|
|
90
209
|
payload: { agent: "crew-lead", ts: new Date().toISOString() },
|
|
91
210
|
}));
|
|
92
|
-
} catch {
|
|
211
|
+
} catch (hbErr) {
|
|
212
|
+
console.warn(`[crew-lead] Heartbeat send failed: ${hbErr.message}`);
|
|
213
|
+
missedHeartbeats++;
|
|
214
|
+
if (missedHeartbeats >= 3) {
|
|
215
|
+
console.error(`[crew-lead] 3 heartbeat failures — triggering reconnect`);
|
|
216
|
+
cleanupConnection();
|
|
217
|
+
scheduleReconnect();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
93
220
|
}, 30000);
|
|
94
221
|
return;
|
|
95
222
|
}
|
|
@@ -103,7 +230,10 @@ export function initWsRouter(deps) {
|
|
|
103
230
|
|
|
104
231
|
if (p.type === "message" && p.envelope) {
|
|
105
232
|
const env = p.envelope;
|
|
106
|
-
if (env.id)
|
|
233
|
+
if (env.id) {
|
|
234
|
+
try { ws.send(JSON.stringify({ type: "ack", messageId: env.id, status: "received" })); }
|
|
235
|
+
catch (ackErr) { console.warn(`[crew-lead] Ack send failed for ${env.id}: ${ackErr.message}`); }
|
|
236
|
+
}
|
|
107
237
|
|
|
108
238
|
const from = env.from || env.sender_agent_id || env.payload?.source || "";
|
|
109
239
|
const msgType = env.messageType || env.type || "";
|
|
@@ -154,8 +284,22 @@ export function initWsRouter(deps) {
|
|
|
154
284
|
emitTaskLifecycle("failed", { taskId: failedTaskId, agentId: failedAgent, taskType: "task", error: { message: errMsg } });
|
|
155
285
|
const dispatch = pendingDispatches.get(failedTaskId);
|
|
156
286
|
if (dispatch && RATE_LIMIT_PATTERN.test(errMsg)) {
|
|
157
|
-
const fallback = getRateLimitFallback(failedAgent);
|
|
158
287
|
const targetSession = dispatch.sessionId || "owner";
|
|
288
|
+
const currentEngine = inferDispatchEngine({ ...dispatch, engineUsed: env.payload?.engineUsed || dispatch.engineUsed }, errMsg);
|
|
289
|
+
const engineFallbackMeta = CODER_AGENT_RE.test(failedAgent)
|
|
290
|
+
? buildEngineFallbackMeta(dispatch, currentEngine, "rate-limit-engine-fallback")
|
|
291
|
+
: null;
|
|
292
|
+
if (engineFallbackMeta) {
|
|
293
|
+
pendingDispatches.delete(failedTaskId);
|
|
294
|
+
const newTaskId = dispatchTask(failedAgent, dispatch.task, targetSession, engineFallbackMeta);
|
|
295
|
+
if (newTaskId) {
|
|
296
|
+
appendHistory("default", targetSession, "system", `[crew-lead] ${failedAgent} hit rate limit on ${currentEngine || "current engine"} (${errMsg.slice(0, 80)}). Re-dispatched same task on ${engineFallbackMeta.engineFallbackTo}.`);
|
|
297
|
+
broadcastSSE({ type: "agent_reply", from: "crew-lead", content: `Rate limit: retried ${failedAgent} on ${engineFallbackMeta.engineFallbackTo}.`, sessionId: targetSession, taskId: failedTaskId, ts: Date.now() });
|
|
298
|
+
console.log(`[crew-lead] Rate limit engine fallback: ${failedAgent} ${currentEngine || "unknown"} → ${engineFallbackMeta.engineFallbackTo}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const fallback = getRateLimitFallback(failedAgent);
|
|
159
303
|
if (fallback !== failedAgent) {
|
|
160
304
|
pendingDispatches.delete(failedTaskId);
|
|
161
305
|
const newTaskId = dispatchTask(fallback, dispatch.task, targetSession, { ...dispatch, pipelineId: dispatch.pipelineId, waveIndex: dispatch.waveIndex });
|
|
@@ -183,12 +327,10 @@ export function initWsRouter(deps) {
|
|
|
183
327
|
setTimeout(() => pendingDispatches.delete(taskId), 600_000);
|
|
184
328
|
}
|
|
185
329
|
|
|
186
|
-
const _autoRetryKey = `_question_retried_${taskId}`;
|
|
187
330
|
const _askedQuestion = /(?:would you like|shall i|should i|do you want|want me to|may i|can i proceed|would it help|do you need|is that correct|shall we|ready to proceed|would you prefer|let me know|please (?:confirm|clarify|specify|advise))\??/i.test(content);
|
|
188
331
|
const _didWork = /@@WRITE_FILE|@@RUN_CMD|wrote|created|updated|fixed|patched|done\.|complete/i.test(content);
|
|
189
|
-
if (_askedQuestion && !_didWork && !pendingPipelines.has(dispatch?.pipelineId) && !
|
|
190
|
-
|
|
191
|
-
setTimeout(() => { delete global[_autoRetryKey]; }, 600_000);
|
|
332
|
+
if (_askedQuestion && !_didWork && !pendingPipelines.has(dispatch?.pipelineId) && !dispatch?._questionRetried) {
|
|
333
|
+
if (dispatch) dispatch._questionRetried = true;
|
|
192
334
|
const _originalTask = dispatch?.task || "";
|
|
193
335
|
const _retryTask = (_originalTask.slice(0, 2000) || content.slice(0, 500)) +
|
|
194
336
|
"\n\nDo NOT ask for permission or confirmation. Proceed immediately with your best judgment. Just do it.";
|
|
@@ -204,15 +346,13 @@ export function initWsRouter(deps) {
|
|
|
204
346
|
return;
|
|
205
347
|
}
|
|
206
348
|
|
|
207
|
-
const
|
|
208
|
-
const _isCoderAgent = /crew-coder|crew-frontend|crew-fixer|crew-ml|crew-coder-back|crew-coder-front/.test(from);
|
|
349
|
+
const _isCoderAgent = CODER_AGENT_RE.test(from);
|
|
209
350
|
const _returnedPlan = !_didWork && content.length > 300 && (
|
|
210
351
|
/##\s+(component|feature|file structure|design|breakdown|overview|plan|approach|implementation plan|technical spec)/i.test(content) ||
|
|
211
352
|
/here'?s? (?:the|my|a|what|how)/i.test(content.slice(0, 200))
|
|
212
353
|
);
|
|
213
|
-
if (_isCoderAgent && _returnedPlan && !
|
|
214
|
-
|
|
215
|
-
setTimeout(() => { delete global[_planRetryKey]; }, 600_000);
|
|
354
|
+
if (_isCoderAgent && _returnedPlan && !dispatch?._planRetried) {
|
|
355
|
+
if (dispatch) dispatch._planRetried = true;
|
|
216
356
|
const _originalTask = dispatch?.task || "";
|
|
217
357
|
const _retryTask = `STOP PLANNING. Your last response was a plan/analysis with no code written.\n\nOriginal task: ${_originalTask.slice(0, 1500)}\n\nNow WRITE THE CODE. Use @@WRITE_FILE for every file. Do not describe what you will do — do it.`;
|
|
218
358
|
console.log(`[crew-lead] Agent ${from} returned a plan instead of code — auto-retrying`);
|
|
@@ -229,17 +369,20 @@ export function initWsRouter(deps) {
|
|
|
229
369
|
return;
|
|
230
370
|
}
|
|
231
371
|
|
|
232
|
-
const _bailRetryKey = `_bail_retried_${taskId}`;
|
|
233
372
|
const _bailed = /couldn'?t complete|could not complete|i'?m sorry[,.]? but|i was unable to|i'?m unable to|session (?:limit|ended|expired)|ran out of|context (?:limit|window)|i (?:apologize|regret)|partial(?:ly)? complete|not (?:all|every|fully) (?:changes?|tasks?|items?|fixes?)/i.test(content);
|
|
234
|
-
if (_bailed && !
|
|
235
|
-
|
|
236
|
-
setTimeout(() => { delete global[_bailRetryKey]; }, 600_000);
|
|
373
|
+
if (_bailed && !dispatch?._bailRetried) {
|
|
374
|
+
if (dispatch) dispatch._bailRetried = true;
|
|
237
375
|
const _originalTask = dispatch?.task || "";
|
|
238
|
-
const
|
|
376
|
+
const currentEngine = inferDispatchEngine(dispatch, content);
|
|
377
|
+
const engineFallbackMeta = _isCoderAgent
|
|
378
|
+
? buildEngineFallbackMeta(dispatch, currentEngine, "auto-retry-bail")
|
|
379
|
+
: null;
|
|
380
|
+
const fallbackAgent = engineFallbackMeta ? from : (_isCoderAgent ? from : (getRateLimitFallback(from) || from));
|
|
239
381
|
const _retryTask = `Your previous attempt at this task was incomplete. You said you couldn't finish.\n\nOriginal task:\n${_originalTask.slice(0, 2000)}\n\nDo not apologize. Do not explain why you couldn't finish. Just complete the remaining work now. Use @@WRITE_FILE for every file you change. If the task is too large, complete the most critical items first.`;
|
|
240
|
-
console.log(`[crew-lead] Agent ${from} bailed out mid-task — auto-retrying with ${fallbackAgent}`);
|
|
241
|
-
appendHistory("default", targetSession, "system", `${from} bailed mid-task — auto-retrying with ${fallbackAgent}.`);
|
|
382
|
+
console.log(`[crew-lead] Agent ${from} bailed out mid-task — auto-retrying with ${engineFallbackMeta?.engineFallbackTo || fallbackAgent}`);
|
|
383
|
+
appendHistory("default", targetSession, "system", `${from} bailed mid-task — auto-retrying with ${engineFallbackMeta?.engineFallbackTo || fallbackAgent}.`);
|
|
242
384
|
dispatchTask(fallbackAgent, _retryTask, targetSession, {
|
|
385
|
+
...(engineFallbackMeta || {}),
|
|
243
386
|
...(dispatch?.pipelineId ? { pipelineId: dispatch.pipelineId } : {}),
|
|
244
387
|
...(dispatch?.projectDir ? { projectDir: dispatch.projectDir } : {}),
|
|
245
388
|
originProjectId: dispatch?.originProjectId,
|
|
@@ -283,6 +426,31 @@ export function initWsRouter(deps) {
|
|
|
283
426
|
}
|
|
284
427
|
|
|
285
428
|
const originChannel = dispatch?.originChannel || dispatch?.originProjectId || null;
|
|
429
|
+
|
|
430
|
+
// Persist agent result to project messages so swarm chat history is complete
|
|
431
|
+
if (originChannel && saveProjectMessage) {
|
|
432
|
+
try {
|
|
433
|
+
saveProjectMessage(originChannel, {
|
|
434
|
+
source: "agent",
|
|
435
|
+
role: "assistant",
|
|
436
|
+
content: content.slice(0, 8000),
|
|
437
|
+
agent: from,
|
|
438
|
+
threadId: dispatch?.originThreadId || `${originChannel}:${targetSession}`,
|
|
439
|
+
parentId: dispatch?.originMessageId || null,
|
|
440
|
+
metadata: {
|
|
441
|
+
agentName: from,
|
|
442
|
+
autonomous: true,
|
|
443
|
+
engineUsed: env.payload?.engineUsed || null,
|
|
444
|
+
durationMs: dispatch?.ts ? Date.now() - dispatch.ts : null,
|
|
445
|
+
triggeredBy: dispatch?.triggeredBy || "dispatch",
|
|
446
|
+
taskId,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
} catch (e) {
|
|
450
|
+
console.warn(`[crew-lead] Failed to save agent result to project messages: ${e.message}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
286
454
|
if (originChannel) {
|
|
287
455
|
void handleAutonomousMentions({
|
|
288
456
|
message: { content },
|
|
@@ -293,6 +461,9 @@ export function initWsRouter(deps) {
|
|
|
293
461
|
projectDir: dispatch?.projectDir || null,
|
|
294
462
|
originMessageId: dispatch?.originMessageId || null,
|
|
295
463
|
originThreadId: dispatch?.originThreadId || `${originChannel}:${targetSession}`,
|
|
464
|
+
appendToChatHistory: (entry) => {
|
|
465
|
+
appendHistory("default", targetSession, "system", entry.content || String(entry));
|
|
466
|
+
},
|
|
296
467
|
broadcastSSE,
|
|
297
468
|
}).catch((err) => {
|
|
298
469
|
console.warn(`[crew-lead] Autonomous mention routing failed for ${from}: ${err.message}`);
|
|
@@ -325,6 +496,27 @@ export function initWsRouter(deps) {
|
|
|
325
496
|
}
|
|
326
497
|
}
|
|
327
498
|
|
|
499
|
+
// Parse @@DISPATCH markers from any agent result (not just crew-pm).
|
|
500
|
+
// This lets crew-orchestrator (and others) fan out tasks via @@DISPATCH
|
|
501
|
+
// even when running on direct-llm without CLI tools.
|
|
502
|
+
if (from !== "crew-pm" && content.includes("@@DISPATCH")) {
|
|
503
|
+
const agentDispatches = parseDispatches(content);
|
|
504
|
+
for (const d of agentDispatches) {
|
|
505
|
+
const ok = dispatchTask(d.agent, d, targetSession, {
|
|
506
|
+
originProjectId: dispatch?.originProjectId || dispatch?.projectId || "general",
|
|
507
|
+
originChannel: dispatch?.originChannel || dispatch?.projectId || "general",
|
|
508
|
+
originThreadId: dispatch?.originThreadId || `${dispatch?.originProjectId || dispatch?.projectId || "general"}:${targetSession}`,
|
|
509
|
+
originMessageId: dispatch?.originMessageId || null,
|
|
510
|
+
projectDir: d.projectDir || dispatch?.projectDir || null,
|
|
511
|
+
triggeredBy: `${from}-dispatch`,
|
|
512
|
+
});
|
|
513
|
+
if (ok) {
|
|
514
|
+
console.log(`[crew-lead] ${from} dispatched to ${d.agent}: "${(d.task || "").slice(0, 120)}"`);
|
|
515
|
+
appendHistory("default", targetSession, "system", `${from} dispatched to ${d.agent}: "${(d.task || "").slice(0, 120)}".`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
328
520
|
if (from === "crew-pm") {
|
|
329
521
|
const pipelineSpec = parsePipeline(content);
|
|
330
522
|
if (pipelineSpec) {
|
|
@@ -410,17 +602,17 @@ export function initWsRouter(deps) {
|
|
|
410
602
|
});
|
|
411
603
|
|
|
412
604
|
ws.on("close", () => {
|
|
413
|
-
|
|
605
|
+
if (thisConnId !== connectionId) return; // stale close event
|
|
606
|
+
cleanupConnection();
|
|
414
607
|
isConnecting = false;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
418
|
-
reconnectTimer = setTimeout(connectRT, 5000);
|
|
608
|
+
console.log("[crew-lead] RT disconnected");
|
|
609
|
+
scheduleReconnect();
|
|
419
610
|
});
|
|
420
611
|
|
|
421
612
|
ws.on("error", (e) => {
|
|
422
613
|
console.error("[crew-lead] RT socket error:", e.message);
|
|
423
614
|
isConnecting = false;
|
|
615
|
+
// close event will fire after error — reconnect happens there
|
|
424
616
|
});
|
|
425
617
|
}
|
|
426
618
|
|
|
@@ -7,6 +7,7 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import { loadAgentList as _defaultLoadAgentList } from "../runtime/config.mjs";
|
|
10
|
+
import { isCodingTask } from "../agents/dispatch.mjs";
|
|
10
11
|
|
|
11
12
|
const ENGINES_BUNDLED_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "engines");
|
|
12
13
|
const ENGINES_USER_DIR = path.join(os.homedir(), ".crewswarm", "engines");
|
|
@@ -153,6 +154,14 @@ function evaluateShouldUse(engineDef, payload, incomingType) {
|
|
|
153
154
|
* Select engine: payload flags → payload runtime → per-agent crewswarm.json → fallback rules
|
|
154
155
|
*/
|
|
155
156
|
export function selectEngine(payload, incomingType) {
|
|
157
|
+
// PRIORITY 0: Chat messages always use direct LLM, not CLI engines.
|
|
158
|
+
// CLI engines (claude-code, cursor, codex, etc.) are only for coding tasks.
|
|
159
|
+
// The agent's configured model handles conversational replies directly.
|
|
160
|
+
const prompt = payload?.prompt || payload?.task || payload?.message || "";
|
|
161
|
+
if (!isCodingTask(incomingType, prompt, payload)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
156
165
|
// PRIORITY 1: Payload flags (from enriched task)
|
|
157
166
|
const explicitEngines = [
|
|
158
167
|
{ key: 'useCodex', id: 'codex' },
|