@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
@@ -15,8 +15,84 @@
15
15
  * - add_subtask
16
16
  * - get_subtasks
17
17
  */
18
- import { validateRequired, validateUUID, validateTaskStatus, validatePriority, validateProgressPercentage, validateEstimatedMinutes, ValidationError, } from '../validators.js';
18
+ import { parseArgs, uuidValidator, taskStatusValidator, priorityValidator, progressValidator, minutesValidator, createEnumValidator, ValidationError, } from '../validators.js';
19
19
  import { getApiClient } from '../api-client.js';
20
+ // Valid task types
21
+ const VALID_TASK_TYPES = [
22
+ 'frontend', 'backend', 'database', 'feature', 'bugfix',
23
+ 'design', 'mcp', 'testing', 'docs', 'infra', 'other'
24
+ ];
25
+ // ============================================================================
26
+ // Argument Schemas
27
+ // ============================================================================
28
+ const getTasksSchema = {
29
+ project_id: { type: 'string', required: true, validate: uuidValidator },
30
+ status: { type: 'string', validate: taskStatusValidator },
31
+ limit: { type: 'number', default: 50 },
32
+ offset: { type: 'number', default: 0 },
33
+ search_query: { type: 'string' },
34
+ include_subtasks: { type: 'boolean', default: false },
35
+ include_metadata: { type: 'boolean', default: false },
36
+ };
37
+ const getNextTaskSchema = {
38
+ project_id: { type: 'string', required: true, validate: uuidValidator },
39
+ };
40
+ const addTaskSchema = {
41
+ project_id: { type: 'string', required: true, validate: uuidValidator },
42
+ title: { type: 'string', required: true },
43
+ description: { type: 'string' },
44
+ priority: { type: 'number', default: 3, validate: priorityValidator },
45
+ estimated_minutes: { type: 'number', validate: minutesValidator },
46
+ blocking: { type: 'boolean', default: false },
47
+ task_type: { type: 'string', validate: createEnumValidator(VALID_TASK_TYPES) },
48
+ };
49
+ const updateTaskSchema = {
50
+ task_id: { type: 'string', required: true, validate: uuidValidator },
51
+ title: { type: 'string' },
52
+ description: { type: 'string' },
53
+ priority: { type: 'number', validate: priorityValidator },
54
+ status: { type: 'string', validate: taskStatusValidator },
55
+ progress_percentage: { type: 'number', validate: progressValidator },
56
+ progress_note: { type: 'string' },
57
+ estimated_minutes: { type: 'number', validate: minutesValidator },
58
+ git_branch: { type: 'string' },
59
+ worktree_path: { type: 'string' },
60
+ task_type: { type: 'string', validate: createEnumValidator(VALID_TASK_TYPES) },
61
+ skip_worktree_requirement: { type: 'boolean', default: false },
62
+ };
63
+ const completeTaskSchema = {
64
+ task_id: { type: 'string', required: true, validate: uuidValidator },
65
+ summary: { type: 'string' },
66
+ };
67
+ const deleteTaskSchema = {
68
+ task_id: { type: 'string', required: true, validate: uuidValidator },
69
+ };
70
+ const addTaskReferenceSchema = {
71
+ task_id: { type: 'string', required: true, validate: uuidValidator },
72
+ url: { type: 'string', required: true },
73
+ label: { type: 'string' },
74
+ };
75
+ const removeTaskReferenceSchema = {
76
+ task_id: { type: 'string', required: true, validate: uuidValidator },
77
+ url: { type: 'string', required: true },
78
+ };
79
+ const batchUpdateTasksSchema = {
80
+ updates: { type: 'array', required: true },
81
+ };
82
+ const batchCompleteTasksSchema = {
83
+ completions: { type: 'array', required: true },
84
+ };
85
+ const addSubtaskSchema = {
86
+ parent_task_id: { type: 'string', required: true, validate: uuidValidator },
87
+ title: { type: 'string', required: true },
88
+ description: { type: 'string' },
89
+ priority: { type: 'number', validate: priorityValidator },
90
+ estimated_minutes: { type: 'number', validate: minutesValidator },
91
+ };
92
+ const getSubtasksSchema = {
93
+ parent_task_id: { type: 'string', required: true, validate: uuidValidator },
94
+ status: { type: 'string', validate: taskStatusValidator },
95
+ };
20
96
  function getTaskCompleteGitInstructions(gitWorkflow, gitMainBranch, gitDevelopBranch, taskBranch, taskTitle, taskId) {
21
97
  if (gitWorkflow === 'none') {
22
98
  return undefined;
@@ -68,21 +144,18 @@ export function getValidationApprovedGitInstructions(config, taskBranch) {
68
144
  // Task Handlers - Using API Client
69
145
  // ============================================================================
70
146
  export const getTasks = async (args, ctx) => {
71
- const { project_id, status, limit = 50, offset = 0, search_query, include_subtasks = false, include_metadata = false } = args;
72
- validateRequired(project_id, 'project_id');
73
- validateUUID(project_id, 'project_id');
74
- validateTaskStatus(status);
147
+ const { project_id, status, limit, offset, search_query, include_subtasks, include_metadata } = parseArgs(args, getTasksSchema);
75
148
  const api = getApiClient();
76
149
  const response = await api.getTasks(project_id, {
77
150
  status,
78
- limit: Math.min(limit, 200),
151
+ limit: Math.min(limit ?? 50, 200),
79
152
  offset,
80
153
  include_subtasks,
81
154
  search_query,
82
155
  include_metadata,
83
156
  });
84
157
  if (!response.ok) {
85
- throw new Error(`Failed to fetch tasks: ${response.error}`);
158
+ return { result: { error: response.error || 'Failed to fetch tasks' }, isError: true };
86
159
  }
87
160
  return {
88
161
  result: {
@@ -93,13 +166,11 @@ export const getTasks = async (args, ctx) => {
93
166
  };
94
167
  };
95
168
  export const getNextTask = async (args, ctx) => {
96
- const { project_id } = args;
97
- validateRequired(project_id, 'project_id');
98
- validateUUID(project_id, 'project_id');
169
+ const { project_id } = parseArgs(args, getNextTaskSchema);
99
170
  const api = getApiClient();
100
171
  const response = await api.getNextTask(project_id, ctx.session.currentSessionId || undefined);
101
172
  if (!response.ok) {
102
- throw new Error(`Failed to get next task: ${response.error}`);
173
+ return { result: { error: response.error || 'Failed to get next task' }, isError: true };
103
174
  }
104
175
  const data = response.data;
105
176
  if (!data) {
@@ -138,12 +209,7 @@ export const getNextTask = async (args, ctx) => {
138
209
  return { result };
139
210
  };
140
211
  export const addTask = async (args, ctx) => {
141
- const { project_id, title, description, priority = 3, estimated_minutes, blocking = false, task_type } = args;
142
- validateRequired(project_id, 'project_id');
143
- validateRequired(title, 'title');
144
- validateUUID(project_id, 'project_id');
145
- validatePriority(priority);
146
- validateEstimatedMinutes(estimated_minutes);
212
+ const { project_id, title, description, priority, estimated_minutes, blocking, task_type } = parseArgs(args, addTaskSchema);
147
213
  const api = getApiClient();
148
214
  const response = await api.createTask(project_id, {
149
215
  title,
@@ -152,9 +218,10 @@ export const addTask = async (args, ctx) => {
152
218
  estimated_minutes,
153
219
  blocking,
154
220
  session_id: ctx.session.currentSessionId || undefined,
221
+ task_type,
155
222
  });
156
223
  if (!response.ok) {
157
- throw new Error(`Failed to add task: ${response.error}`);
224
+ return { result: { error: response.error || 'Failed to add task' }, isError: true };
158
225
  }
159
226
  const data = response.data;
160
227
  const result = {
@@ -169,13 +236,24 @@ export const addTask = async (args, ctx) => {
169
236
  return { result };
170
237
  };
171
238
  export const updateTask = async (args, ctx) => {
172
- const { task_id, progress_note, ...updates } = args;
173
- validateRequired(task_id, 'task_id');
174
- validateUUID(task_id, 'task_id');
175
- validateTaskStatus(updates.status);
176
- validatePriority(updates.priority);
177
- validateProgressPercentage(updates.progress_percentage);
178
- validateEstimatedMinutes(updates.estimated_minutes);
239
+ 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);
240
+ const updates = { title, description, priority, status, progress_percentage, estimated_minutes, git_branch, worktree_path, task_type };
241
+ // Enforce worktree creation: require git_branch when marking task as in_progress
242
+ // This ensures multi-agent collaboration works properly with isolated worktrees
243
+ if (status === 'in_progress' && !git_branch && !skip_worktree_requirement) {
244
+ return {
245
+ result: {
246
+ error: 'worktree_required',
247
+ message: 'git_branch is required when marking a task as in_progress. Create a worktree first and provide the branch name.',
248
+ 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.',
249
+ worktree_example: {
250
+ command: `git worktree add ../worktree-${task_id.substring(0, 8)} -b feature/${task_id.substring(0, 8)}-task develop`,
251
+ then: `update_task(task_id: "${task_id}", status: "in_progress", git_branch: "feature/${task_id.substring(0, 8)}-task")`,
252
+ },
253
+ skip_option: 'If this project does not use git branching (trunk-based or no git workflow), pass skip_worktree_requirement: true',
254
+ },
255
+ };
256
+ }
179
257
  const api = getApiClient();
180
258
  const response = await api.updateTask(task_id, {
181
259
  ...updates,
@@ -208,7 +286,17 @@ export const updateTask = async (args, ctx) => {
208
286
  },
209
287
  };
210
288
  }
211
- throw new Error(`Failed to update task: ${response.error}`);
289
+ if (response.error?.includes('branch_conflict')) {
290
+ return {
291
+ result: {
292
+ error: 'branch_conflict',
293
+ message: response.error,
294
+ conflicting_task_id: response.data?.conflicting_task_id,
295
+ conflicting_task_title: response.data?.conflicting_task_title,
296
+ },
297
+ };
298
+ }
299
+ return { result: { error: response.error || 'Failed to update task' }, isError: true };
212
300
  }
213
301
  // Build result - include git workflow info when transitioning to in_progress
214
302
  const data = response.data;
@@ -222,28 +310,21 @@ export const updateTask = async (args, ctx) => {
222
310
  if (data?.next_step) {
223
311
  result.next_step = data.next_step;
224
312
  }
225
- // Warn if transitioning to in_progress without git_branch
226
- if (updates.status === 'in_progress' && !updates.git_branch) {
227
- result.warning = 'git_branch not set. For multi-agent collaboration, set git_branch when marking in_progress to track your worktree.';
228
- result.hint = 'Call update_task again with git_branch parameter after creating your worktree.';
229
- }
230
313
  return { result };
231
314
  };
232
315
  export const completeTask = async (args, ctx) => {
233
- const { task_id, summary } = args;
234
- validateRequired(task_id, 'task_id');
235
- validateUUID(task_id, 'task_id');
316
+ const { task_id, summary } = parseArgs(args, completeTaskSchema);
236
317
  const api = getApiClient();
237
318
  const response = await api.completeTask(task_id, {
238
319
  summary,
239
320
  session_id: ctx.session.currentSessionId || undefined,
240
321
  });
241
322
  if (!response.ok) {
242
- throw new Error(`Failed to complete task: ${response.error}`);
323
+ return { result: { error: response.error || 'Failed to complete task' }, isError: true };
243
324
  }
244
325
  const data = response.data;
245
326
  if (!data) {
246
- throw new Error('No response data from complete task');
327
+ return { result: { error: 'No response data from complete task' }, isError: true };
247
328
  }
248
329
  // Build result matching expected format
249
330
  const result = {
@@ -266,28 +347,23 @@ export const completeTask = async (args, ctx) => {
266
347
  return { result };
267
348
  };
268
349
  export const deleteTask = async (args, ctx) => {
269
- const { task_id } = args;
270
- validateRequired(task_id, 'task_id');
271
- validateUUID(task_id, 'task_id');
350
+ const { task_id } = parseArgs(args, deleteTaskSchema);
272
351
  const api = getApiClient();
273
352
  const response = await api.deleteTask(task_id);
274
353
  if (!response.ok) {
275
- throw new Error(`Failed to delete task: ${response.error}`);
354
+ return { result: { error: response.error || 'Failed to delete task' }, isError: true };
276
355
  }
277
356
  return { result: { success: true, deleted_id: task_id } };
278
357
  };
279
358
  export const addTaskReference = async (args, ctx) => {
280
- const { task_id, url, label } = args;
281
- validateRequired(task_id, 'task_id');
282
- validateUUID(task_id, 'task_id');
283
- validateRequired(url, 'url');
359
+ const { task_id, url, label } = parseArgs(args, addTaskReferenceSchema);
284
360
  const api = getApiClient();
285
361
  const response = await api.addTaskReference(task_id, url, label);
286
362
  if (!response.ok) {
287
363
  if (response.error?.includes('already exists')) {
288
364
  return { result: { success: false, error: 'Reference with this URL already exists' } };
289
365
  }
290
- throw new Error(`Failed to add reference: ${response.error}`);
366
+ return { result: { error: response.error || 'Failed to add reference' }, isError: true };
291
367
  }
292
368
  return {
293
369
  result: {
@@ -297,86 +373,74 @@ export const addTaskReference = async (args, ctx) => {
297
373
  };
298
374
  };
299
375
  export const removeTaskReference = async (args, ctx) => {
300
- const { task_id, url } = args;
301
- validateRequired(task_id, 'task_id');
302
- validateUUID(task_id, 'task_id');
303
- validateRequired(url, 'url');
376
+ const { task_id, url } = parseArgs(args, removeTaskReferenceSchema);
304
377
  const api = getApiClient();
305
378
  const response = await api.removeTaskReference(task_id, url);
306
379
  if (!response.ok) {
307
380
  if (response.error?.includes('not found')) {
308
381
  return { result: { success: false, error: 'Reference with this URL not found' } };
309
382
  }
310
- throw new Error(`Failed to remove reference: ${response.error}`);
383
+ return { result: { error: response.error || 'Failed to remove reference' }, isError: true };
311
384
  }
312
385
  return { result: { success: true } };
313
386
  };
314
387
  export const batchUpdateTasks = async (args, ctx) => {
315
- const { updates } = args;
316
- if (!updates || !Array.isArray(updates) || updates.length === 0) {
388
+ const { updates } = parseArgs(args, batchUpdateTasksSchema);
389
+ const typedUpdates = updates;
390
+ if (!Array.isArray(typedUpdates) || typedUpdates.length === 0) {
317
391
  throw new ValidationError('updates must be a non-empty array', {
318
392
  field: 'updates',
319
393
  hint: 'Provide an array of task updates with at least one item',
320
394
  });
321
395
  }
322
- if (updates.length > 50) {
396
+ if (typedUpdates.length > 50) {
323
397
  throw new ValidationError('Too many updates. Maximum is 50 per batch.', {
324
398
  field: 'updates',
325
399
  hint: 'Split your updates into smaller batches',
326
400
  });
327
401
  }
328
- // Validate all inputs first
329
- for (const update of updates) {
330
- validateRequired(update.task_id, 'task_id');
331
- validateUUID(update.task_id, 'task_id');
332
- validateTaskStatus(update.status);
333
- validatePriority(update.priority);
334
- validateProgressPercentage(update.progress_percentage);
335
- }
402
+ // Individual item validation happens at API level
336
403
  const api = getApiClient();
337
- const response = await api.batchUpdateTasks(updates);
404
+ const response = await api.batchUpdateTasks(typedUpdates);
338
405
  if (!response.ok) {
339
- throw new Error(`Failed to batch update tasks: ${response.error}`);
406
+ return { result: { error: response.error || 'Failed to batch update tasks' }, isError: true };
340
407
  }
341
408
  return {
342
409
  result: {
343
410
  success: response.data?.success || false,
344
- total: updates.length,
411
+ total: typedUpdates.length,
345
412
  succeeded: response.data?.updated_count || 0,
346
413
  },
347
414
  };
348
415
  };
349
416
  export const batchCompleteTasks = async (args, ctx) => {
350
- const { completions } = args;
351
- if (!completions || !Array.isArray(completions) || completions.length === 0) {
417
+ const { completions } = parseArgs(args, batchCompleteTasksSchema);
418
+ const typedCompletions = completions;
419
+ if (!Array.isArray(typedCompletions) || typedCompletions.length === 0) {
352
420
  throw new ValidationError('completions must be a non-empty array', {
353
421
  field: 'completions',
354
422
  hint: 'Provide an array of task completions with at least one item',
355
423
  });
356
424
  }
357
- if (completions.length > 50) {
425
+ if (typedCompletions.length > 50) {
358
426
  throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
359
427
  field: 'completions',
360
428
  hint: 'Split your completions into smaller batches',
361
429
  });
362
430
  }
363
- // Validate all inputs first
364
- for (const completion of completions) {
365
- validateRequired(completion.task_id, 'task_id');
366
- validateUUID(completion.task_id, 'task_id');
367
- }
431
+ // Individual item validation happens at API level
368
432
  const api = getApiClient();
369
- const response = await api.batchCompleteTasks(completions);
433
+ const response = await api.batchCompleteTasks(typedCompletions);
370
434
  if (!response.ok) {
371
- throw new Error(`Failed to batch complete tasks: ${response.error}`);
435
+ return { result: { error: response.error || 'Failed to batch complete tasks' }, isError: true };
372
436
  }
373
437
  const data = response.data;
374
438
  return {
375
439
  result: {
376
440
  success: data?.success || false,
377
- total: completions.length,
441
+ total: typedCompletions.length,
378
442
  succeeded: data?.completed_count || 0,
379
- failed: completions.length - (data?.completed_count || 0),
443
+ failed: typedCompletions.length - (data?.completed_count || 0),
380
444
  next_task: data?.next_task,
381
445
  },
382
446
  };
@@ -385,14 +449,7 @@ export const batchCompleteTasks = async (args, ctx) => {
385
449
  // Subtask Handlers
386
450
  // ============================================================================
387
451
  export const addSubtask = async (args, ctx) => {
388
- const { parent_task_id, title, description, priority, estimated_minutes } = args;
389
- validateRequired(parent_task_id, 'parent_task_id');
390
- validateUUID(parent_task_id, 'parent_task_id');
391
- validateRequired(title, 'title');
392
- if (priority !== undefined)
393
- validatePriority(priority);
394
- if (estimated_minutes !== undefined)
395
- validateEstimatedMinutes(estimated_minutes);
452
+ const { parent_task_id, title, description, priority, estimated_minutes } = parseArgs(args, addSubtaskSchema);
396
453
  const api = getApiClient();
397
454
  const response = await api.addSubtask(parent_task_id, {
398
455
  title,
@@ -410,7 +467,7 @@ export const addSubtask = async (args, ctx) => {
410
467
  },
411
468
  };
412
469
  }
413
- throw new Error(`Failed to add subtask: ${response.error}`);
470
+ return { result: { error: response.error || 'Failed to add subtask' }, isError: true };
414
471
  }
415
472
  return {
416
473
  result: {
@@ -421,15 +478,11 @@ export const addSubtask = async (args, ctx) => {
421
478
  };
422
479
  };
423
480
  export const getSubtasks = async (args, ctx) => {
424
- const { parent_task_id, status } = args;
425
- validateRequired(parent_task_id, 'parent_task_id');
426
- validateUUID(parent_task_id, 'parent_task_id');
427
- if (status)
428
- validateTaskStatus(status);
481
+ const { parent_task_id, status } = parseArgs(args, getSubtasksSchema);
429
482
  const api = getApiClient();
430
483
  const response = await api.getSubtasks(parent_task_id, status);
431
484
  if (!response.ok) {
432
- throw new Error(`Failed to fetch subtasks: ${response.error}`);
485
+ return { result: { error: response.error || 'Failed to fetch subtasks' }, isError: true };
433
486
  }
434
487
  return {
435
488
  result: {
@@ -442,6 +495,48 @@ export const getSubtasks = async (args, ctx) => {
442
495
  },
443
496
  };
444
497
  };
498
+ // ============================================================================
499
+ // Worktree Cleanup Handlers
500
+ // ============================================================================
501
+ const getStaleWorktreesSchema = {
502
+ project_id: { type: 'string', required: true, validate: uuidValidator },
503
+ };
504
+ const clearWorktreePathSchema = {
505
+ task_id: { type: 'string', required: true, validate: uuidValidator },
506
+ };
507
+ export const getStaleWorktrees = async (args, ctx) => {
508
+ const { project_id } = parseArgs(args, getStaleWorktreesSchema);
509
+ const api = getApiClient();
510
+ const response = await api.getStaleWorktrees(project_id);
511
+ if (!response.ok) {
512
+ return { result: { error: response.error || 'Failed to get stale worktrees' }, isError: true };
513
+ }
514
+ const data = response.data;
515
+ return {
516
+ result: {
517
+ project_id: data?.project_id,
518
+ project_name: data?.project_name,
519
+ stale_worktrees: data?.stale_worktrees || [],
520
+ count: data?.count || 0,
521
+ cleanup_instructions: data?.cleanup_instructions,
522
+ },
523
+ };
524
+ };
525
+ export const clearWorktreePath = async (args, ctx) => {
526
+ const { task_id } = parseArgs(args, clearWorktreePathSchema);
527
+ const api = getApiClient();
528
+ const response = await api.clearWorktreePath(task_id);
529
+ if (!response.ok) {
530
+ return { result: { error: response.error || 'Failed to clear worktree path' }, isError: true };
531
+ }
532
+ return {
533
+ result: {
534
+ success: true,
535
+ task_id,
536
+ message: 'Worktree path cleared. The worktree can now be safely removed if not already done.',
537
+ },
538
+ };
539
+ };
445
540
  /**
446
541
  * Task handlers registry
447
542
  */
@@ -459,4 +554,7 @@ export const taskHandlers = {
459
554
  // Subtask handlers
460
555
  add_subtask: addSubtask,
461
556
  get_subtasks: getSubtasks,
557
+ // Worktree cleanup handlers
558
+ get_stale_worktrees: getStaleWorktrees,
559
+ clear_worktree_path: clearWorktreePath,
462
560
  };
@@ -82,9 +82,40 @@ export interface HandlerContext {
82
82
  extractProjectNameFromGitUrl?: (gitUrl: string) => string;
83
83
  }
84
84
  /**
85
- * Result returned by handlers
85
+ * Success result with typed data
86
86
  */
87
- export interface HandlerResult {
87
+ export interface SuccessResult<T = unknown> {
88
+ result: T;
89
+ content?: Array<{
90
+ type: string;
91
+ text: string;
92
+ }>;
93
+ isError?: false;
94
+ }
95
+ /**
96
+ * Error result with error information
97
+ */
98
+ export interface ErrorResult {
99
+ result: {
100
+ error: string;
101
+ [key: string]: unknown;
102
+ };
103
+ content?: Array<{
104
+ type: string;
105
+ text: string;
106
+ }>;
107
+ isError: true;
108
+ }
109
+ /**
110
+ * Result returned by handlers - discriminated union for type safety
111
+ * Use the helper functions success() and error() to create properly typed results.
112
+ */
113
+ export type HandlerResult<T = unknown> = SuccessResult<T> | ErrorResult;
114
+ /**
115
+ * Legacy HandlerResult interface for backward compatibility
116
+ * @deprecated Use HandlerResult<T> discriminated union instead
117
+ */
118
+ export interface LegacyHandlerResult {
88
119
  result?: unknown;
89
120
  content?: Array<{
90
121
  type: string;
@@ -92,6 +123,37 @@ export interface HandlerResult {
92
123
  }>;
93
124
  isError?: boolean;
94
125
  }
126
+ /**
127
+ * Create a success result with typed data
128
+ * @example
129
+ * return success({ tasks: data, total_count: count });
130
+ */
131
+ export declare function success<T>(data: T): SuccessResult<T>;
132
+ /**
133
+ * Create an error result
134
+ * @example
135
+ * return error('Task not found');
136
+ * return error('Validation failed', { field: 'title', reason: 'too long' });
137
+ */
138
+ export declare function error(message: string, details?: Record<string, unknown>): ErrorResult;
139
+ /**
140
+ * Check if a handler result is a success (not an error)
141
+ * @example
142
+ * const result = await handler(args, ctx);
143
+ * if (isSuccess(result)) {
144
+ * console.log(result.result); // typed as T
145
+ * }
146
+ */
147
+ export declare function isSuccess<T>(result: HandlerResult<T>): result is SuccessResult<T>;
148
+ /**
149
+ * Check if a handler result is an error
150
+ * @example
151
+ * const result = await handler(args, ctx);
152
+ * if (isError(result)) {
153
+ * console.log(result.result.error); // string
154
+ * }
155
+ */
156
+ export declare function isError(result: HandlerResult<unknown>): result is ErrorResult;
95
157
  /**
96
158
  * Handler function type
97
159
  */
@@ -1 +1,48 @@
1
- export {};
1
+ // ============================================================================
2
+ // Result Factory Functions - use these for type-safe handler results
3
+ // ============================================================================
4
+ /**
5
+ * Create a success result with typed data
6
+ * @example
7
+ * return success({ tasks: data, total_count: count });
8
+ */
9
+ export function success(data) {
10
+ return { result: data };
11
+ }
12
+ /**
13
+ * Create an error result
14
+ * @example
15
+ * return error('Task not found');
16
+ * return error('Validation failed', { field: 'title', reason: 'too long' });
17
+ */
18
+ export function error(message, details) {
19
+ return {
20
+ result: { error: message, ...details },
21
+ isError: true
22
+ };
23
+ }
24
+ // ============================================================================
25
+ // Type Predicates - use for runtime type narrowing
26
+ // ============================================================================
27
+ /**
28
+ * Check if a handler result is a success (not an error)
29
+ * @example
30
+ * const result = await handler(args, ctx);
31
+ * if (isSuccess(result)) {
32
+ * console.log(result.result); // typed as T
33
+ * }
34
+ */
35
+ export function isSuccess(result) {
36
+ return !result.isError;
37
+ }
38
+ /**
39
+ * Check if a handler result is an error
40
+ * @example
41
+ * const result = await handler(args, ctx);
42
+ * if (isError(result)) {
43
+ * console.log(result.result.error); // string
44
+ * }
45
+ */
46
+ export function isError(result) {
47
+ return result.isError === true;
48
+ }
@@ -6,39 +6,43 @@
6
6
  * - claim_validation
7
7
  * - validate_task
8
8
  */
9
- import { validateRequired, validateUUID } from '../validators.js';
9
+ import { parseArgs, uuidValidator } from '../validators.js';
10
10
  import { getApiClient } from '../api-client.js';
11
- export const getTasksAwaitingValidation = async (args, ctx) => {
12
- const { project_id } = args;
13
- validateRequired(project_id, 'project_id');
14
- validateUUID(project_id, 'project_id');
11
+ // Argument schemas for type-safe parsing
12
+ const getTasksAwaitingValidationSchema = {
13
+ project_id: { type: 'string', required: true, validate: uuidValidator },
14
+ };
15
+ const claimValidationSchema = {
16
+ task_id: { type: 'string', required: true, validate: uuidValidator },
17
+ };
18
+ const validateTaskSchema = {
19
+ task_id: { type: 'string', required: true, validate: uuidValidator },
20
+ approved: { type: 'boolean', required: true },
21
+ validation_notes: { type: 'string' },
22
+ skip_pr_check: { type: 'boolean' },
23
+ };
24
+ export const getTasksAwaitingValidation = async (args, _ctx) => {
25
+ const { project_id } = parseArgs(args, getTasksAwaitingValidationSchema);
15
26
  const apiClient = getApiClient();
16
27
  const response = await apiClient.getTasksAwaitingValidation(project_id);
17
28
  if (!response.ok) {
18
- throw new Error(response.error || 'Failed to fetch tasks awaiting validation');
29
+ return { result: { error: response.error || 'Failed to fetch tasks awaiting validation' }, isError: true };
19
30
  }
20
31
  return { result: response.data };
21
32
  };
22
33
  export const claimValidation = async (args, ctx) => {
23
- const { task_id } = args;
24
- validateRequired(task_id, 'task_id');
25
- validateUUID(task_id, 'task_id');
34
+ const { task_id } = parseArgs(args, claimValidationSchema);
26
35
  const { session } = ctx;
27
36
  const currentSessionId = session.currentSessionId;
28
37
  const apiClient = getApiClient();
29
38
  const response = await apiClient.claimValidation(task_id, currentSessionId || undefined);
30
39
  if (!response.ok) {
31
- throw new Error(response.error || 'Failed to claim task for validation');
40
+ return { result: { error: response.error || 'Failed to claim task for validation' }, isError: true };
32
41
  }
33
42
  return { result: response.data };
34
43
  };
35
44
  export const validateTask = async (args, ctx) => {
36
- const { task_id, validation_notes, approved, skip_pr_check } = args;
37
- validateRequired(task_id, 'task_id');
38
- validateUUID(task_id, 'task_id');
39
- if (approved === undefined) {
40
- throw new Error('approved is required');
41
- }
45
+ const { task_id, approved, validation_notes, skip_pr_check } = parseArgs(args, validateTaskSchema);
42
46
  const { session } = ctx;
43
47
  const currentSessionId = session.currentSessionId;
44
48
  const apiClient = getApiClient();
@@ -59,7 +63,7 @@ export const validateTask = async (args, ctx) => {
59
63
  },
60
64
  };
61
65
  }
62
- throw new Error(response.error || 'Failed to validate task');
66
+ return { result: { error: response.error || 'Failed to validate task' }, isError: true };
63
67
  }
64
68
  return { result: response.data };
65
69
  };