@vibescope/mcp-server 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/dist/api-client.d.ts +120 -2
  3. package/dist/api-client.js +51 -5
  4. package/dist/handlers/bodies-of-work.js +84 -50
  5. package/dist/handlers/cost.js +62 -54
  6. package/dist/handlers/decisions.js +29 -16
  7. package/dist/handlers/deployment.js +114 -107
  8. package/dist/handlers/discovery.d.ts +3 -0
  9. package/dist/handlers/discovery.js +55 -657
  10. package/dist/handlers/fallback.js +42 -28
  11. package/dist/handlers/file-checkouts.d.ts +18 -0
  12. package/dist/handlers/file-checkouts.js +101 -0
  13. package/dist/handlers/findings.d.ts +14 -1
  14. package/dist/handlers/findings.js +104 -28
  15. package/dist/handlers/git-issues.js +36 -32
  16. package/dist/handlers/ideas.js +44 -26
  17. package/dist/handlers/index.d.ts +2 -0
  18. package/dist/handlers/index.js +6 -0
  19. package/dist/handlers/milestones.js +34 -27
  20. package/dist/handlers/organizations.js +86 -78
  21. package/dist/handlers/progress.js +22 -11
  22. package/dist/handlers/project.js +62 -22
  23. package/dist/handlers/requests.js +15 -11
  24. package/dist/handlers/roles.d.ts +18 -0
  25. package/dist/handlers/roles.js +130 -0
  26. package/dist/handlers/session.js +52 -15
  27. package/dist/handlers/sprints.js +78 -65
  28. package/dist/handlers/tasks.js +135 -74
  29. package/dist/handlers/tool-docs.d.ts +4 -3
  30. package/dist/handlers/tool-docs.js +252 -5
  31. package/dist/handlers/validation.js +30 -14
  32. package/dist/index.js +25 -7
  33. package/dist/tools.js +417 -4
  34. package/package.json +1 -1
  35. package/src/api-client.ts +161 -8
  36. package/src/handlers/__test-setup__.ts +12 -0
  37. package/src/handlers/bodies-of-work.ts +127 -111
  38. package/src/handlers/cost.test.ts +34 -44
  39. package/src/handlers/cost.ts +77 -92
  40. package/src/handlers/decisions.test.ts +3 -2
  41. package/src/handlers/decisions.ts +32 -27
  42. package/src/handlers/deployment.ts +144 -190
  43. package/src/handlers/discovery.test.ts +4 -5
  44. package/src/handlers/discovery.ts +60 -746
  45. package/src/handlers/fallback.test.ts +78 -0
  46. package/src/handlers/fallback.ts +51 -38
  47. package/src/handlers/file-checkouts.test.ts +477 -0
  48. package/src/handlers/file-checkouts.ts +127 -0
  49. package/src/handlers/findings.test.ts +274 -2
  50. package/src/handlers/findings.ts +123 -57
  51. package/src/handlers/git-issues.ts +40 -80
  52. package/src/handlers/ideas.ts +56 -54
  53. package/src/handlers/index.ts +6 -0
  54. package/src/handlers/milestones.test.ts +1 -1
  55. package/src/handlers/milestones.ts +47 -45
  56. package/src/handlers/organizations.ts +104 -129
  57. package/src/handlers/progress.ts +24 -22
  58. package/src/handlers/project.ts +89 -57
  59. package/src/handlers/requests.ts +18 -14
  60. package/src/handlers/roles.test.ts +303 -0
  61. package/src/handlers/roles.ts +208 -0
  62. package/src/handlers/session.test.ts +37 -2
  63. package/src/handlers/session.ts +64 -21
  64. package/src/handlers/sprints.ts +114 -134
  65. package/src/handlers/tasks.test.ts +61 -0
  66. package/src/handlers/tasks.ts +170 -139
  67. package/src/handlers/tool-docs.ts +1024 -0
  68. package/src/handlers/validation.test.ts +53 -1
  69. package/src/handlers/validation.ts +32 -21
  70. package/src/index.ts +25 -7
  71. package/src/tools.ts +417 -4
  72. package/dist/config/tool-categories.d.ts +0 -31
  73. package/dist/config/tool-categories.js +0 -253
  74. package/dist/knowledge.d.ts +0 -6
  75. package/dist/knowledge.js +0 -218
  76. package/src/knowledge.ts +0 -230
@@ -10,23 +10,47 @@
10
10
  */
11
11
 
12
12
  import type { Handler, HandlerRegistry, TokenUsage } from './types.js';
13
- import { KNOWLEDGE_BASE } from '../knowledge.js';
13
+ import { parseArgs, createEnumValidator } from '../validators.js';
14
14
  import { getApiClient } from '../api-client.js';
15
15
 
16
+ const VALID_MODES = ['lite', 'full'] as const;
17
+ const VALID_MODELS = ['opus', 'sonnet', 'haiku'] as const;
18
+ const VALID_ROLES = ['developer', 'validator', 'deployer', 'reviewer', 'maintainer'] as const;
19
+
20
+ type SessionMode = typeof VALID_MODES[number];
21
+ type SessionModel = typeof VALID_MODELS[number];
22
+ type SessionRole = typeof VALID_ROLES[number];
23
+
24
+ // Argument schemas for type-safe parsing
25
+ const startWorkSessionSchema = {
26
+ project_id: { type: 'string' as const },
27
+ git_url: { type: 'string' as const },
28
+ mode: { type: 'string' as const, default: 'lite', validate: createEnumValidator(VALID_MODES) },
29
+ model: { type: 'string' as const, validate: createEnumValidator(VALID_MODELS) },
30
+ role: { type: 'string' as const, default: 'developer', validate: createEnumValidator(VALID_ROLES) },
31
+ };
32
+
33
+ const heartbeatSchema = {
34
+ session_id: { type: 'string' as const },
35
+ current_worktree_path: { type: 'string' as const },
36
+ };
37
+
38
+ const endWorkSessionSchema = {
39
+ session_id: { type: 'string' as const },
40
+ };
41
+
42
+ const getHelpSchema = {
43
+ topic: { type: 'string' as const, required: true as const },
44
+ };
45
+
16
46
  export const startWorkSession: Handler = async (args, ctx) => {
17
- const { project_id, git_url, mode = 'lite', model, role = 'developer' } = args as {
18
- project_id?: string;
19
- git_url?: string;
20
- mode?: 'lite' | 'full';
21
- model?: 'opus' | 'sonnet' | 'haiku';
22
- role?: 'developer' | 'validator' | 'deployer' | 'reviewer' | 'maintainer';
23
- };
47
+ const { project_id, git_url, mode, model, role } = parseArgs(args, startWorkSessionSchema);
24
48
 
25
49
  const { session, updateSession } = ctx;
26
50
 
27
51
  // Reset token tracking for new session with model info
28
52
  const normalizedModel = model ? model.toLowerCase().replace(/^claude[- ]*/i, '') : null;
29
- const validModel = normalizedModel && ['opus', 'sonnet', 'haiku'].includes(normalizedModel)
53
+ const validModel = normalizedModel && VALID_MODELS.includes(normalizedModel as SessionModel)
30
54
  ? normalizedModel
31
55
  : null;
32
56
 
@@ -53,9 +77,9 @@ export const startWorkSession: Handler = async (args, ctx) => {
53
77
  const response = await apiClient.startSession({
54
78
  project_id,
55
79
  git_url,
56
- mode,
57
- model,
58
- role
80
+ mode: mode as SessionMode,
81
+ model: model as SessionModel | undefined,
82
+ role: role as SessionRole
59
83
  });
60
84
 
61
85
  if (!response.ok) {
@@ -140,7 +164,7 @@ export const startWorkSession: Handler = async (args, ctx) => {
140
164
  };
141
165
 
142
166
  export const heartbeat: Handler = async (args, ctx) => {
143
- const { session_id } = args as { session_id?: string };
167
+ const { session_id, current_worktree_path } = parseArgs(args, heartbeatSchema);
144
168
  const { session } = ctx;
145
169
  const targetSession = session_id || session.currentSessionId;
146
170
 
@@ -154,8 +178,10 @@ export const heartbeat: Handler = async (args, ctx) => {
154
178
 
155
179
  const apiClient = getApiClient();
156
180
 
157
- // Send heartbeat
158
- const heartbeatResponse = await apiClient.heartbeat(targetSession);
181
+ // Send heartbeat with optional worktree path
182
+ const heartbeatResponse = await apiClient.heartbeat(targetSession, {
183
+ current_worktree_path,
184
+ });
159
185
 
160
186
  if (!heartbeatResponse.ok) {
161
187
  return {
@@ -182,7 +208,7 @@ export const heartbeat: Handler = async (args, ctx) => {
182
208
  };
183
209
 
184
210
  export const endWorkSession: Handler = async (args, ctx) => {
185
- const { session_id } = args as { session_id?: string };
211
+ const { session_id } = parseArgs(args, endWorkSessionSchema);
186
212
  const { session, updateSession } = ctx;
187
213
  const targetSession = session_id || session.currentSessionId;
188
214
 
@@ -247,19 +273,36 @@ export const endWorkSession: Handler = async (args, ctx) => {
247
273
  };
248
274
 
249
275
  export const getHelp: Handler = async (args, _ctx) => {
250
- const { topic } = args as { topic: string };
276
+ const { topic } = parseArgs(args, getHelpSchema);
277
+
278
+ const apiClient = getApiClient();
279
+ const response = await apiClient.getHelpTopic(topic);
280
+
281
+ if (!response.ok) {
282
+ // If database fetch fails, return error
283
+ return {
284
+ result: {
285
+ error: response.error || `Failed to fetch help topic: ${topic}`,
286
+ },
287
+ };
288
+ }
289
+
290
+ if (!response.data) {
291
+ // Topic not found - fetch available topics
292
+ const topicsResponse = await apiClient.getHelpTopics();
293
+ const available = topicsResponse.ok && topicsResponse.data
294
+ ? topicsResponse.data.map(t => t.slug)
295
+ : ['getting_started', 'tasks', 'validation', 'deployment', 'git', 'blockers', 'milestones', 'fallback', 'session', 'tokens', 'sprints', 'topics'];
251
296
 
252
- const content = KNOWLEDGE_BASE[topic];
253
- if (!content) {
254
297
  return {
255
298
  result: {
256
299
  error: `Unknown topic: ${topic}`,
257
- available: Object.keys(KNOWLEDGE_BASE),
300
+ available,
258
301
  },
259
302
  };
260
303
  }
261
304
 
262
- return { result: { topic, content } };
305
+ return { result: { topic, content: response.data.content } };
263
306
  };
264
307
 
265
308
  // Model pricing rates (USD per 1M tokens)
@@ -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,29 +113,7 @@ 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);
@@ -126,26 +178,7 @@ 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) {
@@ -182,13 +215,11 @@ export const updateSprint: Handler = async (args, ctx) => {
182
215
  };
183
216
 
184
217
  export const getSprint: Handler = async (args, ctx) => {
185
- const { sprint_id } = args as { sprint_id: string };
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
 
222
+ // Response type varies based on summary_only
192
223
  const response = await apiClient.proxy<{
193
224
  id: string;
194
225
  title: string;
@@ -200,11 +231,23 @@ export const getSprint: Handler = async (args, ctx) => {
200
231
  progress_percentage: number;
201
232
  velocity_points: number;
202
233
  committed_points: number;
203
- pre_tasks: unknown[];
204
- core_tasks: unknown[];
205
- post_tasks: unknown[];
206
- total_tasks: number;
207
- }>('get_sprint', { sprint_id });
234
+ // Full response includes task arrays
235
+ pre_tasks?: unknown[];
236
+ core_tasks?: unknown[];
237
+ post_tasks?: unknown[];
238
+ total_tasks?: number;
239
+ // Summary response includes counts and next task
240
+ task_counts?: {
241
+ pre: { total: number; completed: number };
242
+ core: { total: number; completed: number };
243
+ post: { total: number; completed: number };
244
+ total: number;
245
+ completed: number;
246
+ in_progress: number;
247
+ };
248
+ current_task?: { id: string; title: string } | null;
249
+ next_task?: { id: string; title: string; priority: number } | null;
250
+ }>('get_sprint', { sprint_id, summary_only });
208
251
 
209
252
  if (!response.ok) {
210
253
  throw new Error(`Failed to get sprint: ${response.error}`);
@@ -214,19 +257,7 @@ export const getSprint: Handler = async (args, ctx) => {
214
257
  };
215
258
 
216
259
  export const getSprints: Handler = async (args, ctx) => {
217
- const { project_id, status, limit = 20, offset = 0 } = args as {
218
- project_id: string;
219
- status?: SprintStatus;
220
- limit?: number;
221
- offset?: number;
222
- };
223
-
224
- validateRequired(project_id, 'project_id');
225
- validateUUID(project_id, 'project_id');
226
-
227
- if (status) {
228
- validateEnum(status, SPRINT_STATUSES, 'status');
229
- }
260
+ const { project_id, status, limit, offset } = parseArgs(args, getSprintsSchema);
230
261
 
231
262
  const apiClient = getApiClient();
232
263
 
@@ -248,7 +279,7 @@ export const getSprints: Handler = async (args, ctx) => {
248
279
  }>('get_sprints', {
249
280
  project_id,
250
281
  status,
251
- limit: Math.min(limit, 100),
282
+ limit: Math.min(limit ?? 20, 100),
252
283
  offset,
253
284
  });
254
285
 
@@ -260,10 +291,7 @@ export const getSprints: Handler = async (args, ctx) => {
260
291
  };
261
292
 
262
293
  export const deleteSprint: Handler = async (args, ctx) => {
263
- const { sprint_id } = args as { sprint_id: string };
264
-
265
- validateRequired(sprint_id, 'sprint_id');
266
- validateUUID(sprint_id, 'sprint_id');
294
+ const { sprint_id } = parseArgs(args, deleteSprintSchema);
267
295
 
268
296
  const apiClient = getApiClient();
269
297
 
@@ -279,10 +307,7 @@ export const deleteSprint: Handler = async (args, ctx) => {
279
307
  };
280
308
 
281
309
  export const startSprint: Handler = async (args, ctx) => {
282
- const { sprint_id } = args as { sprint_id: string };
283
-
284
- validateRequired(sprint_id, 'sprint_id');
285
- validateUUID(sprint_id, 'sprint_id');
310
+ const { sprint_id } = parseArgs(args, startSprintSchema);
286
311
 
287
312
  const apiClient = getApiClient();
288
313
 
@@ -302,14 +327,7 @@ export const startSprint: Handler = async (args, ctx) => {
302
327
  };
303
328
 
304
329
  export const completeSprint: Handler = async (args, ctx) => {
305
- const { sprint_id, retrospective_notes, skip_retrospective } = args as {
306
- sprint_id: string;
307
- retrospective_notes?: string;
308
- skip_retrospective?: boolean;
309
- };
310
-
311
- validateRequired(sprint_id, 'sprint_id');
312
- validateUUID(sprint_id, 'sprint_id');
330
+ const { sprint_id, retrospective_notes, skip_retrospective } = parseArgs(args, completeSprintSchema);
313
331
 
314
332
  const apiClient = getApiClient();
315
333
 
@@ -335,21 +353,7 @@ export const completeSprint: Handler = async (args, ctx) => {
335
353
  };
336
354
 
337
355
  export const addTaskToSprint: Handler = async (args, ctx) => {
338
- const { sprint_id, task_id, story_points, phase } = args as {
339
- sprint_id: string;
340
- task_id: string;
341
- story_points?: number;
342
- phase?: TaskPhase;
343
- };
344
-
345
- validateRequired(sprint_id, 'sprint_id');
346
- validateUUID(sprint_id, 'sprint_id');
347
- validateRequired(task_id, 'task_id');
348
- validateUUID(task_id, 'task_id');
349
-
350
- if (phase) {
351
- validateEnum(phase, TASK_PHASES, 'phase');
352
- }
356
+ const { sprint_id, task_id, story_points, phase } = parseArgs(args, addTaskToSprintSchema);
353
357
 
354
358
  if (story_points !== undefined && (story_points < 0 || !Number.isInteger(story_points))) {
355
359
  throw new Error('story_points must be a non-negative integer');
@@ -379,15 +383,7 @@ export const addTaskToSprint: Handler = async (args, ctx) => {
379
383
  };
380
384
 
381
385
  export const removeTaskFromSprint: Handler = async (args, ctx) => {
382
- const { sprint_id, task_id } = args as {
383
- sprint_id: string;
384
- task_id: string;
385
- };
386
-
387
- validateRequired(sprint_id, 'sprint_id');
388
- validateUUID(sprint_id, 'sprint_id');
389
- validateRequired(task_id, 'task_id');
390
- validateUUID(task_id, 'task_id');
386
+ const { sprint_id, task_id } = parseArgs(args, removeTaskFromSprintSchema);
391
387
 
392
388
  const apiClient = getApiClient();
393
389
 
@@ -410,17 +406,7 @@ export const removeTaskFromSprint: Handler = async (args, ctx) => {
410
406
  };
411
407
 
412
408
  export const getSprintBacklog: Handler = async (args, ctx) => {
413
- const { project_id, sprint_id } = args as {
414
- project_id: string;
415
- sprint_id?: string;
416
- };
417
-
418
- validateRequired(project_id, 'project_id');
419
- validateUUID(project_id, 'project_id');
420
-
421
- if (sprint_id) {
422
- validateUUID(sprint_id, 'sprint_id');
423
- }
409
+ const { project_id, sprint_id } = parseArgs(args, getSprintBacklogSchema);
424
410
 
425
411
  const apiClient = getApiClient();
426
412
 
@@ -447,13 +433,7 @@ export const getSprintBacklog: Handler = async (args, ctx) => {
447
433
  };
448
434
 
449
435
  export const getSprintVelocity: Handler = async (args, ctx) => {
450
- const { project_id, limit = 10 } = args as {
451
- project_id: string;
452
- limit?: number;
453
- };
454
-
455
- validateRequired(project_id, 'project_id');
456
- validateUUID(project_id, 'project_id');
436
+ const { project_id, limit } = parseArgs(args, getSprintVelocitySchema);
457
437
 
458
438
  const apiClient = getApiClient();
459
439
 
@@ -469,7 +449,7 @@ export const getSprintVelocity: Handler = async (args, ctx) => {
469
449
  total_sprints: number;
470
450
  }>('get_sprint_velocity', {
471
451
  project_id,
472
- limit: Math.min(limit, 50),
452
+ limit: Math.min(limit ?? 10, 50),
473
453
  });
474
454
 
475
455
  if (!response.ok) {
@@ -352,6 +352,67 @@ describe('updateTask', () => {
352
352
  error: 'task_claimed',
353
353
  });
354
354
  });
355
+
356
+ it('should warn when setting in_progress without git_branch', async () => {
357
+ mockApiClient.updateTask.mockResolvedValue({
358
+ ok: true,
359
+ data: { success: true },
360
+ });
361
+
362
+ const ctx = createMockContext();
363
+ const result = await updateTask(
364
+ {
365
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
366
+ status: 'in_progress',
367
+ },
368
+ ctx
369
+ );
370
+
371
+ expect(result.result).toMatchObject({
372
+ success: true,
373
+ warning: expect.stringContaining('git_branch not set'),
374
+ hint: expect.stringContaining('update_task again'),
375
+ });
376
+ });
377
+
378
+ it('should not warn when setting in_progress with git_branch', async () => {
379
+ mockApiClient.updateTask.mockResolvedValue({
380
+ ok: true,
381
+ data: { success: true },
382
+ });
383
+
384
+ const ctx = createMockContext();
385
+ const result = await updateTask(
386
+ {
387
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
388
+ status: 'in_progress',
389
+ git_branch: 'feature/my-task',
390
+ },
391
+ ctx
392
+ );
393
+
394
+ expect(result.result).toMatchObject({ success: true });
395
+ expect(result.result).not.toHaveProperty('warning');
396
+ });
397
+
398
+ it('should not warn when updating status other than in_progress', async () => {
399
+ mockApiClient.updateTask.mockResolvedValue({
400
+ ok: true,
401
+ data: { success: true },
402
+ });
403
+
404
+ const ctx = createMockContext();
405
+ const result = await updateTask(
406
+ {
407
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
408
+ status: 'completed',
409
+ },
410
+ ctx
411
+ );
412
+
413
+ expect(result.result).toMatchObject({ success: true });
414
+ expect(result.result).not.toHaveProperty('warning');
415
+ });
355
416
  });
356
417
 
357
418
  // ============================================================================