code-graph-context 2.5.3 → 2.6.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.
- package/dist/core/embeddings/natural-language-to-cypher.service.js +1 -1
- package/dist/mcp/constants.js +70 -557
- package/dist/mcp/tools/detect-dead-code.tool.js +0 -16
- package/dist/mcp/tools/detect-duplicate-code.tool.js +0 -27
- package/dist/mcp/tools/impact-analysis.tool.js +0 -13
- package/dist/mcp/tools/index.js +4 -15
- package/dist/mcp/tools/list-projects.tool.js +0 -2
- package/dist/mcp/tools/list-watchers.tool.js +0 -2
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +0 -6
- package/dist/mcp/tools/search-codebase.tool.js +0 -19
- package/dist/mcp/tools/swarm-claim-task.tool.js +220 -93
- package/dist/mcp/tools/swarm-cleanup.tool.js +76 -32
- package/dist/mcp/tools/swarm-complete-task.tool.js +79 -88
- package/dist/mcp/tools/swarm-get-tasks.tool.js +0 -9
- package/dist/mcp/tools/swarm-orchestrate.tool.js +55 -92
- package/dist/mcp/tools/swarm-post-task.tool.js +32 -11
- package/dist/mcp/tools/traverse-from-node.tool.js +0 -15
- package/package.json +1 -1
|
@@ -76,10 +76,11 @@ const FAIL_TASK_QUERY = `
|
|
|
76
76
|
`;
|
|
77
77
|
/**
|
|
78
78
|
* Query to mark task as needs_review
|
|
79
|
+
* Accepts both in_progress and claimed states (agent may have done work without calling start)
|
|
79
80
|
*/
|
|
80
81
|
const REVIEW_TASK_QUERY = `
|
|
81
82
|
MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
|
|
82
|
-
WHERE t.status
|
|
83
|
+
WHERE t.status IN ['in_progress', 'claimed'] AND t.claimedBy = $agentId
|
|
83
84
|
|
|
84
85
|
SET t.status = 'needs_review',
|
|
85
86
|
t.reviewRequestedAt = timestamp(),
|
|
@@ -180,6 +181,63 @@ const RETRY_TASK_QUERY = `
|
|
|
180
181
|
t.status as status,
|
|
181
182
|
t.retryCount as retryCount
|
|
182
183
|
`;
|
|
184
|
+
/**
|
|
185
|
+
* Query to get current task state for better error messages
|
|
186
|
+
*/
|
|
187
|
+
const GET_TASK_STATE_QUERY = `
|
|
188
|
+
MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
|
|
189
|
+
RETURN t.id as id,
|
|
190
|
+
t.title as title,
|
|
191
|
+
t.status as status,
|
|
192
|
+
t.claimedBy as claimedBy,
|
|
193
|
+
t.claimedAt as claimedAt,
|
|
194
|
+
t.startedAt as startedAt,
|
|
195
|
+
t.completedAt as completedAt,
|
|
196
|
+
t.failedAt as failedAt,
|
|
197
|
+
t.retryable as retryable,
|
|
198
|
+
t.abandonCount as abandonCount
|
|
199
|
+
`;
|
|
200
|
+
/**
|
|
201
|
+
* Helper to get task state and format error message
|
|
202
|
+
*/
|
|
203
|
+
async function getTaskStateError(neo4jService, taskId, projectId, action, agentId) {
|
|
204
|
+
const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
|
|
205
|
+
taskId,
|
|
206
|
+
projectId,
|
|
207
|
+
});
|
|
208
|
+
if (stateResult.length === 0) {
|
|
209
|
+
return `Task ${taskId} not found.`;
|
|
210
|
+
}
|
|
211
|
+
const state = stateResult[0];
|
|
212
|
+
const claimedBy = state.claimedBy || 'none';
|
|
213
|
+
const isOwner = claimedBy === agentId;
|
|
214
|
+
let suggestion = '';
|
|
215
|
+
if (action === 'complete' || action === 'fail') {
|
|
216
|
+
if (state.status === 'available') {
|
|
217
|
+
suggestion = 'You must claim the task first using swarm_claim_task.';
|
|
218
|
+
}
|
|
219
|
+
else if (!isOwner) {
|
|
220
|
+
suggestion = `Task is claimed by "${claimedBy}", not you.`;
|
|
221
|
+
}
|
|
222
|
+
else if (state.status === 'completed') {
|
|
223
|
+
suggestion = 'Task is already completed.';
|
|
224
|
+
}
|
|
225
|
+
else if (state.status === 'failed') {
|
|
226
|
+
suggestion = 'Task has failed. Use action="retry" to make it available again.';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (action === 'request_review') {
|
|
230
|
+
if (!isOwner) {
|
|
231
|
+
suggestion = `Task is claimed by "${claimedBy}", not you.`;
|
|
232
|
+
}
|
|
233
|
+
else if (state.status === 'available') {
|
|
234
|
+
suggestion = 'You must claim and work on the task first.';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return (`Cannot ${action} task ${taskId}. ` +
|
|
238
|
+
`Current state: ${state.status}, claimedBy: ${claimedBy}. ` +
|
|
239
|
+
(suggestion || 'Check that you own the task and it is in a valid state.'));
|
|
240
|
+
}
|
|
183
241
|
export const createSwarmCompleteTaskTool = (server) => {
|
|
184
242
|
server.registerTool(TOOL_NAMES.swarmCompleteTask, {
|
|
185
243
|
title: TOOL_METADATA[TOOL_NAMES.swarmCompleteTask].title,
|
|
@@ -246,14 +304,7 @@ export const createSwarmCompleteTaskTool = (server) => {
|
|
|
246
304
|
}
|
|
247
305
|
const resolvedProjectId = projectResult.projectId;
|
|
248
306
|
try {
|
|
249
|
-
await debugLog('Swarm complete task', {
|
|
250
|
-
action,
|
|
251
|
-
taskId,
|
|
252
|
-
projectId: resolvedProjectId,
|
|
253
|
-
agentId,
|
|
254
|
-
});
|
|
255
307
|
let result;
|
|
256
|
-
let responseData = { success: true, action };
|
|
257
308
|
switch (action) {
|
|
258
309
|
case 'complete':
|
|
259
310
|
if (!summary) {
|
|
@@ -270,23 +321,14 @@ export const createSwarmCompleteTaskTool = (server) => {
|
|
|
270
321
|
linesRemoved: linesRemoved || 0,
|
|
271
322
|
});
|
|
272
323
|
if (result.length === 0) {
|
|
273
|
-
|
|
324
|
+
const errorMsg = await getTaskStateError(neo4jService, taskId, resolvedProjectId, 'complete', agentId);
|
|
325
|
+
return createErrorResponse(errorMsg);
|
|
274
326
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
? result[0].completedAt.toNumber()
|
|
281
|
-
: result[0].completedAt,
|
|
282
|
-
summary: result[0].summary,
|
|
283
|
-
claimedBy: result[0].claimedBy,
|
|
284
|
-
};
|
|
285
|
-
responseData.unblockedTasks = result[0].unblockedTaskIds || [];
|
|
286
|
-
responseData.message = responseData.unblockedTasks.length > 0
|
|
287
|
-
? `Task completed. ${responseData.unblockedTasks.length} dependent task(s) are now available.`
|
|
288
|
-
: 'Task completed successfully.';
|
|
289
|
-
break;
|
|
327
|
+
return createSuccessResponse(JSON.stringify({
|
|
328
|
+
action: 'completed',
|
|
329
|
+
taskId: result[0].id,
|
|
330
|
+
...(result[0].unblockedTaskIds?.length > 0 && { unblockedTasks: result[0].unblockedTaskIds }),
|
|
331
|
+
}));
|
|
290
332
|
case 'fail':
|
|
291
333
|
if (!reason) {
|
|
292
334
|
return createErrorResponse('reason is required for fail action');
|
|
@@ -300,22 +342,10 @@ export const createSwarmCompleteTaskTool = (server) => {
|
|
|
300
342
|
retryable,
|
|
301
343
|
});
|
|
302
344
|
if (result.length === 0) {
|
|
303
|
-
|
|
345
|
+
const errorMsg = await getTaskStateError(neo4jService, taskId, resolvedProjectId, 'fail', agentId);
|
|
346
|
+
return createErrorResponse(errorMsg);
|
|
304
347
|
}
|
|
305
|
-
|
|
306
|
-
id: result[0].id,
|
|
307
|
-
title: result[0].title,
|
|
308
|
-
status: result[0].status,
|
|
309
|
-
failedAt: typeof result[0].failedAt === 'object'
|
|
310
|
-
? result[0].failedAt.toNumber()
|
|
311
|
-
: result[0].failedAt,
|
|
312
|
-
failureReason: result[0].failureReason,
|
|
313
|
-
retryable: result[0].retryable,
|
|
314
|
-
};
|
|
315
|
-
responseData.message = retryable
|
|
316
|
-
? 'Task marked as failed. Use action="retry" to make it available again.'
|
|
317
|
-
: 'Task marked as failed (not retryable).';
|
|
318
|
-
break;
|
|
348
|
+
return createSuccessResponse(JSON.stringify({ action: 'failed', taskId: result[0].id, retryable: result[0].retryable }));
|
|
319
349
|
case 'request_review':
|
|
320
350
|
if (!summary) {
|
|
321
351
|
return createErrorResponse('summary is required for request_review action');
|
|
@@ -330,20 +360,10 @@ export const createSwarmCompleteTaskTool = (server) => {
|
|
|
330
360
|
reviewNotes: reviewNotes || null,
|
|
331
361
|
});
|
|
332
362
|
if (result.length === 0) {
|
|
333
|
-
|
|
363
|
+
const errorMsg = await getTaskStateError(neo4jService, taskId, resolvedProjectId, 'request_review', agentId);
|
|
364
|
+
return createErrorResponse(errorMsg);
|
|
334
365
|
}
|
|
335
|
-
|
|
336
|
-
id: result[0].id,
|
|
337
|
-
title: result[0].title,
|
|
338
|
-
status: result[0].status,
|
|
339
|
-
reviewRequestedAt: typeof result[0].reviewRequestedAt === 'object'
|
|
340
|
-
? result[0].reviewRequestedAt.toNumber()
|
|
341
|
-
: result[0].reviewRequestedAt,
|
|
342
|
-
summary: result[0].summary,
|
|
343
|
-
claimedBy: result[0].claimedBy,
|
|
344
|
-
};
|
|
345
|
-
responseData.message = 'Task submitted for review.';
|
|
346
|
-
break;
|
|
366
|
+
return createSuccessResponse(JSON.stringify({ action: 'review_requested', taskId: result[0].id }));
|
|
347
367
|
case 'approve':
|
|
348
368
|
if (!reviewerId) {
|
|
349
369
|
return createErrorResponse('reviewerId is required for approve action');
|
|
@@ -357,20 +377,11 @@ export const createSwarmCompleteTaskTool = (server) => {
|
|
|
357
377
|
if (result.length === 0) {
|
|
358
378
|
return createErrorResponse(`Cannot approve task ${taskId}. It may not exist or not be in needs_review status.`);
|
|
359
379
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
? result[0].completedAt.toNumber()
|
|
366
|
-
: result[0].completedAt,
|
|
367
|
-
approvedBy: result[0].approvedBy,
|
|
368
|
-
};
|
|
369
|
-
responseData.unblockedTasks = result[0].unblockedTaskIds || [];
|
|
370
|
-
responseData.message = responseData.unblockedTasks.length > 0
|
|
371
|
-
? `Task approved. ${responseData.unblockedTasks.length} dependent task(s) are now available.`
|
|
372
|
-
: 'Task approved and completed.';
|
|
373
|
-
break;
|
|
380
|
+
return createSuccessResponse(JSON.stringify({
|
|
381
|
+
action: 'approved',
|
|
382
|
+
taskId: result[0].id,
|
|
383
|
+
...(result[0].unblockedTaskIds?.length > 0 && { unblockedTasks: result[0].unblockedTaskIds }),
|
|
384
|
+
}));
|
|
374
385
|
case 'reject':
|
|
375
386
|
if (!reviewerId) {
|
|
376
387
|
return createErrorResponse('reviewerId is required for reject action');
|
|
@@ -385,17 +396,7 @@ export const createSwarmCompleteTaskTool = (server) => {
|
|
|
385
396
|
if (result.length === 0) {
|
|
386
397
|
return createErrorResponse(`Cannot reject task ${taskId}. It may not exist or not be in needs_review status.`);
|
|
387
398
|
}
|
|
388
|
-
|
|
389
|
-
id: result[0].id,
|
|
390
|
-
title: result[0].title,
|
|
391
|
-
status: result[0].status,
|
|
392
|
-
claimedBy: result[0].claimedBy,
|
|
393
|
-
rejectionNotes: result[0].rejectionNotes,
|
|
394
|
-
};
|
|
395
|
-
responseData.message = markAsFailed
|
|
396
|
-
? 'Task rejected and marked as failed.'
|
|
397
|
-
: 'Task rejected and returned to in_progress for the original agent to fix.';
|
|
398
|
-
break;
|
|
399
|
+
return createSuccessResponse(JSON.stringify({ action: 'rejected', taskId: result[0].id, status: result[0].status }));
|
|
399
400
|
case 'retry':
|
|
400
401
|
result = await neo4jService.run(RETRY_TASK_QUERY, {
|
|
401
402
|
taskId,
|
|
@@ -404,20 +405,10 @@ export const createSwarmCompleteTaskTool = (server) => {
|
|
|
404
405
|
if (result.length === 0) {
|
|
405
406
|
return createErrorResponse(`Cannot retry task ${taskId}. It may not exist, not be failed, or not be retryable.`);
|
|
406
407
|
}
|
|
407
|
-
|
|
408
|
-
id: result[0].id,
|
|
409
|
-
title: result[0].title,
|
|
410
|
-
status: result[0].status,
|
|
411
|
-
retryCount: typeof result[0].retryCount === 'object'
|
|
412
|
-
? result[0].retryCount.toNumber()
|
|
413
|
-
: result[0].retryCount,
|
|
414
|
-
};
|
|
415
|
-
responseData.message = `Task is now available for retry (attempt #${responseData.task.retryCount + 1}).`;
|
|
416
|
-
break;
|
|
408
|
+
return createSuccessResponse(JSON.stringify({ action: 'retried', taskId: result[0].id, status: 'available' }));
|
|
417
409
|
default:
|
|
418
410
|
return createErrorResponse(`Unknown action: ${action}`);
|
|
419
411
|
}
|
|
420
|
-
return createSuccessResponse(JSON.stringify(responseData));
|
|
421
412
|
}
|
|
422
413
|
catch (error) {
|
|
423
414
|
await debugLog('Swarm complete task error', { error: String(error) });
|
|
@@ -212,15 +212,6 @@ export const createSwarmGetTasksTool = (server) => {
|
|
|
212
212
|
}
|
|
213
213
|
const resolvedProjectId = projectResult.projectId;
|
|
214
214
|
try {
|
|
215
|
-
await debugLog('Swarm get tasks', {
|
|
216
|
-
projectId: resolvedProjectId,
|
|
217
|
-
swarmId,
|
|
218
|
-
taskId,
|
|
219
|
-
statuses,
|
|
220
|
-
types,
|
|
221
|
-
claimedBy,
|
|
222
|
-
limit,
|
|
223
|
-
});
|
|
224
215
|
// If taskId is provided, get single task with full details
|
|
225
216
|
if (taskId) {
|
|
226
217
|
const result = await neo4jService.run(GET_TASK_BY_ID_QUERY, {
|
|
@@ -86,6 +86,21 @@ const CREATE_PHEROMONE_QUERY = `
|
|
|
86
86
|
MERGE (p)-[:MARKS]->(target)
|
|
87
87
|
RETURN p.nodeId AS nodeId
|
|
88
88
|
`;
|
|
89
|
+
/**
|
|
90
|
+
* Query to get node IDs for a file path (fallback when decomposition has no nodeIds)
|
|
91
|
+
* Uses ENDS WITH for flexible matching (handles absolute vs relative paths)
|
|
92
|
+
*/
|
|
93
|
+
const GET_NODES_FOR_FILE_QUERY = `
|
|
94
|
+
MATCH (n)
|
|
95
|
+
WHERE (n.filePath = $filePath OR n.filePath ENDS WITH $filePath)
|
|
96
|
+
AND n.projectId = $projectId
|
|
97
|
+
AND n.id IS NOT NULL
|
|
98
|
+
AND NOT n:Pheromone
|
|
99
|
+
AND NOT n:SwarmTask
|
|
100
|
+
RETURN n.id AS id, n.name AS name, n.coreType AS coreType
|
|
101
|
+
ORDER BY n.startLine
|
|
102
|
+
LIMIT 20
|
|
103
|
+
`;
|
|
89
104
|
/**
|
|
90
105
|
* Query to create a SwarmTask node
|
|
91
106
|
*/
|
|
@@ -183,15 +198,7 @@ export const createSwarmOrchestrateTool = (server) => {
|
|
|
183
198
|
return projectResult.error;
|
|
184
199
|
}
|
|
185
200
|
const resolvedProjectId = projectResult.projectId;
|
|
186
|
-
await debugLog('Swarm orchestration started', {
|
|
187
|
-
swarmId,
|
|
188
|
-
projectId: resolvedProjectId,
|
|
189
|
-
task,
|
|
190
|
-
maxAgents,
|
|
191
|
-
dryRun,
|
|
192
|
-
});
|
|
193
201
|
// Step 2: Semantic search to find affected nodes
|
|
194
|
-
await debugLog('Searching for affected nodes', { task });
|
|
195
202
|
let embedding;
|
|
196
203
|
try {
|
|
197
204
|
embedding = await embeddingsService.embedText(task);
|
|
@@ -218,12 +225,7 @@ export const createSwarmOrchestrateTool = (server) => {
|
|
|
218
225
|
startLine: typeof r.startLine === 'object' ? r.startLine.toNumber() : r.startLine,
|
|
219
226
|
endLine: typeof r.endLine === 'object' ? r.endLine.toNumber() : r.endLine,
|
|
220
227
|
}));
|
|
221
|
-
await debugLog('Found affected nodes', {
|
|
222
|
-
count: affectedNodes.length,
|
|
223
|
-
files: [...new Set(affectedNodes.map((n) => n.filePath))].length,
|
|
224
|
-
});
|
|
225
228
|
// Step 3: Run impact analysis on each node
|
|
226
|
-
await debugLog('Running impact analysis', { nodeCount: affectedNodes.length });
|
|
227
229
|
const impactMap = new Map();
|
|
228
230
|
for (const node of affectedNodes) {
|
|
229
231
|
const impactResult = await neo4jService.run(IMPACT_QUERY, {
|
|
@@ -247,22 +249,27 @@ export const createSwarmOrchestrateTool = (server) => {
|
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
// Step 4: Decompose task into atomic tasks
|
|
250
|
-
await debugLog('Decomposing task', { nodeCount: affectedNodes.length });
|
|
251
252
|
const decomposition = await taskDecomposer.decomposeTask(task, affectedNodes, impactMap, priority);
|
|
252
253
|
if (decomposition.tasks.length === 0) {
|
|
253
254
|
return createErrorResponse('Task decomposition produced no actionable tasks');
|
|
254
255
|
}
|
|
255
|
-
await debugLog('Task decomposition complete', {
|
|
256
|
-
totalTasks: decomposition.tasks.length,
|
|
257
|
-
parallelizable: decomposition.summary.parallelizable,
|
|
258
|
-
});
|
|
259
256
|
// Step 5: Create SwarmTasks on the blackboard (unless dry run)
|
|
260
257
|
if (!dryRun) {
|
|
261
|
-
await debugLog('Creating SwarmTasks', { count: decomposition.tasks.length });
|
|
262
258
|
for (const atomicTask of decomposition.tasks) {
|
|
263
259
|
// Determine initial status based on dependencies
|
|
264
260
|
const hasUnmetDeps = atomicTask.dependencies.length > 0;
|
|
265
261
|
const initialStatus = hasUnmetDeps ? 'blocked' : 'available';
|
|
262
|
+
// Filter out null/undefined nodeIds, then fallback to file query if empty
|
|
263
|
+
let targetNodeIds = (atomicTask.nodeIds || []).filter((id) => typeof id === 'string' && id.length > 0);
|
|
264
|
+
if (targetNodeIds.length === 0 && atomicTask.filePath) {
|
|
265
|
+
const fileNodes = await neo4jService.run(GET_NODES_FOR_FILE_QUERY, {
|
|
266
|
+
filePath: atomicTask.filePath,
|
|
267
|
+
projectId: resolvedProjectId,
|
|
268
|
+
});
|
|
269
|
+
if (fileNodes.length > 0) {
|
|
270
|
+
targetNodeIds = fileNodes.map((n) => n.id).filter(Boolean);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
266
273
|
await neo4jService.run(CREATE_TASK_QUERY, {
|
|
267
274
|
taskId: atomicTask.id,
|
|
268
275
|
projectId: resolvedProjectId,
|
|
@@ -273,14 +280,15 @@ export const createSwarmOrchestrateTool = (server) => {
|
|
|
273
280
|
priority: atomicTask.priority,
|
|
274
281
|
priorityScore: atomicTask.priorityScore,
|
|
275
282
|
status: initialStatus,
|
|
276
|
-
targetNodeIds
|
|
283
|
+
targetNodeIds,
|
|
277
284
|
targetFilePaths: [atomicTask.filePath],
|
|
278
285
|
dependencies: atomicTask.dependencies,
|
|
279
286
|
createdBy: 'orchestrator',
|
|
280
287
|
metadata: JSON.stringify(atomicTask.metadata ?? {}),
|
|
281
288
|
});
|
|
289
|
+
// Update the atomicTask.nodeIds for pheromone creation below
|
|
290
|
+
atomicTask.nodeIds = targetNodeIds;
|
|
282
291
|
}
|
|
283
|
-
await debugLog('SwarmTasks created', { swarmId, count: decomposition.tasks.length });
|
|
284
292
|
// Step 5b: Leave "proposal" pheromones on all target nodes
|
|
285
293
|
// This signals to other agents that work is planned for these nodes
|
|
286
294
|
const uniqueNodeIds = new Set();
|
|
@@ -289,7 +297,6 @@ export const createSwarmOrchestrateTool = (server) => {
|
|
|
289
297
|
uniqueNodeIds.add(nodeId);
|
|
290
298
|
}
|
|
291
299
|
}
|
|
292
|
-
await debugLog('Creating proposal pheromones', { nodeCount: uniqueNodeIds.size });
|
|
293
300
|
for (const nodeId of uniqueNodeIds) {
|
|
294
301
|
await neo4jService.run(CREATE_PHEROMONE_QUERY, {
|
|
295
302
|
nodeId,
|
|
@@ -302,7 +309,6 @@ export const createSwarmOrchestrateTool = (server) => {
|
|
|
302
309
|
data: JSON.stringify({ task, swarmId }),
|
|
303
310
|
});
|
|
304
311
|
}
|
|
305
|
-
await debugLog('Proposal pheromones created', { swarmId, count: uniqueNodeIds.size });
|
|
306
312
|
}
|
|
307
313
|
// Step 6: Generate worker instructions
|
|
308
314
|
const workerInstructions = generateWorkerInstructions(swarmId, resolvedProjectId, maxAgents, decomposition.tasks.length);
|
|
@@ -331,11 +337,6 @@ export const createSwarmOrchestrateTool = (server) => {
|
|
|
331
337
|
? `Dry run complete. ${decomposition.tasks.length} tasks planned but not created.`
|
|
332
338
|
: `Swarm ready! ${decomposition.tasks.length} tasks created. ${decomposition.summary.parallelizable} can run in parallel.`,
|
|
333
339
|
};
|
|
334
|
-
await debugLog('Swarm orchestration complete', {
|
|
335
|
-
swarmId,
|
|
336
|
-
status: result.status,
|
|
337
|
-
totalTasks: result.plan.totalTasks,
|
|
338
|
-
});
|
|
339
340
|
return createSuccessResponse(JSON.stringify(result, null, 2));
|
|
340
341
|
}
|
|
341
342
|
catch (error) {
|
|
@@ -354,103 +355,65 @@ function generateWorkerInstructions(swarmId, projectId, maxAgents, taskCount) {
|
|
|
354
355
|
const recommendedAgents = Math.min(maxAgents, Math.ceil(taskCount / 2), taskCount);
|
|
355
356
|
// Generate unique agent IDs for each worker
|
|
356
357
|
const agentIds = Array.from({ length: recommendedAgents }, (_, i) => `${swarmId}_worker_${i + 1}`);
|
|
357
|
-
const workerPrompt = `You are a swarm worker agent.
|
|
358
|
+
const workerPrompt = `You are a swarm worker agent with access to a code graph.
|
|
358
359
|
- Agent ID: {AGENT_ID}
|
|
359
360
|
- Swarm ID: ${swarmId}
|
|
360
361
|
- Project: ${projectId}
|
|
361
362
|
|
|
362
|
-
##
|
|
363
|
-
1.
|
|
364
|
-
2.
|
|
365
|
-
3.
|
|
366
|
-
4. Exit when swarm_claim_task returns "no_tasks"
|
|
363
|
+
## RULES
|
|
364
|
+
1. Use graph tools (traverse_from_node, search_codebase) for context
|
|
365
|
+
2. Tasks provide: targets (best), targetNodeIds (good), targetFilePaths (fallback)
|
|
366
|
+
3. Exit when swarm_claim_task returns "no_tasks"
|
|
367
367
|
|
|
368
|
-
## WORKFLOW
|
|
368
|
+
## WORKFLOW
|
|
369
369
|
|
|
370
|
-
### Step 1: Claim a task
|
|
370
|
+
### Step 1: Claim a task
|
|
371
371
|
swarm_claim_task({
|
|
372
372
|
projectId: "${projectId}",
|
|
373
373
|
swarmId: "${swarmId}",
|
|
374
374
|
agentId: "{AGENT_ID}"
|
|
375
375
|
})
|
|
376
|
-
//
|
|
377
|
-
//
|
|
376
|
+
// Returns: { task: { id, targets: [{nodeId, name, filePath}], targetFilePaths, ... } }
|
|
377
|
+
// If "no_tasks" → exit
|
|
378
378
|
|
|
379
|
-
### Step 2:
|
|
380
|
-
|
|
381
|
-
projectId: "${projectId}",
|
|
382
|
-
swarmId: "${swarmId}",
|
|
383
|
-
agentId: "{AGENT_ID}",
|
|
384
|
-
taskId: "<TASK_ID_FROM_STEP_1>",
|
|
385
|
-
action: "start"
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
// Check if another agent is working on related code
|
|
389
|
-
swarm_sense({
|
|
390
|
-
projectId: "${projectId}",
|
|
391
|
-
swarmId: "${swarmId}",
|
|
392
|
-
types: ["modifying", "warning"],
|
|
393
|
-
excludeAgentId: "{AGENT_ID}"
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
### Step 3: Understand the code context (USE GRAPH TOOLS!)
|
|
397
|
-
// Use traverse_from_node to see relationships and callers
|
|
379
|
+
### Step 2: Understand context via graph (USE NODE IDs!)
|
|
380
|
+
// Priority: task.targets[0].nodeId > task.targetNodeIds[0] > search
|
|
398
381
|
traverse_from_node({
|
|
399
382
|
projectId: "${projectId}",
|
|
400
|
-
|
|
401
|
-
maxDepth: 2
|
|
402
|
-
includeCode: true
|
|
383
|
+
nodeId: "<nodeId_FROM_task.targets>",
|
|
384
|
+
maxDepth: 2
|
|
403
385
|
})
|
|
404
386
|
|
|
405
|
-
//
|
|
406
|
-
search_codebase({
|
|
407
|
-
projectId: "${projectId}",
|
|
408
|
-
query: "<WHAT_YOU_NEED_TO_UNDERSTAND>"
|
|
409
|
-
})
|
|
387
|
+
// Fallback if no nodeIds:
|
|
388
|
+
search_codebase({ projectId: "${projectId}", query: "<TASK_DESCRIPTION>" })
|
|
410
389
|
|
|
411
|
-
### Step
|
|
412
|
-
- Use Read tool for full source code of files to modify
|
|
413
|
-
- Use Edit tool to make changes
|
|
414
|
-
- Mark nodes you're modifying:
|
|
390
|
+
### Step 3: Mark nodes you're analyzing/modifying
|
|
415
391
|
swarm_pheromone({
|
|
416
392
|
projectId: "${projectId}",
|
|
417
|
-
nodeId: "<
|
|
393
|
+
nodeId: "<nodeId>",
|
|
418
394
|
type: "modifying",
|
|
419
395
|
agentId: "{AGENT_ID}",
|
|
420
396
|
swarmId: "${swarmId}"
|
|
421
397
|
})
|
|
422
398
|
|
|
423
|
-
### Step
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
nodeId: "<NODE_ID>",
|
|
427
|
-
type: "completed",
|
|
428
|
-
agentId: "{AGENT_ID}",
|
|
429
|
-
swarmId: "${swarmId}",
|
|
430
|
-
data: { summary: "<WHAT_YOU_DID>" }
|
|
431
|
-
})
|
|
399
|
+
### Step 4: Do the work
|
|
400
|
+
- Read tool for full source
|
|
401
|
+
- Edit tool for changes
|
|
432
402
|
|
|
403
|
+
### Step 5: Complete the task
|
|
433
404
|
swarm_complete_task({
|
|
434
405
|
projectId: "${projectId}",
|
|
435
406
|
taskId: "<TASK_ID>",
|
|
436
407
|
agentId: "{AGENT_ID}",
|
|
437
408
|
action: "complete",
|
|
438
|
-
summary: "<
|
|
439
|
-
filesChanged: ["<
|
|
409
|
+
summary: "<WHAT_YOU_DID>",
|
|
410
|
+
filesChanged: ["<FILES>"]
|
|
440
411
|
})
|
|
441
412
|
|
|
442
|
-
### Step 6: Loop
|
|
443
|
-
Claim the next available task. Continue until no tasks remain.
|
|
413
|
+
### Step 6: Loop to Step 1
|
|
444
414
|
|
|
445
|
-
## IF
|
|
446
|
-
swarm_complete_task({
|
|
447
|
-
projectId: "${projectId}",
|
|
448
|
-
taskId: "<TASK_ID>",
|
|
449
|
-
agentId: "{AGENT_ID}",
|
|
450
|
-
action: "fail",
|
|
451
|
-
reason: "<WHY_YOU_ARE_STUCK>",
|
|
452
|
-
retryable: true
|
|
453
|
-
})
|
|
415
|
+
## IF STUCK
|
|
416
|
+
swarm_complete_task({ ..., action: "fail", reason: "<WHY>", retryable: true })
|
|
454
417
|
Then claim another task.`;
|
|
455
418
|
const taskCalls = agentIds.map(agentId => {
|
|
456
419
|
const prompt = workerPrompt.replace(/\{AGENT_ID\}/g, agentId);
|
|
@@ -7,6 +7,21 @@ import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
|
7
7
|
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
8
|
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
9
9
|
import { TASK_PRIORITIES, TASK_TYPES, generateTaskId, } from './swarm-constants.js';
|
|
10
|
+
/**
|
|
11
|
+
* Query to get node IDs for a file path (fallback when no nodeIds provided)
|
|
12
|
+
* Uses ENDS WITH for flexible matching (handles absolute vs relative paths)
|
|
13
|
+
*/
|
|
14
|
+
const GET_NODES_FOR_FILE_QUERY = `
|
|
15
|
+
MATCH (n)
|
|
16
|
+
WHERE (n.filePath = $filePath OR n.filePath ENDS WITH $filePath)
|
|
17
|
+
AND n.projectId = $projectId
|
|
18
|
+
AND n.id IS NOT NULL
|
|
19
|
+
AND NOT n:Pheromone
|
|
20
|
+
AND NOT n:SwarmTask
|
|
21
|
+
RETURN n.id AS id
|
|
22
|
+
ORDER BY n.startLine
|
|
23
|
+
LIMIT 20
|
|
24
|
+
`;
|
|
10
25
|
/**
|
|
11
26
|
* Neo4j query to create a new SwarmTask node
|
|
12
27
|
*/
|
|
@@ -138,16 +153,22 @@ export const createSwarmPostTaskTool = (server) => {
|
|
|
138
153
|
const taskId = generateTaskId();
|
|
139
154
|
const priorityScore = TASK_PRIORITIES[priority];
|
|
140
155
|
const metadataJson = metadata ? JSON.stringify(metadata) : null;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
// Filter out null/undefined, then fallback to file query if empty
|
|
157
|
+
let resolvedNodeIds = (targetNodeIds || []).filter((id) => typeof id === 'string' && id.length > 0);
|
|
158
|
+
if (resolvedNodeIds.length === 0 && targetFilePaths.length > 0) {
|
|
159
|
+
for (const filePath of targetFilePaths) {
|
|
160
|
+
const fileNodes = await neo4jService.run(GET_NODES_FOR_FILE_QUERY, {
|
|
161
|
+
filePath,
|
|
162
|
+
projectId: resolvedProjectId,
|
|
163
|
+
});
|
|
164
|
+
if (fileNodes.length > 0) {
|
|
165
|
+
resolvedNodeIds = [
|
|
166
|
+
...resolvedNodeIds,
|
|
167
|
+
...fileNodes.map((n) => n.id).filter(Boolean),
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
151
172
|
// Create the task
|
|
152
173
|
const result = await neo4jService.run(CREATE_TASK_QUERY, {
|
|
153
174
|
taskId,
|
|
@@ -158,7 +179,7 @@ export const createSwarmPostTaskTool = (server) => {
|
|
|
158
179
|
type,
|
|
159
180
|
priority,
|
|
160
181
|
priorityScore,
|
|
161
|
-
targetNodeIds,
|
|
182
|
+
targetNodeIds: resolvedNodeIds,
|
|
162
183
|
targetFilePaths,
|
|
163
184
|
dependencies,
|
|
164
185
|
createdBy,
|
|
@@ -109,21 +109,6 @@ export const createTraverseFromNodeTool = (server) => {
|
|
|
109
109
|
}
|
|
110
110
|
const sanitizedMaxDepth = sanitizeNumericInput(maxDepth, DEFAULTS.traversalDepth, MAX_TRAVERSAL_DEPTH);
|
|
111
111
|
const sanitizedSkip = sanitizeNumericInput(skip, DEFAULTS.skipOffset);
|
|
112
|
-
await debugLog('Node traversal started', {
|
|
113
|
-
projectId: resolvedProjectId,
|
|
114
|
-
nodeId: resolvedNodeId,
|
|
115
|
-
filePath,
|
|
116
|
-
maxDepth: sanitizedMaxDepth,
|
|
117
|
-
skip: sanitizedSkip,
|
|
118
|
-
limit,
|
|
119
|
-
direction,
|
|
120
|
-
relationshipTypes,
|
|
121
|
-
includeCode,
|
|
122
|
-
maxNodesPerChain,
|
|
123
|
-
summaryOnly,
|
|
124
|
-
snippetLength,
|
|
125
|
-
maxTotalNodes,
|
|
126
|
-
});
|
|
127
112
|
// Safety check - resolvedNodeId should be set at this point
|
|
128
113
|
if (!resolvedNodeId) {
|
|
129
114
|
return createErrorResponse('Could not resolve node ID from provided parameters.');
|
package/package.json
CHANGED