@vibescope/mcp-server 0.2.1 → 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.
- package/README.md +60 -7
- package/dist/api-client.d.ts +187 -0
- package/dist/api-client.js +48 -0
- package/dist/handlers/blockers.js +9 -8
- package/dist/handlers/bodies-of-work.js +14 -14
- package/dist/handlers/connectors.d.ts +45 -0
- package/dist/handlers/connectors.js +183 -0
- package/dist/handlers/cost.d.ts +10 -0
- package/dist/handlers/cost.js +54 -0
- package/dist/handlers/decisions.js +3 -3
- package/dist/handlers/deployment.js +35 -19
- package/dist/handlers/discovery.d.ts +7 -0
- package/dist/handlers/discovery.js +61 -2
- package/dist/handlers/fallback.js +5 -4
- package/dist/handlers/file-checkouts.d.ts +2 -0
- package/dist/handlers/file-checkouts.js +38 -6
- package/dist/handlers/findings.js +13 -12
- package/dist/handlers/git-issues.js +4 -4
- package/dist/handlers/ideas.js +5 -5
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +3 -0
- package/dist/handlers/milestones.js +5 -5
- package/dist/handlers/organizations.js +13 -13
- package/dist/handlers/progress.js +2 -2
- package/dist/handlers/project.js +6 -6
- package/dist/handlers/requests.js +3 -3
- package/dist/handlers/session.js +28 -9
- package/dist/handlers/sprints.js +17 -17
- package/dist/handlers/tasks.d.ts +2 -0
- package/dist/handlers/tasks.js +78 -20
- package/dist/handlers/types.d.ts +64 -2
- package/dist/handlers/types.js +48 -1
- package/dist/handlers/validation.js +3 -3
- package/dist/index.js +7 -2716
- package/dist/token-tracking.d.ts +74 -0
- package/dist/token-tracking.js +122 -0
- package/dist/tools.js +298 -9
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +17 -0
- package/docs/TOOLS.md +2053 -0
- package/package.json +4 -1
- package/scripts/generate-docs.ts +212 -0
- package/src/api-client.test.ts +718 -0
- package/src/api-client.ts +231 -0
- package/src/handlers/__test-setup__.ts +9 -0
- package/src/handlers/blockers.test.ts +31 -19
- package/src/handlers/blockers.ts +9 -8
- package/src/handlers/bodies-of-work.test.ts +55 -32
- package/src/handlers/bodies-of-work.ts +14 -14
- package/src/handlers/connectors.test.ts +834 -0
- package/src/handlers/connectors.ts +229 -0
- package/src/handlers/cost.ts +66 -0
- package/src/handlers/decisions.test.ts +34 -25
- package/src/handlers/decisions.ts +3 -3
- package/src/handlers/deployment.ts +39 -19
- package/src/handlers/discovery.ts +61 -2
- package/src/handlers/fallback.test.ts +26 -22
- package/src/handlers/fallback.ts +5 -4
- package/src/handlers/file-checkouts.test.ts +242 -49
- package/src/handlers/file-checkouts.ts +44 -6
- package/src/handlers/findings.test.ts +38 -24
- package/src/handlers/findings.ts +13 -12
- package/src/handlers/git-issues.test.ts +51 -43
- package/src/handlers/git-issues.ts +4 -4
- package/src/handlers/ideas.test.ts +28 -23
- package/src/handlers/ideas.ts +5 -5
- package/src/handlers/index.ts +3 -0
- package/src/handlers/milestones.test.ts +33 -28
- package/src/handlers/milestones.ts +5 -5
- package/src/handlers/organizations.test.ts +104 -83
- package/src/handlers/organizations.ts +13 -13
- package/src/handlers/progress.test.ts +20 -14
- package/src/handlers/progress.ts +2 -2
- package/src/handlers/project.test.ts +34 -27
- package/src/handlers/project.ts +6 -6
- package/src/handlers/requests.test.ts +27 -18
- package/src/handlers/requests.ts +3 -3
- package/src/handlers/session.test.ts +47 -0
- package/src/handlers/session.ts +32 -9
- package/src/handlers/sprints.test.ts +71 -50
- package/src/handlers/sprints.ts +17 -17
- package/src/handlers/tasks.test.ts +77 -15
- package/src/handlers/tasks.ts +90 -21
- package/src/handlers/tool-categories.test.ts +66 -0
- package/src/handlers/types.ts +81 -2
- package/src/handlers/validation.test.ts +78 -45
- package/src/handlers/validation.ts +3 -3
- package/src/index.ts +12 -2732
- package/src/token-tracking.test.ts +453 -0
- package/src/token-tracking.ts +164 -0
- package/src/tools.ts +298 -9
- package/src/utils.test.ts +2 -2
- package/src/utils.ts +17 -0
package/src/handlers/sprints.ts
CHANGED
|
@@ -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
|
-
|
|
122
|
+
return { result: { error: 'Invalid start_date format. Use YYYY-MM-DD' }, isError: true };
|
|
123
123
|
}
|
|
124
124
|
if (isNaN(endDateObj.getTime())) {
|
|
125
|
-
|
|
125
|
+
return { result: { error: 'Invalid end_date format. Use YYYY-MM-DD' }, isError: true };
|
|
126
126
|
}
|
|
127
127
|
if (endDateObj < startDateObj) {
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
373
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
432
|
+
expect(result.result).not.toHaveProperty('error');
|
|
433
|
+
expect(mockApiClient.updateTask).toHaveBeenCalled();
|
|
396
434
|
});
|
|
397
435
|
|
|
398
|
-
it('should not
|
|
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('
|
|
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
|
|
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
|
|
491
|
-
|
|
492
|
-
).
|
|
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
|
|
package/src/handlers/tasks.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|