code-graph-context 2.5.4 → 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.
@@ -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 = 'in_progress' AND t.claimedBy = $agentId
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
- return createErrorResponse(`Cannot complete task ${taskId}. It may not exist, not be in_progress, or you don't own it.`);
324
+ const errorMsg = await getTaskStateError(neo4jService, taskId, resolvedProjectId, 'complete', agentId);
325
+ return createErrorResponse(errorMsg);
274
326
  }
275
- responseData.task = {
276
- id: result[0].id,
277
- title: result[0].title,
278
- status: result[0].status,
279
- completedAt: typeof result[0].completedAt === 'object'
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
- return createErrorResponse(`Cannot fail task ${taskId}. It may not exist, not be in_progress/claimed, or you don't own it.`);
345
+ const errorMsg = await getTaskStateError(neo4jService, taskId, resolvedProjectId, 'fail', agentId);
346
+ return createErrorResponse(errorMsg);
304
347
  }
305
- responseData.task = {
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
- return createErrorResponse(`Cannot request review for task ${taskId}. It may not exist, not be in_progress, or you don't own it.`);
363
+ const errorMsg = await getTaskStateError(neo4jService, taskId, resolvedProjectId, 'request_review', agentId);
364
+ return createErrorResponse(errorMsg);
334
365
  }
335
- responseData.task = {
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
- responseData.task = {
361
- id: result[0].id,
362
- title: result[0].title,
363
- status: result[0].status,
364
- completedAt: typeof result[0].completedAt === 'object'
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
- responseData.task = {
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
- responseData.task = {
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: atomicTask.nodeIds,
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
- ## CRITICAL RULES
363
- 1. NEVER fabricate node IDs - get them from graph tool responses
364
- 2. ALWAYS use the blackboard task queue (swarm_claim_task, swarm_complete_task)
365
- 3. Use graph tools (traverse_from_node, search_codebase) to understand context
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 - Follow these steps exactly:
368
+ ## WORKFLOW
369
369
 
370
- ### Step 1: Claim a task from the blackboard
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
- // If returns "no_tasks" exit, swarm is complete
377
- // Otherwise you now own the returned task
376
+ // Returns: { task: { id, targets: [{nodeId, name, filePath}], targetFilePaths, ... } }
377
+ // If "no_tasks" exit
378
378
 
379
- ### Step 2: Start working and check for conflicts
380
- swarm_claim_task({
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
- filePath: "<TARGET_FILE_FROM_TASK>",
401
- maxDepth: 2,
402
- includeCode: true
383
+ nodeId: "<nodeId_FROM_task.targets>",
384
+ maxDepth: 2
403
385
  })
404
386
 
405
- // Or search for related code
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 4: Do the work
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: "<NODE_ID_FROM_GRAPH>",
393
+ nodeId: "<nodeId>",
418
394
  type: "modifying",
419
395
  agentId: "{AGENT_ID}",
420
396
  swarmId: "${swarmId}"
421
397
  })
422
398
 
423
- ### Step 5: Complete the task via blackboard
424
- swarm_pheromone({
425
- projectId: "${projectId}",
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: "<DESCRIBE_WHAT_YOU_DID>",
439
- filesChanged: ["<LIST_OF_FILES_YOU_MODIFIED>"]
409
+ summary: "<WHAT_YOU_DID>",
410
+ filesChanged: ["<FILES>"]
440
411
  })
441
412
 
442
- ### Step 6: Loop back to Step 1
443
- Claim the next available task. Continue until no tasks remain.
413
+ ### Step 6: Loop to Step 1
444
414
 
445
- ## IF YOU GET STUCK
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
- await debugLog('Creating swarm task', {
142
- taskId,
143
- projectId: resolvedProjectId,
144
- swarmId,
145
- title,
146
- type,
147
- priority,
148
- targetNodeIds: targetNodeIds.length,
149
- dependencies: dependencies.length,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-context",
3
- "version": "2.5.4",
3
+ "version": "2.6.0",
4
4
  "description": "MCP server that builds code graphs to provide rich context to LLMs",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/drewdrewH/code-graph-context#readme",