crewswarm 0.9.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.env.example +8 -1
  2. package/README.md +58 -9
  3. package/apps/dashboard/README.md +49 -0
  4. package/apps/dashboard/dist/assets/{index-D-sRshvg.css → index-C5-vlIwl.css} +1 -1
  5. package/apps/dashboard/dist/assets/index-CSooN9fi.js +2 -0
  6. package/apps/dashboard/dist/assets/index-CSooN9fi.js.br +0 -0
  7. package/apps/dashboard/dist/assets/tab-spending-tab-DcXD5TQY.js +1 -0
  8. package/apps/dashboard/dist/assets/tab-spending-tab-DcXD5TQY.js.br +0 -0
  9. package/apps/dashboard/dist/assets/tab-testing-tab-Ea5K-rsb.js +1 -0
  10. package/apps/dashboard/dist/index.html +85 -7
  11. package/apps/dashboard/dist/index.html.br +0 -0
  12. package/contrib/openclaw-plugin/index.ts +20 -11
  13. package/install.sh +2 -2
  14. package/lib/autoharness/index.mjs +151 -1
  15. package/lib/chat/history.mjs +1 -1
  16. package/lib/contacts/identity-linker.mjs +24 -3
  17. package/lib/contacts/index.mjs +2 -1
  18. package/lib/crew-lead/chat-handler.mjs +56 -33
  19. package/lib/crew-lead/llm-caller.mjs +71 -14
  20. package/lib/crew-lead/prompts.mjs +4 -2
  21. package/lib/crew-lead/wave-dispatcher.mjs +53 -3
  22. package/lib/crew-lead/worktree.mjs +258 -0
  23. package/lib/crew-lead/ws-router.mjs +43 -0
  24. package/lib/engines/rt-envelope.mjs +4 -1
  25. package/lib/memory/relevance-scorer.mjs +199 -0
  26. package/lib/memory/shared-adapter.mjs +85 -19
  27. package/package.json +10 -3
  28. package/scripts/dashboard.mjs +398 -28
  29. package/scripts/health-check.mjs +70 -28
  30. package/scripts/install-docker.sh +1 -1
  31. package/scripts/restart-all-from-repo.sh +25 -21
  32. package/scripts/start.mjs +81 -26
  33. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
  34. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
  35. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  36. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  37. package/apps/dashboard/dist/assets/index-BeVllEj_.js +0 -2
  38. package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
  39. package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
  40. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  41. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  42. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  43. package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
  44. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  45. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  46. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  47. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  48. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
  49. package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
  50. package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
  51. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  52. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  53. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
  54. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js +0 -1
  56. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +0 -1
  60. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  62. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  64. package/apps/dashboard/dist/index.html.gz +0 -0
@@ -1068,11 +1068,14 @@ export async function handleRealtimeEnvelope(envelope, client, bridge) {
1068
1068
  /and nothing else\b/i.test(prompt || "");
1069
1069
 
1070
1070
  // Append original task spec for self-verification (LangChain pattern)
1071
- // Skip strict-output prompts where any extra text would violate the task.
1071
+ // Skip strict-output prompts and trivial/empty replies where the reminder adds noise.
1072
+ const replyStripped = (reply || "").replace(/\s+/g, " ").trim();
1073
+ const isTrivialReply = replyStripped.length < 50 || /^\(completed\)$/i.test(replyStripped);
1072
1074
  if (
1073
1075
  reply &&
1074
1076
  prompt &&
1075
1077
  !requestsExactReply &&
1078
+ !isTrivialReply &&
1076
1079
  !reply.includes("[ORIGINAL TASK]")
1077
1080
  ) {
1078
1081
  const taskSpecReminder = `\n\n---\n**[ORIGINAL TASK]:**\n${prompt.slice(0, 500)}${prompt.length > 500 ? "..." : ""}\n\nDoes your implementation address ALL requirements above?`;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Memory Relevance Scorer
3
+ *
4
+ * Score memories by relevance to a query using:
5
+ * 1. Recency — newer memories score higher (exponential decay, ~30-day half-life)
6
+ * 2. Frequency — memories accessed more often score higher (normalised log)
7
+ * 3. Keyword — TF-IDF-like scoring against query terms (inverse-length weighting)
8
+ * 4. Context — memories from the same project/agent/session score higher
9
+ *
10
+ * Pure functions only — zero I/O, zero dependencies.
11
+ */
12
+
13
+ // ─── Internal helpers ────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Tokenise a string into lowercase alpha-numeric tokens of length >= 2.
17
+ * @param {string} text
18
+ * @returns {string[]}
19
+ */
20
+ function tokenise(text) {
21
+ if (!text || typeof text !== 'string') return [];
22
+ return text
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9\s_-]/g, ' ')
25
+ .split(/\s+/)
26
+ .filter(t => t.length >= 2);
27
+ }
28
+
29
+ // ─── Individual scoring components ──────────────────────────────────────────
30
+
31
+ /**
32
+ * Recency score: exponential decay with ~30-day half-life.
33
+ * Returns 1.0 for brand-new memories, approaching 0 for very old ones.
34
+ *
35
+ * @param {string|number|Date} timestamp - ISO string, epoch ms, or Date
36
+ * @param {number} nowMs - current epoch ms (injectable for testing)
37
+ * @returns {number} [0, 1]
38
+ */
39
+ export function computeRecency(timestamp, nowMs = Date.now()) {
40
+ if (timestamp == null) return 0;
41
+ const createdAt = timestamp instanceof Date
42
+ ? timestamp.getTime()
43
+ : new Date(timestamp).getTime();
44
+ if (Number.isNaN(createdAt)) return 0;
45
+ const daysSince = Math.max(0, (nowMs - createdAt) / (1000 * 60 * 60 * 24));
46
+ return Math.exp(-daysSince / 30);
47
+ }
48
+
49
+ /**
50
+ * Frequency score: log-normalised access count relative to a max.
51
+ * Both accessCount and maxAccessCount must be >= 0.
52
+ *
53
+ * @param {number} accessCount
54
+ * @param {number} maxAccessCount - upper bound for normalisation (default 100)
55
+ * @returns {number} [0, 1]
56
+ */
57
+ export function computeFrequency(accessCount, maxAccessCount = 100) {
58
+ const count = Math.max(0, Number(accessCount) || 0);
59
+ const maxCount = Math.max(1, Number(maxAccessCount) || 100);
60
+ return Math.min(1, Math.log(1 + count) / Math.log(1 + maxCount));
61
+ }
62
+
63
+ /**
64
+ * Keyword match score: TF-IDF-like overlap between query tokens and memory content.
65
+ * Rarer (longer) query words are weighted more heavily.
66
+ *
67
+ * @param {string} content - memory content
68
+ * @param {string} query
69
+ * @returns {number} [0, 1]
70
+ */
71
+ export function computeKeywordMatch(content, query) {
72
+ const queryTokens = tokenise(query);
73
+ const contentTokens = tokenise(content);
74
+
75
+ if (queryTokens.length === 0 || contentTokens.length === 0) return 0;
76
+
77
+ const contentSet = new Set(contentTokens);
78
+
79
+ // Weight each query token by its length (longer words are more specific)
80
+ let weightedMatch = 0;
81
+ let totalWeight = 0;
82
+
83
+ for (const token of queryTokens) {
84
+ // IDF proxy: weight proportional to token length (longer = rarer heuristic)
85
+ const weight = Math.log(1 + token.length);
86
+ totalWeight += weight;
87
+ if (contentSet.has(token)) {
88
+ weightedMatch += weight;
89
+ }
90
+ }
91
+
92
+ if (totalWeight === 0) return 0;
93
+ return weightedMatch / totalWeight;
94
+ }
95
+
96
+ /**
97
+ * Context match score: bonus points for shared project / agent / session.
98
+ *
99
+ * @param {object} memory - memory object with optional projectId, agentId, sessionId
100
+ * @param {object} context - { projectId?, agentId?, sessionId? }
101
+ * @returns {number} [0, 1]
102
+ */
103
+ export function computeContextMatch(memory, context = {}) {
104
+ if (!memory || !context) return 0;
105
+
106
+ let score = 0;
107
+
108
+ if (context.projectId && memory.projectId &&
109
+ context.projectId === memory.projectId) {
110
+ score += 0.5;
111
+ }
112
+
113
+ if (context.agentId && memory.agentId &&
114
+ context.agentId === memory.agentId) {
115
+ score += 0.3;
116
+ }
117
+
118
+ if (context.sessionId && memory.sessionId &&
119
+ context.sessionId === memory.sessionId) {
120
+ score += 0.2;
121
+ }
122
+
123
+ // Cap at 1.0
124
+ return Math.min(1, score);
125
+ }
126
+
127
+ // ─── Public API ──────────────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Score a single memory object for relevance to a query + context.
131
+ *
132
+ * Expected memory shape (all fields optional except content):
133
+ * {
134
+ * content: string,
135
+ * timestamp: string|number|Date, // ISO or epoch ms
136
+ * accessCount: number,
137
+ * projectId: string,
138
+ * agentId: string,
139
+ * sessionId: string,
140
+ * }
141
+ *
142
+ * @param {object} memory
143
+ * @param {string} query
144
+ * @param {object} [context] - { projectId?, agentId?, sessionId? }
145
+ * @param {object} [opts]
146
+ * @param {number} [opts.nowMs] - override current time (for testing)
147
+ * @param {number} [opts.maxAccessCount] - normalisation ceiling for frequency
148
+ * @returns {number} weighted relevance score in [0, 1]
149
+ */
150
+ export function scoreMemory(memory, query, context = {}, opts = {}) {
151
+ if (!memory) return 0;
152
+
153
+ const nowMs = opts.nowMs != null ? opts.nowMs : Date.now();
154
+ const maxAccessCount = opts.maxAccessCount != null ? opts.maxAccessCount : 100;
155
+
156
+ const recencyScore = computeRecency(memory.timestamp, nowMs);
157
+ const frequencyScore = computeFrequency(memory.accessCount || 0, maxAccessCount);
158
+ const keywordScore = computeKeywordMatch(memory.content || '', query);
159
+ const contextScore = computeContextMatch(memory, context);
160
+
161
+ return (
162
+ 0.30 * recencyScore +
163
+ 0.20 * frequencyScore +
164
+ 0.35 * keywordScore +
165
+ 0.15 * contextScore
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Rank an array of memories by relevance and return the top N.
171
+ * Attaches a `relevanceScore` property to each returned object.
172
+ *
173
+ * @param {object[]} memories
174
+ * @param {string} query
175
+ * @param {object} [context] - { projectId?, agentId?, sessionId? }
176
+ * @param {number} [maxResults=10]
177
+ * @param {object} [opts] - forwarded to scoreMemory
178
+ * @returns {object[]} sorted slice with relevanceScore attached
179
+ */
180
+ export function rankMemories(memories, query, context = {}, maxResults = 10, opts = {}) {
181
+ if (!Array.isArray(memories) || memories.length === 0) return [];
182
+
183
+ return memories
184
+ .map(m => ({ ...m, relevanceScore: scoreMemory(m, query, context, opts) }))
185
+ .sort((a, b) => b.relevanceScore - a.relevanceScore)
186
+ .slice(0, Math.max(1, maxResults));
187
+ }
188
+
189
+ /**
190
+ * Derive the max accessCount from a collection of memories.
191
+ * Useful for caller-side normalisation when passing opts.maxAccessCount.
192
+ *
193
+ * @param {object[]} memories
194
+ * @returns {number}
195
+ */
196
+ export function maxAccessCount(memories) {
197
+ if (!Array.isArray(memories) || memories.length === 0) return 0;
198
+ return memories.reduce((max, m) => Math.max(max, m.accessCount || 0), 0);
199
+ }
@@ -12,6 +12,7 @@ import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import os from 'node:os';
14
14
  import { createRequire } from 'node:module';
15
+ import { rankMemories, maxAccessCount } from './relevance-scorer.mjs';
15
16
 
16
17
  const require = createRequire(import.meta.url);
17
18
 
@@ -124,41 +125,106 @@ export function rememberFact(agentId, content, options = {}) {
124
125
 
125
126
  /**
126
127
  * Recall memory context for a task using MemoryBroker (blends AgentKeeper + AgentMemory + Collections).
128
+ * ENHANCED: Applies relevance scoring (recency + frequency + keyword + context) and tracks access metadata.
127
129
  * ENHANCED: Also includes relevant conversation history from project messages.
128
130
  * @param {string} projectDir - Project directory
129
131
  * @param {string} query - Task description or search query
130
- * @param {object} options - { maxResults?, includeDocs?, includeCode?, pathHints?, preferSuccessful?, userId?, projectId? }
132
+ * @param {object} options - { maxResults?, includeDocs?, includeCode?, pathHints?, preferSuccessful?, userId?, projectId?, agentId?, sessionId? }
131
133
  * @returns {Promise<string>} - Formatted context block
132
134
  */
133
135
  export async function recallMemoryContext(projectDir, query, options = {}) {
134
136
  const broker = getMemoryBroker(projectDir, { crewId: options.crewId || 'crew-lead' });
135
-
137
+
136
138
  let memoryContext = '';
137
-
139
+
138
140
  // Get standard memory (AgentKeeper + AgentMemory + Collections)
139
- // NEW: Filter for success-weighted, high-signal artifacts only
141
+ // Fetch a larger candidate set so the relevance ranker has room to reorder
140
142
  if (broker) {
141
- memoryContext = await broker.recallAsContext(query, {
142
- maxResults: options.maxResults || 5,
143
- includeDocs: options.includeDocs !== false,
144
- includeCode: Boolean(options.includeCode),
145
- pathHints: options.pathHints || [],
146
- preferSuccessful: options.preferSuccessful !== false,
147
- // NEW: Quality filters to prevent context contamination
148
- minScore: 0.7, // Only high-confidence matches
149
- excludeFailed: true, // Filter out failed tasks
150
- excludeErrors: true, // Filter out error-only entries
151
- excludeTimeouts: true // Filter out timeout failures
152
- });
143
+ const maxResults = options.maxResults || 5;
144
+ const candidateLimit = Math.max(maxResults * 3, 15);
145
+
146
+ // Pull raw structured hits when available, fall back to formatted context
147
+ let rawHits = null;
148
+ if (typeof broker.recall === 'function') {
149
+ try {
150
+ rawHits = await broker.recall(query, {
151
+ maxResults: candidateLimit,
152
+ includeDocs: options.includeDocs !== false,
153
+ includeCode: Boolean(options.includeCode),
154
+ pathHints: options.pathHints || [],
155
+ preferSuccessful: options.preferSuccessful !== false,
156
+ minScore: 0.7,
157
+ excludeFailed: true,
158
+ excludeErrors: true,
159
+ excludeTimeouts: true
160
+ });
161
+ } catch {
162
+ rawHits = null;
163
+ }
164
+ }
165
+
166
+ if (rawHits && Array.isArray(rawHits) && rawHits.length > 0) {
167
+ // Build a relevance context for the ranker
168
+ const scoringContext = {
169
+ projectId: options.projectId,
170
+ agentId: options.agentId,
171
+ sessionId: options.sessionId
172
+ };
173
+
174
+ // Normalise hits to the shape scoreMemory expects
175
+ const now = Date.now();
176
+ const normalised = rawHits.map(hit => ({
177
+ ...hit,
178
+ content: hit.text || hit.content || '',
179
+ timestamp: hit.metadata?.timestamp || hit.timestamp || new Date(now - 86400000).toISOString(),
180
+ accessCount: (hit.accessCount || 0) + 1, // count this retrieval
181
+ lastAccessed: new Date(now).toISOString(),
182
+ projectId: hit.metadata?.projectId || hit.projectId,
183
+ agentId: hit.metadata?.agentId || hit.agentId,
184
+ sessionId: hit.metadata?.sessionId || hit.sessionId
185
+ }));
186
+
187
+ const scoringOpts = { nowMs: now, maxAccessCount: maxAccessCount(normalised) };
188
+ const ranked = rankMemories(normalised, query, scoringContext, maxResults, scoringOpts);
189
+
190
+ // Re-serialise to context string (mirrors broker.recallAsContext format)
191
+ if (ranked.length > 0) {
192
+ const lines = ranked.map((hit, i) => {
193
+ const score = hit.relevanceScore.toFixed(3);
194
+ const source = hit.source || hit.metadata?.source || 'memory';
195
+ const title = hit.title || hit.metadata?.title || `Result ${i + 1}`;
196
+ return `[${source}] ${title} (relevance: ${score})\n${hit.content}`;
197
+ });
198
+ memoryContext = lines.join('\n\n---\n\n');
199
+ }
200
+ } else {
201
+ // Fallback: broker doesn't expose raw hits — use formatted context as-is
202
+ try {
203
+ memoryContext = await broker.recallAsContext(query, {
204
+ maxResults: options.maxResults || 5,
205
+ includeDocs: options.includeDocs !== false,
206
+ includeCode: Boolean(options.includeCode),
207
+ pathHints: options.pathHints || [],
208
+ preferSuccessful: options.preferSuccessful !== false,
209
+ // Quality filters to prevent context contamination
210
+ minScore: 0.7,
211
+ excludeFailed: true,
212
+ excludeErrors: true,
213
+ excludeTimeouts: true
214
+ });
215
+ } catch {
216
+ memoryContext = '';
217
+ }
218
+ }
153
219
  }
154
-
220
+
155
221
  // ENHANCEMENT: Add relevant conversation history from project messages
156
222
  // This lets agents see past discussions about the same topic
157
223
  if (options.projectId) {
158
224
  try {
159
225
  const ragModule = await getProjectMessagesRag();
160
226
  const conversationContext = ragModule?.getConversationContext(options.projectId, query, 3);
161
-
227
+
162
228
  if (conversationContext) {
163
229
  memoryContext += (memoryContext ? '\n\n' : '') + conversationContext;
164
230
  }
@@ -167,7 +233,7 @@ export async function recallMemoryContext(projectDir, query, options = {}) {
167
233
  console.warn('[shared-adapter] Failed to load conversation context:', e.message);
168
234
  }
169
235
  }
170
-
236
+
171
237
  return memoryContext;
172
238
  }
173
239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewswarm",
3
- "version": "0.9.4",
3
+ "version": "1.0.0",
4
4
  "description": "Local-first multi-agent orchestration platform — coordinate AI coding agents, LLMs, and tools from a single dashboard",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,6 +8,10 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/crewswarm/crewswarm.git"
10
10
  },
11
+ "homepage": "https://crewswarm.ai",
12
+ "bugs": {
13
+ "url": "https://github.com/crewswarm/crewswarm/issues"
14
+ },
11
15
  "keywords": [
12
16
  "ai",
13
17
  "agents",
@@ -110,6 +114,7 @@
110
114
  "vibe:start": "cd apps/vibe && NODE_DISABLE_COMPILE_CACHE=1 npm start",
111
115
  "vibe:watch": "NODE_DISABLE_COMPILE_CACHE=1 node apps/vibe/watch-server.mjs",
112
116
  "vibe:full": "bash scripts/start-studio-full.sh",
117
+ "test:playwright": "npx playwright test --reporter=line",
113
118
  "test:e2e:vibe": "node node_modules/playwright/cli.js test --config=playwright.config.js",
114
119
  "test:e2e:vibe:headed": "node node_modules/playwright/cli.js test --config=playwright.config.js --headed",
115
120
  "crew-lead": "node crew-lead.mjs",
@@ -128,10 +133,12 @@
128
133
  "release:check": "bash scripts/release-check.sh",
129
134
  "test:report": "node scripts/test-report-summary.mjs",
130
135
  "test:rerun": "node scripts/test-rerun.mjs",
131
- "test:stale": "node scripts/test-rerun.mjs --stale"
136
+ "test:stale": "node scripts/test-rerun.mjs --stale",
137
+ "typecheck": "tsc -p tsconfig.json"
132
138
  },
133
139
  "devDependencies": {
134
140
  "@playwright/test": "^1.58.2",
135
- "puppeteer-core": "^24.40.0"
141
+ "puppeteer-core": "^24.40.0",
142
+ "typescript": "^5.9.3"
136
143
  }
137
144
  }