@vibescope/mcp-server 0.2.0 → 0.2.2

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 (104) hide show
  1. package/README.md +60 -7
  2. package/dist/api-client.d.ts +251 -1
  3. package/dist/api-client.js +82 -3
  4. package/dist/handlers/blockers.js +9 -8
  5. package/dist/handlers/bodies-of-work.js +96 -63
  6. package/dist/handlers/connectors.d.ts +45 -0
  7. package/dist/handlers/connectors.js +183 -0
  8. package/dist/handlers/cost.d.ts +10 -0
  9. package/dist/handlers/cost.js +112 -50
  10. package/dist/handlers/decisions.js +32 -19
  11. package/dist/handlers/deployment.js +144 -122
  12. package/dist/handlers/discovery.d.ts +7 -0
  13. package/dist/handlers/discovery.js +96 -7
  14. package/dist/handlers/fallback.js +29 -23
  15. package/dist/handlers/file-checkouts.d.ts +20 -0
  16. package/dist/handlers/file-checkouts.js +133 -0
  17. package/dist/handlers/findings.d.ts +6 -0
  18. package/dist/handlers/findings.js +96 -40
  19. package/dist/handlers/git-issues.js +40 -36
  20. package/dist/handlers/ideas.js +49 -31
  21. package/dist/handlers/index.d.ts +3 -0
  22. package/dist/handlers/index.js +9 -0
  23. package/dist/handlers/milestones.js +39 -32
  24. package/dist/handlers/organizations.js +99 -91
  25. package/dist/handlers/progress.js +24 -13
  26. package/dist/handlers/project.js +68 -28
  27. package/dist/handlers/requests.js +18 -14
  28. package/dist/handlers/roles.d.ts +18 -0
  29. package/dist/handlers/roles.js +130 -0
  30. package/dist/handlers/session.js +58 -17
  31. package/dist/handlers/sprints.js +93 -81
  32. package/dist/handlers/tasks.d.ts +2 -0
  33. package/dist/handlers/tasks.js +189 -91
  34. package/dist/handlers/types.d.ts +64 -2
  35. package/dist/handlers/types.js +48 -1
  36. package/dist/handlers/validation.js +21 -17
  37. package/dist/index.js +7 -2716
  38. package/dist/token-tracking.d.ts +74 -0
  39. package/dist/token-tracking.js +122 -0
  40. package/dist/tools.js +685 -9
  41. package/dist/utils.d.ts +5 -0
  42. package/dist/utils.js +17 -0
  43. package/docs/TOOLS.md +2053 -0
  44. package/package.json +4 -1
  45. package/scripts/generate-docs.ts +212 -0
  46. package/src/api-client.test.ts +718 -0
  47. package/src/api-client.ts +320 -6
  48. package/src/handlers/__test-setup__.ts +16 -0
  49. package/src/handlers/blockers.test.ts +31 -19
  50. package/src/handlers/blockers.ts +9 -8
  51. package/src/handlers/bodies-of-work.test.ts +55 -32
  52. package/src/handlers/bodies-of-work.ts +115 -115
  53. package/src/handlers/connectors.test.ts +834 -0
  54. package/src/handlers/connectors.ts +229 -0
  55. package/src/handlers/cost.test.ts +34 -44
  56. package/src/handlers/cost.ts +136 -85
  57. package/src/handlers/decisions.test.ts +37 -27
  58. package/src/handlers/decisions.ts +35 -30
  59. package/src/handlers/deployment.ts +180 -208
  60. package/src/handlers/discovery.test.ts +4 -5
  61. package/src/handlers/discovery.ts +98 -8
  62. package/src/handlers/fallback.test.ts +26 -22
  63. package/src/handlers/fallback.ts +36 -33
  64. package/src/handlers/file-checkouts.test.ts +670 -0
  65. package/src/handlers/file-checkouts.ts +165 -0
  66. package/src/handlers/findings.test.ts +178 -19
  67. package/src/handlers/findings.ts +112 -74
  68. package/src/handlers/git-issues.test.ts +51 -43
  69. package/src/handlers/git-issues.ts +44 -84
  70. package/src/handlers/ideas.test.ts +28 -23
  71. package/src/handlers/ideas.ts +61 -59
  72. package/src/handlers/index.ts +9 -0
  73. package/src/handlers/milestones.test.ts +33 -28
  74. package/src/handlers/milestones.ts +52 -50
  75. package/src/handlers/organizations.test.ts +104 -83
  76. package/src/handlers/organizations.ts +117 -142
  77. package/src/handlers/progress.test.ts +20 -14
  78. package/src/handlers/progress.ts +26 -24
  79. package/src/handlers/project.test.ts +34 -27
  80. package/src/handlers/project.ts +95 -63
  81. package/src/handlers/requests.test.ts +27 -18
  82. package/src/handlers/requests.ts +21 -17
  83. package/src/handlers/roles.test.ts +303 -0
  84. package/src/handlers/roles.ts +208 -0
  85. package/src/handlers/session.test.ts +47 -0
  86. package/src/handlers/session.ts +71 -26
  87. package/src/handlers/sprints.test.ts +71 -50
  88. package/src/handlers/sprints.ts +113 -146
  89. package/src/handlers/tasks.test.ts +77 -15
  90. package/src/handlers/tasks.ts +231 -156
  91. package/src/handlers/tool-categories.test.ts +66 -0
  92. package/src/handlers/types.ts +81 -2
  93. package/src/handlers/validation.test.ts +78 -45
  94. package/src/handlers/validation.ts +23 -25
  95. package/src/index.ts +12 -2732
  96. package/src/token-tracking.test.ts +453 -0
  97. package/src/token-tracking.ts +164 -0
  98. package/src/tools.ts +685 -9
  99. package/src/utils.test.ts +2 -2
  100. package/src/utils.ts +17 -0
  101. package/dist/config/tool-categories.d.ts +0 -31
  102. package/dist/config/tool-categories.js +0 -253
  103. package/dist/knowledge.d.ts +0 -6
  104. package/dist/knowledge.js +0 -218
@@ -18,16 +18,107 @@
18
18
 
19
19
  import type { Handler, HandlerRegistry } from './types.js';
20
20
  import {
21
- validateRequired,
22
- validateUUID,
23
- validateTaskStatus,
24
- validatePriority,
25
- validateProgressPercentage,
26
- validateEstimatedMinutes,
21
+ parseArgs,
22
+ uuidValidator,
23
+ taskStatusValidator,
24
+ priorityValidator,
25
+ progressValidator,
26
+ minutesValidator,
27
+ createEnumValidator,
27
28
  ValidationError,
28
29
  } from '../validators.js';
29
30
  import { getApiClient } from '../api-client.js';
30
31
 
32
+ // Valid task types
33
+ const VALID_TASK_TYPES = [
34
+ 'frontend', 'backend', 'database', 'feature', 'bugfix',
35
+ 'design', 'mcp', 'testing', 'docs', 'infra', 'other'
36
+ ] as const;
37
+
38
+ // ============================================================================
39
+ // Argument Schemas
40
+ // ============================================================================
41
+
42
+ const getTasksSchema = {
43
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
44
+ status: { type: 'string' as const, validate: taskStatusValidator },
45
+ limit: { type: 'number' as const, default: 50 },
46
+ offset: { type: 'number' as const, default: 0 },
47
+ search_query: { type: 'string' as const },
48
+ include_subtasks: { type: 'boolean' as const, default: false },
49
+ include_metadata: { type: 'boolean' as const, default: false },
50
+ };
51
+
52
+ const getNextTaskSchema = {
53
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
54
+ };
55
+
56
+ const addTaskSchema = {
57
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
58
+ title: { type: 'string' as const, required: true as const },
59
+ description: { type: 'string' as const },
60
+ priority: { type: 'number' as const, default: 3, validate: priorityValidator },
61
+ estimated_minutes: { type: 'number' as const, validate: minutesValidator },
62
+ blocking: { type: 'boolean' as const, default: false },
63
+ task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
64
+ };
65
+
66
+ const updateTaskSchema = {
67
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
68
+ title: { type: 'string' as const },
69
+ description: { type: 'string' as const },
70
+ priority: { type: 'number' as const, validate: priorityValidator },
71
+ status: { type: 'string' as const, validate: taskStatusValidator },
72
+ progress_percentage: { type: 'number' as const, validate: progressValidator },
73
+ progress_note: { type: 'string' as const },
74
+ estimated_minutes: { type: 'number' as const, validate: minutesValidator },
75
+ git_branch: { type: 'string' as const },
76
+ worktree_path: { type: 'string' as const },
77
+ task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
78
+ skip_worktree_requirement: { type: 'boolean' as const, default: false },
79
+ };
80
+
81
+ const completeTaskSchema = {
82
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
83
+ summary: { type: 'string' as const },
84
+ };
85
+
86
+ const deleteTaskSchema = {
87
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
88
+ };
89
+
90
+ const addTaskReferenceSchema = {
91
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
92
+ url: { type: 'string' as const, required: true as const },
93
+ label: { type: 'string' as const },
94
+ };
95
+
96
+ const removeTaskReferenceSchema = {
97
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
98
+ url: { type: 'string' as const, required: true as const },
99
+ };
100
+
101
+ const batchUpdateTasksSchema = {
102
+ updates: { type: 'array' as const, required: true as const },
103
+ };
104
+
105
+ const batchCompleteTasksSchema = {
106
+ completions: { type: 'array' as const, required: true as const },
107
+ };
108
+
109
+ const addSubtaskSchema = {
110
+ parent_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
111
+ title: { type: 'string' as const, required: true as const },
112
+ description: { type: 'string' as const },
113
+ priority: { type: 'number' as const, validate: priorityValidator },
114
+ estimated_minutes: { type: 'number' as const, validate: minutesValidator },
115
+ };
116
+
117
+ const getSubtasksSchema = {
118
+ parent_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
119
+ status: { type: 'string' as const, validate: taskStatusValidator },
120
+ };
121
+
31
122
  // ============================================================================
32
123
  // Git workflow helpers (used by complete_task response)
33
124
  // ============================================================================
@@ -126,24 +217,12 @@ export function getValidationApprovedGitInstructions(
126
217
  // ============================================================================
127
218
 
128
219
  export const getTasks: Handler = async (args, ctx) => {
129
- const { project_id, status, limit = 50, offset = 0, search_query, include_subtasks = false, include_metadata = false } = args as {
130
- project_id: string;
131
- status?: string;
132
- limit?: number;
133
- offset?: number;
134
- search_query?: string;
135
- include_subtasks?: boolean;
136
- include_metadata?: boolean; // When true, returns all task fields; when false (default), only id/title/priority/status
137
- };
138
-
139
- validateRequired(project_id, 'project_id');
140
- validateUUID(project_id, 'project_id');
141
- validateTaskStatus(status);
220
+ const { project_id, status, limit, offset, search_query, include_subtasks, include_metadata } = parseArgs(args, getTasksSchema);
142
221
 
143
222
  const api = getApiClient();
144
223
  const response = await api.getTasks(project_id, {
145
224
  status,
146
- limit: Math.min(limit, 200),
225
+ limit: Math.min(limit ?? 50, 200),
147
226
  offset,
148
227
  include_subtasks,
149
228
  search_query,
@@ -151,7 +230,7 @@ export const getTasks: Handler = async (args, ctx) => {
151
230
  });
152
231
 
153
232
  if (!response.ok) {
154
- throw new Error(`Failed to fetch tasks: ${response.error}`);
233
+ return { result: { error: response.error || 'Failed to fetch tasks' }, isError: true };
155
234
  }
156
235
 
157
236
  return {
@@ -164,16 +243,13 @@ export const getTasks: Handler = async (args, ctx) => {
164
243
  };
165
244
 
166
245
  export const getNextTask: Handler = async (args, ctx) => {
167
- const { project_id } = args as { project_id: string };
168
-
169
- validateRequired(project_id, 'project_id');
170
- validateUUID(project_id, 'project_id');
246
+ const { project_id } = parseArgs(args, getNextTaskSchema);
171
247
 
172
248
  const api = getApiClient();
173
249
  const response = await api.getNextTask(project_id, ctx.session.currentSessionId || undefined);
174
250
 
175
251
  if (!response.ok) {
176
- throw new Error(`Failed to get next task: ${response.error}`);
252
+ return { result: { error: response.error || 'Failed to get next task' }, isError: true };
177
253
  }
178
254
 
179
255
  const data = response.data;
@@ -211,21 +287,7 @@ export const getNextTask: Handler = async (args, ctx) => {
211
287
  };
212
288
 
213
289
  export const addTask: Handler = async (args, ctx) => {
214
- const { project_id, title, description, priority = 3, estimated_minutes, blocking = false, task_type } = args as {
215
- project_id: string;
216
- title: string;
217
- description?: string;
218
- priority?: number;
219
- estimated_minutes?: number;
220
- blocking?: boolean;
221
- task_type?: string;
222
- };
223
-
224
- validateRequired(project_id, 'project_id');
225
- validateRequired(title, 'title');
226
- validateUUID(project_id, 'project_id');
227
- validatePriority(priority);
228
- validateEstimatedMinutes(estimated_minutes);
290
+ const { project_id, title, description, priority, estimated_minutes, blocking, task_type } = parseArgs(args, addTaskSchema);
229
291
 
230
292
  const api = getApiClient();
231
293
  const response = await api.createTask(project_id, {
@@ -235,10 +297,11 @@ export const addTask: Handler = async (args, ctx) => {
235
297
  estimated_minutes,
236
298
  blocking,
237
299
  session_id: ctx.session.currentSessionId || undefined,
300
+ task_type,
238
301
  });
239
302
 
240
303
  if (!response.ok) {
241
- throw new Error(`Failed to add task: ${response.error}`);
304
+ return { result: { error: response.error || 'Failed to add task' }, isError: true };
242
305
  }
243
306
 
244
307
  const data = response.data;
@@ -257,25 +320,25 @@ export const addTask: Handler = async (args, ctx) => {
257
320
  };
258
321
 
259
322
  export const updateTask: Handler = async (args, ctx) => {
260
- const { task_id, progress_note, ...updates } = args as {
261
- task_id: string;
262
- title?: string;
263
- description?: string;
264
- priority?: number;
265
- status?: string;
266
- progress_percentage?: number;
267
- progress_note?: string;
268
- estimated_minutes?: number;
269
- git_branch?: string;
270
- task_type?: string;
271
- };
323
+ const { task_id, title, description, priority, status, progress_percentage, progress_note, estimated_minutes, git_branch, worktree_path, task_type, skip_worktree_requirement } = parseArgs(args, updateTaskSchema);
324
+ const updates = { title, description, priority, status, progress_percentage, estimated_minutes, git_branch, worktree_path, task_type };
272
325
 
273
- validateRequired(task_id, 'task_id');
274
- validateUUID(task_id, 'task_id');
275
- validateTaskStatus(updates.status);
276
- validatePriority(updates.priority);
277
- validateProgressPercentage(updates.progress_percentage);
278
- validateEstimatedMinutes(updates.estimated_minutes);
326
+ // Enforce worktree creation: require git_branch when marking task as in_progress
327
+ // This ensures multi-agent collaboration works properly with isolated worktrees
328
+ if (status === 'in_progress' && !git_branch && !skip_worktree_requirement) {
329
+ return {
330
+ result: {
331
+ error: 'worktree_required',
332
+ message: 'git_branch is required when marking a task as in_progress. Create a worktree first and provide the branch name.',
333
+ hint: 'Create a worktree with: git worktree add ../PROJECT-task-TASKID -b feature/TASKID-description BASE_BRANCH, then call update_task with both status and git_branch parameters.',
334
+ worktree_example: {
335
+ command: `git worktree add ../worktree-${task_id.substring(0, 8)} -b feature/${task_id.substring(0, 8)}-task develop`,
336
+ then: `update_task(task_id: "${task_id}", status: "in_progress", git_branch: "feature/${task_id.substring(0, 8)}-task")`,
337
+ },
338
+ skip_option: 'If this project does not use git branching (trunk-based or no git workflow), pass skip_worktree_requirement: true',
339
+ },
340
+ };
341
+ }
279
342
 
280
343
  const api = getApiClient();
281
344
  const response = await api.updateTask(task_id, {
@@ -310,7 +373,17 @@ export const updateTask: Handler = async (args, ctx) => {
310
373
  },
311
374
  };
312
375
  }
313
- throw new Error(`Failed to update task: ${response.error}`);
376
+ if (response.error?.includes('branch_conflict')) {
377
+ return {
378
+ result: {
379
+ error: 'branch_conflict',
380
+ message: response.error,
381
+ conflicting_task_id: (response.data as { conflicting_task_id?: string })?.conflicting_task_id,
382
+ conflicting_task_title: (response.data as { conflicting_task_title?: string })?.conflicting_task_title,
383
+ },
384
+ };
385
+ }
386
+ return { result: { error: response.error || 'Failed to update task' }, isError: true };
314
387
  }
315
388
 
316
389
  // Build result - include git workflow info when transitioning to in_progress
@@ -327,23 +400,11 @@ export const updateTask: Handler = async (args, ctx) => {
327
400
  result.next_step = data.next_step;
328
401
  }
329
402
 
330
- // Warn if transitioning to in_progress without git_branch
331
- if (updates.status === 'in_progress' && !updates.git_branch) {
332
- result.warning = 'git_branch not set. For multi-agent collaboration, set git_branch when marking in_progress to track your worktree.';
333
- result.hint = 'Call update_task again with git_branch parameter after creating your worktree.';
334
- }
335
-
336
403
  return { result };
337
404
  };
338
405
 
339
406
  export const completeTask: Handler = async (args, ctx) => {
340
- const { task_id, summary } = args as {
341
- task_id: string;
342
- summary?: string;
343
- };
344
-
345
- validateRequired(task_id, 'task_id');
346
- validateUUID(task_id, 'task_id');
407
+ const { task_id, summary } = parseArgs(args, completeTaskSchema);
347
408
 
348
409
  const api = getApiClient();
349
410
  const response = await api.completeTask(task_id, {
@@ -352,12 +413,12 @@ export const completeTask: Handler = async (args, ctx) => {
352
413
  });
353
414
 
354
415
  if (!response.ok) {
355
- throw new Error(`Failed to complete task: ${response.error}`);
416
+ return { result: { error: response.error || 'Failed to complete task' }, isError: true };
356
417
  }
357
418
 
358
419
  const data = response.data;
359
420
  if (!data) {
360
- throw new Error('No response data from complete task');
421
+ return { result: { error: 'No response data from complete task' }, isError: true };
361
422
  }
362
423
 
363
424
  // Build result matching expected format
@@ -386,27 +447,20 @@ export const completeTask: Handler = async (args, ctx) => {
386
447
  };
387
448
 
388
449
  export const deleteTask: Handler = async (args, ctx) => {
389
- const { task_id } = args as { task_id: string };
390
-
391
- validateRequired(task_id, 'task_id');
392
- validateUUID(task_id, 'task_id');
450
+ const { task_id } = parseArgs(args, deleteTaskSchema);
393
451
 
394
452
  const api = getApiClient();
395
453
  const response = await api.deleteTask(task_id);
396
454
 
397
455
  if (!response.ok) {
398
- throw new Error(`Failed to delete task: ${response.error}`);
456
+ return { result: { error: response.error || 'Failed to delete task' }, isError: true };
399
457
  }
400
458
 
401
459
  return { result: { success: true, deleted_id: task_id } };
402
460
  };
403
461
 
404
462
  export const addTaskReference: Handler = async (args, ctx) => {
405
- const { task_id, url, label } = args as { task_id: string; url: string; label?: string };
406
-
407
- validateRequired(task_id, 'task_id');
408
- validateUUID(task_id, 'task_id');
409
- validateRequired(url, 'url');
463
+ const { task_id, url, label } = parseArgs(args, addTaskReferenceSchema);
410
464
 
411
465
  const api = getApiClient();
412
466
  const response = await api.addTaskReference(task_id, url, label);
@@ -415,7 +469,7 @@ export const addTaskReference: Handler = async (args, ctx) => {
415
469
  if (response.error?.includes('already exists')) {
416
470
  return { result: { success: false, error: 'Reference with this URL already exists' } };
417
471
  }
418
- throw new Error(`Failed to add reference: ${response.error}`);
472
+ return { result: { error: response.error || 'Failed to add reference' }, isError: true };
419
473
  }
420
474
 
421
475
  return {
@@ -427,11 +481,7 @@ export const addTaskReference: Handler = async (args, ctx) => {
427
481
  };
428
482
 
429
483
  export const removeTaskReference: Handler = async (args, ctx) => {
430
- const { task_id, url } = args as { task_id: string; url: string };
431
-
432
- validateRequired(task_id, 'task_id');
433
- validateUUID(task_id, 'task_id');
434
- validateRequired(url, 'url');
484
+ const { task_id, url } = parseArgs(args, removeTaskReferenceSchema);
435
485
 
436
486
  const api = getApiClient();
437
487
  const response = await api.removeTaskReference(task_id, url);
@@ -440,104 +490,92 @@ export const removeTaskReference: Handler = async (args, ctx) => {
440
490
  if (response.error?.includes('not found')) {
441
491
  return { result: { success: false, error: 'Reference with this URL not found' } };
442
492
  }
443
- throw new Error(`Failed to remove reference: ${response.error}`);
493
+ return { result: { error: response.error || 'Failed to remove reference' }, isError: true };
444
494
  }
445
495
 
446
496
  return { result: { success: true } };
447
497
  };
448
498
 
449
499
  export const batchUpdateTasks: Handler = async (args, ctx) => {
450
- const { updates } = args as {
451
- updates: Array<{
452
- task_id: string;
453
- status?: string;
454
- progress_percentage?: number;
455
- progress_note?: string;
456
- priority?: number;
457
- }>;
458
- };
500
+ const { updates } = parseArgs(args, batchUpdateTasksSchema);
459
501
 
460
- if (!updates || !Array.isArray(updates) || updates.length === 0) {
502
+ const typedUpdates = updates as Array<{
503
+ task_id: string;
504
+ status?: string;
505
+ progress_percentage?: number;
506
+ progress_note?: string;
507
+ priority?: number;
508
+ }>;
509
+
510
+ if (!Array.isArray(typedUpdates) || typedUpdates.length === 0) {
461
511
  throw new ValidationError('updates must be a non-empty array', {
462
512
  field: 'updates',
463
513
  hint: 'Provide an array of task updates with at least one item',
464
514
  });
465
515
  }
466
516
 
467
- if (updates.length > 50) {
517
+ if (typedUpdates.length > 50) {
468
518
  throw new ValidationError('Too many updates. Maximum is 50 per batch.', {
469
519
  field: 'updates',
470
520
  hint: 'Split your updates into smaller batches',
471
521
  });
472
522
  }
473
523
 
474
- // Validate all inputs first
475
- for (const update of updates) {
476
- validateRequired(update.task_id, 'task_id');
477
- validateUUID(update.task_id, 'task_id');
478
- validateTaskStatus(update.status);
479
- validatePriority(update.priority);
480
- validateProgressPercentage(update.progress_percentage);
481
- }
482
-
524
+ // Individual item validation happens at API level
483
525
  const api = getApiClient();
484
- const response = await api.batchUpdateTasks(updates);
526
+ const response = await api.batchUpdateTasks(typedUpdates);
485
527
 
486
528
  if (!response.ok) {
487
- throw new Error(`Failed to batch update tasks: ${response.error}`);
529
+ return { result: { error: response.error || 'Failed to batch update tasks' }, isError: true };
488
530
  }
489
531
 
490
532
  return {
491
533
  result: {
492
534
  success: response.data?.success || false,
493
- total: updates.length,
535
+ total: typedUpdates.length,
494
536
  succeeded: response.data?.updated_count || 0,
495
537
  },
496
538
  };
497
539
  };
498
540
 
499
541
  export const batchCompleteTasks: Handler = async (args, ctx) => {
500
- const { completions } = args as {
501
- completions: Array<{
502
- task_id: string;
503
- summary?: string;
504
- }>;
505
- };
542
+ const { completions } = parseArgs(args, batchCompleteTasksSchema);
506
543
 
507
- if (!completions || !Array.isArray(completions) || completions.length === 0) {
544
+ const typedCompletions = completions as Array<{
545
+ task_id: string;
546
+ summary?: string;
547
+ }>;
548
+
549
+ if (!Array.isArray(typedCompletions) || typedCompletions.length === 0) {
508
550
  throw new ValidationError('completions must be a non-empty array', {
509
551
  field: 'completions',
510
552
  hint: 'Provide an array of task completions with at least one item',
511
553
  });
512
554
  }
513
555
 
514
- if (completions.length > 50) {
556
+ if (typedCompletions.length > 50) {
515
557
  throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
516
558
  field: 'completions',
517
559
  hint: 'Split your completions into smaller batches',
518
560
  });
519
561
  }
520
562
 
521
- // Validate all inputs first
522
- for (const completion of completions) {
523
- validateRequired(completion.task_id, 'task_id');
524
- validateUUID(completion.task_id, 'task_id');
525
- }
563
+ // Individual item validation happens at API level
526
564
 
527
565
  const api = getApiClient();
528
- const response = await api.batchCompleteTasks(completions);
566
+ const response = await api.batchCompleteTasks(typedCompletions);
529
567
 
530
568
  if (!response.ok) {
531
- throw new Error(`Failed to batch complete tasks: ${response.error}`);
569
+ return { result: { error: response.error || 'Failed to batch complete tasks' }, isError: true };
532
570
  }
533
571
 
534
572
  const data = response.data;
535
573
  return {
536
574
  result: {
537
575
  success: data?.success || false,
538
- total: completions.length,
576
+ total: typedCompletions.length,
539
577
  succeeded: data?.completed_count || 0,
540
- failed: completions.length - (data?.completed_count || 0),
578
+ failed: typedCompletions.length - (data?.completed_count || 0),
541
579
  next_task: data?.next_task,
542
580
  },
543
581
  };
@@ -548,19 +586,7 @@ export const batchCompleteTasks: Handler = async (args, ctx) => {
548
586
  // ============================================================================
549
587
 
550
588
  export const addSubtask: Handler = async (args, ctx) => {
551
- const { parent_task_id, title, description, priority, estimated_minutes } = args as {
552
- parent_task_id: string;
553
- title: string;
554
- description?: string;
555
- priority?: number;
556
- estimated_minutes?: number;
557
- };
558
-
559
- validateRequired(parent_task_id, 'parent_task_id');
560
- validateUUID(parent_task_id, 'parent_task_id');
561
- validateRequired(title, 'title');
562
- if (priority !== undefined) validatePriority(priority);
563
- if (estimated_minutes !== undefined) validateEstimatedMinutes(estimated_minutes);
589
+ const { parent_task_id, title, description, priority, estimated_minutes } = parseArgs(args, addSubtaskSchema);
564
590
 
565
591
  const api = getApiClient();
566
592
  const response = await api.addSubtask(parent_task_id, {
@@ -580,7 +606,7 @@ export const addSubtask: Handler = async (args, ctx) => {
580
606
  },
581
607
  };
582
608
  }
583
- throw new Error(`Failed to add subtask: ${response.error}`);
609
+ return { result: { error: response.error || 'Failed to add subtask' }, isError: true };
584
610
  }
585
611
 
586
612
  return {
@@ -593,20 +619,13 @@ export const addSubtask: Handler = async (args, ctx) => {
593
619
  };
594
620
 
595
621
  export const getSubtasks: Handler = async (args, ctx) => {
596
- const { parent_task_id, status } = args as {
597
- parent_task_id: string;
598
- status?: string;
599
- };
600
-
601
- validateRequired(parent_task_id, 'parent_task_id');
602
- validateUUID(parent_task_id, 'parent_task_id');
603
- if (status) validateTaskStatus(status);
622
+ const { parent_task_id, status } = parseArgs(args, getSubtasksSchema);
604
623
 
605
624
  const api = getApiClient();
606
625
  const response = await api.getSubtasks(parent_task_id, status);
607
626
 
608
627
  if (!response.ok) {
609
- throw new Error(`Failed to fetch subtasks: ${response.error}`);
628
+ return { result: { error: response.error || 'Failed to fetch subtasks' }, isError: true };
610
629
  }
611
630
 
612
631
  return {
@@ -621,6 +640,59 @@ export const getSubtasks: Handler = async (args, ctx) => {
621
640
  };
622
641
  };
623
642
 
643
+ // ============================================================================
644
+ // Worktree Cleanup Handlers
645
+ // ============================================================================
646
+
647
+ const getStaleWorktreesSchema = {
648
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
649
+ };
650
+
651
+ const clearWorktreePathSchema = {
652
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
653
+ };
654
+
655
+ export const getStaleWorktrees: Handler = async (args, ctx) => {
656
+ const { project_id } = parseArgs(args, getStaleWorktreesSchema);
657
+
658
+ const api = getApiClient();
659
+ const response = await api.getStaleWorktrees(project_id);
660
+
661
+ if (!response.ok) {
662
+ return { result: { error: response.error || 'Failed to get stale worktrees' }, isError: true };
663
+ }
664
+
665
+ const data = response.data;
666
+ return {
667
+ result: {
668
+ project_id: data?.project_id,
669
+ project_name: data?.project_name,
670
+ stale_worktrees: data?.stale_worktrees || [],
671
+ count: data?.count || 0,
672
+ cleanup_instructions: data?.cleanup_instructions,
673
+ },
674
+ };
675
+ };
676
+
677
+ export const clearWorktreePath: Handler = async (args, ctx) => {
678
+ const { task_id } = parseArgs(args, clearWorktreePathSchema);
679
+
680
+ const api = getApiClient();
681
+ const response = await api.clearWorktreePath(task_id);
682
+
683
+ if (!response.ok) {
684
+ return { result: { error: response.error || 'Failed to clear worktree path' }, isError: true };
685
+ }
686
+
687
+ return {
688
+ result: {
689
+ success: true,
690
+ task_id,
691
+ message: 'Worktree path cleared. The worktree can now be safely removed if not already done.',
692
+ },
693
+ };
694
+ };
695
+
624
696
  /**
625
697
  * Task handlers registry
626
698
  */
@@ -638,4 +710,7 @@ export const taskHandlers: HandlerRegistry = {
638
710
  // Subtask handlers
639
711
  add_subtask: addSubtask,
640
712
  get_subtasks: getSubtasks,
713
+ // Worktree cleanup handlers
714
+ get_stale_worktrees: getStaleWorktrees,
715
+ clear_worktree_path: clearWorktreePath,
641
716
  };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Tests for the documentation generator
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { tools } from '../tools.js';
7
+ import { TOOL_CATEGORIES } from './discovery.js';
8
+
9
+ describe('Documentation Generator Prerequisites', () => {
10
+ it('should have tools defined', () => {
11
+ expect(tools).toBeDefined();
12
+ expect(Array.isArray(tools)).toBe(true);
13
+ expect(tools.length).toBeGreaterThan(0);
14
+ });
15
+
16
+ it('should have TOOL_CATEGORIES exported', () => {
17
+ expect(TOOL_CATEGORIES).toBeDefined();
18
+ expect(typeof TOOL_CATEGORIES).toBe('object');
19
+ });
20
+
21
+ it('should have all tools categorized', () => {
22
+ const categorizedTools = new Set<string>();
23
+ for (const category of Object.values(TOOL_CATEGORIES)) {
24
+ for (const tool of category.tools) {
25
+ categorizedTools.add(tool.name);
26
+ }
27
+ }
28
+
29
+ const uncategorized = tools.filter((t) => !categorizedTools.has(t.name)).map((t) => t.name);
30
+
31
+ expect(uncategorized).toEqual([]);
32
+ });
33
+
34
+ it('should have valid tool schemas', () => {
35
+ for (const tool of tools) {
36
+ expect(tool.name).toBeDefined();
37
+ expect(typeof tool.name).toBe('string');
38
+ expect(tool.description).toBeDefined();
39
+ expect(typeof tool.description).toBe('string');
40
+ expect(tool.inputSchema).toBeDefined();
41
+ expect(tool.inputSchema.type).toBe('object');
42
+ }
43
+ });
44
+
45
+ it('should have category tools that exist in tools array', () => {
46
+ const toolNames = new Set(tools.map((t) => t.name));
47
+
48
+ for (const [categoryName, category] of Object.entries(TOOL_CATEGORIES)) {
49
+ for (const toolRef of category.tools) {
50
+ expect(toolNames.has(toolRef.name)).toBe(true);
51
+ }
52
+ }
53
+ });
54
+
55
+ it('should have unique tool names', () => {
56
+ const names = tools.map((t) => t.name);
57
+ const uniqueNames = new Set(names);
58
+ expect(names.length).toBe(uniqueNames.size);
59
+ });
60
+
61
+ it('should have unique category names', () => {
62
+ const names = Object.keys(TOOL_CATEGORIES);
63
+ const uniqueNames = new Set(names);
64
+ expect(names.length).toBe(uniqueNames.size);
65
+ });
66
+ });