@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
@@ -16,18 +16,92 @@
16
16
  */
17
17
 
18
18
  import type { Handler, HandlerRegistry } from './types.js';
19
- import { validateRequired, validateUUID, validateEnum } from '../validators.js';
19
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
20
20
  import { getApiClient } from '../api-client.js';
21
21
 
22
- type SprintStatus = 'planning' | 'active' | 'in_review' | 'retrospective' | 'completed' | 'cancelled';
23
- type TaskPhase = 'pre' | 'core' | 'post';
24
- type DeployEnvironment = 'development' | 'staging' | 'production';
25
- type VersionBump = 'patch' | 'minor' | 'major';
22
+ const SPRINT_STATUSES = ['planning', 'active', 'in_review', 'retrospective', 'completed', 'cancelled'] as const;
23
+ const TASK_PHASES = ['pre', 'core', 'post'] as const;
24
+ const DEPLOY_ENVIRONMENTS = ['development', 'staging', 'production'] as const;
25
+ const VERSION_BUMPS = ['patch', 'minor', 'major'] as const;
26
+
27
+ type SprintStatus = typeof SPRINT_STATUSES[number];
28
+ type TaskPhase = typeof TASK_PHASES[number];
29
+ type DeployEnvironment = typeof DEPLOY_ENVIRONMENTS[number];
30
+ type VersionBump = typeof VERSION_BUMPS[number];
31
+
32
+ // ============================================================================
33
+ // Argument Schemas
34
+ // ============================================================================
35
+
36
+ const createSprintSchema = {
37
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
38
+ title: { type: 'string' as const, required: true as const },
39
+ goal: { type: 'string' as const },
40
+ start_date: { type: 'string' as const, required: true as const },
41
+ end_date: { type: 'string' as const, required: true as const },
42
+ auto_deploy_on_completion: { type: 'boolean' as const },
43
+ deploy_environment: { type: 'string' as const, validate: createEnumValidator(DEPLOY_ENVIRONMENTS) },
44
+ deploy_version_bump: { type: 'string' as const, validate: createEnumValidator(VERSION_BUMPS) },
45
+ };
46
+
47
+ const updateSprintSchema = {
48
+ sprint_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
49
+ title: { type: 'string' as const },
50
+ goal: { type: 'string' as const },
51
+ start_date: { type: 'string' as const },
52
+ end_date: { type: 'string' as const },
53
+ auto_deploy_on_completion: { type: 'boolean' as const },
54
+ deploy_environment: { type: 'string' as const, validate: createEnumValidator(DEPLOY_ENVIRONMENTS) },
55
+ deploy_version_bump: { type: 'string' as const, validate: createEnumValidator(VERSION_BUMPS) },
56
+ };
57
+
58
+ const getSprintSchema = {
59
+ sprint_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
60
+ summary_only: { type: 'boolean' as const, default: false },
61
+ };
62
+
63
+ const getSprintsSchema = {
64
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
65
+ status: { type: 'string' as const, validate: createEnumValidator(SPRINT_STATUSES) },
66
+ limit: { type: 'number' as const, default: 20 },
67
+ offset: { type: 'number' as const, default: 0 },
68
+ };
69
+
70
+ const deleteSprintSchema = {
71
+ sprint_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
72
+ };
73
+
74
+ const startSprintSchema = {
75
+ sprint_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
76
+ };
77
+
78
+ const completeSprintSchema = {
79
+ sprint_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
80
+ retrospective_notes: { type: 'string' as const },
81
+ skip_retrospective: { type: 'boolean' as const },
82
+ };
83
+
84
+ const addTaskToSprintSchema = {
85
+ sprint_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
86
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
87
+ story_points: { type: 'number' as const },
88
+ phase: { type: 'string' as const, validate: createEnumValidator(TASK_PHASES) },
89
+ };
90
+
91
+ const removeTaskFromSprintSchema = {
92
+ sprint_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
93
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
94
+ };
95
+
96
+ const getSprintBacklogSchema = {
97
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
98
+ sprint_id: { type: 'string' as const, validate: uuidValidator },
99
+ };
26
100
 
27
- const SPRINT_STATUSES: SprintStatus[] = ['planning', 'active', 'in_review', 'retrospective', 'completed', 'cancelled'];
28
- const TASK_PHASES: TaskPhase[] = ['pre', 'core', 'post'];
29
- const DEPLOY_ENVIRONMENTS: DeployEnvironment[] = ['development', 'staging', 'production'];
30
- const VERSION_BUMPS: VersionBump[] = ['patch', 'minor', 'major'];
101
+ const getSprintVelocitySchema = {
102
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
103
+ limit: { type: 'number' as const, default: 10 },
104
+ };
31
105
 
32
106
  export const createSprint: Handler = async (args, ctx) => {
33
107
  const {
@@ -39,41 +113,19 @@ export const createSprint: Handler = async (args, ctx) => {
39
113
  auto_deploy_on_completion,
40
114
  deploy_environment,
41
115
  deploy_version_bump,
42
- } = args as {
43
- project_id: string;
44
- title: string;
45
- goal?: string;
46
- start_date: string;
47
- end_date: string;
48
- auto_deploy_on_completion?: boolean;
49
- deploy_environment?: DeployEnvironment;
50
- deploy_version_bump?: VersionBump;
51
- };
52
-
53
- validateRequired(project_id, 'project_id');
54
- validateUUID(project_id, 'project_id');
55
- validateRequired(title, 'title');
56
- validateRequired(start_date, 'start_date');
57
- validateRequired(end_date, 'end_date');
58
-
59
- if (deploy_environment) {
60
- validateEnum(deploy_environment, DEPLOY_ENVIRONMENTS, 'deploy_environment');
61
- }
62
- if (deploy_version_bump) {
63
- validateEnum(deploy_version_bump, VERSION_BUMPS, 'deploy_version_bump');
64
- }
116
+ } = parseArgs(args, createSprintSchema);
65
117
 
66
118
  // Validate date format and order
67
119
  const startDateObj = new Date(start_date);
68
120
  const endDateObj = new Date(end_date);
69
121
  if (isNaN(startDateObj.getTime())) {
70
- 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 };
71
123
  }
72
124
  if (isNaN(endDateObj.getTime())) {
73
- 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 };
74
126
  }
75
127
  if (endDateObj < startDateObj) {
76
- 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 };
77
129
  }
78
130
 
79
131
  const { session } = ctx;
@@ -99,7 +151,7 @@ export const createSprint: Handler = async (args, ctx) => {
99
151
  });
100
152
 
101
153
  if (!response.ok) {
102
- throw new Error(`Failed to create sprint: ${response.error}`);
154
+ return { result: { error: response.error || 'Failed to create sprint' }, isError: true };
103
155
  }
104
156
 
105
157
  return {
@@ -126,38 +178,19 @@ export const updateSprint: Handler = async (args, ctx) => {
126
178
  auto_deploy_on_completion,
127
179
  deploy_environment,
128
180
  deploy_version_bump,
129
- } = args as {
130
- sprint_id: string;
131
- title?: string;
132
- goal?: string;
133
- start_date?: string;
134
- end_date?: string;
135
- auto_deploy_on_completion?: boolean;
136
- deploy_environment?: DeployEnvironment;
137
- deploy_version_bump?: VersionBump;
138
- };
139
-
140
- validateRequired(sprint_id, 'sprint_id');
141
- validateUUID(sprint_id, 'sprint_id');
142
-
143
- if (deploy_environment) {
144
- validateEnum(deploy_environment, DEPLOY_ENVIRONMENTS, 'deploy_environment');
145
- }
146
- if (deploy_version_bump) {
147
- validateEnum(deploy_version_bump, VERSION_BUMPS, 'deploy_version_bump');
148
- }
181
+ } = parseArgs(args, updateSprintSchema);
149
182
 
150
183
  // Validate dates if provided
151
184
  if (start_date) {
152
185
  const startDateObj = new Date(start_date);
153
186
  if (isNaN(startDateObj.getTime())) {
154
- 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 };
155
188
  }
156
189
  }
157
190
  if (end_date) {
158
191
  const endDateObj = new Date(end_date);
159
192
  if (isNaN(endDateObj.getTime())) {
160
- 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 };
161
194
  }
162
195
  }
163
196
 
@@ -175,17 +208,14 @@ export const updateSprint: Handler = async (args, ctx) => {
175
208
  });
176
209
 
177
210
  if (!response.ok) {
178
- throw new Error(`Failed to update sprint: ${response.error}`);
211
+ return { result: { error: response.error || 'Failed to update sprint' }, isError: true };
179
212
  }
180
213
 
181
214
  return { result: { success: true, sprint_id } };
182
215
  };
183
216
 
184
217
  export const getSprint: Handler = async (args, ctx) => {
185
- const { sprint_id, summary_only = false } = args as { sprint_id: string; summary_only?: boolean };
186
-
187
- validateRequired(sprint_id, 'sprint_id');
188
- validateUUID(sprint_id, 'sprint_id');
218
+ const { sprint_id, summary_only } = parseArgs(args, getSprintSchema);
189
219
 
190
220
  const apiClient = getApiClient();
191
221
 
@@ -220,26 +250,14 @@ export const getSprint: Handler = async (args, ctx) => {
220
250
  }>('get_sprint', { sprint_id, summary_only });
221
251
 
222
252
  if (!response.ok) {
223
- throw new Error(`Failed to get sprint: ${response.error}`);
253
+ return { result: { error: response.error || 'Failed to get sprint' }, isError: true };
224
254
  }
225
255
 
226
256
  return { result: response.data };
227
257
  };
228
258
 
229
259
  export const getSprints: Handler = async (args, ctx) => {
230
- const { project_id, status, limit = 20, offset = 0 } = args as {
231
- project_id: string;
232
- status?: SprintStatus;
233
- limit?: number;
234
- offset?: number;
235
- };
236
-
237
- validateRequired(project_id, 'project_id');
238
- validateUUID(project_id, 'project_id');
239
-
240
- if (status) {
241
- validateEnum(status, SPRINT_STATUSES, 'status');
242
- }
260
+ const { project_id, status, limit, offset } = parseArgs(args, getSprintsSchema);
243
261
 
244
262
  const apiClient = getApiClient();
245
263
 
@@ -261,22 +279,19 @@ export const getSprints: Handler = async (args, ctx) => {
261
279
  }>('get_sprints', {
262
280
  project_id,
263
281
  status,
264
- limit: Math.min(limit, 100),
282
+ limit: Math.min(limit ?? 20, 100),
265
283
  offset,
266
284
  });
267
285
 
268
286
  if (!response.ok) {
269
- throw new Error(`Failed to fetch sprints: ${response.error}`);
287
+ return { result: { error: response.error || 'Failed to fetch sprints' }, isError: true };
270
288
  }
271
289
 
272
290
  return { result: response.data };
273
291
  };
274
292
 
275
293
  export const deleteSprint: Handler = async (args, ctx) => {
276
- const { sprint_id } = args as { sprint_id: string };
277
-
278
- validateRequired(sprint_id, 'sprint_id');
279
- validateUUID(sprint_id, 'sprint_id');
294
+ const { sprint_id } = parseArgs(args, deleteSprintSchema);
280
295
 
281
296
  const apiClient = getApiClient();
282
297
 
@@ -285,17 +300,14 @@ export const deleteSprint: Handler = async (args, ctx) => {
285
300
  });
286
301
 
287
302
  if (!response.ok) {
288
- throw new Error(`Failed to delete sprint: ${response.error}`);
303
+ return { result: { error: response.error || 'Failed to delete sprint' }, isError: true };
289
304
  }
290
305
 
291
306
  return { result: { success: true, message: 'Sprint deleted. Tasks are preserved.' } };
292
307
  };
293
308
 
294
309
  export const startSprint: Handler = async (args, ctx) => {
295
- const { sprint_id } = args as { sprint_id: string };
296
-
297
- validateRequired(sprint_id, 'sprint_id');
298
- validateUUID(sprint_id, 'sprint_id');
310
+ const { sprint_id } = parseArgs(args, startSprintSchema);
299
311
 
300
312
  const apiClient = getApiClient();
301
313
 
@@ -308,21 +320,14 @@ export const startSprint: Handler = async (args, ctx) => {
308
320
  }>('start_sprint', { sprint_id });
309
321
 
310
322
  if (!response.ok) {
311
- throw new Error(`Failed to start sprint: ${response.error}`);
323
+ return { result: { error: response.error || 'Failed to start sprint' }, isError: true };
312
324
  }
313
325
 
314
326
  return { result: response.data };
315
327
  };
316
328
 
317
329
  export const completeSprint: Handler = async (args, ctx) => {
318
- const { sprint_id, retrospective_notes, skip_retrospective } = args as {
319
- sprint_id: string;
320
- retrospective_notes?: string;
321
- skip_retrospective?: boolean;
322
- };
323
-
324
- validateRequired(sprint_id, 'sprint_id');
325
- validateUUID(sprint_id, 'sprint_id');
330
+ const { sprint_id, retrospective_notes, skip_retrospective } = parseArgs(args, completeSprintSchema);
326
331
 
327
332
  const apiClient = getApiClient();
328
333
 
@@ -341,31 +346,17 @@ export const completeSprint: Handler = async (args, ctx) => {
341
346
  });
342
347
 
343
348
  if (!response.ok) {
344
- throw new Error(`Failed to complete sprint: ${response.error}`);
349
+ return { result: { error: response.error || 'Failed to complete sprint' }, isError: true };
345
350
  }
346
351
 
347
352
  return { result: response.data };
348
353
  };
349
354
 
350
355
  export const addTaskToSprint: Handler = async (args, ctx) => {
351
- const { sprint_id, task_id, story_points, phase } = args as {
352
- sprint_id: string;
353
- task_id: string;
354
- story_points?: number;
355
- phase?: TaskPhase;
356
- };
357
-
358
- validateRequired(sprint_id, 'sprint_id');
359
- validateUUID(sprint_id, 'sprint_id');
360
- validateRequired(task_id, 'task_id');
361
- validateUUID(task_id, 'task_id');
362
-
363
- if (phase) {
364
- validateEnum(phase, TASK_PHASES, 'phase');
365
- }
356
+ const { sprint_id, task_id, story_points, phase } = parseArgs(args, addTaskToSprintSchema);
366
357
 
367
358
  if (story_points !== undefined && (story_points < 0 || !Number.isInteger(story_points))) {
368
- 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 };
369
360
  }
370
361
 
371
362
  const apiClient = getApiClient();
@@ -385,22 +376,14 @@ export const addTaskToSprint: Handler = async (args, ctx) => {
385
376
  });
386
377
 
387
378
  if (!response.ok) {
388
- 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 };
389
380
  }
390
381
 
391
382
  return { result: response.data };
392
383
  };
393
384
 
394
385
  export const removeTaskFromSprint: Handler = async (args, ctx) => {
395
- const { sprint_id, task_id } = args as {
396
- sprint_id: string;
397
- task_id: string;
398
- };
399
-
400
- validateRequired(sprint_id, 'sprint_id');
401
- validateUUID(sprint_id, 'sprint_id');
402
- validateRequired(task_id, 'task_id');
403
- validateUUID(task_id, 'task_id');
386
+ const { sprint_id, task_id } = parseArgs(args, removeTaskFromSprintSchema);
404
387
 
405
388
  const apiClient = getApiClient();
406
389
 
@@ -416,24 +399,14 @@ export const removeTaskFromSprint: Handler = async (args, ctx) => {
416
399
  });
417
400
 
418
401
  if (!response.ok) {
419
- 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 };
420
403
  }
421
404
 
422
405
  return { result: response.data };
423
406
  };
424
407
 
425
408
  export const getSprintBacklog: Handler = async (args, ctx) => {
426
- const { project_id, sprint_id } = args as {
427
- project_id: string;
428
- sprint_id?: string;
429
- };
430
-
431
- validateRequired(project_id, 'project_id');
432
- validateUUID(project_id, 'project_id');
433
-
434
- if (sprint_id) {
435
- validateUUID(sprint_id, 'sprint_id');
436
- }
409
+ const { project_id, sprint_id } = parseArgs(args, getSprintBacklogSchema);
437
410
 
438
411
  const apiClient = getApiClient();
439
412
 
@@ -453,20 +426,14 @@ export const getSprintBacklog: Handler = async (args, ctx) => {
453
426
  });
454
427
 
455
428
  if (!response.ok) {
456
- throw new Error(`Failed to get sprint backlog: ${response.error}`);
429
+ return { result: { error: response.error || 'Failed to get sprint backlog' }, isError: true };
457
430
  }
458
431
 
459
432
  return { result: response.data };
460
433
  };
461
434
 
462
435
  export const getSprintVelocity: Handler = async (args, ctx) => {
463
- const { project_id, limit = 10 } = args as {
464
- project_id: string;
465
- limit?: number;
466
- };
467
-
468
- validateRequired(project_id, 'project_id');
469
- validateUUID(project_id, 'project_id');
436
+ const { project_id, limit } = parseArgs(args, getSprintVelocitySchema);
470
437
 
471
438
  const apiClient = getApiClient();
472
439
 
@@ -482,11 +449,11 @@ export const getSprintVelocity: Handler = async (args, ctx) => {
482
449
  total_sprints: number;
483
450
  }>('get_sprint_velocity', {
484
451
  project_id,
485
- limit: Math.min(limit, 50),
452
+ limit: Math.min(limit ?? 10, 50),
486
453
  });
487
454
 
488
455
  if (!response.ok) {
489
- throw new Error(`Failed to get sprint velocity: ${response.error}`);
456
+ return { result: { error: response.error || 'Failed to get sprint velocity' }, isError: true };
490
457
  }
491
458
 
492
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