@grec0/memory-bank-mcp 0.2.11 → 0.2.12
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.
|
@@ -129,6 +129,18 @@ export class AgentBoard {
|
|
|
129
129
|
getPendingTasks() {
|
|
130
130
|
return this.sqlite.getPendingTasks();
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Get all tasks for this project (regardless of status)
|
|
134
|
+
*/
|
|
135
|
+
getAllTasks() {
|
|
136
|
+
return this.sqlite.getAllTasks();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get completed tasks for this project
|
|
140
|
+
*/
|
|
141
|
+
getCompletedTasks() {
|
|
142
|
+
return this.sqlite.getCompletedTasks();
|
|
143
|
+
}
|
|
132
144
|
/**
|
|
133
145
|
* Claim a task
|
|
134
146
|
*/
|
|
@@ -219,6 +219,30 @@ export class AgentBoardSqlite {
|
|
|
219
219
|
`).all(this.projectId);
|
|
220
220
|
return rows.map(this.mapTaskRow);
|
|
221
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Get all tasks for this project (regardless of status)
|
|
224
|
+
*/
|
|
225
|
+
getAllTasks() {
|
|
226
|
+
const db = databaseManager.getConnection();
|
|
227
|
+
const rows = db.prepare(`
|
|
228
|
+
SELECT * FROM tasks
|
|
229
|
+
WHERE project_id = ?
|
|
230
|
+
ORDER BY created_at DESC
|
|
231
|
+
`).all(this.projectId);
|
|
232
|
+
return rows.map(this.mapTaskRow);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get completed tasks for this project
|
|
236
|
+
*/
|
|
237
|
+
getCompletedTasks() {
|
|
238
|
+
const db = databaseManager.getConnection();
|
|
239
|
+
const rows = db.prepare(`
|
|
240
|
+
SELECT * FROM tasks
|
|
241
|
+
WHERE project_id = ? AND status = 'COMPLETED'
|
|
242
|
+
ORDER BY completed_at DESC
|
|
243
|
+
`).all(this.projectId);
|
|
244
|
+
return rows.map(this.mapTaskRow);
|
|
245
|
+
}
|
|
222
246
|
/**
|
|
223
247
|
* Claim a task (agent takes ownership)
|
|
224
248
|
*/
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Text Similarity Utilities
|
|
3
|
+
* Provides functions to compare text similarity for task deduplication
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Calculates Levenshtein distance between two strings
|
|
7
|
+
* (minimum number of single-character edits to transform one string into another)
|
|
8
|
+
*/
|
|
9
|
+
function levenshteinDistance(str1, str2) {
|
|
10
|
+
const len1 = str1.length;
|
|
11
|
+
const len2 = str2.length;
|
|
12
|
+
// Create a 2D array for dynamic programming
|
|
13
|
+
const dp = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
|
|
14
|
+
// Initialize base cases
|
|
15
|
+
for (let i = 0; i <= len1; i++) {
|
|
16
|
+
dp[i][0] = i;
|
|
17
|
+
}
|
|
18
|
+
for (let j = 0; j <= len2; j++) {
|
|
19
|
+
dp[0][j] = j;
|
|
20
|
+
}
|
|
21
|
+
// Fill the dp table
|
|
22
|
+
for (let i = 1; i <= len1; i++) {
|
|
23
|
+
for (let j = 1; j <= len2; j++) {
|
|
24
|
+
if (str1[i - 1] === str2[j - 1]) {
|
|
25
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, // deletion
|
|
29
|
+
dp[i][j - 1] + 1, // insertion
|
|
30
|
+
dp[i - 1][j - 1] + 1 // substitution
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return dp[len1][len2];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Calculates similarity score between two strings (0-1 range)
|
|
39
|
+
* 1.0 = identical, 0.0 = completely different
|
|
40
|
+
*/
|
|
41
|
+
export function textSimilarity(str1, str2) {
|
|
42
|
+
if (!str1 && !str2)
|
|
43
|
+
return 1.0; // Both empty = identical
|
|
44
|
+
if (!str1 || !str2)
|
|
45
|
+
return 0; // One empty, one not = different
|
|
46
|
+
// Normalize: lowercase, trim, and remove extra spaces
|
|
47
|
+
const s1 = str1.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
48
|
+
const s2 = str2.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
49
|
+
// After normalization, check again
|
|
50
|
+
if (s1 === s2)
|
|
51
|
+
return 1.0;
|
|
52
|
+
if (s1.length === 0 && s2.length === 0)
|
|
53
|
+
return 1.0;
|
|
54
|
+
if (s1.length === 0 || s2.length === 0)
|
|
55
|
+
return 0;
|
|
56
|
+
const maxLen = Math.max(s1.length, s2.length);
|
|
57
|
+
const distance = levenshteinDistance(s1, s2);
|
|
58
|
+
return 1 - (distance / maxLen);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Checks if two texts are similar based on a threshold
|
|
62
|
+
* @param str1 First string
|
|
63
|
+
* @param str2 Second string
|
|
64
|
+
* @param threshold Similarity threshold (0-1), default 0.8
|
|
65
|
+
* @returns true if similarity >= threshold
|
|
66
|
+
*/
|
|
67
|
+
export function areSimilar(str1, str2, threshold = 0.8) {
|
|
68
|
+
return textSimilarity(str1, str2) >= threshold;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Finds the most similar text from a list
|
|
72
|
+
* @param target Target text to compare
|
|
73
|
+
* @param candidates List of candidate texts
|
|
74
|
+
* @param minScore Minimum similarity score to consider (default 0.7)
|
|
75
|
+
* @returns Object with best match and score, or null if no match above threshold
|
|
76
|
+
*/
|
|
77
|
+
export function findMostSimilar(target, candidates, minScore = 0.7) {
|
|
78
|
+
if (!target || !candidates || candidates.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
let bestMatch = null;
|
|
82
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
83
|
+
const score = textSimilarity(target, candidates[i]);
|
|
84
|
+
if (score >= minScore && (!bestMatch || score > bestMatch.score)) {
|
|
85
|
+
bestMatch = { text: candidates[i], score, index: i };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return bestMatch;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Normalizes a text for comparison (lowercase, trim, remove extra spaces)
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeText(text) {
|
|
94
|
+
return text.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Checks if text contains all keywords (case-insensitive)
|
|
98
|
+
*/
|
|
99
|
+
export function containsAllKeywords(text, keywords) {
|
|
100
|
+
const normalized = normalizeText(text);
|
|
101
|
+
return keywords.every(keyword => normalized.includes(normalizeText(keyword)));
|
|
102
|
+
}
|
package/dist/common/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Version of the MCP Kanban server
|
|
2
|
-
export const VERSION = "0.2.
|
|
2
|
+
export const VERSION = "0.2.12";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RegistryManager } from '../common/registryManager.js';
|
|
2
2
|
import { AgentBoard } from '../common/agentBoard.js';
|
|
3
|
+
import { textSimilarity } from '../common/textSimilarity.js';
|
|
3
4
|
export async function delegateTaskTool(params) {
|
|
4
5
|
const registryManager = new RegistryManager();
|
|
5
6
|
const targetProject = await registryManager.getProject(params.targetProjectId);
|
|
@@ -11,12 +12,33 @@ export async function delegateTaskTool(params) {
|
|
|
11
12
|
}
|
|
12
13
|
try {
|
|
13
14
|
// Initialize board for the TARGET project path
|
|
14
|
-
// We use targetProject.path as the workspace root for that board
|
|
15
15
|
const targetBoard = new AgentBoard(targetProject.path, targetProject.projectId);
|
|
16
|
+
// ========================================================================
|
|
17
|
+
// DOUBLE CHECK: Verify no duplicate task exists before creating
|
|
18
|
+
// (Protection against race conditions between route and delegate)
|
|
19
|
+
// ========================================================================
|
|
20
|
+
const existingTasks = targetBoard.getAllTasks();
|
|
21
|
+
const TITLE_SIMILARITY_THRESHOLD = 0.85;
|
|
22
|
+
for (const existingTask of existingTasks) {
|
|
23
|
+
const similarity = textSimilarity(params.title, existingTask.title);
|
|
24
|
+
if (similarity >= TITLE_SIMILARITY_THRESHOLD) {
|
|
25
|
+
// Found a duplicate - don't create
|
|
26
|
+
console.error(` ⚠️ Duplicate task detected during delegation: ${existingTask.id} (similarity: ${(similarity * 100).toFixed(0)}%)`);
|
|
27
|
+
return {
|
|
28
|
+
success: true, // Not an error, just already exists
|
|
29
|
+
taskId: existingTask.id,
|
|
30
|
+
isDuplicate: true,
|
|
31
|
+
existingStatus: existingTask.status,
|
|
32
|
+
message: `Task already exists in '${params.targetProjectId}' as ${existingTask.id} (status: ${existingTask.status}). No duplicate created.`
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// No duplicate found - safe to create
|
|
16
37
|
const taskId = await targetBoard.createExternalTask(params.title, params.projectId, `${params.description}\n\nContext:\n${params.context}`);
|
|
17
38
|
return {
|
|
18
39
|
success: true,
|
|
19
40
|
taskId,
|
|
41
|
+
isDuplicate: false,
|
|
20
42
|
message: `Task successfully delegated to project '${params.targetProjectId}' (Task ID: ${taskId})`
|
|
21
43
|
};
|
|
22
44
|
}
|
package/dist/tools/routeTask.js
CHANGED
|
@@ -10,6 +10,8 @@ import OpenAI from "openai";
|
|
|
10
10
|
import { RegistryManager } from "../common/registryManager.js";
|
|
11
11
|
import { searchMemory } from "./searchMemory.js";
|
|
12
12
|
import { saveOrchestratorLog } from "../common/agentBoardSqlite.js";
|
|
13
|
+
import { AgentBoard } from "../common/agentBoard.js";
|
|
14
|
+
import { textSimilarity } from "../common/textSimilarity.js";
|
|
13
15
|
/**
|
|
14
16
|
* Builds a context string describing all projects and their responsibilities
|
|
15
17
|
*/
|
|
@@ -172,6 +174,104 @@ IMPORTANT:
|
|
|
172
174
|
- Responde siempre en ESPAÑOL
|
|
173
175
|
|
|
174
176
|
Respond ONLY with the JSON object after completing your analysis.`;
|
|
177
|
+
/**
|
|
178
|
+
* Checks if a delegation is a duplicate of an existing task in the target project
|
|
179
|
+
* @param delegation The proposed delegation
|
|
180
|
+
* @param targetProject The target project card
|
|
181
|
+
* @param registryManager Registry manager for resolving project paths
|
|
182
|
+
* @returns Updated delegation with deduplication metadata
|
|
183
|
+
*/
|
|
184
|
+
async function checkDelegationDuplicate(delegation, targetProject, registryManager) {
|
|
185
|
+
try {
|
|
186
|
+
// Initialize AgentBoard for the target project
|
|
187
|
+
const targetBoard = new AgentBoard(targetProject.path, targetProject.projectId);
|
|
188
|
+
// Get all tasks (pending, in-progress, and completed)
|
|
189
|
+
const allTasks = targetBoard.getAllTasks();
|
|
190
|
+
if (!allTasks || allTasks.length === 0) {
|
|
191
|
+
// No tasks in the target project, no duplicates
|
|
192
|
+
return delegation;
|
|
193
|
+
}
|
|
194
|
+
// Check for duplicates based on title and description similarity
|
|
195
|
+
const TITLE_SIMILARITY_THRESHOLD = 0.85; // 85% similar titles = likely duplicate
|
|
196
|
+
const DESC_SIMILARITY_THRESHOLD = 0.75; // 75% similar descriptions = likely duplicate
|
|
197
|
+
for (const existingTask of allTasks) {
|
|
198
|
+
// Calculate similarities
|
|
199
|
+
const titleSimilarity = textSimilarity(delegation.taskTitle, existingTask.title);
|
|
200
|
+
const descSimilarity = existingTask.description
|
|
201
|
+
? textSimilarity(delegation.taskDescription, existingTask.description)
|
|
202
|
+
: 0;
|
|
203
|
+
// Check if it's a duplicate
|
|
204
|
+
const isDuplicateByTitle = titleSimilarity >= TITLE_SIMILARITY_THRESHOLD;
|
|
205
|
+
const isDuplicateByDesc = descSimilarity >= DESC_SIMILARITY_THRESHOLD;
|
|
206
|
+
if (isDuplicateByTitle || (isDuplicateByDesc && descSimilarity > 0.5)) {
|
|
207
|
+
// Found a duplicate!
|
|
208
|
+
const maxSimilarity = Math.max(titleSimilarity, descSimilarity);
|
|
209
|
+
let skipReason = `Task already exists in ${targetProject.projectId}: `;
|
|
210
|
+
if (existingTask.status === 'COMPLETED') {
|
|
211
|
+
skipReason += `completed as ${existingTask.id}`;
|
|
212
|
+
}
|
|
213
|
+
else if (existingTask.status === 'IN_PROGRESS') {
|
|
214
|
+
skipReason += `in progress as ${existingTask.id}${existingTask.claimedBy ? ` (claimed by ${existingTask.claimedBy})` : ''}`;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
skipReason += `pending as ${existingTask.id}`;
|
|
218
|
+
}
|
|
219
|
+
skipReason += ` (similarity: ${(maxSimilarity * 100).toFixed(0)}%)`;
|
|
220
|
+
return {
|
|
221
|
+
...delegation,
|
|
222
|
+
isDuplicate: true,
|
|
223
|
+
existingTaskId: existingTask.id,
|
|
224
|
+
existingTaskStatus: existingTask.status,
|
|
225
|
+
skipReason,
|
|
226
|
+
similarity: maxSimilarity,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// No duplicates found
|
|
231
|
+
return delegation;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.error(` Warning: Failed to check duplicates for ${delegation.targetProject}: ${error.message}`);
|
|
235
|
+
// In case of error, proceed without deduplication metadata
|
|
236
|
+
return delegation;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Filters delegations by checking against existing tasks in target projects
|
|
241
|
+
* @param delegations Array of proposed delegations
|
|
242
|
+
* @param registryManager Registry manager for resolving projects
|
|
243
|
+
* @returns Object with filtered delegations and skipped ones with reasons
|
|
244
|
+
*/
|
|
245
|
+
async function filterDuplicateDelegations(delegations, registryManager) {
|
|
246
|
+
if (!delegations || delegations.length === 0) {
|
|
247
|
+
return { validDelegations: [], duplicateDelegations: [] };
|
|
248
|
+
}
|
|
249
|
+
console.error(`\n=== Checking for duplicate delegations ===`);
|
|
250
|
+
const validDelegations = [];
|
|
251
|
+
const duplicateDelegations = [];
|
|
252
|
+
for (const delegation of delegations) {
|
|
253
|
+
// Resolve target project
|
|
254
|
+
const targetProject = await registryManager.getProject(delegation.targetProject);
|
|
255
|
+
if (!targetProject) {
|
|
256
|
+
console.error(` Warning: Target project '${delegation.targetProject}' not found. Keeping delegation.`);
|
|
257
|
+
validDelegations.push(delegation);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// Check for duplicates
|
|
261
|
+
const checkedDelegation = await checkDelegationDuplicate(delegation, targetProject, registryManager);
|
|
262
|
+
if (checkedDelegation.isDuplicate) {
|
|
263
|
+
console.error(` ✗ DUPLICATE: ${delegation.taskTitle} → ${delegation.targetProject}`);
|
|
264
|
+
console.error(` ${checkedDelegation.skipReason}`);
|
|
265
|
+
duplicateDelegations.push(checkedDelegation);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.error(` ✓ VALID: ${delegation.taskTitle} → ${delegation.targetProject}`);
|
|
269
|
+
validDelegations.push(checkedDelegation);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.error(`\nDeduplication results: ${validDelegations.length} valid, ${duplicateDelegations.length} duplicates`);
|
|
273
|
+
return { validDelegations, duplicateDelegations };
|
|
274
|
+
}
|
|
175
275
|
/**
|
|
176
276
|
* Routes a task to the appropriate project(s) based on responsibilities
|
|
177
277
|
* Uses function calling to allow the AI to perform semantic searches when needed
|
|
@@ -387,13 +487,40 @@ export async function routeTaskTool(params, indexManager) {
|
|
|
387
487
|
console.error(` → ${d.targetProject}: ${d.taskTitle}`);
|
|
388
488
|
}
|
|
389
489
|
}
|
|
490
|
+
// ========================================================================
|
|
491
|
+
// DEDUPLICATION: Check against existing tasks in target project boards
|
|
492
|
+
// ========================================================================
|
|
493
|
+
let finalDelegations = result.delegations || [];
|
|
494
|
+
let duplicateDelegations = [];
|
|
495
|
+
if (finalDelegations.length > 0) {
|
|
496
|
+
const { validDelegations, duplicateDelegations: duplicates } = await filterDuplicateDelegations(finalDelegations, registryManager);
|
|
497
|
+
finalDelegations = validDelegations;
|
|
498
|
+
duplicateDelegations = duplicates;
|
|
499
|
+
// Update action if all delegations were duplicates
|
|
500
|
+
if (validDelegations.length === 0 && result.delegations.length > 0) {
|
|
501
|
+
if (result.myResponsibilities && result.myResponsibilities.length > 0) {
|
|
502
|
+
result.action = 'proceed'; // Had delegations but all were duplicates, only local work remains
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
result.action = 'proceed'; // Everything was a duplicate, nothing to do
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Build architecture notes with deduplication info
|
|
510
|
+
let architectureNotes = result.architectureNotes || '';
|
|
511
|
+
if (duplicateDelegations.length > 0) {
|
|
512
|
+
architectureNotes += `\n\n**Delegaciones filtradas (duplicadas):**\n`;
|
|
513
|
+
for (const dup of duplicateDelegations) {
|
|
514
|
+
architectureNotes += `- ${dup.taskTitle} → ${dup.targetProject}: ${dup.skipReason}\n`;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
390
517
|
const routeResult = {
|
|
391
518
|
success: true,
|
|
392
519
|
action: result.action || 'proceed',
|
|
393
520
|
myResponsibilities: result.myResponsibilities || [],
|
|
394
|
-
delegations:
|
|
521
|
+
delegations: finalDelegations, // Only non-duplicate delegations
|
|
395
522
|
suggestedImports: result.suggestedImports || [],
|
|
396
|
-
architectureNotes
|
|
523
|
+
architectureNotes,
|
|
397
524
|
warning: result.warning,
|
|
398
525
|
};
|
|
399
526
|
// Persist to SQLite for extension visualization
|