crewswarm 0.9.2 → 0.9.4
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-uXb_C0GM.js +1 -0
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
- package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
- package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
- package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -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-pm-loop-tab-DiAPTJXu.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
- package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
- package/apps/dashboard/dist/index.html +135 -15
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/vibe/README.md +2 -2
- package/apps/vibe/package.json +1 -1
- package/apps/vibe/server.mjs +101 -56
- package/crew-lead.mjs +34 -4
- package/lib/bridges/cli-executor.mjs +1 -1
- package/lib/bridges/gateway-ws.mjs +4 -0
- package/lib/browser/passthrough-stderr.js +1 -0
- package/lib/chat/project-messages.mjs +3 -5
- package/lib/cli-process-tracker.mjs +3 -2
- package/lib/contacts/identity-linker.mjs +1 -0
- package/lib/crew-judge/judge.mjs +19 -18
- package/lib/crew-lead/agent-manager.mjs +1 -1
- package/lib/crew-lead/background.mjs +14 -1
- package/lib/crew-lead/chat-handler.mjs +38 -1
- package/lib/crew-lead/http-server.mjs +106 -57
- package/lib/crew-lead/llm-caller.mjs +24 -8
- package/lib/crew-lead/prompts.mjs +14 -1
- package/lib/crew-lead/tools.mjs +3 -2
- package/lib/crew-lead/wave-dispatcher.mjs +19 -5
- package/lib/crew-lead/ws-router.mjs +219 -27
- package/lib/engines/crew-cli.mjs +1 -1
- package/lib/engines/engine-registry.mjs +14 -3
- package/lib/engines/rt-envelope.mjs +1 -0
- package/lib/engines/runners.mjs +28 -4
- package/lib/gemini-cli-passthrough-noise.mjs +1 -1
- package/lib/integrations/code-search.mjs +4 -3
- package/lib/memory/shared-adapter.mjs +23 -10
- package/lib/pipeline/manager.mjs +2 -1
- package/lib/runtime/config.mjs +1 -1
- package/lib/runtime/paths.mjs +12 -8
- package/lib/runtime/spending.mjs +2 -1
- package/package.json +42 -14
- 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 +76 -0
- package/scripts/dashboard.mjs +1667 -551
- package/scripts/generate-openapi.mjs +683 -277
- 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/restart-service.sh +12 -9
- 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 +0 -1
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.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/tab-models-tab-BLEjmd19.js +0 -1
- 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-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 +0 -1
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +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
|
@@ -1,1274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat handler — extracted from crew-lead.mjs
|
|
3
|
-
* Handles user messages: LLM response, dispatch, pipelines, skills, tools.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import os from "node:os";
|
|
9
|
-
import crypto from "node:crypto";
|
|
10
|
-
import {
|
|
11
|
-
isSharedMemoryAvailable,
|
|
12
|
-
recallMemoryContext,
|
|
13
|
-
rememberFact,
|
|
14
|
-
getMemoryStats,
|
|
15
|
-
getKeeperStats,
|
|
16
|
-
searchMemory,
|
|
17
|
-
} from "../memory/shared-adapter.mjs";
|
|
18
|
-
|
|
19
|
-
let _deps = {};
|
|
20
|
-
|
|
21
|
-
export function initChatHandler(deps) {
|
|
22
|
-
_deps = { ...deps };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function handleChat({ message, sessionId = "default", userId = "default", firstName = "User", projectId = null }) {
|
|
26
|
-
const cfg = _deps.loadConfig();
|
|
27
|
-
let history = _deps.loadHistory(userId, sessionId);
|
|
28
|
-
|
|
29
|
-
// Fetch project context early (needed for both memory and project context injection)
|
|
30
|
-
let activeProjectOutputDir = null;
|
|
31
|
-
let projectContext = "";
|
|
32
|
-
let currentProject = null;
|
|
33
|
-
|
|
34
|
-
if (projectId) {
|
|
35
|
-
try {
|
|
36
|
-
const projRes = await fetch(`${_deps.DASHBOARD}/api/projects`, { signal: AbortSignal.timeout(2000) });
|
|
37
|
-
const projData = await projRes.json();
|
|
38
|
-
currentProject = (projData.projects || []).find(p => p.id === projectId);
|
|
39
|
-
if (currentProject?.outputDir) {
|
|
40
|
-
activeProjectOutputDir = currentProject.outputDir;
|
|
41
|
-
const roadmapNote = currentProject.roadmapFile ? `\nROADMAP: ${currentProject.roadmapFile}` : "";
|
|
42
|
-
projectContext = `\n\n[Active project: "${currentProject.name}" at ${currentProject.outputDir}${roadmapNote}. Use this path only when dispatching (in the task or context)—do not repeat the path in every reply. When dispatching to crew-qa, specify: Write your report to ${currentProject.outputDir}/qa-report.md.]`;
|
|
43
|
-
}
|
|
44
|
-
} catch { /* project lookup failed — proceed without context */ }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Inject shared memory once at session start — lands in history and gets
|
|
48
|
-
// prefix-cached on all subsequent calls (effectively free after first message).
|
|
49
|
-
// Now uses CLI's MemoryBroker for unified retrieval (AgentKeeper + AgentMemory + Collections).
|
|
50
|
-
if (history.length === 0) {
|
|
51
|
-
try {
|
|
52
|
-
let memoryContext = '';
|
|
53
|
-
|
|
54
|
-
// Try CLI's MemoryBroker first (blends all memory sources)
|
|
55
|
-
if (isSharedMemoryAvailable()) {
|
|
56
|
-
const projectDir = activeProjectOutputDir || process.cwd();
|
|
57
|
-
memoryContext = await recallMemoryContext(projectDir, 'session initialization chat context', {
|
|
58
|
-
maxResults: 8,
|
|
59
|
-
includeDocs: true,
|
|
60
|
-
includeCode: false,
|
|
61
|
-
preferSuccessful: true,
|
|
62
|
-
crewId: 'crew-lead',
|
|
63
|
-
userId: userId // NEW: user-scoped memory
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Fallback to legacy brain.md files if shared memory not available or empty
|
|
68
|
-
if (!memoryContext) {
|
|
69
|
-
const memDir = path.join(process.cwd(), "memory");
|
|
70
|
-
const homeDir = os.homedir();
|
|
71
|
-
const readMem = (p) => { try { return fs.readFileSync(p, "utf8").trim(); } catch { return ""; } };
|
|
72
|
-
|
|
73
|
-
const brain = readMem(_deps.BRAIN_PATH);
|
|
74
|
-
const lessons = readMem(path.join(memDir, "lessons.md"));
|
|
75
|
-
const decisions = readMem(path.join(memDir, "decisions.md"));
|
|
76
|
-
const rules = readMem(path.join(homeDir, ".crewswarm", "global-rules.md"));
|
|
77
|
-
|
|
78
|
-
const sections = [];
|
|
79
|
-
if (brain) sections.push(`## Shared Brain (accumulated facts)\n${brain}`);
|
|
80
|
-
if (lessons) sections.push(`## Lessons Learned\n${lessons}`);
|
|
81
|
-
if (decisions) sections.push(`## Key Decisions\n${decisions}`);
|
|
82
|
-
if (rules) sections.push(`## Global Rules\n${rules}`);
|
|
83
|
-
|
|
84
|
-
if (sections.length > 0) {
|
|
85
|
-
memoryContext = `[Shared memory — injected once at session start]\n${sections.join("\n\n")}`;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (memoryContext) {
|
|
90
|
-
_deps.appendHistory(userId, sessionId, "system", memoryContext);
|
|
91
|
-
history = _deps.loadHistory(userId, sessionId);
|
|
92
|
-
console.log(`[crew-lead] shared memory injected into session ${userId}:${sessionId} (${memoryContext.length} chars, source: ${isSharedMemoryAvailable() ? 'MemoryBroker' : 'legacy brain.md'})`);
|
|
93
|
-
}
|
|
94
|
-
} catch (e) {
|
|
95
|
-
console.error("[crew-lead] shared memory injection failed:", e.message);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// If a project is active, inject its brain + ROADMAP + PDD once per session —
|
|
100
|
-
// all cached in the history prefix so subsequent messages get them for free.
|
|
101
|
-
// If the project changes mid-session, inject the new project context with a
|
|
102
|
-
// clear "switched to" marker so crew-lead knows which project is now active.
|
|
103
|
-
if (currentProject?.outputDir) {
|
|
104
|
-
try {
|
|
105
|
-
const projName = currentProject.name || projectId;
|
|
106
|
-
const outDir = currentProject.outputDir;
|
|
107
|
-
|
|
108
|
-
// Check if THIS specific project's context is already in history
|
|
109
|
-
const alreadyInjected = history.some(h => h.content?.includes(`[Project memory — ${projName}`));
|
|
110
|
-
|
|
111
|
-
if (!alreadyInjected) {
|
|
112
|
-
const readSafe = (p) => { try { return fs.readFileSync(p, "utf8").trim(); } catch { return ""; } };
|
|
113
|
-
|
|
114
|
-
const projBrain = readSafe(path.join(outDir, ".crewswarm", "brain.md"));
|
|
115
|
-
const roadmap = readSafe(path.join(outDir, "ROADMAP.md"));
|
|
116
|
-
const pddPaths = ["PDD.md", "pdd.md", `${projName}-pdd.md`, "design.md", "DESIGN.md"];
|
|
117
|
-
const pdd = pddPaths.map(f => readSafe(path.join(outDir, f))).find(c => c) || "";
|
|
118
|
-
|
|
119
|
-
// Detect mid-session project switch (another project was already injected)
|
|
120
|
-
const prevProject = history.find(h => h.content?.includes("[Project memory —"));
|
|
121
|
-
const isSwitch = !!prevProject;
|
|
122
|
-
const switchNote = isSwitch ? `⚠️ User switched active project to "${projName}". Previous project context above is no longer active — use this project's context from here on.\n\n` : "";
|
|
123
|
-
|
|
124
|
-
const sections = [];
|
|
125
|
-
if (projBrain) sections.push(`### Project Brain\n${projBrain}`);
|
|
126
|
-
if (roadmap) sections.push(`### ROADMAP\n${roadmap}`);
|
|
127
|
-
if (pdd) sections.push(`### PDD / Design\n${pdd.slice(0, 2000)}`);
|
|
128
|
-
|
|
129
|
-
const combined = `[Project memory — ${projName} at ${outDir}]\n${switchNote}${sections.join("\n\n") || "(no project files found yet)"}`;
|
|
130
|
-
_deps.appendHistory(userId, sessionId, "system", combined);
|
|
131
|
-
history = _deps.loadHistory(sessionId);
|
|
132
|
-
console.log(`[crew-lead] project context ${isSwitch ? "switched" : "injected"} for "${projName}": brain=${projBrain.length} roadmap=${roadmap.length} pdd=${pdd.length} chars`);
|
|
133
|
-
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error("[crew-lead] project context injection failed:", e.message);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── Direct @@DISPATCH in user message — bypass LLM and execute immediately ──
|
|
140
|
-
// This prevents the LLM from intercepting user-issued dispatch commands
|
|
141
|
-
const userDispatch = _deps.parseDispatch(message, message);
|
|
142
|
-
if (userDispatch) {
|
|
143
|
-
console.log(`[crew-lead] Direct @@DISPATCH detected from user — bypassing LLM`);
|
|
144
|
-
const resolvedAgent = _deps.resolveAgentId(cfg, userDispatch.agent) || userDispatch.agent;
|
|
145
|
-
if (cfg.knownAgents.includes(resolvedAgent)) {
|
|
146
|
-
userDispatch.agent = resolvedAgent;
|
|
147
|
-
const pipelineMeta = activeProjectOutputDir ? { projectDir: activeProjectOutputDir } : null;
|
|
148
|
-
const taskId = _deps.dispatchTask(resolvedAgent, userDispatch, sessionId, pipelineMeta);
|
|
149
|
-
if (taskId) {
|
|
150
|
-
const reply = `Dispatched to ${resolvedAgent} — reply will show here when they finish.`;
|
|
151
|
-
_deps.appendHistory(userId, sessionId, "user", message);
|
|
152
|
-
_deps.appendHistory(userId, sessionId, "system", `You dispatched to ${resolvedAgent}: "${(userDispatch.task || "").slice(0, 200)}".`);
|
|
153
|
-
_deps.appendHistory(userId, sessionId, "assistant", reply);
|
|
154
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "user", content: message });
|
|
155
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "assistant", content: reply });
|
|
156
|
-
return { reply, dispatched: userDispatch, pendingProject: null, pipeline: null };
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
const reply = `Agent "${userDispatch.agent}" not found. Available: ${cfg.knownAgents.join(", ")}`;
|
|
160
|
-
_deps.appendHistory(userId, sessionId, "user", message);
|
|
161
|
-
_deps.appendHistory(userId, sessionId, "assistant", reply);
|
|
162
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "user", content: message });
|
|
163
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "assistant", content: reply });
|
|
164
|
-
return { reply, dispatched: null, pendingProject: null, pipeline: null };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── Programmatic service control — fire immediately, don't wait for LLM ──────
|
|
169
|
-
// This prevents the LLM from ever claiming it "can't" restart services.
|
|
170
|
-
const serviceIntent = _deps.parseServiceIntent(message);
|
|
171
|
-
if (serviceIntent) {
|
|
172
|
-
const { action, id } = serviceIntent;
|
|
173
|
-
const actionLabel = action === "stop" ? "stopped" : "restarted";
|
|
174
|
-
try {
|
|
175
|
-
const isSpecificAgent = id.startsWith("crew-") && id !== "crew-lead";
|
|
176
|
-
let ok = false;
|
|
177
|
-
if (isSpecificAgent) {
|
|
178
|
-
const r = await fetch(`http://127.0.0.1:${_deps.PORT}/api/agents/${id}/restart`, {
|
|
179
|
-
method: "POST",
|
|
180
|
-
headers: _deps.getRTToken() ? { authorization: `Bearer ${_deps.getRTToken()}` } : {},
|
|
181
|
-
signal: AbortSignal.timeout(8000),
|
|
182
|
-
});
|
|
183
|
-
ok = (await r.json())?.ok !== false;
|
|
184
|
-
} else {
|
|
185
|
-
const endpoint = action === "stop" ? "/api/services/stop" : "/api/services/restart";
|
|
186
|
-
const r = await fetch(`${_deps.DASHBOARD}${endpoint}`, {
|
|
187
|
-
method: "POST",
|
|
188
|
-
headers: { "content-type": "application/json" },
|
|
189
|
-
body: JSON.stringify({ id }),
|
|
190
|
-
signal: AbortSignal.timeout(8000),
|
|
191
|
-
});
|
|
192
|
-
const data = await r.json();
|
|
193
|
-
if (data?.ok === false && data?.message) {
|
|
194
|
-
const reply = `${data.message}`;
|
|
195
|
-
_deps.appendHistory(userId, sessionId, "user", message);
|
|
196
|
-
_deps.appendHistory(userId, sessionId, "assistant", reply);
|
|
197
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "user", content: message });
|
|
198
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "assistant", content: reply });
|
|
199
|
-
return { reply, dispatched: null, pendingProject: null, pipeline: null };
|
|
200
|
-
}
|
|
201
|
-
ok = true;
|
|
202
|
-
}
|
|
203
|
-
if (ok) {
|
|
204
|
-
const reply = `On it — **${id}** ${actionLabel}. Give it 3–5 seconds to reconnect to the RT bus, then ask me "agents online?" for a fresh count.`;
|
|
205
|
-
_deps.appendHistory(userId, sessionId, "user", message);
|
|
206
|
-
_deps.appendHistory(userId, sessionId, "assistant", reply);
|
|
207
|
-
_deps.appendHistory(userId, sessionId, "system", `Service ${id} ${actionLabel} via direct intent detection.`);
|
|
208
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "user", content: message });
|
|
209
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "assistant", content: reply });
|
|
210
|
-
console.log(`[crew-lead] service intent: ${action} ${id} → ok`);
|
|
211
|
-
return { reply, dispatched: null, pendingProject: null, pipeline: null };
|
|
212
|
-
}
|
|
213
|
-
} catch (e) {
|
|
214
|
-
console.error(`[crew-lead] service intent failed: ${e.message}`);
|
|
215
|
-
// fall through to normal LLM response on error
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Autonomous PM loop: user says "run until done" / "build until done" → we auto-ping PM on each handback
|
|
220
|
-
if (/run\s+until\s+done|autonomous\s+build|build\s+until\s+done/i.test(message.trim())) {
|
|
221
|
-
_deps.autonomousPmLoopSessions.add(sessionId);
|
|
222
|
-
}
|
|
223
|
-
if (/stop\s+autonomous|stop\s+(the\s+)?build/i.test(message.trim())) {
|
|
224
|
-
_deps.autonomousPmLoopSessions.delete(sessionId);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Hard kill: "kill everything", "kill all agents", "nuke it" — SIGTERM bridges + PM loops
|
|
228
|
-
if (/\bkill\s+(everything|all|it\s+all|all\s+agents?|the\s+agents?)\b|\bnuke\s+it\b|\bnuke\s+everything\b/i.test(message.trim())) {
|
|
229
|
-
const pipelineCancelled = _deps.cancelAllPipelines(sessionId);
|
|
230
|
-
_deps.autonomousPmLoopSessions.clear();
|
|
231
|
-
let pmLoopsKilled = 0, bridgesKilled = 0;
|
|
232
|
-
try {
|
|
233
|
-
const { execSync } = await import("node:child_process");
|
|
234
|
-
const logsDir = _deps.orchestratorLogsDir;
|
|
235
|
-
if (fs.existsSync(logsDir)) {
|
|
236
|
-
for (const f of fs.readdirSync(logsDir)) {
|
|
237
|
-
if (f.startsWith("pm-loop") && f.endsWith(".pid")) {
|
|
238
|
-
fs.writeFileSync(path.join(logsDir, f.replace(".pid", ".stop")), new Date().toISOString());
|
|
239
|
-
const pid = parseInt(fs.readFileSync(path.join(logsDir, f), "utf8").trim(), 10);
|
|
240
|
-
if (pid) { try { process.kill(pid, "SIGTERM"); pmLoopsKilled++; } catch {} }
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
try { execSync(`pkill -f "gateway-bridge.mjs" 2>/dev/null`, { stdio: "ignore", shell: true }); bridgesKilled = 1; } catch {}
|
|
245
|
-
} catch {}
|
|
246
|
-
const parts = [];
|
|
247
|
-
if (pipelineCancelled > 0) parts.push(`${pipelineCancelled} pipeline(s) cancelled`);
|
|
248
|
-
if (pmLoopsKilled > 0) parts.push(`${pmLoopsKilled} PM loop(s) killed`);
|
|
249
|
-
if (bridgesKilled) parts.push("all agent bridges killed");
|
|
250
|
-
if (parts.length === 0) parts.push("nothing was running");
|
|
251
|
-
const reply = `💀 Hard kill executed: ${parts.join(", ")}. Use \`@@SERVICE restart agents\` or the Services tab to bring bridges back up.`;
|
|
252
|
-
_deps.broadcastSSE({ type: "chat", from: "crew-lead", content: reply, sessionId, ts: Date.now() });
|
|
253
|
-
_deps.broadcastSSE({ type: "kill_all", ts: Date.now(), summary: parts.join(", ") });
|
|
254
|
-
_deps.appendHistory(userId, sessionId, "assistant", reply);
|
|
255
|
-
return { reply, sessionId };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Graceful stop: "stop everything", "stop all", "emergency stop", "halt everything"
|
|
259
|
-
if (/\b(stop|halt|abort|cancel)\s+(everything|all|it\s+all)\b|\bemergency\s+stop\b/i.test(message.trim())) {
|
|
260
|
-
const pipelineCancelled = _deps.cancelAllPipelines(sessionId);
|
|
261
|
-
const wasAutonomous = _deps.autonomousPmLoopSessions.has(sessionId);
|
|
262
|
-
_deps.autonomousPmLoopSessions.clear();
|
|
263
|
-
let pmLoopsStopped = 0;
|
|
264
|
-
try {
|
|
265
|
-
const logsDir = _deps.orchestratorLogsDir;
|
|
266
|
-
if (fs.existsSync(logsDir)) {
|
|
267
|
-
for (const f of fs.readdirSync(logsDir)) {
|
|
268
|
-
if (f.startsWith("pm-loop") && f.endsWith(".pid")) {
|
|
269
|
-
fs.writeFileSync(path.join(logsDir, f.replace(".pid", ".stop")), new Date().toISOString());
|
|
270
|
-
pmLoopsStopped++;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
} catch {}
|
|
275
|
-
const parts = [];
|
|
276
|
-
if (pipelineCancelled > 0) parts.push(`${pipelineCancelled} pipeline(s) cancelled`);
|
|
277
|
-
if (pmLoopsStopped > 0) parts.push(`${pmLoopsStopped} PM loop(s) signalled to stop after current task`);
|
|
278
|
-
if (wasAutonomous) parts.push("autonomous mode cleared");
|
|
279
|
-
if (parts.length === 0) parts.push("nothing was running — all clear");
|
|
280
|
-
const reply = `🛑 Graceful stop: ${parts.join(", ")}. PM loops will finish their current task then halt. Say "kill everything" to hard-kill agent bridges immediately.`;
|
|
281
|
-
_deps.broadcastSSE({ type: "chat", from: "crew-lead", content: reply, sessionId, ts: Date.now() });
|
|
282
|
-
_deps.broadcastSSE({ type: "stop_all", ts: Date.now(), summary: parts.join(", ") });
|
|
283
|
-
_deps.appendHistory(userId, sessionId, "assistant", reply);
|
|
284
|
-
return { reply, sessionId };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Pipeline cancel: "stop pipeline", "cancel pipeline", "abort", "stop everything", "kill it"
|
|
288
|
-
if (/\b(stop|cancel|abort|kill|halt)\b.*(pipeline|dispatch|task|everything|it|all|them)\b|\b(pipeline|dispatch).*(stop|cancel|abort|kill|halt)\b/i.test(message.trim())) {
|
|
289
|
-
const count = _deps.cancelAllPipelines(sessionId);
|
|
290
|
-
if (count > 0) {
|
|
291
|
-
const reply = `Cancelled ${count} running pipeline(s). All pending waves have been dropped. You can re-dispatch when ready.`;
|
|
292
|
-
_deps.broadcastSSE({ type: "chat", from: "crew-lead", content: reply, sessionId, ts: Date.now() });
|
|
293
|
-
return { reply, sessionId };
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const needsSearch = _deps.messageNeedsSearch(message);
|
|
298
|
-
// Inject health snapshot broadly — lightweight pgrep + file reads, not HTTP
|
|
299
|
-
const needsHealth = message.length < 6
|
|
300
|
-
? false
|
|
301
|
-
: /health|status|running|crashed|down\b|restart|skill|agent|workflow|pipeline|who.s.up|who.s.online|anyone.up|each.*up|up\?|is.*up|are.*up|online|services?|telegram|tg\b|opencode|project.*dir|projects?\b|registered.project|what.projects|list.project|settings|what.s.going|what.s.happening|recent.activity|rt.bus|eyes|see.what/i.test(message);
|
|
302
|
-
const needsBenchmarkCatalog = message.length > 4 && /benchmark|zeroeval|leaderboard|llm-stats|swe-bench|livecodebench|mmlu|gpqa|humaneval|gsm8k|what\.tests?|which\.tests?|available\.tests?/i.test(message);
|
|
303
|
-
let braveResults = null;
|
|
304
|
-
let codebaseResults = null;
|
|
305
|
-
let healthData = null;
|
|
306
|
-
let benchmarkCatalog = null;
|
|
307
|
-
const fetchBenchmarkCatalog = async () => {
|
|
308
|
-
const r = await fetch("https://api.zeroeval.com/leaderboard/benchmarks", { signal: AbortSignal.timeout(10000) });
|
|
309
|
-
const arr = await r.json();
|
|
310
|
-
if (!Array.isArray(arr) || !arr.length) return null;
|
|
311
|
-
const limit = 250;
|
|
312
|
-
const rows = arr.slice(0, limit).map(b => {
|
|
313
|
-
const id = b.benchmark_id || b.id || "";
|
|
314
|
-
const name = (b.name || id).slice(0, 35);
|
|
315
|
-
const desc = (b.description || "").slice(0, 60).replace(/\n/g, " ");
|
|
316
|
-
return `${id} | ${name} | ${desc}`;
|
|
317
|
-
});
|
|
318
|
-
const suffix = arr.length > limit ? `\n… and ${arr.length - limit} more (full list at api.zeroeval.com)` : "";
|
|
319
|
-
return `[Benchmark catalog from ZeroEval — use benchmark_id in @@SKILL zeroeval.benchmark {"benchmark_id":"<id>"}]\n${rows.join("\n")}${suffix}`;
|
|
320
|
-
};
|
|
321
|
-
try {
|
|
322
|
-
const [b, c, h, bc] = await Promise.all([
|
|
323
|
-
needsSearch ? _deps.searchWithBrave(message).catch(() => null) : null,
|
|
324
|
-
needsSearch ? Promise.resolve(_deps.searchCodebase(message)).catch(() => null) : null,
|
|
325
|
-
needsHealth ? (async () => {
|
|
326
|
-
try {
|
|
327
|
-
const cfgRaw = _deps.tryRead(path.join(os.homedir(), ".crewswarm", "config.json")) || {};
|
|
328
|
-
const skillsDir = path.join(os.homedir(), ".crewswarm", "skills");
|
|
329
|
-
let skillsDetail = [];
|
|
330
|
-
try {
|
|
331
|
-
const files = fs.readdirSync(skillsDir).filter(f => f.endsWith(".json"));
|
|
332
|
-
for (const f of files) {
|
|
333
|
-
const name = f.replace(".json", "");
|
|
334
|
-
try {
|
|
335
|
-
const def = JSON.parse(fs.readFileSync(path.join(skillsDir, f), "utf8"));
|
|
336
|
-
const desc = (def.description || "").replace(/\s+/g, " ").trim().slice(0, 100);
|
|
337
|
-
const notes = def.paramNotes || "";
|
|
338
|
-
const example = `@@SKILL ${name} ${JSON.stringify(def.defaultParams || {})}`;
|
|
339
|
-
let line = ` - ${name}: ${desc}`;
|
|
340
|
-
if (notes) line += ` | Params: ${notes}`;
|
|
341
|
-
if (def.listUrl && def.listUrlIdField) {
|
|
342
|
-
try {
|
|
343
|
-
const r = await fetch(def.listUrl, { signal: AbortSignal.timeout(5000) });
|
|
344
|
-
const arr = await r.json();
|
|
345
|
-
if (Array.isArray(arr) && arr.length) {
|
|
346
|
-
const idField = def.listUrlIdField;
|
|
347
|
-
const ids = arr.slice(0, 50).map(b => b[idField]).filter(Boolean);
|
|
348
|
-
line += ` | IDs (live): ${ids.join(", ")}${arr.length > 50 ? ` … +${arr.length - 50} more` : ""}`;
|
|
349
|
-
}
|
|
350
|
-
} catch {}
|
|
351
|
-
}
|
|
352
|
-
line += ` | Example: ${example}`;
|
|
353
|
-
skillsDetail.push(line);
|
|
354
|
-
} catch { skillsDetail.push(` - ${name}: (parse failed)`); }
|
|
355
|
-
}
|
|
356
|
-
} catch {}
|
|
357
|
-
// Quick service status via pgrep (processes, not agent connections)
|
|
358
|
-
const { execSync: esc } = await import("node:child_process");
|
|
359
|
-
const isRunning = (pat) => { try { esc(`pgrep -f "${pat}"`, { stdio: "ignore" }); return true; } catch { return false; } };
|
|
360
|
-
|
|
361
|
-
// Per-agent bridge status from RT bus — who is actually connected right now
|
|
362
|
-
let rtAgentsOnline = new Set();
|
|
363
|
-
try {
|
|
364
|
-
const rtResp = await fetch("http://127.0.0.1:18889/status", { signal: AbortSignal.timeout(1500) });
|
|
365
|
-
const rtData = await rtResp.json();
|
|
366
|
-
if (Array.isArray(rtData.agents)) rtAgentsOnline = new Set(rtData.agents);
|
|
367
|
-
} catch {}
|
|
368
|
-
|
|
369
|
-
const allAgents = cfg.agentRoster.length
|
|
370
|
-
? cfg.agentRoster
|
|
371
|
-
: cfg.knownAgents.map(id => ({ id, emoji: "🤖", model: "" }));
|
|
372
|
-
|
|
373
|
-
const agentRows = allAgents.map(a => {
|
|
374
|
-
const online = rtAgentsOnline.has(a.id);
|
|
375
|
-
const status = online ? "✅" : "❌";
|
|
376
|
-
return ` ${status} ${a.emoji||"🤖"} ${a.id}: tools=${_deps.readAgentTools(a.id).tools.join(",")||"(default)"} model=${a.model||"??"}`;
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
const rtBusUp = isRunning("opencrew-rt-daemon");
|
|
380
|
-
const services = [
|
|
381
|
-
`RT bus (18889): ${rtBusUp ? `✅ running — ${rtAgentsOnline.size} agents connected: ${[...rtAgentsOnline].join(", ")||"none"}` : "❌ DOWN — use @@SERVICE restart rt-bus"}`,
|
|
382
|
-
`Telegram bridge: ${isRunning("telegram-bridge") ? "✅ running" : "⚠️ stopped — use @@SERVICE restart telegram"}`,
|
|
383
|
-
`OpenCode (4096): ${isRunning("opencode serve") ? "✅ running" : "⚠️ stopped"}`,
|
|
384
|
-
];
|
|
385
|
-
|
|
386
|
-
let projectsLine = "Registered projects (dashboard Projects tab): (none)";
|
|
387
|
-
try {
|
|
388
|
-
const projRes = await fetch(`${_deps.DASHBOARD}/api/projects`, { signal: AbortSignal.timeout(2000) });
|
|
389
|
-
if (projRes.ok) {
|
|
390
|
-
const projData = await projRes.json();
|
|
391
|
-
const projects = projData.projects || [];
|
|
392
|
-
if (projects.length) {
|
|
393
|
-
projectsLine = `Registered projects (${projects.length}): ${projects.map(p => `${p.name || p.id} → ${p.outputDir || p.roadmapFile || "?"}`).join("; ")}`;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
} catch {}
|
|
397
|
-
const projectsSnapshot = projectsLine;
|
|
398
|
-
|
|
399
|
-
const rtActivityLog = _deps.rtActivityLog || [];
|
|
400
|
-
return [
|
|
401
|
-
`[System health snapshot — live data from your local machine, fetched right now]`,
|
|
402
|
-
`crew-lead: ${cfg.providerKey}/${cfg.modelId} | RT connected: ${!!_deps.getRtPublish()} | uptime: ${Math.floor(process.uptime()/300)*5}min`,
|
|
403
|
-
`crew-lead (this process) can create skills: use @@DEFINE_SKILL name + JSON + @@END_SKILL when the user asks. The agent list below is bridge agents only.`,
|
|
404
|
-
`OpenCode project dir: ${cfgRaw.opencodeProject || "(not set — agents write to repo root)"}`,
|
|
405
|
-
projectsSnapshot,
|
|
406
|
-
`Skills installed (${skillsDetail.length}):`,
|
|
407
|
-
...(skillsDetail.length ? skillsDetail : ["(none)"]),
|
|
408
|
-
``,
|
|
409
|
-
(() => {
|
|
410
|
-
let pipelineNames = [];
|
|
411
|
-
try {
|
|
412
|
-
const pipelinesDir = path.join(os.homedir(), ".crewswarm", "pipelines");
|
|
413
|
-
if (fs.existsSync(pipelinesDir)) pipelineNames = fs.readdirSync(pipelinesDir).filter(f => f.endsWith(".json")).map(f => f.replace(".json", ""));
|
|
414
|
-
} catch {}
|
|
415
|
-
return `Workflows (pipelines for cron) (${pipelineNames.length}): ${pipelineNames.length ? pipelineNames.join(", ") : "(none)"}`;
|
|
416
|
-
})(),
|
|
417
|
-
``,
|
|
418
|
-
`Services:`,
|
|
419
|
-
...services,
|
|
420
|
-
``,
|
|
421
|
-
`Agents (${agentRows.length}):`,
|
|
422
|
-
...agentRows,
|
|
423
|
-
...(rtActivityLog.length > 0 ? [
|
|
424
|
-
``,
|
|
425
|
-
`Recent RT activity (newest last; crew-lead sees all bus traffic):`,
|
|
426
|
-
...rtActivityLog.slice(-25).map((e) => ` ${e.time} [${e.channel}] ${e.summary}`),
|
|
427
|
-
``,
|
|
428
|
-
] : []),
|
|
429
|
-
`[Use this data to answer the user's question. Do NOT say you cannot reach localhost.]`,
|
|
430
|
-
].join("\n");
|
|
431
|
-
} catch { return null; }
|
|
432
|
-
})() : null,
|
|
433
|
-
needsBenchmarkCatalog ? fetchBenchmarkCatalog().catch(() => null) : null,
|
|
434
|
-
]);
|
|
435
|
-
braveResults = b;
|
|
436
|
-
codebaseResults = c;
|
|
437
|
-
healthData = h;
|
|
438
|
-
benchmarkCatalog = bc;
|
|
439
|
-
} catch (e) {
|
|
440
|
-
console.error("[crew-lead] context fetch failed:", e?.message || e);
|
|
441
|
-
}
|
|
442
|
-
// Auto-search history for context when user asks about past conversations
|
|
443
|
-
const needsHistorySearch = message.length > 8 && /\b(last time|before|earlier|previously|we discussed|you said|i asked|remember when|what did|history of|past conversation|mentioned|talked about|said something|tell me again)\b/i.test(message);
|
|
444
|
-
let historyContext = null;
|
|
445
|
-
|
|
446
|
-
if (needsHistorySearch) {
|
|
447
|
-
try {
|
|
448
|
-
// Extract key terms from the question
|
|
449
|
-
const terms = message
|
|
450
|
-
.replace(/\b(what|when|where|who|how|why|did|do|does|is|are|was|were|have|has|had|tell|show|find|search|remember|mentioned|discussed|said|talked)\b/gi, "")
|
|
451
|
-
.replace(/[^\w\s]/g, " ")
|
|
452
|
-
.split(/\s+/)
|
|
453
|
-
.filter(w => w.length > 4)
|
|
454
|
-
.slice(0, 3)
|
|
455
|
-
.join(" ");
|
|
456
|
-
|
|
457
|
-
if (terms.trim()) {
|
|
458
|
-
const histDir = path.join(os.homedir(), ".crewswarm", "chat-history");
|
|
459
|
-
if (fs.existsSync(histDir)) {
|
|
460
|
-
const files = fs.readdirSync(histDir).filter(f => f.endsWith(".jsonl")).sort().reverse().slice(0, 10); // Last 10 sessions
|
|
461
|
-
const lq = terms.toLowerCase();
|
|
462
|
-
const hits = [];
|
|
463
|
-
|
|
464
|
-
for (const file of files) {
|
|
465
|
-
const lines = fs.readFileSync(path.join(histDir, file), "utf8").split("\n");
|
|
466
|
-
for (const line of lines) {
|
|
467
|
-
if (!line.trim()) continue;
|
|
468
|
-
try {
|
|
469
|
-
const entry = JSON.parse(line);
|
|
470
|
-
if ((entry.content || "").toLowerCase().includes(lq)) {
|
|
471
|
-
const snippet = (entry.content || "").slice(0, 400).replace(/\n/g, " ");
|
|
472
|
-
hits.push(`[${entry.role}] ${snippet}`);
|
|
473
|
-
if (hits.length >= 3) break;
|
|
474
|
-
}
|
|
475
|
-
} catch {}
|
|
476
|
-
}
|
|
477
|
-
if (hits.length >= 3) break;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (hits.length > 0) {
|
|
481
|
-
historyContext = `[Past conversation context - automatically retrieved]\n${hits.join("\n\n")}`;
|
|
482
|
-
console.log(`[crew-lead] Auto-retrieved ${hits.length} history matches for: ${terms}`);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
} catch (e) {
|
|
487
|
-
console.error(`[crew-lead] Auto history search failed: ${e.message}`);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const parts = [message + projectContext];
|
|
492
|
-
if (historyContext) parts.push(historyContext);
|
|
493
|
-
if (braveResults) parts.push(`[Web context from Brave Search]\n${braveResults}`);
|
|
494
|
-
if (codebaseResults) parts.push(`[Codebase context from workspace]\n${codebaseResults}`);
|
|
495
|
-
if (healthData) parts.push(healthData);
|
|
496
|
-
if (benchmarkCatalog) parts.push(benchmarkCatalog);
|
|
497
|
-
const userContent = parts.length > 1 ? parts.join("\n\n") : (message + projectContext);
|
|
498
|
-
|
|
499
|
-
// Many chat APIs use only the first system message; agent completions (e.g. [crew-pm completed task]) are stored as "system" in history and would be dropped. Send them as "user" with a prefix so Stinki always sees them.
|
|
500
|
-
const historyAsMessages = history.map(h => {
|
|
501
|
-
if (h.role === "system") {
|
|
502
|
-
return { role: "user", content: `[Crew update — use this when answering]\n${h.content}` };
|
|
503
|
-
}
|
|
504
|
-
return { role: h.role, content: h.content };
|
|
505
|
-
});
|
|
506
|
-
const messages = [
|
|
507
|
-
{ role: "system", content: _deps.buildSystemPrompt(cfg) },
|
|
508
|
-
...historyAsMessages,
|
|
509
|
-
{ role: "user", content: userContent },
|
|
510
|
-
];
|
|
511
|
-
|
|
512
|
-
_deps.appendHistory(userId, sessionId, "user", message);
|
|
513
|
-
// Audit trail: record what Brave actually injected so later turns (and you) can verify — no "context #5" confabulation
|
|
514
|
-
if (braveResults) {
|
|
515
|
-
const count = (braveResults.match(/\n\n/g) || []).length + 1;
|
|
516
|
-
// Keep full numbered list (1. ... 2. ... 5. ...) in history so later turns can cite accurately — ~1200 chars usually includes result #5
|
|
517
|
-
const preview = braveResults.replace(/\n/g, " ").slice(0, 1200);
|
|
518
|
-
_deps.appendHistory(userId, sessionId, "system", `[Brave search] query="${message.slice(0, 60)}${message.length > 60 ? "…" : ""}" → ${count} results. Preview: ${preview}${braveResults.length > 1200 ? "…" : ""}`);
|
|
519
|
-
}
|
|
520
|
-
_deps.broadcastSSE({ type: "chat_message", sessionId, role: "user", content: message });
|
|
521
|
-
|
|
522
|
-
const llmResult = await _deps.callLLM(messages, cfg);
|
|
523
|
-
let fullReply = llmResult.reply;
|
|
524
|
-
const usedFallback = llmResult.usedFallback;
|
|
525
|
-
const activeModel = llmResult.model;
|
|
526
|
-
const fallbackReason = llmResult.reason;
|
|
527
|
-
|
|
528
|
-
// ── Direct tool execution (all crew-lead native tools) ──────────────────
|
|
529
|
-
const hasDirectTools = /@@READ_FILE[ \t]|@@WRITE_FILE[ \t]|@@WEB_SEARCH[ \t]|@@WEB_FETCH[ \t]|@@MKDIR[ \t]|@@RUN_CMD[ \t]|@@TELEGRAM[ \t]|@@WHATSAPP[ \t]|@@SEARCH_HISTORY[ \t]/.test(fullReply);
|
|
530
|
-
if (hasDirectTools) {
|
|
531
|
-
const toolResults = await _deps.execCrewLeadTools(fullReply);
|
|
532
|
-
if (toolResults.length > 0) {
|
|
533
|
-
// Follow-up LLM call: show the tool results so crew-lead can give a proper answer
|
|
534
|
-
const followUpMessages = [
|
|
535
|
-
{ role: "system", content: _deps.buildSystemPrompt(cfg) },
|
|
536
|
-
...historyAsMessages,
|
|
537
|
-
{ role: "user", content: userContent },
|
|
538
|
-
{ role: "assistant", content: fullReply },
|
|
539
|
-
{ role: "user", content: `[Tool results]\n${toolResults.join("\n\n")}\n\nUsing only the above results, give a concise, direct answer to the user. IMPORTANT: Do NOT emit any @@ tags in your reply (no @@DISPATCH, @@PIPELINE, @@READ_FILE, @@RUN_CMD, @@WEB_SEARCH, or any other @@command). The tool phase is complete — just answer in plain text.` },
|
|
540
|
-
];
|
|
541
|
-
try {
|
|
542
|
-
const followUp = await _deps.callLLM(followUpMessages, cfg);
|
|
543
|
-
fullReply = followUp.reply;
|
|
544
|
-
} catch (e) {
|
|
545
|
-
// fallback: append raw tool results if follow-up fails
|
|
546
|
-
fullReply = fullReply + "\n\n---\n" + toolResults.join("\n\n");
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// ── @@TOOLS — permission grant/revoke command ──────────────────────────────
|
|
552
|
-
const toolsCmd = (() => {
|
|
553
|
-
const m = fullReply.match(/@@TOOLS\s+(\{[^}]+\})/);
|
|
554
|
-
if (!m) return null;
|
|
555
|
-
try { return JSON.parse(m[1]); } catch { return null; }
|
|
556
|
-
})();
|
|
557
|
-
|
|
558
|
-
// ── @@PROMPT — read/write an agent's system prompt ─────────────────────────
|
|
559
|
-
// Parsed from LLM reply OR user message so pasting @@PROMPT in chat always runs (LLM often doesn't echo it)
|
|
560
|
-
// @@PROMPT {"agent":"crew-qa","set":"You are a QA specialist..."}
|
|
561
|
-
// @@PROMPT {"agent":"crew-qa","append":"- Always use @@READ_FILE before auditing"}
|
|
562
|
-
const promptCmd = (() => {
|
|
563
|
-
const promptRe = /@@PROMPT\s+(\{[\s\S]*?\})\s*(?:\n|$)/;
|
|
564
|
-
const fromReply = fullReply.match(promptRe);
|
|
565
|
-
if (fromReply) { try { return JSON.parse(fromReply[1]); } catch {} }
|
|
566
|
-
const fromUser = (message || "").match(promptRe);
|
|
567
|
-
if (fromUser) { try { return JSON.parse(fromUser[1]); } catch {} }
|
|
568
|
-
return null;
|
|
569
|
-
})();
|
|
570
|
-
|
|
571
|
-
// ── @@CREATE_AGENT — dynamically create a new specialist agent ────────────
|
|
572
|
-
// @@CREATE_AGENT {"id":"crew-ml","role":"coder","displayName":"MLBot","description":"AI/ML and data science"}
|
|
573
|
-
const createAgentCmd = (() => {
|
|
574
|
-
const m = fullReply.match(/@@CREATE_AGENT\s+(\{[^}]+\})/);
|
|
575
|
-
if (!m) return null;
|
|
576
|
-
try { return JSON.parse(m[1]); } catch { return null; }
|
|
577
|
-
})();
|
|
578
|
-
|
|
579
|
-
// ── @@REMOVE_AGENT — remove a dynamically created agent ─────────────────
|
|
580
|
-
// @@REMOVE_AGENT crew-ml
|
|
581
|
-
const removeAgentCmd = (() => {
|
|
582
|
-
const m = fullReply.match(/@@REMOVE_AGENT\s+(crew-[a-zA-Z0-9_-]+)/);
|
|
583
|
-
return m ? m[1] : null;
|
|
584
|
-
})();
|
|
585
|
-
|
|
586
|
-
// ── @@BRAIN — append a fact to shared brain.md ─────────────────────────────
|
|
587
|
-
// @@BRAIN crew-lead: some durable fact worth remembering
|
|
588
|
-
const BRAIN_PLACEHOLDERS = /^(note text|some fact|placeholder|example|durable fact|fact here|your fact|insert fact|crew-lead: note|crew-lead: some)/i;
|
|
589
|
-
const brainCmd = (() => {
|
|
590
|
-
const m = fullReply.match(/@@BRAIN\s+([^\n]+)/);
|
|
591
|
-
if (!m) return null;
|
|
592
|
-
const entry = m[1].trim();
|
|
593
|
-
if (BRAIN_PLACEHOLDERS.test(entry) || entry.length < 10) return null; // skip template leakage
|
|
594
|
-
return entry;
|
|
595
|
-
})();
|
|
596
|
-
|
|
597
|
-
// ── @@MEMORY — search shared memory (AgentKeeper + AgentMemory + Collections)
|
|
598
|
-
// @@MEMORY search "query text"
|
|
599
|
-
// @@MEMORY stats
|
|
600
|
-
const memoryCmd = (() => {
|
|
601
|
-
const searchMatch = fullReply.match(/@@MEMORY\s+search\s+"([^"]+)"/i) ||
|
|
602
|
-
fullReply.match(/@@MEMORY\s+search\s+([^\n]+)/i);
|
|
603
|
-
if (searchMatch) return { action: 'search', query: searchMatch[1].trim() };
|
|
604
|
-
|
|
605
|
-
if (/@@MEMORY\s+stats\b/i.test(fullReply)) return { action: 'stats' };
|
|
606
|
-
|
|
607
|
-
return null;
|
|
608
|
-
})();
|
|
609
|
-
|
|
610
|
-
// ── @@GLOBALRULE — append a rule to global-rules.md (injected into all agents)
|
|
611
|
-
// @@GLOBALRULE Always reply in the language the user wrote in
|
|
612
|
-
const globalRuleCmd = (() => {
|
|
613
|
-
const m = fullReply.match(/@@GLOBALRULE\s+([^\n]+)/);
|
|
614
|
-
return m ? m[1].trim() : null;
|
|
615
|
-
})();
|
|
616
|
-
|
|
617
|
-
// ── @@STOP — graceful stop: cancel pipelines + signal PM loops + clear autonomous ─
|
|
618
|
-
// PM loops finish their current task before halting. Agent bridges keep running.
|
|
619
|
-
if (/@@STOP\b/.test(fullReply) && !/@@KILL\b/.test(fullReply)) {
|
|
620
|
-
const pipelineCancelled = _deps.cancelAllPipelines(sessionId);
|
|
621
|
-
const wasAutonomous = _deps.autonomousPmLoopSessions.has(sessionId);
|
|
622
|
-
_deps.autonomousPmLoopSessions.clear();
|
|
623
|
-
let pmLoopsStopped = 0;
|
|
624
|
-
try {
|
|
625
|
-
const logsDir = _deps.orchestratorLogsDir;
|
|
626
|
-
if (fs.existsSync(logsDir)) {
|
|
627
|
-
for (const f of fs.readdirSync(logsDir)) {
|
|
628
|
-
if (f.startsWith("pm-loop") && f.endsWith(".pid")) {
|
|
629
|
-
fs.writeFileSync(path.join(logsDir, f.replace(".pid", ".stop")), new Date().toISOString());
|
|
630
|
-
pmLoopsStopped++;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
} catch {}
|
|
635
|
-
const parts = [];
|
|
636
|
-
if (pipelineCancelled > 0) parts.push(`${pipelineCancelled} pipeline(s) cancelled`);
|
|
637
|
-
if (pmLoopsStopped > 0) parts.push(`${pmLoopsStopped} PM loop(s) signalled to stop after current task`);
|
|
638
|
-
if (wasAutonomous) parts.push("autonomous mode cleared");
|
|
639
|
-
if (parts.length === 0) parts.push("nothing was running");
|
|
640
|
-
console.log(`[crew-lead] @@STOP executed: ${parts.join(", ")}`);
|
|
641
|
-
_deps.broadcastSSE({ type: "stop_all", ts: Date.now(), summary: parts.join(", ") });
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// ── @@KILL — hard kill: everything @@STOP does + SIGTERM agent bridges + PM loop procs ─
|
|
645
|
-
// Use when agents are stuck, looping, or unresponsive and you need them dead now.
|
|
646
|
-
if (/@@KILL\b/.test(fullReply)) {
|
|
647
|
-
const pipelineCancelled = _deps.cancelAllPipelines(sessionId);
|
|
648
|
-
_deps.autonomousPmLoopSessions.clear();
|
|
649
|
-
let pmLoopsKilled = 0;
|
|
650
|
-
let bridgesKilled = 0;
|
|
651
|
-
try {
|
|
652
|
-
const { execSync } = await import("node:child_process");
|
|
653
|
-
const logsDir = _deps.orchestratorLogsDir;
|
|
654
|
-
// Hard-kill each PM loop process by PID
|
|
655
|
-
if (fs.existsSync(logsDir)) {
|
|
656
|
-
for (const f of fs.readdirSync(logsDir)) {
|
|
657
|
-
if (f.startsWith("pm-loop") && f.endsWith(".pid")) {
|
|
658
|
-
fs.writeFileSync(path.join(logsDir, f.replace(".pid", ".stop")), new Date().toISOString());
|
|
659
|
-
const pid = parseInt(fs.readFileSync(path.join(logsDir, f), "utf8").trim(), 10);
|
|
660
|
-
if (pid) { try { process.kill(pid, "SIGTERM"); pmLoopsKilled++; } catch {} }
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
// Kill all agent gateway bridges (they will be respawnable via @@SERVICE restart agents)
|
|
665
|
-
try { execSync(`pkill -f "gateway-bridge.mjs" 2>/dev/null`, { stdio: "ignore", shell: true }); bridgesKilled = 1; } catch {}
|
|
666
|
-
} catch {}
|
|
667
|
-
const parts = [];
|
|
668
|
-
if (pipelineCancelled > 0) parts.push(`${pipelineCancelled} pipeline(s) cancelled`);
|
|
669
|
-
if (pmLoopsKilled > 0) parts.push(`${pmLoopsKilled} PM loop process(es) SIGTERM'd`);
|
|
670
|
-
if (bridgesKilled) parts.push("all agent bridges killed (restart with @@SERVICE restart agents)");
|
|
671
|
-
if (parts.length === 0) parts.push("nothing was running");
|
|
672
|
-
console.log(`[crew-lead] @@KILL executed: ${parts.join(", ")}`);
|
|
673
|
-
_deps.broadcastSSE({ type: "kill_all", ts: Date.now(), summary: parts.join(", ") });
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// ── @@SERVICE — restart/stop a service or agent bridge ──────────────────────
|
|
677
|
-
// @@SERVICE restart telegram @@SERVICE stop agents @@SERVICE restart crew-coder
|
|
678
|
-
const serviceCmd = (() => {
|
|
679
|
-
const m = fullReply.match(/@@SERVICE\s+(restart|stop|start)\s+([a-zA-Z0-9_\-]+)/);
|
|
680
|
-
return m ? { action: m[1], id: m[2] } : null;
|
|
681
|
-
})();
|
|
682
|
-
|
|
683
|
-
// ── @@DEFINE_SKILL — crew-lead writes a skill JSON to ~/.crewswarm/skills/
|
|
684
|
-
// @@DEFINE_SKILL skillname\n{...json...}\n@@END_SKILL
|
|
685
|
-
const defineSkillCmds = [];
|
|
686
|
-
const defineSkillRe = /@@DEFINE_SKILL[ \t]+([a-zA-Z0-9_\-.]+)\n([\s\S]*?)@@END_SKILL/g;
|
|
687
|
-
let dsMatch;
|
|
688
|
-
while ((dsMatch = defineSkillRe.exec(fullReply)) !== null) {
|
|
689
|
-
const skillName = dsMatch[1].trim();
|
|
690
|
-
const rawJson = dsMatch[2].trim();
|
|
691
|
-
try {
|
|
692
|
-
const def = JSON.parse(rawJson);
|
|
693
|
-
const skillsDir = path.join(os.homedir(), ".crewswarm", "skills");
|
|
694
|
-
fs.mkdirSync(skillsDir, { recursive: true });
|
|
695
|
-
fs.writeFileSync(path.join(skillsDir, skillName + ".json"), JSON.stringify(def, null, 2));
|
|
696
|
-
defineSkillCmds.push({ name: skillName, ok: true });
|
|
697
|
-
console.log(`[crew-lead] @@DEFINE_SKILL saved: ${skillName}`);
|
|
698
|
-
} catch (e) {
|
|
699
|
-
defineSkillCmds.push({ name: skillName, ok: false, error: e.message });
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// ── @@DEFINE_WORKFLOW — create or replace a scheduled pipeline (stages: agent + task per stage)
|
|
704
|
-
// @@DEFINE_WORKFLOW name\n[...]\n@@END_WORKFLOW
|
|
705
|
-
const defineWorkflowCmds = [];
|
|
706
|
-
const defineWorkflowRe = /@@DEFINE_WORKFLOW[ \t]+([a-zA-Z0-9_\-]+)\n([\s\S]*?)@@END_WORKFLOW/g;
|
|
707
|
-
let dwMatch;
|
|
708
|
-
while ((dwMatch = defineWorkflowRe.exec(fullReply)) !== null) {
|
|
709
|
-
const wfName = dwMatch[1].trim();
|
|
710
|
-
const rawJson = dwMatch[2].trim();
|
|
711
|
-
try {
|
|
712
|
-
const stages = JSON.parse(rawJson);
|
|
713
|
-
if (!Array.isArray(stages)) throw new Error("stages must be a JSON array");
|
|
714
|
-
const valid = stages
|
|
715
|
-
.filter(s => s && s.agent && (s.task || s.taskText))
|
|
716
|
-
.map(s => ({ agent: s.agent, task: s.task || s.taskText || "", tool: s.tool || undefined }));
|
|
717
|
-
const pipelinesDir = path.join(os.homedir(), ".crewswarm", "pipelines");
|
|
718
|
-
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
719
|
-
fs.writeFileSync(path.join(pipelinesDir, wfName + ".json"), JSON.stringify({ stages: valid }, null, 2));
|
|
720
|
-
defineWorkflowCmds.push({ name: wfName, ok: true, stageCount: valid.length });
|
|
721
|
-
console.log(`[crew-lead] @@DEFINE_WORKFLOW saved: ${wfName} (${valid.length} stages)`);
|
|
722
|
-
} catch (e) {
|
|
723
|
-
defineWorkflowCmds.push({ name: wfName, ok: false, error: e.message });
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const pipelineSteps = _deps.parsePipeline(fullReply);
|
|
728
|
-
const projectSpec = !pipelineSteps ? _deps.parseProject(fullReply) : null;
|
|
729
|
-
const dispatch = !projectSpec && !pipelineSteps ? _deps.parseDispatch(fullReply, message) : null;
|
|
730
|
-
|
|
731
|
-
if (dispatch) {
|
|
732
|
-
console.log(`[chat-handler] parseDispatch found: agent=${dispatch.agent}, isDispatchIntended=${_deps.isDispatchIntended(message)}`);
|
|
733
|
-
}
|
|
734
|
-
// ── @@SKILL — crew-lead executes skills directly (no dispatch needed)
|
|
735
|
-
const skillCalls = [];
|
|
736
|
-
// Require skill name to be followed by JSON params or end-of-line — prevents "@@SKILL line" in prose
|
|
737
|
-
const skillRe = /@@SKILL[ \t]+([a-zA-Z0-9_\-\.]+)[ \t]*(\{[^\n]*\})?(?=[ \t]*$|[ \t]*\n|[ \t]*\{)/gm;
|
|
738
|
-
let skMatch;
|
|
739
|
-
while ((skMatch = skillRe.exec(fullReply)) !== null) {
|
|
740
|
-
const skillName = skMatch[1].trim();
|
|
741
|
-
let params = {};
|
|
742
|
-
try { if (skMatch[2]) params = JSON.parse(skMatch[2]); } catch {}
|
|
743
|
-
skillCalls.push({ skillName, params });
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
let cleanReply = _deps.stripThink(_deps.stripPipeline(_deps.stripProject(_deps.stripDispatch(fullReply))))
|
|
747
|
-
.replace(/@@SKILL[ \t]+[a-zA-Z0-9_\-\.]+[ \t]*(\{[^\n]*\})?(?=[ \t]*$|[ \t]*\n|[ \t]*\{)/gm, "")
|
|
748
|
-
.replace(/@@TOOLS\s+\{[^}]+\}/g, "")
|
|
749
|
-
.replace(/@@PROMPT\s+\{[\s\S]*?\}\s*(?:\n|$)/g, "")
|
|
750
|
-
.replace(/@@BRAIN\s+[^\n]+/g, "")
|
|
751
|
-
.replace(/@@MEMORY\s+(search|stats)\s*[^\n]*/gi, "")
|
|
752
|
-
.replace(/@@GLOBALRULE\s+[^\n]+/g, "")
|
|
753
|
-
.replace(/@@DEFINE_SKILL[ \t]+[a-zA-Z0-9_\-.]+\n[\s\S]*?@@END_SKILL/g, "")
|
|
754
|
-
.replace(/@@DEFINE_WORKFLOW[ \t]+[a-zA-Z0-9_\-]+\n[\s\S]*?@@END_WORKFLOW/g, "")
|
|
755
|
-
.replace(/@@SERVICE\s+(restart|stop|start)\s+[a-zA-Z0-9_\-]+/g, "")
|
|
756
|
-
.replace(/@@STOP\b/g, "")
|
|
757
|
-
.replace(/@@KILL\b/g, "")
|
|
758
|
-
.replace(/@@CREATE_AGENT\s+\{[^}]+\}/g, "")
|
|
759
|
-
.replace(/@@REMOVE_AGENT\s+crew-[a-zA-Z0-9_-]+/g, "")
|
|
760
|
-
.trim();
|
|
761
|
-
|
|
762
|
-
let dispatched = null;
|
|
763
|
-
let pendingProject = null;
|
|
764
|
-
let pipeline = null;
|
|
765
|
-
|
|
766
|
-
if (projectSpec?.name && projectSpec?.outputDir) {
|
|
767
|
-
try {
|
|
768
|
-
pendingProject = await _deps.draftProject(projectSpec, sessionId);
|
|
769
|
-
_deps.appendHistory(userId, sessionId, "system", `Roadmap drafted for "${projectSpec.name}" — awaiting user approval.`);
|
|
770
|
-
if (pendingProject) _deps.broadcastSSE({ type: "pending_project", sessionId, pendingProject });
|
|
771
|
-
} catch (e) {
|
|
772
|
-
console.error(`[crew-lead] Roadmap draft failed: ${e.message}`);
|
|
773
|
-
_deps.appendHistory(userId, sessionId, "system", `Roadmap draft failed: ${e.message}`);
|
|
774
|
-
}
|
|
775
|
-
} else if (pipelineSteps) {
|
|
776
|
-
const pipelineId = crypto.randomUUID();
|
|
777
|
-
const { steps, waves } = pipelineSteps;
|
|
778
|
-
// Use the explicitly selected project only — never infer projectDir from LLM-generated task text,
|
|
779
|
-
// which can contain stale paths from brain.md and incorrectly lock the pipeline to the wrong project.
|
|
780
|
-
const _projectDir = activeProjectOutputDir || null;
|
|
781
|
-
_deps.pendingPipelines.set(pipelineId, { steps, waves, currentWave: 0, pendingTaskIds: new Set(), waveResults: [], sessionId, projectDir: _projectDir });
|
|
782
|
-
_deps.dispatchPipelineWave(pipelineId);
|
|
783
|
-
const waveDesc = waves.length > 1 ? ` in ${waves.length} waves` : "";
|
|
784
|
-
_deps.appendHistory(userId, sessionId, "system", `Pipeline started (${steps.length} steps${waveDesc}): ${waves.map(w => w.map(s => s.agent).join("+")).join(" → ")}`);
|
|
785
|
-
const agentFlow = waves.map(w => w.length > 1 ? `[${w.map(s => s.agent).join(" ∥ ")}]` : w[0].agent).join(" → ");
|
|
786
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Pipeline started (${steps.length} steps${waveDesc}): ${agentFlow}`;
|
|
787
|
-
pipeline = { pipelineId, steps, waves };
|
|
788
|
-
} else if (dispatch) {
|
|
789
|
-
console.log(`[chat-handler] LLM dispatch detected! Agent: ${dispatch.agent}, Task: ${(dispatch.task || "").slice(0, 60)}`);
|
|
790
|
-
const resolvedAgent = _deps.resolveAgentId(cfg, dispatch.agent) || dispatch.agent;
|
|
791
|
-
if (!cfg.knownAgents.length || cfg.knownAgents.includes(resolvedAgent)) {
|
|
792
|
-
dispatch.agent = resolvedAgent;
|
|
793
|
-
}
|
|
794
|
-
// QA always writes to projectDir/qa-report.md so crew-lead doesn't tell them a random path
|
|
795
|
-
const isQa = dispatch.agent === "crew-qa" || (dispatch.agent && dispatch.agent.includes("qa"));
|
|
796
|
-
if (isQa && activeProjectOutputDir && !/qa-report\.md|Write your report to/i.test(dispatch.task || "")) {
|
|
797
|
-
dispatch.task = (dispatch.task || "").trimEnd() + `\n\nWrite your report to ${activeProjectOutputDir}/qa-report.md (no other filename).`;
|
|
798
|
-
}
|
|
799
|
-
const pipelineMeta = activeProjectOutputDir ? { projectDir: activeProjectOutputDir } : null;
|
|
800
|
-
if (cfg.knownAgents.includes(dispatch.agent)) {
|
|
801
|
-
// Pass full dispatch spec so verify/done criteria are injected into task text
|
|
802
|
-
const ok = _deps.dispatchTask(dispatch.agent, dispatch, sessionId, pipelineMeta);
|
|
803
|
-
if (ok) {
|
|
804
|
-
dispatched = dispatch;
|
|
805
|
-
_deps.appendHistory(userId, sessionId, "system", `You dispatched to ${dispatch.agent}: "${(dispatch.task || "").slice(0, 200)}".`);
|
|
806
|
-
const dispatchLine = _deps.getRtPublish()
|
|
807
|
-
? `\n\n↳ Dispatched to ${dispatch.agent} — reply will show here when they finish.`
|
|
808
|
-
: `\n\n↳ Dispatched to ${dispatch.agent} (via ctl — check RT Messages tab for reply).`;
|
|
809
|
-
cleanReply = (cleanReply || "").trimEnd() + dispatchLine;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// ── Detect "dispatch lie" — LLM claims it dispatched but no @@DISPATCH was parsed ──
|
|
815
|
-
if (!dispatched && !pipeline && cleanReply) {
|
|
816
|
-
// Only trigger if Stinki explicitly claims to have ALREADY dispatched (past tense with confirmation)
|
|
817
|
-
const liedPattern = /\b(?:I (?:just |already |have )?(?:dispatched|sent it|forwarded it|sicced)|consider it done|they'(?:re|ve) got it|it'?s on its way to crew-|I'?ve tasked crew-)/i;
|
|
818
|
-
if (liedPattern.test(cleanReply)) {
|
|
819
|
-
console.log(`[crew-lead] Dispatch-lie detected — auto-retrying with extraction call`);
|
|
820
|
-
let _lieRetryOk = false;
|
|
821
|
-
try {
|
|
822
|
-
const _lieRetryMsgs = [
|
|
823
|
-
...messages,
|
|
824
|
-
{ role: "assistant", content: fullReply },
|
|
825
|
-
{ role: "user", content: "You described dispatching but the @@DISPATCH line was missing. Emit ONLY the @@DISPATCH JSON now. No prose, no explanation.\nFormat: @@DISPATCH {\"agent\":\"crew-X\",\"task\":\"...\"}" },
|
|
826
|
-
];
|
|
827
|
-
const _lieResult = await _deps.callLLM(_lieRetryMsgs, cfg);
|
|
828
|
-
const _lieDispatch = !projectSpec ? _deps.parseDispatch(_lieResult.reply || "", message) : null;
|
|
829
|
-
if (_lieDispatch && _lieDispatch.agent) {
|
|
830
|
-
console.log(`[crew-lead] Dispatch-lie retry succeeded -> ${_lieDispatch.agent}`);
|
|
831
|
-
const _lieTaskId = _deps.dispatchTask(_lieDispatch.agent, _lieDispatch.task, sessionId);
|
|
832
|
-
if (_lieTaskId) {
|
|
833
|
-
dispatched = _lieDispatch;
|
|
834
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n\u21b3 Dispatched to ${_lieDispatch.agent} \u2014 reply will show here when they finish.`;
|
|
835
|
-
_lieRetryOk = true;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
} catch (_lieErr) {
|
|
839
|
-
console.error(`[crew-lead] Dispatch-lie retry error: ${_lieErr.message}`);
|
|
840
|
-
}
|
|
841
|
-
if (!_lieRetryOk) {
|
|
842
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n\u26a0\ufe0f Dispatch failed even after retry \u2014 please ask me again.`;
|
|
843
|
-
_deps.appendHistory(userId, sessionId, "system", `Warning: LLM described dispatching without emitting @@DISPATCH — retry also failed.`);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// ── Execute @@TOOLS permission change ──────────────────────────────────────
|
|
849
|
-
if (toolsCmd?.agent) {
|
|
850
|
-
try {
|
|
851
|
-
const current = _deps.readAgentTools(toolsCmd.agent).tools;
|
|
852
|
-
let updated;
|
|
853
|
-
if (Array.isArray(toolsCmd.set)) {
|
|
854
|
-
updated = toolsCmd.set;
|
|
855
|
-
} else {
|
|
856
|
-
const granted = Array.isArray(toolsCmd.grant) ? toolsCmd.grant : [];
|
|
857
|
-
const revoked = Array.isArray(toolsCmd.revoke) ? toolsCmd.revoke : [];
|
|
858
|
-
updated = [...new Set([...current, ...granted].filter(t => !revoked.includes(t)))];
|
|
859
|
-
}
|
|
860
|
-
const saved = _deps.writeAgentTools(toolsCmd.agent, updated);
|
|
861
|
-
const note = `\n\n↳ Tool permissions updated for **${toolsCmd.agent}**: ${saved.join(", ")} — restart its bridge for changes to take effect.`;
|
|
862
|
-
cleanReply = (cleanReply || "").trimEnd() + note;
|
|
863
|
-
_deps.appendHistory(userId, sessionId, "system", `Tool permissions for ${toolsCmd.agent} updated to: ${saved.join(", ")}`);
|
|
864
|
-
console.log(`[crew-lead] @@TOOLS: ${toolsCmd.agent} → ${saved.join(", ")}`);
|
|
865
|
-
} catch (e) {
|
|
866
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to update tools for ${toolsCmd.agent}: ${e.message}`;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// ── Execute @@PROMPT system prompt edit ────────────────────────────────────
|
|
871
|
-
if (promptCmd?.agent) {
|
|
872
|
-
try {
|
|
873
|
-
const existing = _deps.getAgentPrompts()[promptCmd.agent] || "";
|
|
874
|
-
let newPrompt;
|
|
875
|
-
if (typeof promptCmd.set === "string") {
|
|
876
|
-
newPrompt = promptCmd.set;
|
|
877
|
-
} else if (typeof promptCmd.append === "string") {
|
|
878
|
-
newPrompt = existing ? `${existing}\n${promptCmd.append}` : promptCmd.append;
|
|
879
|
-
} else {
|
|
880
|
-
newPrompt = existing;
|
|
881
|
-
}
|
|
882
|
-
_deps.writeAgentPrompt(promptCmd.agent, newPrompt);
|
|
883
|
-
const preview = newPrompt.slice(0, 120).replace(/\n/g, " ");
|
|
884
|
-
const restartNote = promptCmd.agent === "crew-lead" ? "Takes effect on your next message; no restart needed." : "Restart its bridge for changes to take effect.";
|
|
885
|
-
const note = `\n\n↳ System prompt updated for **${promptCmd.agent}**: "${preview}${newPrompt.length > 120 ? "…" : ""}" — ${restartNote}`;
|
|
886
|
-
cleanReply = (cleanReply || "").trimEnd() + note;
|
|
887
|
-
_deps.appendHistory(userId, sessionId, "system", `Prompt for ${promptCmd.agent} updated.`);
|
|
888
|
-
console.log(`[crew-lead] @@PROMPT: ${promptCmd.agent} updated (${newPrompt.length} chars)`);
|
|
889
|
-
} catch (e) {
|
|
890
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to update prompt for ${promptCmd.agent}: ${e.message}`;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// ── Execute @@BRAIN append ──────────────────────────────────────────────────
|
|
895
|
-
// Routes to <projectDir>/.crewswarm/brain.md when a project is active, global brain.md otherwise
|
|
896
|
-
// Also stores in AgentMemory if shared memory is available
|
|
897
|
-
if (brainCmd) {
|
|
898
|
-
try {
|
|
899
|
-
const block = _deps.appendToBrain("crew-lead", brainCmd, activeProjectOutputDir || null);
|
|
900
|
-
const dest = activeProjectOutputDir ? `${path.basename(activeProjectOutputDir)}/.crewswarm/brain.md` : "memory/brain.md";
|
|
901
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Added to ${dest}: "${block.slice(0, 100)}"`;
|
|
902
|
-
console.log(`[crew-lead] @@BRAIN → ${dest}: ${brainCmd.slice(0, 80)}`);
|
|
903
|
-
|
|
904
|
-
// Also store in shared AgentMemory for cross-system access
|
|
905
|
-
if (isSharedMemoryAvailable()) {
|
|
906
|
-
const factId = rememberFact('crew-lead', brainCmd, {
|
|
907
|
-
critical: brainCmd.includes('CRITICAL') || brainCmd.includes('MUST'),
|
|
908
|
-
tags: ['brain', 'crew-lead'],
|
|
909
|
-
provider: 'crew-lead-chat'
|
|
910
|
-
});
|
|
911
|
-
if (factId) {
|
|
912
|
-
console.log(`[crew-lead] Fact also stored in shared AgentMemory: ${factId}`);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
} catch (e) {
|
|
916
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to write brain: ${e.message}`;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// ── Execute @@MEMORY commands ───────────────────────────────────────────────
|
|
921
|
-
if (memoryCmd) {
|
|
922
|
-
try {
|
|
923
|
-
if (memoryCmd.action === 'search') {
|
|
924
|
-
if (!isSharedMemoryAvailable()) {
|
|
925
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Shared memory not available. Run: cd crew-cli && npm run build`;
|
|
926
|
-
} else {
|
|
927
|
-
const projectDir = activeProjectOutputDir || process.cwd();
|
|
928
|
-
const hits = await searchMemory(projectDir, memoryCmd.query, {
|
|
929
|
-
maxResults: 10,
|
|
930
|
-
includeDocs: true,
|
|
931
|
-
includeCode: false,
|
|
932
|
-
preferSuccessful: true,
|
|
933
|
-
crewId: 'crew-lead'
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
if (hits.length === 0) {
|
|
937
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ **Memory search:** No results for "${memoryCmd.query}"`;
|
|
938
|
-
} else {
|
|
939
|
-
const results = hits.map(h =>
|
|
940
|
-
`- [${h.source}] **${h.title}** (score: ${h.score.toFixed(3)})\n ${h.text.slice(0, 150)}${h.text.length > 150 ? '...' : ''}`
|
|
941
|
-
).join('\n\n');
|
|
942
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ **Memory search results (${hits.length}):**\n\n${results}`;
|
|
943
|
-
}
|
|
944
|
-
console.log(`[crew-lead] @@MEMORY search "${memoryCmd.query}": ${hits.length} hits`);
|
|
945
|
-
}
|
|
946
|
-
} else if (memoryCmd.action === 'stats') {
|
|
947
|
-
if (!isSharedMemoryAvailable()) {
|
|
948
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Shared memory not available. Run: cd crew-cli && npm run build`;
|
|
949
|
-
} else {
|
|
950
|
-
const factStats = getMemoryStats('crew-lead');
|
|
951
|
-
const keeperStats = await getKeeperStats(activeProjectOutputDir || process.cwd());
|
|
952
|
-
|
|
953
|
-
const lines = [
|
|
954
|
-
'↳ **Shared Memory Statistics:**',
|
|
955
|
-
'',
|
|
956
|
-
'**AgentMemory (cognitive facts):**',
|
|
957
|
-
`- Total facts: ${factStats?.totalFacts || 0}`,
|
|
958
|
-
`- Critical facts: ${factStats?.criticalFacts || 0}`,
|
|
959
|
-
`- Providers: ${factStats?.providers?.join(', ') || 'none'}`,
|
|
960
|
-
'',
|
|
961
|
-
'**AgentKeeper (task memory):**',
|
|
962
|
-
`- Total entries: ${keeperStats?.entries || 0}`,
|
|
963
|
-
`- Storage bytes: ${keeperStats?.bytes ? (keeperStats.bytes / 1024).toFixed(1) + 'KB' : '0KB'}`,
|
|
964
|
-
`- By tier: ${keeperStats?.byTier ? Object.entries(keeperStats.byTier).map(([k,v]) => `${k}=${v}`).join(', ') : 'none'}`,
|
|
965
|
-
`- By agent: ${keeperStats?.byAgent ? Object.entries(keeperStats.byAgent).map(([k,v]) => `${k}=${v}`).join(', ') : 'none'}`
|
|
966
|
-
];
|
|
967
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n${lines.join('\n')}`;
|
|
968
|
-
console.log(`[crew-lead] @@MEMORY stats: ${factStats?.totalFacts || 0} facts, ${keeperStats?.entries || 0} keeper entries`);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
} catch (e) {
|
|
972
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Memory command failed: ${e.message}`;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// ── Execute @@GLOBALRULE append ────────────────────────────────────────────
|
|
977
|
-
if (globalRuleCmd) {
|
|
978
|
-
try {
|
|
979
|
-
_deps.appendGlobalRule(globalRuleCmd);
|
|
980
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Global rule added (all agents): "${globalRuleCmd}" — restart bridges to apply.`;
|
|
981
|
-
_deps.appendHistory(userId, sessionId, "system", `Global rule added: ${globalRuleCmd}`);
|
|
982
|
-
console.log(`[crew-lead] @@GLOBALRULE: ${globalRuleCmd}`);
|
|
983
|
-
} catch (e) {
|
|
984
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to write global-rules.md: ${e.message}`;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// ── Execute @@SERVICE control ───────────────────────────────────────────────
|
|
989
|
-
if (serviceCmd) {
|
|
990
|
-
const { action, id } = serviceCmd;
|
|
991
|
-
try {
|
|
992
|
-
// Per-agent restart: if ID looks like a specific agent, use the agents/:id/restart endpoint
|
|
993
|
-
const isSpecificAgent = id.startsWith("crew-") && id !== "crew-lead";
|
|
994
|
-
let result;
|
|
995
|
-
|
|
996
|
-
const authHeader = _deps.getRTToken() ? { authorization: `Bearer ${_deps.getRTToken()}` } : {};
|
|
997
|
-
if (action === "stop" && !isSpecificAgent) {
|
|
998
|
-
const r = await fetch(`${_deps.DASHBOARD}/api/services/stop`, {
|
|
999
|
-
method: "POST",
|
|
1000
|
-
headers: { "content-type": "application/json" },
|
|
1001
|
-
body: JSON.stringify({ id }),
|
|
1002
|
-
signal: AbortSignal.timeout(8000),
|
|
1003
|
-
});
|
|
1004
|
-
result = await r.json();
|
|
1005
|
-
} else if (isSpecificAgent && action !== "stop") {
|
|
1006
|
-
// Single agent bridge restart via dedicated endpoint
|
|
1007
|
-
const r = await fetch(`http://127.0.0.1:${_deps.PORT}/api/agents/${id}/restart`, {
|
|
1008
|
-
method: "POST",
|
|
1009
|
-
headers: authHeader,
|
|
1010
|
-
signal: AbortSignal.timeout(8000),
|
|
1011
|
-
});
|
|
1012
|
-
result = await r.json();
|
|
1013
|
-
} else {
|
|
1014
|
-
const r = await fetch(`${_deps.DASHBOARD}/api/services/restart`, {
|
|
1015
|
-
method: "POST",
|
|
1016
|
-
headers: { "content-type": "application/json" },
|
|
1017
|
-
body: JSON.stringify({ id }),
|
|
1018
|
-
signal: AbortSignal.timeout(8000),
|
|
1019
|
-
});
|
|
1020
|
-
result = await r.json();
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
if (result?.ok === false && result?.message) {
|
|
1024
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Service **${id}** — ${result.message}`;
|
|
1025
|
-
} else {
|
|
1026
|
-
const actionLabel = action === "stop" ? "stopped" : "restarted";
|
|
1027
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ **${id}** ${actionLabel}. Give it 2–3 seconds to come back online.`;
|
|
1028
|
-
_deps.appendHistory(userId, sessionId, "system", `Service ${id} ${actionLabel} via @@SERVICE.`);
|
|
1029
|
-
console.log(`[crew-lead] @@SERVICE ${action} ${id}: ok`);
|
|
1030
|
-
}
|
|
1031
|
-
} catch (e) {
|
|
1032
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to ${action} **${id}**: ${e.message}`;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// ── Surface @@DEFINE_SKILL results ─────────────────────────────────────────
|
|
1037
|
-
for (const ds of defineSkillCmds) {
|
|
1038
|
-
if (ds.ok) {
|
|
1039
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Skill **${ds.name}** saved to ~/.crewswarm/skills/${ds.name}.json — agents with 'skill' permission can now call it.`;
|
|
1040
|
-
_deps.appendHistory(userId, sessionId, "system", `Skill "${ds.name}" defined and saved.`);
|
|
1041
|
-
} else {
|
|
1042
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to save skill **${ds.name}**: ${ds.error}`;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// ── Surface @@DEFINE_WORKFLOW results ─────────────────────────────────────
|
|
1047
|
-
for (const dw of defineWorkflowCmds) {
|
|
1048
|
-
if (dw.ok) {
|
|
1049
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Workflow **${dw.name}** saved to ~/.crewswarm/pipelines/${dw.name}.json (${dw.stageCount} stages). Run with: \`node scripts/run-scheduled-pipeline.mjs ${dw.name}\` or add to crontab.`;
|
|
1050
|
-
_deps.appendHistory(userId, sessionId, "system", `Workflow "${dw.name}" defined (${dw.stageCount} stages).`);
|
|
1051
|
-
} else {
|
|
1052
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to save workflow **${dw.name}**: ${dw.error}`;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// ── Execute @@CREATE_AGENT — create a new specialist agent ────────────────
|
|
1057
|
-
if (createAgentCmd?.id) {
|
|
1058
|
-
try {
|
|
1059
|
-
const result = _deps.createAgent(createAgentCmd);
|
|
1060
|
-
const ocNote = result.useOpenCode ? `, OpenCode: ${result.useOpenCode}` : "";
|
|
1061
|
-
|
|
1062
|
-
// Spawn the bridge directly via start-crew.mjs --agent (no dashboard round-trip)
|
|
1063
|
-
let spawnNote = "";
|
|
1064
|
-
try {
|
|
1065
|
-
const startScript = path.join(process.cwd(), "scripts", "start-crew.mjs");
|
|
1066
|
-
const { execSync: exec2 } = await import("node:child_process");
|
|
1067
|
-
exec2(`node ${startScript} --agent ${result.id}`, {
|
|
1068
|
-
timeout: 10000,
|
|
1069
|
-
env: { ...process.env, CREWSWARM_RT_AUTH_TOKEN: _deps.getRTToken() },
|
|
1070
|
-
stdio: "pipe",
|
|
1071
|
-
});
|
|
1072
|
-
// Verify bridge is running
|
|
1073
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
1074
|
-
try {
|
|
1075
|
-
const psOut = exec2("ps aux", { encoding: "utf8" });
|
|
1076
|
-
const running = psOut.includes(result.id);
|
|
1077
|
-
spawnNote = running
|
|
1078
|
-
? " — bridge spawned and online"
|
|
1079
|
-
: " — bridge spawned (verifying…)";
|
|
1080
|
-
} catch {
|
|
1081
|
-
spawnNote = " — bridge spawned";
|
|
1082
|
-
}
|
|
1083
|
-
} catch (spawnErr) {
|
|
1084
|
-
console.error(`[crew-lead] Failed to spawn bridge for ${result.id}:`, spawnErr.message);
|
|
1085
|
-
spawnNote = " — restart bridges to bring it online (`npm run restart-all`)";
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Agent **${result.id}** created (role: ${result.role}, tools: ${result.tools.join(", ")}${ocNote})${spawnNote}. You can now dispatch tasks to it.`;
|
|
1089
|
-
_deps.appendHistory(userId, sessionId, "system", `Dynamic agent ${result.id} created (role: ${result.role}, openCode: ${!!result.useOpenCode}).`);
|
|
1090
|
-
console.log(`[crew-lead] @@CREATE_AGENT: ${result.id} (role: ${result.role}, tools: ${result.tools.join(",")}, openCode: ${!!result.useOpenCode})`);
|
|
1091
|
-
_deps.broadcastSSE({ type: "agent_created", agent: result, ts: Date.now() });
|
|
1092
|
-
} catch (e) {
|
|
1093
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to create agent: ${e.message}`;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// ── Execute @@REMOVE_AGENT — remove a dynamically created agent ──────────
|
|
1098
|
-
if (removeAgentCmd) {
|
|
1099
|
-
try {
|
|
1100
|
-
_deps.removeDynamicAgent(removeAgentCmd);
|
|
1101
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Agent **${removeAgentCmd}** removed. Restart bridges to clean up.`;
|
|
1102
|
-
_deps.appendHistory(userId, sessionId, "system", `Dynamic agent ${removeAgentCmd} removed.`);
|
|
1103
|
-
console.log(`[crew-lead] @@REMOVE_AGENT: ${removeAgentCmd}`);
|
|
1104
|
-
} catch (e) {
|
|
1105
|
-
cleanReply = (cleanReply || "").trimEnd() + `\n\n↳ Failed to remove agent: ${e.message}`;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (usedFallback) {
|
|
1110
|
-
const primaryLabel = `${cfg.providerKey}/${cfg.modelId}`;
|
|
1111
|
-
const fbUrl = `${cfg.fallbackProvider.baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
1112
|
-
// Strip any fallback line the model echoed (avoids duplicate banner)
|
|
1113
|
-
const stripped = (cleanReply || "").replace(/^⚡\s*\*fallback:[^*]*\*[^\n]*\n?/gm, "").trimStart();
|
|
1114
|
-
cleanReply = `⚡ *fallback: ${activeModel}* @ ${fbUrl} (primary ${primaryLabel} failed: ${fallbackReason})\n\n${stripped}`;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// Trim blank space left by stripped @@SKILL-only replies before appending results
|
|
1118
|
-
if (skillCalls.length > 0) cleanReply = (cleanReply || "").trim();
|
|
1119
|
-
|
|
1120
|
-
// Execute @@SKILL calls — collect display blocks (for user) and feedback blocks (for LLM second pass)
|
|
1121
|
-
const skillDisplayBlocks = [];
|
|
1122
|
-
const skillFeedbackBlocks = [];
|
|
1123
|
-
|
|
1124
|
-
for (const { skillName, params } of skillCalls) {
|
|
1125
|
-
try {
|
|
1126
|
-
const result = await _deps.executeSkillFromCrewLead(skillName, params);
|
|
1127
|
-
console.log(`[crew-lead] @@SKILL ${skillName} → OK`);
|
|
1128
|
-
const isBenchmark = skillName === "zeroeval.benchmark" || skillName === "benchmark" || skillName === "benchmarks";
|
|
1129
|
-
const _skillCount = isBenchmark && result?.models?.length ? ` · ${Math.min(Number(params?.limit ?? params?.top ?? 100), result.models.length)}/${result.models.length} models` : "";
|
|
1130
|
-
const skillTag = `↳ *skill: ${skillName}${_skillCount}*`;
|
|
1131
|
-
|
|
1132
|
-
let displayBlock = "";
|
|
1133
|
-
let feedbackBlock = "";
|
|
1134
|
-
|
|
1135
|
-
if (isBenchmark && Array.isArray(result) && result.length) {
|
|
1136
|
-
const list = result.slice(0, 50).map(b => {
|
|
1137
|
-
const id = typeof b === "object" ? b.benchmark_id : b;
|
|
1138
|
-
const name = typeof b === "object" ? b.name || "" : "";
|
|
1139
|
-
return ` - ${id}${name ? `: ${name}` : ""}`;
|
|
1140
|
-
}).join("\n");
|
|
1141
|
-
const body = `**Available benchmarks** (omit benchmark_id or use empty to list):\n${list}${result.length > 50 ? `\n … and ${result.length - 50} more` : ""}`;
|
|
1142
|
-
displayBlock = `${skillTag}\n\n${body}`;
|
|
1143
|
-
feedbackBlock = `[Skill result: ${skillName} — benchmark list]\n${body}`;
|
|
1144
|
-
} else if (isBenchmark && result && !result.models?.length && !Array.isArray(result)) {
|
|
1145
|
-
const name = result.name || result.benchmark_id || skillName;
|
|
1146
|
-
displayBlock = `${skillTag}\n\n*${name}* — no models found for this benchmark yet.`;
|
|
1147
|
-
feedbackBlock = `[Skill result: ${skillName}]\nNo models found for benchmark "${name}".`;
|
|
1148
|
-
} else if (isBenchmark && result?.models?.length) {
|
|
1149
|
-
const limit = Number(params?.limit ?? params?.top ?? 100);
|
|
1150
|
-
const top = result.models.slice(0, limit);
|
|
1151
|
-
const rows = top.map(m => {
|
|
1152
|
-
const pct = ((m.normalized_score ?? m.score ?? 0) * 100).toFixed(1);
|
|
1153
|
-
const inC = m.input_cost_per_million ?? 0;
|
|
1154
|
-
const outC = m.output_cost_per_million ?? 0;
|
|
1155
|
-
const inCents = inC > 0 ? Math.round(inC * 100) + '¢' : '?';
|
|
1156
|
-
const outCents = outC > 0 ? Math.round(outC * 100) + '¢' : '?';
|
|
1157
|
-
const cost = (inC > 0 || outC > 0) ? ` @ ${inCents} → ${outCents}` : "";
|
|
1158
|
-
const score = (m.normalized_score ?? m.score) ?? 0;
|
|
1159
|
-
const centsPerPt = (inC + outC) > 0 && score > 0 ? ` → ${((inC + outC) * 100 / (score * 100)).toFixed(1)} ¢/pt` : "";
|
|
1160
|
-
return ` ${m.rank}. **${m.model_name}** (${m.organization_name}) — ${pct}%${cost}${centsPerPt}`;
|
|
1161
|
-
}).join("\n");
|
|
1162
|
-
const showing = top.length < result.models.length ? ` (showing ${top.length} of ${result.models.length} — add "limit":N for more)` : ` (all ${result.models.length} models)`;
|
|
1163
|
-
const body = `**${result.name || "Benchmark"}** — top ${top.length}${showing}:\n${rows}`;
|
|
1164
|
-
displayBlock = `${skillTag}\n\n${body}`;
|
|
1165
|
-
feedbackBlock = `[Skill result: ${skillName}]\n${body}`;
|
|
1166
|
-
} else if (result?.output !== undefined) {
|
|
1167
|
-
const out = String(result.output).trim();
|
|
1168
|
-
displayBlock = `${skillTag}\n\n\`\`\`\n${out.slice(0, 3000)}${out.length > 3000 ? "\n… (truncated)" : ""}\n\`\`\``;
|
|
1169
|
-
feedbackBlock = `[Skill result: ${skillName}]\n${out.slice(0, 3000)}`;
|
|
1170
|
-
} else {
|
|
1171
|
-
const raw = typeof result === "object" ? JSON.stringify(result) : String(result);
|
|
1172
|
-
displayBlock = `${skillTag}: ${raw.slice(0, 600)}${raw.length > 600 ? "…" : ""}`;
|
|
1173
|
-
feedbackBlock = `[Skill result: ${skillName}]\n${raw.slice(0, 600)}`;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Check feedbackLoop flag from skill JSON (default: true — opt out with "feedbackLoop": false)
|
|
1177
|
-
const resolvedSkillName = _deps.resolveSkillAlias(skillName);
|
|
1178
|
-
const skillDefPath = path.join(os.homedir(), ".crewswarm", "skills", `${resolvedSkillName}.json`);
|
|
1179
|
-
const skillDefRaw = fs.existsSync(skillDefPath) ? JSON.parse(fs.readFileSync(skillDefPath, "utf8")) : {};
|
|
1180
|
-
const wantsFeedback = skillDefRaw.feedbackLoop !== false;
|
|
1181
|
-
|
|
1182
|
-
skillDisplayBlocks.push(displayBlock);
|
|
1183
|
-
if (wantsFeedback) skillFeedbackBlocks.push(feedbackBlock);
|
|
1184
|
-
|
|
1185
|
-
} catch (e) {
|
|
1186
|
-
console.error(`[crew-lead] @@SKILL ${skillName} failed:`, e.message);
|
|
1187
|
-
const errBlock = `↳ *${skillName}* failed: ${e.message}`;
|
|
1188
|
-
skillDisplayBlocks.push(errBlock);
|
|
1189
|
-
skillFeedbackBlocks.push(`[Skill result: ${skillName}]\nFailed: ${e.message}`);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
// Append display blocks so the user always sees the raw skill data
|
|
1194
|
-
if (skillDisplayBlocks.length > 0) {
|
|
1195
|
-
cleanReply = (cleanReply ? cleanReply + "\n\n" : "") + skillDisplayBlocks.join("\n\n");
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// Feedback loop: second LLM call so the model actually reads the results and responds
|
|
1199
|
-
if (skillFeedbackBlocks.length > 0) {
|
|
1200
|
-
try {
|
|
1201
|
-
const feedbackUserMsg = skillFeedbackBlocks.join("\n\n")
|
|
1202
|
-
+ "\n\nBased ONLY on the skill results above, respond to the user's original question. "
|
|
1203
|
-
+ "Be concise and specific. Do not invent numbers or models not in the results above.";
|
|
1204
|
-
const feedbackMessages = [
|
|
1205
|
-
...messages,
|
|
1206
|
-
{ role: "assistant", content: fullReply },
|
|
1207
|
-
{ role: "user", content: feedbackUserMsg },
|
|
1208
|
-
];
|
|
1209
|
-
console.log(`[crew-lead] Skill feedback loop — second LLM call (${skillFeedbackBlocks.length} skill(s))`);
|
|
1210
|
-
const feedbackResult = await _deps.callLLM(feedbackMessages, cfg);
|
|
1211
|
-
if (feedbackResult.reply?.trim()) {
|
|
1212
|
-
cleanReply = cleanReply + "\n\n" + feedbackResult.reply.trim();
|
|
1213
|
-
}
|
|
1214
|
-
} catch (fbErr) {
|
|
1215
|
-
console.error(`[crew-lead] Skill feedback loop failed:`, fbErr.message);
|
|
1216
|
-
// Non-fatal — user still sees raw skill data above
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// Auto-learn from conversation: extract important facts and append to brain
|
|
1221
|
-
// Trigger on: user confirms something, agent discovers/fixes something, or decisions are made
|
|
1222
|
-
const shouldAutoLearn = (msg) => {
|
|
1223
|
-
const lower = msg.toLowerCase();
|
|
1224
|
-
// Trigger phrases that indicate learning opportunities
|
|
1225
|
-
return /\b(discovered|learned|figured out|found that|turns out|confirmed|fixed by|solution was|root cause|the issue is|mistake was|now i know|remember that|important:|note:|fyi:|btw:)\b/i.test(msg)
|
|
1226
|
-
|| /\b(always|never|every time|from now on|in future|going forward)\b/i.test(lower)
|
|
1227
|
-
|| (msg.includes("✅") && msg.length > 30); // Success markers with substance
|
|
1228
|
-
};
|
|
1229
|
-
|
|
1230
|
-
// Auto-append to brain when significant facts emerge (after LLM reply)
|
|
1231
|
-
if (cleanReply && shouldAutoLearn(cleanReply + " " + message)) {
|
|
1232
|
-
try {
|
|
1233
|
-
// Extract key fact from the conversation
|
|
1234
|
-
const extractFact = (text) => {
|
|
1235
|
-
// Look for sentences with learning indicators
|
|
1236
|
-
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 20);
|
|
1237
|
-
for (const sent of sentences) {
|
|
1238
|
-
if (shouldAutoLearn(sent)) {
|
|
1239
|
-
const clean = sent.trim()
|
|
1240
|
-
.replace(/^(discovered|learned|figured out|found that|turns out|confirmed|fixed by|solution was|the issue is|mistake was|now i know|remember that|important:|note:|fyi:|btw:)\s*/i, "")
|
|
1241
|
-
.replace(/\s+/g, " ")
|
|
1242
|
-
.slice(0, 200);
|
|
1243
|
-
if (clean.length > 15) return clean;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return null;
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
const fact = extractFact(cleanReply) || extractFact(message);
|
|
1250
|
-
if (fact && fact.length > 15 && fact.length < 300) {
|
|
1251
|
-
const brainEntry = `crew-lead (auto): ${fact}`;
|
|
1252
|
-
_deps.appendToBrain("crew-lead", brainEntry, activeProjectOutputDir || null);
|
|
1253
|
-
console.log(`[crew-lead] Auto-learned to brain: ${fact.slice(0, 80)}...`);
|
|
1254
|
-
}
|
|
1255
|
-
} catch (e) {
|
|
1256
|
-
console.error(`[crew-lead] Auto-learn failed: ${e.message}`);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
// not conversation context. Storing it pollutes future LLM requests with noisy boilerplate.
|
|
1260
|
-
const historyReply = usedFallback
|
|
1261
|
-
? (cleanReply || "").replace(/^⚡\s*\*fallback:[^*]*\*[^\n]*\n?/gm, "").trimStart()
|
|
1262
|
-
: cleanReply;
|
|
1263
|
-
_deps.appendHistory(userId, sessionId, "assistant", historyReply);
|
|
1264
|
-
_deps.broadcastSSE({
|
|
1265
|
-
type: "chat_message",
|
|
1266
|
-
sessionId,
|
|
1267
|
-
role: "assistant",
|
|
1268
|
-
content: cleanReply,
|
|
1269
|
-
model: activeModel, // Always send model info
|
|
1270
|
-
...(usedFallback ? { fallbackModel: activeModel, fallbackReason } : {})
|
|
1271
|
-
});
|
|
1272
|
-
|
|
1273
|
-
return { reply: cleanReply, dispatched, pendingProject, pipeline };
|
|
1274
|
-
}
|