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.
- package/dist/core/embeddings/natural-language-to-cypher.service.js +2 -4
- package/dist/mcp/constants.js +56 -228
- package/dist/mcp/handlers/swarm/abandon.handler.js +61 -0
- package/dist/mcp/handlers/swarm/advance.handler.js +78 -0
- package/dist/mcp/handlers/swarm/claim.handler.js +61 -0
- package/dist/mcp/handlers/swarm/index.js +5 -0
- package/dist/mcp/handlers/swarm/queries.js +140 -0
- package/dist/mcp/handlers/swarm/release.handler.js +41 -0
- package/dist/mcp/handlers/swarm-worker.handler.js +2 -13
- package/dist/mcp/tools/detect-dead-code.tool.js +33 -65
- package/dist/mcp/tools/detect-duplicate-code.tool.js +44 -53
- package/dist/mcp/tools/impact-analysis.tool.js +1 -1
- package/dist/mcp/tools/index.js +9 -9
- package/dist/mcp/tools/list-projects.tool.js +2 -2
- package/dist/mcp/tools/list-watchers.tool.js +2 -5
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +2 -2
- package/dist/mcp/tools/parse-typescript-project.tool.js +7 -17
- package/dist/mcp/tools/search-codebase.tool.js +11 -26
- package/dist/mcp/tools/session-bookmark.tool.js +7 -11
- package/dist/mcp/tools/session-cleanup.tool.js +2 -6
- package/dist/mcp/tools/session-note.tool.js +6 -21
- package/dist/mcp/tools/session-recall.tool.js +293 -0
- package/dist/mcp/tools/session-save.tool.js +280 -0
- package/dist/mcp/tools/start-watch-project.tool.js +1 -1
- package/dist/mcp/tools/swarm-advance-task.tool.js +56 -0
- package/dist/mcp/tools/swarm-claim-task.tool.js +24 -388
- package/dist/mcp/tools/swarm-cleanup.tool.js +3 -7
- package/dist/mcp/tools/swarm-complete-task.tool.js +14 -17
- package/dist/mcp/tools/swarm-get-tasks.tool.js +8 -26
- package/dist/mcp/tools/swarm-message.tool.js +10 -25
- package/dist/mcp/tools/swarm-pheromone.tool.js +7 -25
- package/dist/mcp/tools/swarm-post-task.tool.js +7 -19
- package/dist/mcp/tools/swarm-release-task.tool.js +53 -0
- package/dist/mcp/tools/swarm-sense.tool.js +10 -30
- package/dist/mcp/tools/traverse-from-node.tool.js +19 -41
- package/dist/mcp/utils.js +41 -1
- 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
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
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
|
|
258
|
-
|
|
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,
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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
|
-
(
|
|
399
|
-
? `Current state: ${
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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 =
|
|
430
|
-
const actionLabel =
|
|
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
|
-
...(
|
|
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
|
|
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 (
|
|
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
|
|
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('
|
|
253
|
-
artifacts: z
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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('
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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('
|
|
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
|
|
168
|
-
category: z
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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
|
|
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('
|
|
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
|
},
|