code-graph-context 2.14.1 → 3.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 (37) hide show
  1. package/dist/core/embeddings/natural-language-to-cypher.service.js +2 -4
  2. package/dist/mcp/constants.js +56 -228
  3. package/dist/mcp/handlers/swarm/abandon.handler.js +61 -0
  4. package/dist/mcp/handlers/swarm/advance.handler.js +78 -0
  5. package/dist/mcp/handlers/swarm/claim.handler.js +61 -0
  6. package/dist/mcp/handlers/swarm/index.js +5 -0
  7. package/dist/mcp/handlers/swarm/queries.js +140 -0
  8. package/dist/mcp/handlers/swarm/release.handler.js +41 -0
  9. package/dist/mcp/handlers/swarm-worker.handler.js +2 -13
  10. package/dist/mcp/tools/detect-dead-code.tool.js +33 -65
  11. package/dist/mcp/tools/detect-duplicate-code.tool.js +44 -53
  12. package/dist/mcp/tools/impact-analysis.tool.js +1 -1
  13. package/dist/mcp/tools/index.js +9 -9
  14. package/dist/mcp/tools/list-projects.tool.js +2 -2
  15. package/dist/mcp/tools/list-watchers.tool.js +2 -5
  16. package/dist/mcp/tools/natural-language-to-cypher.tool.js +2 -2
  17. package/dist/mcp/tools/parse-typescript-project.tool.js +7 -17
  18. package/dist/mcp/tools/search-codebase.tool.js +11 -26
  19. package/dist/mcp/tools/session-bookmark.tool.js +7 -11
  20. package/dist/mcp/tools/session-cleanup.tool.js +2 -6
  21. package/dist/mcp/tools/session-note.tool.js +6 -21
  22. package/dist/mcp/tools/session-recall.tool.js +293 -0
  23. package/dist/mcp/tools/session-save.tool.js +280 -0
  24. package/dist/mcp/tools/start-watch-project.tool.js +1 -1
  25. package/dist/mcp/tools/swarm-advance-task.tool.js +56 -0
  26. package/dist/mcp/tools/swarm-claim-task.tool.js +24 -388
  27. package/dist/mcp/tools/swarm-cleanup.tool.js +3 -7
  28. package/dist/mcp/tools/swarm-complete-task.tool.js +14 -17
  29. package/dist/mcp/tools/swarm-get-tasks.tool.js +8 -26
  30. package/dist/mcp/tools/swarm-message.tool.js +10 -25
  31. package/dist/mcp/tools/swarm-pheromone.tool.js +7 -25
  32. package/dist/mcp/tools/swarm-post-task.tool.js +7 -19
  33. package/dist/mcp/tools/swarm-release-task.tool.js +53 -0
  34. package/dist/mcp/tools/swarm-sense.tool.js +10 -30
  35. package/dist/mcp/tools/traverse-from-node.tool.js +19 -41
  36. package/dist/mcp/utils.js +41 -1
  37. package/package.json +1 -1
@@ -2,239 +2,16 @@
2
2
  * Swarm Claim Task Tool
3
3
  * Allow an agent to claim an available task from the blackboard
4
4
  *
5
- * Phase 1 improvements:
6
- * - Atomic claim_and_start action (eliminates race window)
7
- * - Retry logic on race loss
8
- * - Recovery actions (abandon, force_start)
5
+ * Handles claim and claim_and_start only.
6
+ * Release, abandon, force_start, and start actions are handled by dedicated tools.
9
7
  */
10
8
  import { z } from 'zod';
11
9
  import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
12
10
  import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
13
- import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
11
+ import { SwarmClaimHandler } from '../handlers/swarm/index.js';
12
+ import { createEmptyResponse, createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog, } from '../utils.js';
14
13
  import { TASK_TYPES, TASK_PRIORITIES } from './swarm-constants.js';
15
14
  import { PENDING_MESSAGES_FOR_AGENT_QUERY, AUTO_ACKNOWLEDGE_QUERY } from './swarm-message.tool.js';
16
- /** Maximum retries when racing for a task */
17
- const MAX_CLAIM_RETRIES = 3;
18
- /** Delay between retries (ms) */
19
- const RETRY_DELAY_BASE_MS = 50;
20
- /**
21
- * Query to claim a specific task by ID
22
- * Uses atomic update to prevent race conditions
23
- */
24
- const CLAIM_TASK_BY_ID_QUERY = `
25
- MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
26
- WHERE t.status IN ['available', 'blocked']
27
-
28
- // Check if dependencies are complete
29
- OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:SwarmTask)
30
- WHERE dep.status <> 'completed'
31
- WITH t, count(dep) as incompleteDeps
32
-
33
- // Only claim if all dependencies are complete
34
- WHERE incompleteDeps = 0
35
-
36
- // Acquire exclusive lock to prevent race conditions
37
- CALL apoc.lock.nodes([t])
38
-
39
- // Double-check status after acquiring lock
40
- WITH t WHERE t.status IN ['available', 'blocked']
41
-
42
- // Atomic claim
43
- SET t.status = $targetStatus,
44
- t.claimedBy = $agentId,
45
- t.claimedAt = timestamp(),
46
- t.startedAt = CASE WHEN $targetStatus = 'in_progress' THEN timestamp() ELSE null END,
47
- t.updatedAt = timestamp()
48
-
49
- // Return task details with target info
50
- WITH t
51
- OPTIONAL MATCH (t)-[:TARGETS]->(target)
52
- RETURN t.id as id,
53
- t.projectId as projectId,
54
- t.swarmId as swarmId,
55
- t.title as title,
56
- t.description as description,
57
- t.type as type,
58
- t.priority as priority,
59
- t.priorityScore as priorityScore,
60
- t.status as status,
61
- t.targetNodeIds as targetNodeIds,
62
- t.targetFilePaths as targetFilePaths,
63
- t.dependencies as dependencies,
64
- t.claimedBy as claimedBy,
65
- t.claimedAt as claimedAt,
66
- t.startedAt as startedAt,
67
- t.createdBy as createdBy,
68
- t.metadata as metadata,
69
- collect(DISTINCT {
70
- id: target.id,
71
- type: labels(target)[0],
72
- name: target.name,
73
- filePath: target.filePath
74
- }) as targets
75
- `;
76
- /**
77
- * Query to claim the highest priority available task matching criteria
78
- * Uses APOC locking to prevent race conditions between parallel workers
79
- * Supports both 'claimed' and 'in_progress' target states for atomic claim_and_start
80
- */
81
- const CLAIM_NEXT_TASK_QUERY = `
82
- // Find available or blocked tasks (blocked tasks may have deps completed now)
83
- MATCH (t:SwarmTask {projectId: $projectId, swarmId: $swarmId})
84
- WHERE t.status IN ['available', 'blocked']
85
- AND ($types IS NULL OR size($types) = 0 OR t.type IN $types)
86
- AND ($minPriority IS NULL OR t.priorityScore >= $minPriority)
87
-
88
- // Exclude tasks with incomplete dependencies
89
- OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:SwarmTask)
90
- WHERE dep.status <> 'completed'
91
- WITH t, count(dep) as incompleteDeps
92
- WHERE incompleteDeps = 0
93
-
94
- // Re-establish context for ordering (required by Cypher syntax)
95
- WITH t
96
- ORDER BY t.priorityScore DESC, t.createdAt ASC
97
- LIMIT 1
98
-
99
- // Acquire exclusive lock to prevent race conditions
100
- CALL apoc.lock.nodes([t])
101
-
102
- // Double-check status after acquiring lock (another worker may have claimed it)
103
- WITH t WHERE t.status IN ['available', 'blocked']
104
-
105
- // Atomic claim - supports both claim and claim_and_start via $targetStatus
106
- SET t.status = $targetStatus,
107
- t.claimedBy = $agentId,
108
- t.claimedAt = timestamp(),
109
- t.startedAt = CASE WHEN $targetStatus = 'in_progress' THEN timestamp() ELSE null END,
110
- t.updatedAt = timestamp()
111
-
112
- // Return task details with target info
113
- WITH t
114
- OPTIONAL MATCH (t)-[:TARGETS]->(target)
115
- RETURN t.id as id,
116
- t.projectId as projectId,
117
- t.swarmId as swarmId,
118
- t.title as title,
119
- t.description as description,
120
- t.type as type,
121
- t.priority as priority,
122
- t.priorityScore as priorityScore,
123
- t.status as status,
124
- t.targetNodeIds as targetNodeIds,
125
- t.targetFilePaths as targetFilePaths,
126
- t.dependencies as dependencies,
127
- t.claimedBy as claimedBy,
128
- t.claimedAt as claimedAt,
129
- t.startedAt as startedAt,
130
- t.createdBy as createdBy,
131
- t.metadata as metadata,
132
- collect(DISTINCT {
133
- id: target.id,
134
- type: labels(target)[0],
135
- name: target.name,
136
- filePath: target.filePath
137
- }) as targets
138
- `;
139
- /**
140
- * Query to start working on a claimed task (transition to in_progress)
141
- */
142
- const START_TASK_QUERY = `
143
- MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
144
- WHERE t.status = 'claimed' AND t.claimedBy = $agentId
145
-
146
- SET t.status = 'in_progress',
147
- t.startedAt = timestamp(),
148
- t.updatedAt = timestamp()
149
-
150
- RETURN t.id as id,
151
- t.status as status,
152
- t.claimedBy as claimedBy,
153
- t.startedAt as startedAt
154
- `;
155
- /**
156
- * Query to release a claimed task (unclaim it)
157
- */
158
- const RELEASE_TASK_QUERY = `
159
- MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
160
- WHERE t.status IN ['claimed', 'in_progress'] AND t.claimedBy = $agentId
161
-
162
- SET t.status = 'available',
163
- t.claimedBy = null,
164
- t.claimedAt = null,
165
- t.startedAt = null,
166
- t.updatedAt = timestamp(),
167
- t.releaseReason = $reason
168
-
169
- RETURN t.id as id,
170
- t.title as title,
171
- t.status as status
172
- `;
173
- /**
174
- * Query to abandon a task - releases it with tracking for debugging
175
- * More explicit than release, tracks abandon history
176
- */
177
- const ABANDON_TASK_QUERY = `
178
- MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
179
- WHERE t.claimedBy = $agentId
180
- AND t.status IN ['claimed', 'in_progress']
181
-
182
- // Track abandon history
183
- SET t.status = 'available',
184
- t.previousClaimedBy = t.claimedBy,
185
- t.claimedBy = null,
186
- t.claimedAt = null,
187
- t.startedAt = null,
188
- t.updatedAt = timestamp(),
189
- t.abandonedBy = $agentId,
190
- t.abandonedAt = timestamp(),
191
- t.abandonReason = $reason,
192
- t.abandonCount = COALESCE(t.abandonCount, 0) + 1
193
-
194
- RETURN t.id as id,
195
- t.title as title,
196
- t.status as status,
197
- t.abandonCount as abandonCount,
198
- t.abandonReason as abandonReason
199
- `;
200
- /**
201
- * Query to force-start a task that's stuck in claimed state
202
- * Allows recovery when the normal start action fails
203
- */
204
- const FORCE_START_QUERY = `
205
- MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
206
- WHERE t.claimedBy = $agentId
207
- AND t.status IN ['claimed', 'available']
208
-
209
- SET t.status = 'in_progress',
210
- t.claimedBy = $agentId,
211
- t.claimedAt = COALESCE(t.claimedAt, timestamp()),
212
- t.startedAt = timestamp(),
213
- t.updatedAt = timestamp(),
214
- t.forceStarted = true,
215
- t.forceStartReason = $reason
216
-
217
- RETURN t.id as id,
218
- t.title as title,
219
- t.status as status,
220
- t.claimedBy as claimedBy,
221
- t.startedAt as startedAt,
222
- t.forceStarted as forceStarted
223
- `;
224
- /**
225
- * Query to get current task state for better error messages
226
- */
227
- const GET_TASK_STATE_QUERY = `
228
- MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
229
- RETURN t.id as id,
230
- t.title as title,
231
- t.status as status,
232
- t.claimedBy as claimedBy,
233
- t.claimedAt as claimedAt,
234
- t.startedAt as startedAt,
235
- t.abandonCount as abandonCount,
236
- t.previousClaimedBy as previousClaimedBy
237
- `;
238
15
  export const createSwarmClaimTaskTool = (server) => {
239
16
  server.registerTool(TOOL_NAMES.swarmClaimTask, {
240
17
  title: TOOL_METADATA[TOOL_NAMES.swarmClaimTask].title,
@@ -247,24 +24,14 @@ export const createSwarmClaimTaskTool = (server) => {
247
24
  .string()
248
25
  .optional()
249
26
  .describe('Specific task ID to claim (if omitted, claims highest priority available task)'),
250
- types: z
251
- .array(z.enum(TASK_TYPES))
252
- .optional()
253
- .describe('Filter by task types when auto-selecting (e.g., ["implement", "fix"])'),
27
+ types: z.array(z.enum(TASK_TYPES)).optional().describe('Filter by task types'),
254
28
  minPriority: z
255
29
  .enum(Object.keys(TASK_PRIORITIES))
256
30
  .optional()
257
- .describe('Minimum priority level when auto-selecting'),
258
- action: z
259
- .enum(['claim', 'claim_and_start', 'start', 'release', 'abandon', 'force_start'])
260
- .optional()
261
- .default('claim_and_start')
262
- .describe('Action: claim_and_start (RECOMMENDED: atomic claim+start), claim (reserve only), ' +
263
- 'start (begin work on claimed task), release (give up task), ' +
264
- 'abandon (release with tracking), force_start (recover from stuck claimed state)'),
265
- releaseReason: z.string().optional().describe('Reason for releasing/abandoning the task'),
31
+ .describe('Minimum priority when auto-selecting'),
32
+ startImmediately: z.boolean().optional().default(true).describe('Start the task immediately after claiming'),
266
33
  },
267
- }, async ({ projectId, swarmId, agentId, taskId, types, minPriority, action = 'claim_and_start', releaseReason }) => {
34
+ }, async ({ projectId, swarmId, agentId, taskId, types, minPriority, startImmediately = true }) => {
268
35
  const neo4jService = new Neo4jService();
269
36
  // Resolve project ID
270
37
  const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
@@ -274,160 +41,29 @@ export const createSwarmClaimTaskTool = (server) => {
274
41
  }
275
42
  const resolvedProjectId = projectResult.projectId;
276
43
  try {
277
- // Handle release action
278
- if (action === 'release') {
279
- if (!taskId) {
280
- return createErrorResponse('taskId is required for release action');
281
- }
282
- const result = await neo4jService.run(RELEASE_TASK_QUERY, {
283
- taskId,
284
- projectId: resolvedProjectId,
285
- agentId,
286
- reason: releaseReason || 'No reason provided',
287
- });
288
- if (result.length === 0) {
289
- // Get current state for better error message
290
- const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
291
- taskId,
292
- projectId: resolvedProjectId,
293
- });
294
- const currentState = stateResult[0];
295
- return createErrorResponse(`Cannot release task ${taskId}. ` +
296
- (currentState
297
- ? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}`
298
- : 'Task not found.'));
299
- }
300
- return createSuccessResponse(JSON.stringify({ action: 'released', taskId: result[0].id }));
301
- }
302
- // Handle abandon action (release with tracking)
303
- if (action === 'abandon') {
304
- if (!taskId) {
305
- return createErrorResponse('taskId is required for abandon action');
306
- }
307
- const result = await neo4jService.run(ABANDON_TASK_QUERY, {
308
- taskId,
309
- projectId: resolvedProjectId,
310
- agentId,
311
- reason: releaseReason || 'No reason provided',
312
- });
313
- if (result.length === 0) {
314
- const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
315
- taskId,
316
- projectId: resolvedProjectId,
317
- });
318
- const currentState = stateResult[0];
319
- return createErrorResponse(`Cannot abandon task ${taskId}. ` +
320
- (currentState
321
- ? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}`
322
- : 'Task not found.'));
323
- }
324
- const abandonCount = typeof result[0].abandonCount === 'object' ? result[0].abandonCount.toNumber() : result[0].abandonCount;
325
- return createSuccessResponse(JSON.stringify({ action: 'abandoned', taskId: result[0].id, abandonCount }));
326
- }
327
- // Handle force_start action (recovery from stuck claimed state)
328
- if (action === 'force_start') {
329
- if (!taskId) {
330
- return createErrorResponse('taskId is required for force_start action');
331
- }
332
- const result = await neo4jService.run(FORCE_START_QUERY, {
333
- taskId,
334
- projectId: resolvedProjectId,
335
- agentId,
336
- reason: releaseReason || 'Recovering from stuck state',
337
- });
338
- if (result.length === 0) {
339
- const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
340
- taskId,
341
- projectId: resolvedProjectId,
342
- });
343
- const currentState = stateResult[0];
344
- return createErrorResponse(`Cannot force_start task ${taskId}. ` +
345
- (currentState
346
- ? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}. ` +
347
- `force_start requires status=claimed|available and you must be the claimant.`
348
- : 'Task not found.'));
349
- }
350
- return createSuccessResponse(JSON.stringify({ action: 'force_started', taskId: result[0].id, status: 'in_progress' }));
351
- }
352
- // Handle start action
353
- if (action === 'start') {
354
- if (!taskId) {
355
- return createErrorResponse('taskId is required for start action');
356
- }
357
- const result = await neo4jService.run(START_TASK_QUERY, {
358
- taskId,
359
- projectId: resolvedProjectId,
360
- agentId,
361
- });
362
- if (result.length === 0) {
363
- // Get current state for better error message
364
- const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
365
- taskId,
366
- projectId: resolvedProjectId,
367
- });
368
- const currentState = stateResult[0];
369
- return createErrorResponse(`Cannot start task ${taskId}. ` +
370
- (currentState
371
- ? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}. ` +
372
- `Tip: Use action="force_start" to recover from stuck claimed state, ` +
373
- `or action="abandon" to release the task.`
374
- : 'Task not found.'));
375
- }
376
- return createSuccessResponse(JSON.stringify({ action: 'started', taskId: result[0].id, status: 'in_progress' }));
377
- }
378
- // Handle claim and claim_and_start actions
379
- // Determine target status based on action
380
- const targetStatus = action === 'claim_and_start' ? 'in_progress' : 'claimed';
381
- let result;
382
- let retryCount = 0;
44
+ const claimHandler = new SwarmClaimHandler(neo4jService);
45
+ const targetStatus = startImmediately ? 'in_progress' : 'claimed';
46
+ let claimResult;
383
47
  if (taskId) {
384
- // Claim specific task
385
- result = await neo4jService.run(CLAIM_TASK_BY_ID_QUERY, {
386
- taskId,
387
- projectId: resolvedProjectId,
388
- agentId,
389
- targetStatus,
390
- });
391
- if (result.length === 0) {
392
- const stateResult = await neo4jService.run(GET_TASK_STATE_QUERY, {
393
- taskId,
394
- projectId: resolvedProjectId,
395
- });
396
- const currentState = stateResult[0];
48
+ claimResult = await claimHandler.claimById(resolvedProjectId, taskId, agentId, targetStatus);
49
+ if (claimResult.error) {
397
50
  return createErrorResponse(`Cannot claim task ${taskId}. ` +
398
- (currentState
399
- ? `Current state: ${currentState.status}, claimedBy: ${currentState.claimedBy || 'none'}`
51
+ (claimResult.data
52
+ ? `Current state: ${claimResult.data.status}, claimedBy: ${claimResult.data.claimedBy || 'none'}`
400
53
  : 'Task not found or has incomplete dependencies.'));
401
54
  }
402
55
  }
403
56
  else {
404
- // Auto-select highest priority available task with retry logic
405
- const minPriorityScore = minPriority ? TASK_PRIORITIES[minPriority] : null;
406
- // Retry loop to handle race conditions
407
- while (retryCount < MAX_CLAIM_RETRIES) {
408
- result = await neo4jService.run(CLAIM_NEXT_TASK_QUERY, {
409
- projectId: resolvedProjectId,
410
- swarmId,
411
- agentId,
412
- types: types || null,
413
- minPriority: minPriorityScore,
414
- targetStatus,
415
- });
416
- if (result.length > 0) {
417
- break; // Successfully claimed a task
418
- }
419
- retryCount++;
420
- if (retryCount < MAX_CLAIM_RETRIES) {
421
- // Wait before retry with exponential backoff
422
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_BASE_MS * Math.pow(2, retryCount - 1)));
423
- }
424
- }
425
- if (!result || result.length === 0) {
426
- return createSuccessResponse(JSON.stringify({ action: 'no_tasks', retryAttempts: retryCount }));
57
+ claimResult = await claimHandler.claimNext(resolvedProjectId, swarmId, agentId, targetStatus, {
58
+ types: types || null,
59
+ minPriority: minPriority || null,
60
+ });
61
+ if (!claimResult.data) {
62
+ return createEmptyResponse('No available tasks matching criteria', 'Check swarm_get_tasks for task statuses, or post new tasks with swarm_post_task.');
427
63
  }
428
64
  }
429
- const task = result[0];
430
- const actionLabel = action === 'claim_and_start' ? 'claimed_and_started' : 'claimed';
65
+ const task = claimResult.data;
66
+ const actionLabel = startImmediately ? 'claimed_and_started' : 'claimed';
431
67
  // Extract valid targets (resolved via :TARGETS relationship)
432
68
  const resolvedTargets = (task.targets || [])
433
69
  .filter((t) => t?.id)
@@ -485,7 +121,7 @@ export const createSwarmClaimTaskTool = (server) => {
485
121
  ...(task.dependencies?.length > 0 && { dependencies: task.dependencies }),
486
122
  },
487
123
  ...(pendingMessages.length > 0 && { messages: pendingMessages }),
488
- ...(retryCount > 0 && { retryAttempts: retryCount }),
124
+ ...(claimResult.retryAttempts > 0 && { retryAttempts: claimResult.retryAttempts }),
489
125
  }));
490
126
  }
491
127
  catch (error) {
@@ -99,17 +99,13 @@ export const createSwarmCleanupTool = (server) => {
99
99
  projectId: z.string().describe('Project ID, name, or path'),
100
100
  swarmId: z.string().optional().describe('Delete all pheromones and tasks from this swarm'),
101
101
  agentId: z.string().optional().describe('Delete all pheromones from this agent'),
102
- all: z.boolean().optional().default(false).describe('Delete ALL pheromones in project (use with caution)'),
102
+ all: z.boolean().optional().default(false).describe('Delete ALL pheromones in project'),
103
103
  includeTasks: z
104
104
  .boolean()
105
105
  .optional()
106
106
  .default(true)
107
- .describe('Also delete SwarmTask nodes (default: true, only applies when swarmId is provided)'),
108
- keepTypes: z
109
- .array(z.string())
110
- .optional()
111
- .default(['warning'])
112
- .describe('Pheromone types to preserve (default: ["warning"])'),
107
+ .describe('Also delete SwarmTask nodes (only applies when swarmId is provided)'),
108
+ keepTypes: z.array(z.string()).optional().default(['warning']).describe('Pheromone types to preserve'),
113
109
  dryRun: z.boolean().optional().default(false).describe('Preview what would be deleted without deleting'),
114
110
  },
115
111
  }, async ({ projectId, swarmId, agentId, all = false, includeTasks = true, keepTypes = ['warning'], dryRun = false, }) => {
@@ -214,7 +214,7 @@ async function getTaskStateError(neo4jService, taskId, projectId, action, agentI
214
214
  let suggestion = '';
215
215
  if (action === 'complete' || action === 'fail') {
216
216
  if (state.status === 'available') {
217
- suggestion = 'You must claim the task first using swarm_claim_task.';
217
+ suggestion = 'You must claim the task first using swarm_claim_task, then start it or use startImmediately=true.';
218
218
  }
219
219
  else if (!isOwner) {
220
220
  suggestion = `Task is claimed by "${claimedBy}", not you.`;
@@ -245,29 +245,26 @@ export const createSwarmCompleteTaskTool = (server) => {
245
245
  inputSchema: {
246
246
  projectId: z.string().describe('Project ID, name, or path'),
247
247
  taskId: z.string().describe('Task ID to complete'),
248
- agentId: z.string().describe('Your agent ID (must match the agent who claimed the task)'),
248
+ agentId: z.string().describe('Your agent ID (must match who claimed the task)'),
249
249
  action: z
250
250
  .enum(['complete', 'fail', 'request_review', 'approve', 'reject', 'retry'])
251
251
  .describe('Action to take on the task'),
252
- summary: z.string().optional().describe('Summary of what was done (required for complete/request_review)'),
253
- artifacts: z
254
- .record(z.unknown())
255
- .optional()
256
- .describe('Artifacts produced: { files: [], commits: [], pullRequests: [], notes: string }'),
257
- filesChanged: z.array(z.string()).optional().describe('List of files that were modified'),
258
- linesAdded: z.number().int().optional().describe('Number of lines added'),
259
- linesRemoved: z.number().int().optional().describe('Number of lines removed'),
260
- reason: z.string().optional().describe('Reason for failure (required if action=fail)'),
261
- errorDetails: z.string().optional().describe('Technical error details for debugging'),
262
- retryable: z.boolean().optional().default(true).describe('Whether the task can be retried after failure'),
263
- reviewNotes: z.string().optional().describe('Notes for the reviewer (for request_review)'),
264
- reviewerId: z.string().optional().describe('ID of the reviewer (required for approve/reject)'),
265
- notes: z.string().optional().describe('Approval/rejection notes'),
252
+ summary: z.string().optional().describe('What was done (required for complete/request_review)'),
253
+ artifacts: z.record(z.unknown()).optional().describe('Artifacts produced by this task'),
254
+ filesChanged: z.array(z.string()).optional().describe('Files modified'),
255
+ linesAdded: z.number().int().optional().describe('Lines added'),
256
+ linesRemoved: z.number().int().optional().describe('Lines removed'),
257
+ reason: z.string().optional().describe('Failure reason (required for action=fail)'),
258
+ errorDetails: z.string().optional().describe('Technical error details'),
259
+ retryable: z.boolean().optional().default(true).describe('Allow retry after failure'),
260
+ reviewNotes: z.string().optional().describe('Notes for the reviewer'),
261
+ reviewerId: z.string().optional().describe('Reviewer ID (required for approve/reject)'),
262
+ notes: z.string().optional().describe('Approval or rejection notes'),
266
263
  markAsFailed: z
267
264
  .boolean()
268
265
  .optional()
269
266
  .default(false)
270
- .describe('If rejecting, mark as failed instead of returning to in_progress'),
267
+ .describe('On reject, mark as failed instead of returning to in_progress'),
271
268
  },
272
269
  }, async ({ projectId, taskId, agentId, action, summary, artifacts, filesChanged, linesAdded, linesRemoved, reason, errorDetails, retryable = true, reviewNotes, reviewerId, notes, markAsFailed = false, }) => {
273
270
  const neo4jService = new Neo4jService();
@@ -163,41 +163,23 @@ export const createSwarmGetTasksTool = (server) => {
163
163
  projectId: z.string().describe('Project ID, name, or path'),
164
164
  swarmId: z.string().optional().describe('Filter by swarm ID'),
165
165
  taskId: z.string().optional().describe('Get a specific task by ID (returns full details)'),
166
- statuses: z
167
- .array(z.enum(TASK_STATUSES))
168
- .optional()
169
- .describe('Filter by task statuses (e.g., ["available", "in_progress"])'),
170
- types: z.array(z.enum(TASK_TYPES)).optional().describe('Filter by task types (e.g., ["implement", "fix"])'),
171
- claimedBy: z.string().optional().describe('Filter tasks claimed by a specific agent'),
172
- createdBy: z.string().optional().describe('Filter tasks created by a specific agent'),
166
+ statuses: z.array(z.enum(TASK_STATUSES)).optional().describe('Filter by task statuses'),
167
+ types: z.array(z.enum(TASK_TYPES)).optional().describe('Filter by task types'),
168
+ claimedBy: z.string().optional().describe('Filter by claiming agent'),
169
+ createdBy: z.string().optional().describe('Filter by creating agent'),
173
170
  minPriority: z
174
171
  .enum(Object.keys(TASK_PRIORITIES))
175
172
  .optional()
176
173
  .describe('Minimum priority level'),
177
- orderBy: z
178
- .enum(['priority', 'created', 'updated'])
179
- .optional()
180
- .default('priority')
181
- .describe('Sort order: priority (highest first), created (newest first), updated'),
182
- limit: z
183
- .number()
184
- .int()
185
- .min(1)
186
- .max(100)
187
- .optional()
188
- .default(20)
189
- .describe('Maximum tasks to return (default: 20)'),
190
- skip: z.number().int().min(0).optional().default(0).describe('Number of tasks to skip for pagination'),
174
+ orderBy: z.enum(['priority', 'created', 'updated']).optional().default('priority').describe('Sort order'),
175
+ limit: z.number().int().min(1).max(100).optional().default(20).describe('Maximum tasks to return'),
176
+ skip: z.number().int().min(0).optional().default(0).describe('Tasks to skip for pagination'),
191
177
  includeStats: z
192
178
  .boolean()
193
179
  .optional()
194
180
  .default(false)
195
181
  .describe('Include aggregate statistics by status/type/agent'),
196
- includeDependencyGraph: z
197
- .boolean()
198
- .optional()
199
- .default(false)
200
- .describe('Include dependency graph for visualization'),
182
+ includeDependencyGraph: z.boolean().optional().default(false).describe('Include dependency graph'),
201
183
  },
202
184
  }, async ({ projectId, swarmId, taskId, statuses, types, claimedBy, createdBy, minPriority, orderBy = 'priority', limit = 20, skip = 0, includeStats = false, includeDependencyGraph = false, }) => {
203
185
  const neo4jService = new Neo4jService();
@@ -162,39 +162,24 @@ export const createSwarmMessageTool = (server) => {
162
162
  agentId: z.string().describe('Your unique agent identifier'),
163
163
  action: z
164
164
  .enum(['send', 'read', 'acknowledge'])
165
- .describe('Action: send (post message), read (get messages), acknowledge (mark as read)'),
165
+ .describe('send: post message; read: get messages; acknowledge: mark as read'),
166
166
  // Send parameters
167
- toAgentId: z.string().optional().describe('Target agent ID. Omit for broadcast to all swarm agents.'),
168
- category: z
169
- .enum(MESSAGE_CATEGORY_KEYS)
170
- .optional()
171
- .describe('Message category: blocked (need help), conflict (resource clash), finding (important discovery), ' +
172
- 'request (direct ask), alert (urgent notification), handoff (context transfer)'),
173
- content: z.string().optional().describe('Message content (required for send action)'),
174
- taskId: z.string().optional().describe('Related task ID for context'),
167
+ toAgentId: z.string().optional().describe('Target agent ID (omit for broadcast)'),
168
+ category: z.enum(MESSAGE_CATEGORY_KEYS).optional().describe('Message category'),
169
+ content: z.string().optional().describe('Message content (required for send)'),
170
+ taskId: z.string().optional().describe('Related task ID'),
175
171
  filePaths: z.array(z.string()).optional().describe('File paths relevant to this message'),
176
- ttlMs: z
177
- .number()
178
- .int()
179
- .optional()
180
- .describe(`Time-to-live in ms (default: ${MESSAGE_DEFAULT_TTL_MS / 3600000}h). Set 0 for swarm lifetime.`),
172
+ ttlMs: z.number().int().optional().describe(`Time-to-live in ms (0 for swarm lifetime)`),
181
173
  // Read parameters
182
- unreadOnly: z.boolean().optional().default(true).describe('Only return unread messages (default: true)'),
174
+ unreadOnly: z.boolean().optional().default(true).describe('Only return unread messages'),
183
175
  categories: z.array(z.enum(MESSAGE_CATEGORY_KEYS)).optional().describe('Filter by message categories'),
184
- fromAgentId: z.string().optional().describe('Filter messages from a specific agent'),
185
- limit: z
186
- .number()
187
- .int()
188
- .min(1)
189
- .max(100)
190
- .optional()
191
- .default(20)
192
- .describe('Maximum messages to return (default: 20)'),
176
+ fromAgentId: z.string().optional().describe('Filter by sending agent'),
177
+ limit: z.number().int().min(1).max(100).optional().default(20).describe('Maximum messages to return'),
193
178
  // Acknowledge parameters
194
179
  messageIds: z
195
180
  .array(z.string())
196
181
  .optional()
197
- .describe('Specific message IDs to acknowledge. Omit to acknowledge all unread.'),
182
+ .describe('Message IDs to acknowledge (omit to acknowledge all unread)'),
198
183
  // Maintenance
199
184
  cleanup: z.boolean().optional().default(false).describe('Also clean up expired messages'),
200
185
  },