code-graph-context 2.4.5 → 2.5.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.
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Swarm Get Tasks Tool
3
+ * Query tasks from the blackboard with various filters
4
+ */
5
+ import { z } from 'zod';
6
+ import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
7
+ import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
8
+ import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
9
+ import { TASK_STATUSES, TASK_TYPES, TASK_PRIORITIES } from './swarm-constants.js';
10
+ /**
11
+ * Main query to get tasks with filters
12
+ */
13
+ const GET_TASKS_QUERY = `
14
+ MATCH (t:SwarmTask {projectId: $projectId})
15
+ WHERE ($swarmId IS NULL OR t.swarmId = $swarmId)
16
+ AND ($statuses IS NULL OR size($statuses) = 0 OR t.status IN $statuses)
17
+ AND ($types IS NULL OR size($types) = 0 OR t.type IN $types)
18
+ AND ($claimedBy IS NULL OR t.claimedBy = $claimedBy)
19
+ AND ($createdBy IS NULL OR t.createdBy = $createdBy)
20
+ AND ($minPriority IS NULL OR t.priorityScore >= $minPriority)
21
+
22
+ // Get dependency info
23
+ OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:SwarmTask)
24
+ WITH t, collect({id: dep.id, title: dep.title, status: dep.status}) as dependencies
25
+
26
+ // Get tasks blocked by this one
27
+ OPTIONAL MATCH (blocked:SwarmTask)-[:DEPENDS_ON]->(t)
28
+ WITH t, dependencies, collect({id: blocked.id, title: blocked.title, status: blocked.status}) as blockedTasks
29
+
30
+ // Get target code nodes
31
+ OPTIONAL MATCH (t)-[:TARGETS]->(target)
32
+ WITH t, dependencies, blockedTasks,
33
+ collect(DISTINCT {id: target.id, type: labels(target)[0], name: target.name, filePath: target.filePath}) as targets
34
+
35
+ RETURN t.id as id,
36
+ t.projectId as projectId,
37
+ t.swarmId as swarmId,
38
+ t.title as title,
39
+ t.description as description,
40
+ t.type as type,
41
+ t.priority as priority,
42
+ t.priorityScore as priorityScore,
43
+ t.status as status,
44
+ t.targetNodeIds as targetNodeIds,
45
+ t.targetFilePaths as targetFilePaths,
46
+ t.claimedBy as claimedBy,
47
+ t.claimedAt as claimedAt,
48
+ t.startedAt as startedAt,
49
+ t.completedAt as completedAt,
50
+ t.createdBy as createdBy,
51
+ t.createdAt as createdAt,
52
+ t.summary as summary,
53
+ t.metadata as metadata,
54
+ dependencies,
55
+ blockedTasks,
56
+ [target IN targets WHERE target.id IS NOT NULL] as targets
57
+
58
+ ORDER BY
59
+ CASE WHEN $orderBy = 'priority' THEN t.priorityScore END DESC,
60
+ CASE WHEN $orderBy = 'created' THEN t.createdAt END DESC,
61
+ CASE WHEN $orderBy = 'updated' THEN t.updatedAt END DESC,
62
+ t.priorityScore DESC,
63
+ t.createdAt ASC
64
+
65
+ SKIP toInteger($skip)
66
+ LIMIT toInteger($limit)
67
+ `;
68
+ /**
69
+ * Query to get task statistics for a swarm
70
+ */
71
+ const GET_TASK_STATS_QUERY = `
72
+ MATCH (t:SwarmTask {projectId: $projectId})
73
+ WHERE ($swarmId IS NULL OR t.swarmId = $swarmId)
74
+
75
+ WITH t.status as status, t.type as type, t.priority as priority,
76
+ t.claimedBy as agent, count(t) as count
77
+
78
+ RETURN status, type, priority, agent, count
79
+ ORDER BY count DESC
80
+ `;
81
+ /**
82
+ * Query to get a single task by ID with full details
83
+ */
84
+ const GET_TASK_BY_ID_QUERY = `
85
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
86
+
87
+ // Get dependencies
88
+ OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:SwarmTask)
89
+ WITH t, collect({
90
+ id: dep.id,
91
+ title: dep.title,
92
+ status: dep.status,
93
+ claimedBy: dep.claimedBy
94
+ }) as dependencies
95
+
96
+ // Get tasks blocked by this one
97
+ OPTIONAL MATCH (blocked:SwarmTask)-[:DEPENDS_ON]->(t)
98
+ WITH t, dependencies, collect({
99
+ id: blocked.id,
100
+ title: blocked.title,
101
+ status: blocked.status
102
+ }) as blockedTasks
103
+
104
+ // Get target code nodes with more detail
105
+ OPTIONAL MATCH (t)-[:TARGETS]->(target)
106
+ WITH t, dependencies, blockedTasks,
107
+ collect(DISTINCT {
108
+ id: target.id,
109
+ type: labels(target)[0],
110
+ name: target.name,
111
+ filePath: target.filePath,
112
+ coreType: target.coreType,
113
+ semanticType: target.semanticType
114
+ }) as targets
115
+
116
+ RETURN t {
117
+ .*,
118
+ dependencies: dependencies,
119
+ blockedTasks: blockedTasks,
120
+ targets: [target IN targets WHERE target.id IS NOT NULL]
121
+ } as task
122
+ `;
123
+ /**
124
+ * Query to get active workers from pheromones
125
+ */
126
+ const GET_ACTIVE_WORKERS_QUERY = `
127
+ MATCH (p:Pheromone {projectId: $projectId})
128
+ WHERE ($swarmId IS NULL OR p.swarmId = $swarmId)
129
+ AND p.type IN ['modifying', 'claiming']
130
+ WITH p.agentId as agentId, p.type as type,
131
+ max(p.updatedAt) as lastActivity,
132
+ count(p) as nodeCount
133
+ RETURN agentId, type,
134
+ lastActivity,
135
+ nodeCount,
136
+ duration.between(datetime({epochMillis: lastActivity}), datetime()).minutes as minutesSinceActivity
137
+ ORDER BY lastActivity DESC
138
+ `;
139
+ /**
140
+ * Query to get the dependency graph for visualization
141
+ */
142
+ const GET_DEPENDENCY_GRAPH_QUERY = `
143
+ MATCH (t:SwarmTask {projectId: $projectId})
144
+ WHERE ($swarmId IS NULL OR t.swarmId = $swarmId)
145
+
146
+ OPTIONAL MATCH (t)-[r:DEPENDS_ON]->(dep:SwarmTask)
147
+
148
+ RETURN collect(DISTINCT {
149
+ id: t.id,
150
+ title: t.title,
151
+ status: t.status,
152
+ priority: t.priority,
153
+ type: t.type,
154
+ claimedBy: t.claimedBy
155
+ }) as nodes,
156
+ collect(DISTINCT CASE WHEN dep IS NOT NULL THEN {from: t.id, to: dep.id} END) as edges
157
+ `;
158
+ export const createSwarmGetTasksTool = (server) => {
159
+ server.registerTool(TOOL_NAMES.swarmGetTasks, {
160
+ title: TOOL_METADATA[TOOL_NAMES.swarmGetTasks].title,
161
+ description: TOOL_METADATA[TOOL_NAMES.swarmGetTasks].description,
162
+ inputSchema: {
163
+ projectId: z.string().describe('Project ID, name, or path'),
164
+ swarmId: z.string().optional().describe('Filter by swarm ID'),
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
171
+ .array(z.enum(TASK_TYPES))
172
+ .optional()
173
+ .describe('Filter by task types (e.g., ["implement", "fix"])'),
174
+ claimedBy: z.string().optional().describe('Filter tasks claimed by a specific agent'),
175
+ createdBy: z.string().optional().describe('Filter tasks created by a specific agent'),
176
+ minPriority: z
177
+ .enum(Object.keys(TASK_PRIORITIES))
178
+ .optional()
179
+ .describe('Minimum priority level'),
180
+ orderBy: z
181
+ .enum(['priority', 'created', 'updated'])
182
+ .optional()
183
+ .default('priority')
184
+ .describe('Sort order: priority (highest first), created (newest first), updated'),
185
+ limit: z
186
+ .number()
187
+ .int()
188
+ .min(1)
189
+ .max(100)
190
+ .optional()
191
+ .default(20)
192
+ .describe('Maximum tasks to return (default: 20)'),
193
+ skip: z.number().int().min(0).optional().default(0).describe('Number of tasks to skip for pagination'),
194
+ includeStats: z
195
+ .boolean()
196
+ .optional()
197
+ .default(false)
198
+ .describe('Include aggregate statistics by status/type/agent'),
199
+ includeDependencyGraph: z
200
+ .boolean()
201
+ .optional()
202
+ .default(false)
203
+ .describe('Include dependency graph for visualization'),
204
+ },
205
+ }, async ({ projectId, swarmId, taskId, statuses, types, claimedBy, createdBy, minPriority, orderBy = 'priority', limit = 20, skip = 0, includeStats = false, includeDependencyGraph = false, }) => {
206
+ const neo4jService = new Neo4jService();
207
+ // Resolve project ID
208
+ const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
209
+ if (!projectResult.success) {
210
+ await neo4jService.close();
211
+ return projectResult.error;
212
+ }
213
+ const resolvedProjectId = projectResult.projectId;
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
+ // If taskId is provided, get single task with full details
225
+ if (taskId) {
226
+ const result = await neo4jService.run(GET_TASK_BY_ID_QUERY, {
227
+ taskId,
228
+ projectId: resolvedProjectId,
229
+ });
230
+ if (result.length === 0) {
231
+ return createErrorResponse(`Task ${taskId} not found`);
232
+ }
233
+ const task = result[0].task;
234
+ // Parse metadata if present
235
+ if (task.metadata) {
236
+ try {
237
+ task.metadata = JSON.parse(task.metadata);
238
+ }
239
+ catch {
240
+ // Keep as string if not valid JSON
241
+ }
242
+ }
243
+ // Parse artifacts if present
244
+ if (task.artifacts) {
245
+ try {
246
+ task.artifacts = JSON.parse(task.artifacts);
247
+ }
248
+ catch {
249
+ // Keep as string if not valid JSON
250
+ }
251
+ }
252
+ // Convert Neo4j integers
253
+ const convertTimestamp = (ts) => typeof ts === 'object' && ts?.toNumber ? ts.toNumber() : ts;
254
+ task.createdAt = convertTimestamp(task.createdAt);
255
+ task.updatedAt = convertTimestamp(task.updatedAt);
256
+ task.claimedAt = convertTimestamp(task.claimedAt);
257
+ task.startedAt = convertTimestamp(task.startedAt);
258
+ task.completedAt = convertTimestamp(task.completedAt);
259
+ return createSuccessResponse(JSON.stringify({ success: true, task }));
260
+ }
261
+ // Get list of tasks
262
+ const minPriorityScore = minPriority
263
+ ? TASK_PRIORITIES[minPriority]
264
+ : null;
265
+ const tasksResult = await neo4jService.run(GET_TASKS_QUERY, {
266
+ projectId: resolvedProjectId,
267
+ swarmId: swarmId || null,
268
+ statuses: statuses || null,
269
+ types: types || null,
270
+ claimedBy: claimedBy || null,
271
+ createdBy: createdBy || null,
272
+ minPriority: minPriorityScore,
273
+ orderBy,
274
+ limit: Math.floor(limit),
275
+ skip: Math.floor(skip),
276
+ });
277
+ const convertTimestamp = (ts) => typeof ts === 'object' && ts?.toNumber ? ts.toNumber() : ts;
278
+ const tasks = tasksResult.map((t) => {
279
+ // Parse metadata if present
280
+ let metadata = t.metadata;
281
+ if (metadata) {
282
+ try {
283
+ metadata = JSON.parse(metadata);
284
+ }
285
+ catch {
286
+ // Keep as string
287
+ }
288
+ }
289
+ return {
290
+ id: t.id,
291
+ projectId: t.projectId,
292
+ swarmId: t.swarmId,
293
+ title: t.title,
294
+ description: t.description,
295
+ type: t.type,
296
+ priority: t.priority,
297
+ priorityScore: t.priorityScore,
298
+ status: t.status,
299
+ targetNodeIds: t.targetNodeIds,
300
+ targetFilePaths: t.targetFilePaths,
301
+ claimedBy: t.claimedBy,
302
+ claimedAt: convertTimestamp(t.claimedAt),
303
+ startedAt: convertTimestamp(t.startedAt),
304
+ completedAt: convertTimestamp(t.completedAt),
305
+ createdBy: t.createdBy,
306
+ createdAt: convertTimestamp(t.createdAt),
307
+ summary: t.summary,
308
+ metadata,
309
+ dependencies: t.dependencies?.filter((d) => d.id !== null) || [],
310
+ blockedTasks: t.blockedTasks?.filter((d) => d.id !== null) || [],
311
+ targets: t.targets || [],
312
+ };
313
+ });
314
+ const response = {
315
+ success: true,
316
+ tasks,
317
+ pagination: {
318
+ skip,
319
+ limit,
320
+ returned: tasks.length,
321
+ hasMore: tasks.length === limit,
322
+ },
323
+ filters: {
324
+ swarmId: swarmId || 'all',
325
+ statuses: statuses || 'all',
326
+ types: types || 'all',
327
+ claimedBy: claimedBy || 'any',
328
+ minPriority: minPriority || 'any',
329
+ },
330
+ };
331
+ // Include statistics if requested
332
+ if (includeStats) {
333
+ const statsResult = await neo4jService.run(GET_TASK_STATS_QUERY, {
334
+ projectId: resolvedProjectId,
335
+ swarmId: swarmId || null,
336
+ });
337
+ const stats = {
338
+ byStatus: {},
339
+ byType: {},
340
+ byPriority: {},
341
+ byAgent: {},
342
+ total: 0,
343
+ };
344
+ for (const row of statsResult) {
345
+ const count = typeof row.count === 'object' ? row.count.toNumber() : row.count;
346
+ if (row.status) {
347
+ stats.byStatus[row.status] = (stats.byStatus[row.status] || 0) + count;
348
+ }
349
+ if (row.type) {
350
+ stats.byType[row.type] = (stats.byType[row.type] || 0) + count;
351
+ }
352
+ if (row.priority) {
353
+ stats.byPriority[row.priority] = (stats.byPriority[row.priority] || 0) + count;
354
+ }
355
+ if (row.agent) {
356
+ stats.byAgent[row.agent] = (stats.byAgent[row.agent] || 0) + count;
357
+ }
358
+ }
359
+ stats.total = Object.values(stats.byStatus).reduce((a, b) => a + b, 0);
360
+ // Calculate progress metrics
361
+ const completed = stats.byStatus['completed'] ?? 0;
362
+ const failed = stats.byStatus['failed'] ?? 0;
363
+ const inProgress = stats.byStatus['in_progress'] ?? 0;
364
+ const available = stats.byStatus['available'] ?? 0;
365
+ const blocked = stats.byStatus['blocked'] ?? 0;
366
+ const done = completed + failed;
367
+ response.stats = stats;
368
+ response.progress = {
369
+ completed,
370
+ failed,
371
+ inProgress,
372
+ available,
373
+ blocked,
374
+ total: stats.total,
375
+ percentComplete: stats.total > 0 ? Math.round((done / stats.total) * 100) : 0,
376
+ isComplete: done === stats.total && stats.total > 0,
377
+ summary: stats.total === 0
378
+ ? 'No tasks'
379
+ : `${completed}/${stats.total} completed (${Math.round((done / stats.total) * 100)}%)`,
380
+ };
381
+ // Get active workers from pheromones
382
+ const workersResult = await neo4jService.run(GET_ACTIVE_WORKERS_QUERY, {
383
+ projectId: resolvedProjectId,
384
+ swarmId: swarmId || null,
385
+ });
386
+ response.activeWorkers = workersResult.map((w) => ({
387
+ agentId: w.agentId,
388
+ status: w.type === 'modifying' ? 'working' : 'claiming',
389
+ lastActivity: typeof w.lastActivity === 'object' ? w.lastActivity.toNumber() : w.lastActivity,
390
+ nodesBeingWorked: typeof w.nodeCount === 'object' ? w.nodeCount.toNumber() : w.nodeCount,
391
+ minutesSinceActivity: typeof w.minutesSinceActivity === 'object'
392
+ ? w.minutesSinceActivity.toNumber()
393
+ : w.minutesSinceActivity,
394
+ }));
395
+ }
396
+ // Include dependency graph if requested
397
+ if (includeDependencyGraph) {
398
+ const graphResult = await neo4jService.run(GET_DEPENDENCY_GRAPH_QUERY, {
399
+ projectId: resolvedProjectId,
400
+ swarmId: swarmId || null,
401
+ });
402
+ if (graphResult.length > 0) {
403
+ response.dependencyGraph = {
404
+ nodes: graphResult[0].nodes || [],
405
+ edges: (graphResult[0].edges || []).filter((e) => e !== null),
406
+ };
407
+ }
408
+ }
409
+ return createSuccessResponse(JSON.stringify(response, null, 2));
410
+ }
411
+ catch (error) {
412
+ await debugLog('Swarm get tasks error', { error: String(error) });
413
+ return createErrorResponse(error instanceof Error ? error : String(error));
414
+ }
415
+ finally {
416
+ await neo4jService.close();
417
+ }
418
+ });
419
+ };