@vibescope/mcp-server 0.2.1 → 0.2.3

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 (93) hide show
  1. package/README.md +63 -38
  2. package/dist/api-client.d.ts +187 -0
  3. package/dist/api-client.js +53 -1
  4. package/dist/handlers/blockers.js +9 -8
  5. package/dist/handlers/bodies-of-work.js +14 -14
  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 +54 -0
  10. package/dist/handlers/decisions.js +3 -3
  11. package/dist/handlers/deployment.js +35 -19
  12. package/dist/handlers/discovery.d.ts +7 -0
  13. package/dist/handlers/discovery.js +61 -2
  14. package/dist/handlers/fallback.js +5 -4
  15. package/dist/handlers/file-checkouts.d.ts +2 -0
  16. package/dist/handlers/file-checkouts.js +38 -6
  17. package/dist/handlers/findings.js +13 -12
  18. package/dist/handlers/git-issues.js +4 -4
  19. package/dist/handlers/ideas.js +5 -5
  20. package/dist/handlers/index.d.ts +1 -0
  21. package/dist/handlers/index.js +3 -0
  22. package/dist/handlers/milestones.js +5 -5
  23. package/dist/handlers/organizations.js +13 -13
  24. package/dist/handlers/progress.js +2 -2
  25. package/dist/handlers/project.js +6 -6
  26. package/dist/handlers/requests.js +3 -3
  27. package/dist/handlers/session.js +28 -9
  28. package/dist/handlers/sprints.js +17 -17
  29. package/dist/handlers/tasks.d.ts +2 -0
  30. package/dist/handlers/tasks.js +78 -20
  31. package/dist/handlers/types.d.ts +64 -2
  32. package/dist/handlers/types.js +48 -1
  33. package/dist/handlers/validation.js +3 -3
  34. package/dist/index.js +7 -2716
  35. package/dist/token-tracking.d.ts +74 -0
  36. package/dist/token-tracking.js +122 -0
  37. package/dist/tools.js +298 -9
  38. package/dist/utils.d.ts +5 -0
  39. package/dist/utils.js +17 -0
  40. package/docs/TOOLS.md +2053 -0
  41. package/package.json +4 -1
  42. package/scripts/generate-docs.ts +212 -0
  43. package/src/api-client.test.ts +723 -0
  44. package/src/api-client.ts +236 -1
  45. package/src/handlers/__test-setup__.ts +9 -0
  46. package/src/handlers/blockers.test.ts +31 -19
  47. package/src/handlers/blockers.ts +9 -8
  48. package/src/handlers/bodies-of-work.test.ts +55 -32
  49. package/src/handlers/bodies-of-work.ts +14 -14
  50. package/src/handlers/connectors.test.ts +834 -0
  51. package/src/handlers/connectors.ts +229 -0
  52. package/src/handlers/cost.ts +66 -0
  53. package/src/handlers/decisions.test.ts +34 -25
  54. package/src/handlers/decisions.ts +3 -3
  55. package/src/handlers/deployment.ts +39 -19
  56. package/src/handlers/discovery.ts +61 -2
  57. package/src/handlers/fallback.test.ts +26 -22
  58. package/src/handlers/fallback.ts +5 -4
  59. package/src/handlers/file-checkouts.test.ts +242 -49
  60. package/src/handlers/file-checkouts.ts +44 -6
  61. package/src/handlers/findings.test.ts +38 -24
  62. package/src/handlers/findings.ts +13 -12
  63. package/src/handlers/git-issues.test.ts +51 -43
  64. package/src/handlers/git-issues.ts +4 -4
  65. package/src/handlers/ideas.test.ts +28 -23
  66. package/src/handlers/ideas.ts +5 -5
  67. package/src/handlers/index.ts +3 -0
  68. package/src/handlers/milestones.test.ts +33 -28
  69. package/src/handlers/milestones.ts +5 -5
  70. package/src/handlers/organizations.test.ts +104 -83
  71. package/src/handlers/organizations.ts +13 -13
  72. package/src/handlers/progress.test.ts +20 -14
  73. package/src/handlers/progress.ts +2 -2
  74. package/src/handlers/project.test.ts +34 -27
  75. package/src/handlers/project.ts +6 -6
  76. package/src/handlers/requests.test.ts +27 -18
  77. package/src/handlers/requests.ts +3 -3
  78. package/src/handlers/session.test.ts +47 -0
  79. package/src/handlers/session.ts +26 -9
  80. package/src/handlers/sprints.test.ts +71 -50
  81. package/src/handlers/sprints.ts +17 -17
  82. package/src/handlers/tasks.test.ts +77 -15
  83. package/src/handlers/tasks.ts +90 -21
  84. package/src/handlers/tool-categories.test.ts +66 -0
  85. package/src/handlers/types.ts +81 -2
  86. package/src/handlers/validation.test.ts +78 -45
  87. package/src/handlers/validation.ts +3 -3
  88. package/src/index.ts +12 -2732
  89. package/src/token-tracking.test.ts +453 -0
  90. package/src/token-tracking.ts +164 -0
  91. package/src/tools.ts +298 -9
  92. package/src/utils.test.ts +2 -2
  93. package/src/utils.ts +17 -0
@@ -119,13 +119,13 @@ export const createSprint: Handler = async (args, ctx) => {
119
119
  const startDateObj = new Date(start_date);
120
120
  const endDateObj = new Date(end_date);
121
121
  if (isNaN(startDateObj.getTime())) {
122
- throw new Error('Invalid start_date format. Use YYYY-MM-DD');
122
+ return { result: { error: 'Invalid start_date format. Use YYYY-MM-DD' }, isError: true };
123
123
  }
124
124
  if (isNaN(endDateObj.getTime())) {
125
- throw new Error('Invalid end_date format. Use YYYY-MM-DD');
125
+ return { result: { error: 'Invalid end_date format. Use YYYY-MM-DD' }, isError: true };
126
126
  }
127
127
  if (endDateObj < startDateObj) {
128
- throw new Error('end_date must be on or after start_date');
128
+ return { result: { error: 'end_date must be on or after start_date' }, isError: true };
129
129
  }
130
130
 
131
131
  const { session } = ctx;
@@ -151,7 +151,7 @@ export const createSprint: Handler = async (args, ctx) => {
151
151
  });
152
152
 
153
153
  if (!response.ok) {
154
- throw new Error(`Failed to create sprint: ${response.error}`);
154
+ return { result: { error: response.error || 'Failed to create sprint' }, isError: true };
155
155
  }
156
156
 
157
157
  return {
@@ -184,13 +184,13 @@ export const updateSprint: Handler = async (args, ctx) => {
184
184
  if (start_date) {
185
185
  const startDateObj = new Date(start_date);
186
186
  if (isNaN(startDateObj.getTime())) {
187
- throw new Error('Invalid start_date format. Use YYYY-MM-DD');
187
+ return { result: { error: 'Invalid start_date format. Use YYYY-MM-DD' }, isError: true };
188
188
  }
189
189
  }
190
190
  if (end_date) {
191
191
  const endDateObj = new Date(end_date);
192
192
  if (isNaN(endDateObj.getTime())) {
193
- throw new Error('Invalid end_date format. Use YYYY-MM-DD');
193
+ return { result: { error: 'Invalid end_date format. Use YYYY-MM-DD' }, isError: true };
194
194
  }
195
195
  }
196
196
 
@@ -208,7 +208,7 @@ export const updateSprint: Handler = async (args, ctx) => {
208
208
  });
209
209
 
210
210
  if (!response.ok) {
211
- throw new Error(`Failed to update sprint: ${response.error}`);
211
+ return { result: { error: response.error || 'Failed to update sprint' }, isError: true };
212
212
  }
213
213
 
214
214
  return { result: { success: true, sprint_id } };
@@ -250,7 +250,7 @@ export const getSprint: Handler = async (args, ctx) => {
250
250
  }>('get_sprint', { sprint_id, summary_only });
251
251
 
252
252
  if (!response.ok) {
253
- throw new Error(`Failed to get sprint: ${response.error}`);
253
+ return { result: { error: response.error || 'Failed to get sprint' }, isError: true };
254
254
  }
255
255
 
256
256
  return { result: response.data };
@@ -284,7 +284,7 @@ export const getSprints: Handler = async (args, ctx) => {
284
284
  });
285
285
 
286
286
  if (!response.ok) {
287
- throw new Error(`Failed to fetch sprints: ${response.error}`);
287
+ return { result: { error: response.error || 'Failed to fetch sprints' }, isError: true };
288
288
  }
289
289
 
290
290
  return { result: response.data };
@@ -300,7 +300,7 @@ export const deleteSprint: Handler = async (args, ctx) => {
300
300
  });
301
301
 
302
302
  if (!response.ok) {
303
- throw new Error(`Failed to delete sprint: ${response.error}`);
303
+ return { result: { error: response.error || 'Failed to delete sprint' }, isError: true };
304
304
  }
305
305
 
306
306
  return { result: { success: true, message: 'Sprint deleted. Tasks are preserved.' } };
@@ -320,7 +320,7 @@ export const startSprint: Handler = async (args, ctx) => {
320
320
  }>('start_sprint', { sprint_id });
321
321
 
322
322
  if (!response.ok) {
323
- throw new Error(`Failed to start sprint: ${response.error}`);
323
+ return { result: { error: response.error || 'Failed to start sprint' }, isError: true };
324
324
  }
325
325
 
326
326
  return { result: response.data };
@@ -346,7 +346,7 @@ export const completeSprint: Handler = async (args, ctx) => {
346
346
  });
347
347
 
348
348
  if (!response.ok) {
349
- throw new Error(`Failed to complete sprint: ${response.error}`);
349
+ return { result: { error: response.error || 'Failed to complete sprint' }, isError: true };
350
350
  }
351
351
 
352
352
  return { result: response.data };
@@ -356,7 +356,7 @@ export const addTaskToSprint: Handler = async (args, ctx) => {
356
356
  const { sprint_id, task_id, story_points, phase } = parseArgs(args, addTaskToSprintSchema);
357
357
 
358
358
  if (story_points !== undefined && (story_points < 0 || !Number.isInteger(story_points))) {
359
- throw new Error('story_points must be a non-negative integer');
359
+ return { result: { error: 'story_points must be a non-negative integer' }, isError: true };
360
360
  }
361
361
 
362
362
  const apiClient = getApiClient();
@@ -376,7 +376,7 @@ export const addTaskToSprint: Handler = async (args, ctx) => {
376
376
  });
377
377
 
378
378
  if (!response.ok) {
379
- throw new Error(`Failed to add task to sprint: ${response.error}`);
379
+ return { result: { error: response.error || 'Failed to add task to sprint' }, isError: true };
380
380
  }
381
381
 
382
382
  return { result: response.data };
@@ -399,7 +399,7 @@ export const removeTaskFromSprint: Handler = async (args, ctx) => {
399
399
  });
400
400
 
401
401
  if (!response.ok) {
402
- throw new Error(`Failed to remove task from sprint: ${response.error}`);
402
+ return { result: { error: response.error || 'Failed to remove task from sprint' }, isError: true };
403
403
  }
404
404
 
405
405
  return { result: response.data };
@@ -426,7 +426,7 @@ export const getSprintBacklog: Handler = async (args, ctx) => {
426
426
  });
427
427
 
428
428
  if (!response.ok) {
429
- throw new Error(`Failed to get sprint backlog: ${response.error}`);
429
+ return { result: { error: response.error || 'Failed to get sprint backlog' }, isError: true };
430
430
  }
431
431
 
432
432
  return { result: response.data };
@@ -453,7 +453,7 @@ export const getSprintVelocity: Handler = async (args, ctx) => {
453
453
  });
454
454
 
455
455
  if (!response.ok) {
456
- throw new Error(`Failed to get sprint velocity: ${response.error}`);
456
+ return { result: { error: response.error || 'Failed to get sprint velocity' }, isError: true };
457
457
  }
458
458
 
459
459
  return { result: response.data };
@@ -306,6 +306,7 @@ describe('updateTask', () => {
306
306
  {
307
307
  task_id: '123e4567-e89b-12d3-a456-426614174000',
308
308
  status: 'in_progress',
309
+ git_branch: 'feature/test-branch',
309
310
  },
310
311
  ctx
311
312
  );
@@ -324,6 +325,7 @@ describe('updateTask', () => {
324
325
  {
325
326
  task_id: '123e4567-e89b-12d3-a456-426614174000',
326
327
  status: 'in_progress',
328
+ git_branch: 'feature/test-branch',
327
329
  },
328
330
  ctx
329
331
  );
@@ -344,6 +346,7 @@ describe('updateTask', () => {
344
346
  {
345
347
  task_id: '123e4567-e89b-12d3-a456-426614174000',
346
348
  status: 'in_progress',
349
+ git_branch: 'feature/test-branch',
347
350
  },
348
351
  ctx
349
352
  );
@@ -353,7 +356,42 @@ describe('updateTask', () => {
353
356
  });
354
357
  });
355
358
 
356
- it('should warn when setting in_progress without git_branch', async () => {
359
+ it('should return error when setting in_progress without git_branch', async () => {
360
+ const ctx = createMockContext();
361
+ const result = await updateTask(
362
+ {
363
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
364
+ status: 'in_progress',
365
+ },
366
+ ctx
367
+ );
368
+
369
+ expect(result.result).toMatchObject({
370
+ error: 'worktree_required',
371
+ message: expect.stringContaining('git_branch is required'),
372
+ hint: expect.stringContaining('Create a worktree'),
373
+ });
374
+ // Should NOT call the API when worktree requirement fails
375
+ expect(mockApiClient.updateTask).not.toHaveBeenCalled();
376
+ });
377
+
378
+ it('should return worktree_example in error response', async () => {
379
+ const ctx = createMockContext();
380
+ const result = await updateTask(
381
+ {
382
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
383
+ status: 'in_progress',
384
+ },
385
+ ctx
386
+ );
387
+
388
+ expect(result.result).toHaveProperty('worktree_example');
389
+ expect(result.result.worktree_example).toHaveProperty('command');
390
+ expect(result.result.worktree_example).toHaveProperty('then');
391
+ expect(result.result).toHaveProperty('skip_option');
392
+ });
393
+
394
+ it('should succeed when setting in_progress with git_branch', async () => {
357
395
  mockApiClient.updateTask.mockResolvedValue({
358
396
  ok: true,
359
397
  data: { success: true },
@@ -364,18 +402,17 @@ describe('updateTask', () => {
364
402
  {
365
403
  task_id: '123e4567-e89b-12d3-a456-426614174000',
366
404
  status: 'in_progress',
405
+ git_branch: 'feature/my-task',
367
406
  },
368
407
  ctx
369
408
  );
370
409
 
371
- expect(result.result).toMatchObject({
372
- success: true,
373
- warning: expect.stringContaining('git_branch not set'),
374
- hint: expect.stringContaining('update_task again'),
375
- });
410
+ expect(result.result).toMatchObject({ success: true });
411
+ expect(result.result).not.toHaveProperty('error');
412
+ expect(mockApiClient.updateTask).toHaveBeenCalled();
376
413
  });
377
414
 
378
- it('should not warn when setting in_progress with git_branch', async () => {
415
+ it('should succeed when using skip_worktree_requirement without git_branch', async () => {
379
416
  mockApiClient.updateTask.mockResolvedValue({
380
417
  ok: true,
381
418
  data: { success: true },
@@ -386,16 +423,17 @@ describe('updateTask', () => {
386
423
  {
387
424
  task_id: '123e4567-e89b-12d3-a456-426614174000',
388
425
  status: 'in_progress',
389
- git_branch: 'feature/my-task',
426
+ skip_worktree_requirement: true,
390
427
  },
391
428
  ctx
392
429
  );
393
430
 
394
431
  expect(result.result).toMatchObject({ success: true });
395
- expect(result.result).not.toHaveProperty('warning');
432
+ expect(result.result).not.toHaveProperty('error');
433
+ expect(mockApiClient.updateTask).toHaveBeenCalled();
396
434
  });
397
435
 
398
- it('should not warn when updating status other than in_progress', async () => {
436
+ it('should not require git_branch when updating status other than in_progress', async () => {
399
437
  mockApiClient.updateTask.mockResolvedValue({
400
438
  ok: true,
401
439
  data: { success: true },
@@ -411,7 +449,28 @@ describe('updateTask', () => {
411
449
  );
412
450
 
413
451
  expect(result.result).toMatchObject({ success: true });
414
- expect(result.result).not.toHaveProperty('warning');
452
+ expect(result.result).not.toHaveProperty('error');
453
+ expect(mockApiClient.updateTask).toHaveBeenCalled();
454
+ });
455
+
456
+ it('should not require git_branch when updating progress only', async () => {
457
+ mockApiClient.updateTask.mockResolvedValue({
458
+ ok: true,
459
+ data: { success: true },
460
+ });
461
+
462
+ const ctx = createMockContext();
463
+ const result = await updateTask(
464
+ {
465
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
466
+ progress_percentage: 50,
467
+ progress_note: 'Halfway done',
468
+ },
469
+ ctx
470
+ );
471
+
472
+ expect(result.result).toMatchObject({ success: true });
473
+ expect(mockApiClient.updateTask).toHaveBeenCalled();
415
474
  });
416
475
  });
417
476
 
@@ -480,16 +539,19 @@ describe('completeTask', () => {
480
539
  );
481
540
  });
482
541
 
483
- it('should throw error when API returns error', async () => {
542
+ it('should return error when API returns error', async () => {
484
543
  mockApiClient.completeTask.mockResolvedValue({
485
544
  ok: false,
486
545
  error: 'Task not found',
487
546
  });
488
547
 
489
548
  const ctx = createMockContext();
490
- await expect(
491
- completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
492
- ).rejects.toThrow('Failed to complete task: Task not found');
549
+ const result = await completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
550
+
551
+ expect(result.isError).toBe(true);
552
+ expect(result.result).toMatchObject({
553
+ error: 'Task not found',
554
+ });
493
555
  });
494
556
  });
495
557
 
@@ -73,7 +73,9 @@ const updateTaskSchema = {
73
73
  progress_note: { type: 'string' as const },
74
74
  estimated_minutes: { type: 'number' as const, validate: minutesValidator },
75
75
  git_branch: { type: 'string' as const },
76
+ worktree_path: { type: 'string' as const },
76
77
  task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
78
+ skip_worktree_requirement: { type: 'boolean' as const, default: false },
77
79
  };
78
80
 
79
81
  const completeTaskSchema = {
@@ -228,7 +230,7 @@ export const getTasks: Handler = async (args, ctx) => {
228
230
  });
229
231
 
230
232
  if (!response.ok) {
231
- throw new Error(`Failed to fetch tasks: ${response.error}`);
233
+ return { result: { error: response.error || 'Failed to fetch tasks' }, isError: true };
232
234
  }
233
235
 
234
236
  return {
@@ -247,7 +249,7 @@ export const getNextTask: Handler = async (args, ctx) => {
247
249
  const response = await api.getNextTask(project_id, ctx.session.currentSessionId || undefined);
248
250
 
249
251
  if (!response.ok) {
250
- throw new Error(`Failed to get next task: ${response.error}`);
252
+ return { result: { error: response.error || 'Failed to get next task' }, isError: true };
251
253
  }
252
254
 
253
255
  const data = response.data;
@@ -299,7 +301,7 @@ export const addTask: Handler = async (args, ctx) => {
299
301
  });
300
302
 
301
303
  if (!response.ok) {
302
- throw new Error(`Failed to add task: ${response.error}`);
304
+ return { result: { error: response.error || 'Failed to add task' }, isError: true };
303
305
  }
304
306
 
305
307
  const data = response.data;
@@ -318,8 +320,25 @@ export const addTask: Handler = async (args, ctx) => {
318
320
  };
319
321
 
320
322
  export const updateTask: Handler = async (args, ctx) => {
321
- const { task_id, title, description, priority, status, progress_percentage, progress_note, estimated_minutes, git_branch, task_type } = parseArgs(args, updateTaskSchema);
322
- const updates = { title, description, priority, status, progress_percentage, estimated_minutes, git_branch, task_type };
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 };
325
+
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
+ }
323
342
 
324
343
  const api = getApiClient();
325
344
  const response = await api.updateTask(task_id, {
@@ -364,7 +383,7 @@ export const updateTask: Handler = async (args, ctx) => {
364
383
  },
365
384
  };
366
385
  }
367
- throw new Error(`Failed to update task: ${response.error}`);
386
+ return { result: { error: response.error || 'Failed to update task' }, isError: true };
368
387
  }
369
388
 
370
389
  // Build result - include git workflow info when transitioning to in_progress
@@ -381,12 +400,6 @@ export const updateTask: Handler = async (args, ctx) => {
381
400
  result.next_step = data.next_step;
382
401
  }
383
402
 
384
- // Warn if transitioning to in_progress without git_branch
385
- if (updates.status === 'in_progress' && !updates.git_branch) {
386
- result.warning = 'git_branch not set. For multi-agent collaboration, set git_branch when marking in_progress to track your worktree.';
387
- result.hint = 'Call update_task again with git_branch parameter after creating your worktree.';
388
- }
389
-
390
403
  return { result };
391
404
  };
392
405
 
@@ -400,12 +413,12 @@ export const completeTask: Handler = async (args, ctx) => {
400
413
  });
401
414
 
402
415
  if (!response.ok) {
403
- throw new Error(`Failed to complete task: ${response.error}`);
416
+ return { result: { error: response.error || 'Failed to complete task' }, isError: true };
404
417
  }
405
418
 
406
419
  const data = response.data;
407
420
  if (!data) {
408
- throw new Error('No response data from complete task');
421
+ return { result: { error: 'No response data from complete task' }, isError: true };
409
422
  }
410
423
 
411
424
  // Build result matching expected format
@@ -440,7 +453,7 @@ export const deleteTask: Handler = async (args, ctx) => {
440
453
  const response = await api.deleteTask(task_id);
441
454
 
442
455
  if (!response.ok) {
443
- throw new Error(`Failed to delete task: ${response.error}`);
456
+ return { result: { error: response.error || 'Failed to delete task' }, isError: true };
444
457
  }
445
458
 
446
459
  return { result: { success: true, deleted_id: task_id } };
@@ -456,7 +469,7 @@ export const addTaskReference: Handler = async (args, ctx) => {
456
469
  if (response.error?.includes('already exists')) {
457
470
  return { result: { success: false, error: 'Reference with this URL already exists' } };
458
471
  }
459
- throw new Error(`Failed to add reference: ${response.error}`);
472
+ return { result: { error: response.error || 'Failed to add reference' }, isError: true };
460
473
  }
461
474
 
462
475
  return {
@@ -477,7 +490,7 @@ export const removeTaskReference: Handler = async (args, ctx) => {
477
490
  if (response.error?.includes('not found')) {
478
491
  return { result: { success: false, error: 'Reference with this URL not found' } };
479
492
  }
480
- throw new Error(`Failed to remove reference: ${response.error}`);
493
+ return { result: { error: response.error || 'Failed to remove reference' }, isError: true };
481
494
  }
482
495
 
483
496
  return { result: { success: true } };
@@ -513,7 +526,7 @@ export const batchUpdateTasks: Handler = async (args, ctx) => {
513
526
  const response = await api.batchUpdateTasks(typedUpdates);
514
527
 
515
528
  if (!response.ok) {
516
- throw new Error(`Failed to batch update tasks: ${response.error}`);
529
+ return { result: { error: response.error || 'Failed to batch update tasks' }, isError: true };
517
530
  }
518
531
 
519
532
  return {
@@ -553,7 +566,7 @@ export const batchCompleteTasks: Handler = async (args, ctx) => {
553
566
  const response = await api.batchCompleteTasks(typedCompletions);
554
567
 
555
568
  if (!response.ok) {
556
- throw new Error(`Failed to batch complete tasks: ${response.error}`);
569
+ return { result: { error: response.error || 'Failed to batch complete tasks' }, isError: true };
557
570
  }
558
571
 
559
572
  const data = response.data;
@@ -593,7 +606,7 @@ export const addSubtask: Handler = async (args, ctx) => {
593
606
  },
594
607
  };
595
608
  }
596
- throw new Error(`Failed to add subtask: ${response.error}`);
609
+ return { result: { error: response.error || 'Failed to add subtask' }, isError: true };
597
610
  }
598
611
 
599
612
  return {
@@ -612,7 +625,7 @@ export const getSubtasks: Handler = async (args, ctx) => {
612
625
  const response = await api.getSubtasks(parent_task_id, status);
613
626
 
614
627
  if (!response.ok) {
615
- throw new Error(`Failed to fetch subtasks: ${response.error}`);
628
+ return { result: { error: response.error || 'Failed to fetch subtasks' }, isError: true };
616
629
  }
617
630
 
618
631
  return {
@@ -627,6 +640,59 @@ export const getSubtasks: Handler = async (args, ctx) => {
627
640
  };
628
641
  };
629
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
+
630
696
  /**
631
697
  * Task handlers registry
632
698
  */
@@ -644,4 +710,7 @@ export const taskHandlers: HandlerRegistry = {
644
710
  // Subtask handlers
645
711
  add_subtask: addSubtask,
646
712
  get_subtasks: getSubtasks,
713
+ // Worktree cleanup handlers
714
+ get_stale_worktrees: getStaleWorktrees,
715
+ clear_worktree_path: clearWorktreePath,
647
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
+ });