code-graph-context 2.4.5 → 2.5.1

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,421 @@
1
+ /**
2
+ * Swarm Complete Task Tool
3
+ * Mark a task as completed, failed, or needs_review with artifacts
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
+ /**
10
+ * Query to complete a task with artifacts
11
+ */
12
+ const COMPLETE_TASK_QUERY = `
13
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
14
+ WHERE t.status = 'in_progress' AND t.claimedBy = $agentId
15
+
16
+ SET t.status = 'completed',
17
+ t.completedAt = timestamp(),
18
+ t.updatedAt = timestamp(),
19
+ t.summary = $summary,
20
+ t.artifacts = $artifacts,
21
+ t.filesChanged = $filesChanged,
22
+ t.linesAdded = $linesAdded,
23
+ t.linesRemoved = $linesRemoved
24
+
25
+ // Check for tasks that were blocked by this one
26
+ WITH t
27
+ OPTIONAL MATCH (waiting:SwarmTask)-[:DEPENDS_ON]->(t)
28
+ WHERE waiting.status = 'blocked'
29
+
30
+ // Check if waiting tasks now have all dependencies completed
31
+ WITH t, collect(waiting) as waitingTasks
32
+ UNWIND (CASE WHEN size(waitingTasks) = 0 THEN [null] ELSE waitingTasks END) as waiting
33
+ OPTIONAL MATCH (waiting)-[:DEPENDS_ON]->(otherDep:SwarmTask)
34
+ WHERE otherDep.status <> 'completed' AND otherDep.id <> t.id
35
+ WITH t, waiting, count(otherDep) as remainingDeps
36
+ WHERE waiting IS NOT NULL AND remainingDeps = 0
37
+
38
+ // Unblock tasks that now have all dependencies met
39
+ SET waiting.status = 'available',
40
+ waiting.updatedAt = timestamp()
41
+
42
+ WITH t, collect(waiting.id) as unblockedTaskIds
43
+
44
+ RETURN t.id as id,
45
+ t.title as title,
46
+ t.status as status,
47
+ t.completedAt as completedAt,
48
+ t.summary as summary,
49
+ t.claimedBy as claimedBy,
50
+ unblockedTaskIds
51
+ `;
52
+ /**
53
+ * Query to mark task as failed
54
+ */
55
+ const FAIL_TASK_QUERY = `
56
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
57
+ WHERE t.status IN ['in_progress', 'claimed'] AND t.claimedBy = $agentId
58
+
59
+ SET t.status = 'failed',
60
+ t.failedAt = timestamp(),
61
+ t.updatedAt = timestamp(),
62
+ t.failureReason = $reason,
63
+ t.errorDetails = $errorDetails,
64
+ t.retryable = $retryable
65
+
66
+ RETURN t.id as id,
67
+ t.title as title,
68
+ t.status as status,
69
+ t.failedAt as failedAt,
70
+ t.failureReason as failureReason,
71
+ t.retryable as retryable
72
+ `;
73
+ /**
74
+ * Query to mark task as needs_review
75
+ */
76
+ const REVIEW_TASK_QUERY = `
77
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
78
+ WHERE t.status = 'in_progress' AND t.claimedBy = $agentId
79
+
80
+ SET t.status = 'needs_review',
81
+ t.reviewRequestedAt = timestamp(),
82
+ t.updatedAt = timestamp(),
83
+ t.summary = $summary,
84
+ t.artifacts = $artifacts,
85
+ t.filesChanged = $filesChanged,
86
+ t.reviewNotes = $reviewNotes
87
+
88
+ RETURN t.id as id,
89
+ t.title as title,
90
+ t.status as status,
91
+ t.reviewRequestedAt as reviewRequestedAt,
92
+ t.summary as summary,
93
+ t.claimedBy as claimedBy
94
+ `;
95
+ /**
96
+ * Query to approve a reviewed task (transition to completed)
97
+ */
98
+ const APPROVE_TASK_QUERY = `
99
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
100
+ WHERE t.status = 'needs_review'
101
+
102
+ SET t.status = 'completed',
103
+ t.completedAt = timestamp(),
104
+ t.updatedAt = timestamp(),
105
+ t.approvedBy = $reviewerId,
106
+ t.approvalNotes = $notes
107
+
108
+ // Check for tasks that were blocked by this one
109
+ WITH t
110
+ OPTIONAL MATCH (waiting:SwarmTask)-[:DEPENDS_ON]->(t)
111
+ WHERE waiting.status = 'blocked'
112
+
113
+ // Check if waiting tasks now have all dependencies completed
114
+ WITH t, collect(waiting) as waitingTasks
115
+ UNWIND (CASE WHEN size(waitingTasks) = 0 THEN [null] ELSE waitingTasks END) as waiting
116
+ OPTIONAL MATCH (waiting)-[:DEPENDS_ON]->(otherDep:SwarmTask)
117
+ WHERE otherDep.status <> 'completed' AND otherDep.id <> t.id
118
+ WITH t, waiting, count(otherDep) as remainingDeps
119
+ WHERE waiting IS NOT NULL AND remainingDeps = 0
120
+
121
+ SET waiting.status = 'available',
122
+ waiting.updatedAt = timestamp()
123
+
124
+ WITH t, collect(waiting.id) as unblockedTaskIds
125
+
126
+ RETURN t.id as id,
127
+ t.title as title,
128
+ t.status as status,
129
+ t.completedAt as completedAt,
130
+ t.approvedBy as approvedBy,
131
+ unblockedTaskIds
132
+ `;
133
+ /**
134
+ * Query to reject a reviewed task (back to in_progress or failed)
135
+ */
136
+ const REJECT_TASK_QUERY = `
137
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
138
+ WHERE t.status = 'needs_review'
139
+
140
+ SET t.status = CASE WHEN $markAsFailed THEN 'failed' ELSE 'in_progress' END,
141
+ t.updatedAt = timestamp(),
142
+ t.rejectedBy = $reviewerId,
143
+ t.rejectionNotes = $notes,
144
+ t.rejectedAt = timestamp()
145
+
146
+ RETURN t.id as id,
147
+ t.title as title,
148
+ t.status as status,
149
+ t.claimedBy as claimedBy,
150
+ t.rejectionNotes as rejectionNotes
151
+ `;
152
+ /**
153
+ * Query to retry a failed task
154
+ */
155
+ const RETRY_TASK_QUERY = `
156
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
157
+ WHERE t.status = 'failed' AND t.retryable = true
158
+
159
+ SET t.status = 'available',
160
+ t.claimedBy = null,
161
+ t.claimedAt = null,
162
+ t.startedAt = null,
163
+ t.failedAt = null,
164
+ t.failureReason = null,
165
+ t.errorDetails = null,
166
+ t.updatedAt = timestamp(),
167
+ t.retryCount = COALESCE(t.retryCount, 0) + 1
168
+
169
+ RETURN t.id as id,
170
+ t.title as title,
171
+ t.status as status,
172
+ t.retryCount as retryCount
173
+ `;
174
+ export const createSwarmCompleteTaskTool = (server) => {
175
+ server.registerTool(TOOL_NAMES.swarmCompleteTask, {
176
+ title: TOOL_METADATA[TOOL_NAMES.swarmCompleteTask].title,
177
+ description: TOOL_METADATA[TOOL_NAMES.swarmCompleteTask].description,
178
+ inputSchema: {
179
+ projectId: z.string().describe('Project ID, name, or path'),
180
+ taskId: z.string().describe('Task ID to complete'),
181
+ agentId: z.string().describe('Your agent ID (must match the agent who claimed the task)'),
182
+ action: z
183
+ .enum(['complete', 'fail', 'request_review', 'approve', 'reject', 'retry'])
184
+ .describe('Action to take on the task'),
185
+ summary: z
186
+ .string()
187
+ .optional()
188
+ .describe('Summary of what was done (required for complete/request_review)'),
189
+ artifacts: z
190
+ .record(z.unknown())
191
+ .optional()
192
+ .describe('Artifacts produced: { files: [], commits: [], pullRequests: [], notes: string }'),
193
+ filesChanged: z
194
+ .array(z.string())
195
+ .optional()
196
+ .describe('List of files that were modified'),
197
+ linesAdded: z.number().int().optional().describe('Number of lines added'),
198
+ linesRemoved: z.number().int().optional().describe('Number of lines removed'),
199
+ reason: z
200
+ .string()
201
+ .optional()
202
+ .describe('Reason for failure (required if action=fail)'),
203
+ errorDetails: z
204
+ .string()
205
+ .optional()
206
+ .describe('Technical error details for debugging'),
207
+ retryable: z
208
+ .boolean()
209
+ .optional()
210
+ .default(true)
211
+ .describe('Whether the task can be retried after failure'),
212
+ reviewNotes: z
213
+ .string()
214
+ .optional()
215
+ .describe('Notes for the reviewer (for request_review)'),
216
+ reviewerId: z
217
+ .string()
218
+ .optional()
219
+ .describe('ID of the reviewer (required for approve/reject)'),
220
+ notes: z
221
+ .string()
222
+ .optional()
223
+ .describe('Approval/rejection notes'),
224
+ markAsFailed: z
225
+ .boolean()
226
+ .optional()
227
+ .default(false)
228
+ .describe('If rejecting, mark as failed instead of returning to in_progress'),
229
+ },
230
+ }, async ({ projectId, taskId, agentId, action, summary, artifacts, filesChanged, linesAdded, linesRemoved, reason, errorDetails, retryable = true, reviewNotes, reviewerId, notes, markAsFailed = false, }) => {
231
+ const neo4jService = new Neo4jService();
232
+ // Resolve project ID
233
+ const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
234
+ if (!projectResult.success) {
235
+ await neo4jService.close();
236
+ return projectResult.error;
237
+ }
238
+ const resolvedProjectId = projectResult.projectId;
239
+ try {
240
+ await debugLog('Swarm complete task', {
241
+ action,
242
+ taskId,
243
+ projectId: resolvedProjectId,
244
+ agentId,
245
+ });
246
+ let result;
247
+ let responseData = { success: true, action };
248
+ switch (action) {
249
+ case 'complete':
250
+ if (!summary) {
251
+ return createErrorResponse('summary is required for complete action');
252
+ }
253
+ result = await neo4jService.run(COMPLETE_TASK_QUERY, {
254
+ taskId,
255
+ projectId: resolvedProjectId,
256
+ agentId,
257
+ summary,
258
+ artifacts: artifacts ? JSON.stringify(artifacts) : null,
259
+ filesChanged: filesChanged || [],
260
+ linesAdded: linesAdded || 0,
261
+ linesRemoved: linesRemoved || 0,
262
+ });
263
+ if (result.length === 0) {
264
+ return createErrorResponse(`Cannot complete task ${taskId}. It may not exist, not be in_progress, or you don't own it.`);
265
+ }
266
+ responseData.task = {
267
+ id: result[0].id,
268
+ title: result[0].title,
269
+ status: result[0].status,
270
+ completedAt: typeof result[0].completedAt === 'object'
271
+ ? result[0].completedAt.toNumber()
272
+ : result[0].completedAt,
273
+ summary: result[0].summary,
274
+ claimedBy: result[0].claimedBy,
275
+ };
276
+ responseData.unblockedTasks = result[0].unblockedTaskIds || [];
277
+ responseData.message = responseData.unblockedTasks.length > 0
278
+ ? `Task completed. ${responseData.unblockedTasks.length} dependent task(s) are now available.`
279
+ : 'Task completed successfully.';
280
+ break;
281
+ case 'fail':
282
+ if (!reason) {
283
+ return createErrorResponse('reason is required for fail action');
284
+ }
285
+ result = await neo4jService.run(FAIL_TASK_QUERY, {
286
+ taskId,
287
+ projectId: resolvedProjectId,
288
+ agentId,
289
+ reason,
290
+ errorDetails: errorDetails || null,
291
+ retryable,
292
+ });
293
+ if (result.length === 0) {
294
+ return createErrorResponse(`Cannot fail task ${taskId}. It may not exist, not be in_progress/claimed, or you don't own it.`);
295
+ }
296
+ responseData.task = {
297
+ id: result[0].id,
298
+ title: result[0].title,
299
+ status: result[0].status,
300
+ failedAt: typeof result[0].failedAt === 'object'
301
+ ? result[0].failedAt.toNumber()
302
+ : result[0].failedAt,
303
+ failureReason: result[0].failureReason,
304
+ retryable: result[0].retryable,
305
+ };
306
+ responseData.message = retryable
307
+ ? 'Task marked as failed. Use action="retry" to make it available again.'
308
+ : 'Task marked as failed (not retryable).';
309
+ break;
310
+ case 'request_review':
311
+ if (!summary) {
312
+ return createErrorResponse('summary is required for request_review action');
313
+ }
314
+ result = await neo4jService.run(REVIEW_TASK_QUERY, {
315
+ taskId,
316
+ projectId: resolvedProjectId,
317
+ agentId,
318
+ summary,
319
+ artifacts: artifacts ? JSON.stringify(artifacts) : null,
320
+ filesChanged: filesChanged || [],
321
+ reviewNotes: reviewNotes || null,
322
+ });
323
+ if (result.length === 0) {
324
+ return createErrorResponse(`Cannot request review for task ${taskId}. It may not exist, not be in_progress, or you don't own it.`);
325
+ }
326
+ responseData.task = {
327
+ id: result[0].id,
328
+ title: result[0].title,
329
+ status: result[0].status,
330
+ reviewRequestedAt: typeof result[0].reviewRequestedAt === 'object'
331
+ ? result[0].reviewRequestedAt.toNumber()
332
+ : result[0].reviewRequestedAt,
333
+ summary: result[0].summary,
334
+ claimedBy: result[0].claimedBy,
335
+ };
336
+ responseData.message = 'Task submitted for review.';
337
+ break;
338
+ case 'approve':
339
+ if (!reviewerId) {
340
+ return createErrorResponse('reviewerId is required for approve action');
341
+ }
342
+ result = await neo4jService.run(APPROVE_TASK_QUERY, {
343
+ taskId,
344
+ projectId: resolvedProjectId,
345
+ reviewerId,
346
+ notes: notes || null,
347
+ });
348
+ if (result.length === 0) {
349
+ return createErrorResponse(`Cannot approve task ${taskId}. It may not exist or not be in needs_review status.`);
350
+ }
351
+ responseData.task = {
352
+ id: result[0].id,
353
+ title: result[0].title,
354
+ status: result[0].status,
355
+ completedAt: typeof result[0].completedAt === 'object'
356
+ ? result[0].completedAt.toNumber()
357
+ : result[0].completedAt,
358
+ approvedBy: result[0].approvedBy,
359
+ };
360
+ responseData.unblockedTasks = result[0].unblockedTaskIds || [];
361
+ responseData.message = responseData.unblockedTasks.length > 0
362
+ ? `Task approved. ${responseData.unblockedTasks.length} dependent task(s) are now available.`
363
+ : 'Task approved and completed.';
364
+ break;
365
+ case 'reject':
366
+ if (!reviewerId) {
367
+ return createErrorResponse('reviewerId is required for reject action');
368
+ }
369
+ result = await neo4jService.run(REJECT_TASK_QUERY, {
370
+ taskId,
371
+ projectId: resolvedProjectId,
372
+ reviewerId,
373
+ notes: notes || 'No notes provided',
374
+ markAsFailed,
375
+ });
376
+ if (result.length === 0) {
377
+ return createErrorResponse(`Cannot reject task ${taskId}. It may not exist or not be in needs_review status.`);
378
+ }
379
+ responseData.task = {
380
+ id: result[0].id,
381
+ title: result[0].title,
382
+ status: result[0].status,
383
+ claimedBy: result[0].claimedBy,
384
+ rejectionNotes: result[0].rejectionNotes,
385
+ };
386
+ responseData.message = markAsFailed
387
+ ? 'Task rejected and marked as failed.'
388
+ : 'Task rejected and returned to in_progress for the original agent to fix.';
389
+ break;
390
+ case 'retry':
391
+ result = await neo4jService.run(RETRY_TASK_QUERY, {
392
+ taskId,
393
+ projectId: resolvedProjectId,
394
+ });
395
+ if (result.length === 0) {
396
+ return createErrorResponse(`Cannot retry task ${taskId}. It may not exist, not be failed, or not be retryable.`);
397
+ }
398
+ responseData.task = {
399
+ id: result[0].id,
400
+ title: result[0].title,
401
+ status: result[0].status,
402
+ retryCount: typeof result[0].retryCount === 'object'
403
+ ? result[0].retryCount.toNumber()
404
+ : result[0].retryCount,
405
+ };
406
+ responseData.message = `Task is now available for retry (attempt #${responseData.task.retryCount + 1}).`;
407
+ break;
408
+ default:
409
+ return createErrorResponse(`Unknown action: ${action}`);
410
+ }
411
+ return createSuccessResponse(JSON.stringify(responseData));
412
+ }
413
+ catch (error) {
414
+ await debugLog('Swarm complete task error', { error: String(error) });
415
+ return createErrorResponse(error instanceof Error ? error : String(error));
416
+ }
417
+ finally {
418
+ await neo4jService.close();
419
+ }
420
+ });
421
+ };
@@ -15,6 +15,50 @@ export const PHEROMONE_CONFIG = {
15
15
  proposal: { halfLife: 60 * 60 * 1000, description: 'Awaiting approval' },
16
16
  needs_review: { halfLife: 30 * 60 * 1000, description: 'Review requested' },
17
17
  };
18
+ /**
19
+ * Task status values for the blackboard task queue
20
+ */
21
+ export const TASK_STATUSES = [
22
+ 'available', // Ready to be claimed by an agent
23
+ 'claimed', // An agent has claimed but not started
24
+ 'in_progress', // Agent is actively working
25
+ 'blocked', // Task is blocked by dependencies or issues
26
+ 'needs_review', // Work done, awaiting review
27
+ 'completed', // Successfully finished
28
+ 'failed', // Task failed
29
+ 'cancelled', // Task was cancelled
30
+ ];
31
+ /**
32
+ * Task priority levels (higher = more urgent)
33
+ */
34
+ export const TASK_PRIORITIES = {
35
+ critical: 100, // Must be done immediately
36
+ high: 75, // Important, do soon
37
+ normal: 50, // Standard priority
38
+ low: 25, // Can wait
39
+ backlog: 0, // Do when nothing else is available
40
+ };
41
+ /**
42
+ * Task types for categorization
43
+ */
44
+ export const TASK_TYPES = [
45
+ 'implement', // Write new code
46
+ 'refactor', // Improve existing code
47
+ 'fix', // Bug fix
48
+ 'test', // Write/fix tests
49
+ 'review', // Code review
50
+ 'document', // Documentation
51
+ 'investigate', // Research/explore
52
+ 'plan', // Planning/design
53
+ ];
54
+ /**
55
+ * Generate a unique task ID
56
+ */
57
+ export const generateTaskId = () => {
58
+ const timestamp = Date.now().toString(36);
59
+ const random = Math.random().toString(36).substring(2, 8);
60
+ return `task_${timestamp}_${random}`;
61
+ };
18
62
  export const PHEROMONE_TYPES = Object.keys(PHEROMONE_CONFIG);
19
63
  /**
20
64
  * Get half-life for a pheromone type.
@@ -33,3 +77,72 @@ export const WORKFLOW_STATES = ['exploring', 'claiming', 'modifying', 'completed
33
77
  * Flags can coexist with workflow states.
34
78
  */
35
79
  export const FLAG_TYPES = ['warning', 'proposal', 'needs_review'];
80
+ // ============================================================================
81
+ // ORCHESTRATOR CONSTANTS
82
+ // ============================================================================
83
+ /**
84
+ * Generate a unique swarm ID for orchestrator runs
85
+ */
86
+ export const generateSwarmId = () => {
87
+ const timestamp = Date.now().toString(36);
88
+ const random = Math.random().toString(36).substring(2, 8);
89
+ return `swarm_${timestamp}_${random}`;
90
+ };
91
+ /**
92
+ * Generate a unique agent ID for worker agents
93
+ */
94
+ export const generateAgentId = (swarmId, index) => {
95
+ return `${swarmId}_agent_${index}`;
96
+ };
97
+ /**
98
+ * Orchestrator configuration defaults
99
+ */
100
+ export const ORCHESTRATOR_CONFIG = {
101
+ /** Default maximum number of concurrent worker agents */
102
+ defaultMaxAgents: 3,
103
+ /** Maximum allowed agents (hard limit) */
104
+ maxAgentsLimit: 10,
105
+ /** Polling interval for monitoring progress (ms) */
106
+ monitorIntervalMs: 1000,
107
+ /** Timeout for waiting on worker agents (ms) - 30 minutes */
108
+ workerTimeoutMs: 30 * 60 * 1000,
109
+ /** Delay between spawning agents (ms) */
110
+ spawnDelayMs: 500,
111
+ /** Minimum nodes to consider for parallelization */
112
+ minNodesForParallel: 3,
113
+ };
114
+ /**
115
+ * Task inference patterns for decomposing natural language tasks
116
+ */
117
+ export const TASK_INFERENCE_PATTERNS = {
118
+ rename: {
119
+ keywords: ['rename', 'change name', 'refactor name'],
120
+ taskType: 'refactor',
121
+ description: (oldName, newName) => `Rename "${oldName}" to "${newName}" and update all references`,
122
+ },
123
+ document: {
124
+ keywords: ['jsdoc', 'document', 'add comments', 'add documentation'],
125
+ taskType: 'document',
126
+ description: (target) => `Add documentation to ${target}`,
127
+ },
128
+ migrate: {
129
+ keywords: ['migrate', 'convert', 'upgrade', 'modernize'],
130
+ taskType: 'refactor',
131
+ description: (from, to) => `Migrate from ${from} to ${to}`,
132
+ },
133
+ deprecate: {
134
+ keywords: ['deprecate', 'deprecation warning', 'mark deprecated'],
135
+ taskType: 'refactor',
136
+ description: (target) => `Add deprecation warning to ${target}`,
137
+ },
138
+ fix: {
139
+ keywords: ['fix', 'repair', 'correct', 'resolve'],
140
+ taskType: 'fix',
141
+ description: (issue) => `Fix ${issue}`,
142
+ },
143
+ test: {
144
+ keywords: ['test', 'add tests', 'write tests', 'unit test'],
145
+ taskType: 'test',
146
+ description: (target) => `Write tests for ${target}`,
147
+ },
148
+ };