crewswarm 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +48 -5
- package/lib/bridges/gateway-ws.mjs +4 -0
- package/lib/bridges/tmux-bridge.mjs +200 -0
- package/lib/cli-process-tracker.mjs +2 -1
- package/lib/crew-lead/chat-handler.mjs +34 -0
- package/lib/crew-lead/http-server.mjs +340 -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 +53 -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 +26 -2
- package/lib/runtime/config.mjs +7 -0
- package/lib/runtime/paths.mjs +12 -8
- package/lib/sessions/session-manager.mjs +287 -0
- 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/index.html +0 -6459
- package/apps/dashboard/package.json +0 -15
- package/apps/dashboard/src/app.js +0 -2823
- 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 -803
- 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
|
@@ -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'.",
|
|
@@ -11,6 +11,8 @@ import { randomUUID } from "node:crypto";
|
|
|
11
11
|
import { getStatePath, getConfigPath } from "../runtime/paths.mjs";
|
|
12
12
|
import { normalizeProjectDir } from "../runtime/project-dir.mjs";
|
|
13
13
|
import { loadProjectMessages } from "../chat/project-messages.mjs";
|
|
14
|
+
import * as tmuxBridge from "../bridges/tmux-bridge.mjs";
|
|
15
|
+
import * as sessionManager from "../sessions/session-manager.mjs";
|
|
14
16
|
|
|
15
17
|
let _deps = {};
|
|
16
18
|
|
|
@@ -375,7 +377,17 @@ export function dispatchPipelineWave(pipelineId) {
|
|
|
375
377
|
// this path: when Cursor CLI fails, orchestrator falls back to plain LLM text
|
|
376
378
|
// with @mentions — subagents never run and the pipeline advances on garbage.
|
|
377
379
|
// Direct per-agent dispatch is reliable for project pipelines.
|
|
378
|
-
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) {
|
|
379
391
|
const waveManifest = {
|
|
380
392
|
wave: currentWave + 1,
|
|
381
393
|
projectDir: pipeline.projectDir || "",
|
|
@@ -425,6 +437,11 @@ export function dispatchPipelineWave(pipelineId) {
|
|
|
425
437
|
...(step.verify ? { verify: step.verify } : {}),
|
|
426
438
|
...(step.done ? { done: step.done } : {}),
|
|
427
439
|
};
|
|
440
|
+
// Build session handoff metadata for tmux-bridge
|
|
441
|
+
const sessionMeta = {};
|
|
442
|
+
if (step.session) sessionMeta.session = step.session;
|
|
443
|
+
else if (pipeline._tmuxSessionId) sessionMeta.session = `handoff:${pipeline._tmuxSessionId}`;
|
|
444
|
+
|
|
428
445
|
const taskId = dispatchTask(step.agent, stepSpec, sessionId, {
|
|
429
446
|
pipelineId,
|
|
430
447
|
waveIndex: currentWave,
|
|
@@ -434,6 +451,7 @@ export function dispatchPipelineWave(pipelineId) {
|
|
|
434
451
|
originThreadId: pipeline.originThreadId,
|
|
435
452
|
originMessageId: pipeline.originMessageId,
|
|
436
453
|
triggeredBy: pipeline.triggeredBy || "pipeline",
|
|
454
|
+
...sessionMeta,
|
|
437
455
|
});
|
|
438
456
|
if (taskId && taskId !== true) pipeline.pendingTaskIds.add(taskId);
|
|
439
457
|
}
|
|
@@ -694,8 +712,10 @@ export function dispatchTask(agent, task, sessionId = "owner", pipelineMeta = nu
|
|
|
694
712
|
task = _deps.writeTaskBrief?.(agent, task, projectDir) ?? task;
|
|
695
713
|
}
|
|
696
714
|
|
|
697
|
-
|
|
698
|
-
|
|
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
|
+
}
|
|
699
719
|
if (rp) {
|
|
700
720
|
try {
|
|
701
721
|
// Build extraFlags: start with global settings, override with pipeline-specific settings
|
|
@@ -762,6 +782,36 @@ export function dispatchTask(agent, task, sessionId = "owner", pipelineMeta = nu
|
|
|
762
782
|
if (pipelineMeta?.mentionedBy) extraFlags.mentionedBy = pipelineMeta.mentionedBy;
|
|
763
783
|
if (pipelineMeta?.autonomous !== undefined) extraFlags.autonomous = pipelineMeta.autonomous;
|
|
764
784
|
|
|
785
|
+
// ── tmux session handoff ──────────────────────────────────────────────
|
|
786
|
+
// If pipelineMeta carries a tmux session, hand it off to this agent
|
|
787
|
+
// or create a new one if session: "persist" is set.
|
|
788
|
+
if (tmuxBridge.detect()) {
|
|
789
|
+
const sessionSpec = pipelineMeta?.session;
|
|
790
|
+
if (typeof sessionSpec === "string" && sessionSpec.startsWith("handoff:")) {
|
|
791
|
+
const existingSessionId = sessionSpec.slice("handoff:".length);
|
|
792
|
+
const prevOwner = sessionManager.getSession(existingSessionId)?.owner;
|
|
793
|
+
if (prevOwner) {
|
|
794
|
+
sessionManager.handoff(existingSessionId, prevOwner, agent);
|
|
795
|
+
}
|
|
796
|
+
const meta = sessionManager.getSession(existingSessionId);
|
|
797
|
+
if (meta?.paneId) extraFlags.tmuxSessionId = meta.paneId;
|
|
798
|
+
} else if (sessionSpec === "persist") {
|
|
799
|
+
const newSessionId = sessionManager.create({
|
|
800
|
+
workspaceId: pipelineMeta?.pipelineId || "default",
|
|
801
|
+
agentId: agent,
|
|
802
|
+
cwd: pipelineMeta?.projectDir || undefined,
|
|
803
|
+
});
|
|
804
|
+
if (newSessionId) {
|
|
805
|
+
const meta = sessionManager.getSession(newSessionId);
|
|
806
|
+
if (meta?.paneId) extraFlags.tmuxSessionId = meta.paneId;
|
|
807
|
+
// Store session ID on pipeline meta so next wave can handoff
|
|
808
|
+
if (pipelineMeta) pipelineMeta._tmuxSessionId = newSessionId;
|
|
809
|
+
}
|
|
810
|
+
} else if (pipelineMeta?.tmuxSessionId) {
|
|
811
|
+
extraFlags.tmuxSessionId = pipelineMeta.tmuxSessionId;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
765
815
|
// Log enrichment for verification
|
|
766
816
|
const engineFlags = Object.keys(extraFlags).filter(k => k.startsWith('use') || k.includes('Model') || k === 'engine');
|
|
767
817
|
if (engineFlags.length > 0) {
|
|
@@ -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' },
|
|
@@ -1612,6 +1612,7 @@ export async function handleRealtimeEnvelope(envelope, client, bridge) {
|
|
|
1612
1612
|
payload: {
|
|
1613
1613
|
source: CREWSWARM_RT_AGENT,
|
|
1614
1614
|
error: message,
|
|
1615
|
+
engineUsed: typeof engineUsed !== "undefined" ? engineUsed : null,
|
|
1615
1616
|
idempotencyKey: dispatchKey,
|
|
1616
1617
|
attempt: dispatchAttempt,
|
|
1617
1618
|
},
|