@vibescope/mcp-server 0.2.9 → 0.3.0

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 (95) hide show
  1. package/CHANGELOG.md +84 -84
  2. package/README.md +194 -194
  3. package/dist/api-client.d.ts +36 -0
  4. package/dist/api-client.js +34 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +30 -38
  7. package/dist/handlers/discovery.js +2 -0
  8. package/dist/handlers/session.d.ts +11 -0
  9. package/dist/handlers/session.js +101 -0
  10. package/dist/handlers/tasks.d.ts +8 -0
  11. package/dist/handlers/tasks.js +163 -3
  12. package/dist/handlers/tool-docs.js +840 -828
  13. package/dist/handlers/validation.js +45 -2
  14. package/dist/index.js +73 -73
  15. package/dist/setup.js +6 -6
  16. package/dist/templates/agent-guidelines.js +185 -185
  17. package/dist/templates/help-content.js +1622 -1544
  18. package/dist/tools.js +126 -74
  19. package/dist/utils.d.ts +15 -11
  20. package/dist/utils.js +53 -28
  21. package/docs/TOOLS.md +2406 -2053
  22. package/package.json +51 -51
  23. package/scripts/generate-docs.ts +212 -212
  24. package/scripts/version-bump.ts +203 -203
  25. package/src/api-client.test.ts +723 -723
  26. package/src/api-client.ts +2561 -2499
  27. package/src/cli.test.ts +24 -8
  28. package/src/cli.ts +204 -212
  29. package/src/handlers/__test-setup__.ts +236 -236
  30. package/src/handlers/__test-utils__.ts +87 -87
  31. package/src/handlers/blockers.test.ts +468 -468
  32. package/src/handlers/blockers.ts +163 -163
  33. package/src/handlers/bodies-of-work.test.ts +704 -704
  34. package/src/handlers/bodies-of-work.ts +526 -526
  35. package/src/handlers/connectors.test.ts +834 -834
  36. package/src/handlers/connectors.ts +229 -229
  37. package/src/handlers/cost.test.ts +462 -462
  38. package/src/handlers/cost.ts +285 -285
  39. package/src/handlers/decisions.test.ts +382 -382
  40. package/src/handlers/decisions.ts +153 -153
  41. package/src/handlers/deployment.test.ts +551 -551
  42. package/src/handlers/deployment.ts +541 -541
  43. package/src/handlers/discovery.test.ts +206 -206
  44. package/src/handlers/discovery.ts +392 -390
  45. package/src/handlers/fallback.test.ts +537 -537
  46. package/src/handlers/fallback.ts +194 -194
  47. package/src/handlers/file-checkouts.test.ts +750 -750
  48. package/src/handlers/file-checkouts.ts +185 -185
  49. package/src/handlers/findings.test.ts +633 -633
  50. package/src/handlers/findings.ts +239 -239
  51. package/src/handlers/git-issues.test.ts +631 -631
  52. package/src/handlers/git-issues.ts +136 -136
  53. package/src/handlers/ideas.test.ts +644 -644
  54. package/src/handlers/ideas.ts +207 -207
  55. package/src/handlers/index.ts +84 -84
  56. package/src/handlers/milestones.test.ts +475 -475
  57. package/src/handlers/milestones.ts +180 -180
  58. package/src/handlers/organizations.test.ts +826 -826
  59. package/src/handlers/organizations.ts +315 -315
  60. package/src/handlers/progress.test.ts +269 -269
  61. package/src/handlers/progress.ts +77 -77
  62. package/src/handlers/project.test.ts +546 -546
  63. package/src/handlers/project.ts +239 -239
  64. package/src/handlers/requests.test.ts +303 -303
  65. package/src/handlers/requests.ts +99 -99
  66. package/src/handlers/roles.test.ts +305 -305
  67. package/src/handlers/roles.ts +219 -219
  68. package/src/handlers/session.test.ts +998 -875
  69. package/src/handlers/session.ts +839 -730
  70. package/src/handlers/sprints.test.ts +732 -732
  71. package/src/handlers/sprints.ts +537 -537
  72. package/src/handlers/tasks.test.ts +931 -907
  73. package/src/handlers/tasks.ts +1121 -945
  74. package/src/handlers/tool-categories.test.ts +66 -66
  75. package/src/handlers/tool-docs.ts +1109 -1096
  76. package/src/handlers/types.test.ts +259 -259
  77. package/src/handlers/types.ts +175 -175
  78. package/src/handlers/validation.test.ts +582 -582
  79. package/src/handlers/validation.ts +159 -113
  80. package/src/index.test.ts +674 -0
  81. package/src/index.ts +792 -792
  82. package/src/setup.test.ts +233 -233
  83. package/src/setup.ts +404 -403
  84. package/src/templates/agent-guidelines.ts +210 -210
  85. package/src/templates/help-content.ts +1751 -1673
  86. package/src/token-tracking.test.ts +463 -463
  87. package/src/token-tracking.ts +166 -166
  88. package/src/tools.test.ts +416 -0
  89. package/src/tools.ts +3607 -3555
  90. package/src/utils.test.ts +785 -683
  91. package/src/utils.ts +469 -436
  92. package/src/validators.test.ts +223 -223
  93. package/src/validators.ts +249 -249
  94. package/tsconfig.json +16 -16
  95. package/vitest.config.ts +14 -14
@@ -1,945 +1,1121 @@
1
- /**
2
- * Task Handlers (Migrated to API Client)
3
- *
4
- * Handles task CRUD and management:
5
- * - get_task (single task by ID)
6
- * - search_tasks (text search)
7
- * - get_tasks_by_priority (priority filter)
8
- * - get_recent_tasks (by date)
9
- * - get_task_stats (aggregate counts)
10
- * - get_next_task
11
- * - add_task
12
- * - update_task
13
- * - complete_task
14
- * - delete_task
15
- * - add_task_reference
16
- * - remove_task_reference
17
- * - batch_update_tasks
18
- * - batch_complete_tasks
19
- * - add_subtask
20
- * - get_subtasks
21
- */
22
-
23
- import os from 'os';
24
- import type { Handler, HandlerRegistry } from './types.js';
25
- import {
26
- parseArgs,
27
- uuidValidator,
28
- taskStatusValidator,
29
- priorityValidator,
30
- progressValidator,
31
- minutesValidator,
32
- createEnumValidator,
33
- ValidationError,
34
- } from '../validators.js';
35
- import { getApiClient } from '../api-client.js';
36
-
37
- // Auto-detect machine hostname for worktree tracking
38
- const MACHINE_HOSTNAME = os.hostname();
39
-
40
- // Valid task types
41
- const VALID_TASK_TYPES = [
42
- 'frontend', 'backend', 'database', 'feature', 'bugfix',
43
- 'design', 'mcp', 'testing', 'docs', 'infra', 'other'
44
- ] as const;
45
-
46
- // ============================================================================
47
- // Argument Schemas
48
- // ============================================================================
49
-
50
- const getNextTaskSchema = {
51
- project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
52
- };
53
-
54
- const addTaskSchema = {
55
- project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
56
- title: { type: 'string' as const, required: true as const },
57
- description: { type: 'string' as const },
58
- priority: { type: 'number' as const, default: 3, validate: priorityValidator },
59
- estimated_minutes: { type: 'number' as const, validate: minutesValidator },
60
- blocking: { type: 'boolean' as const, default: false },
61
- task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
62
- };
63
-
64
- const updateTaskSchema = {
65
- task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
66
- title: { type: 'string' as const },
67
- description: { type: 'string' as const },
68
- priority: { type: 'number' as const, validate: priorityValidator },
69
- status: { type: 'string' as const, validate: taskStatusValidator },
70
- progress_percentage: { type: 'number' as const, validate: progressValidator },
71
- progress_note: { type: 'string' as const },
72
- estimated_minutes: { type: 'number' as const, validate: minutesValidator },
73
- git_branch: { type: 'string' as const },
74
- worktree_path: { type: 'string' as const },
75
- task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
76
- skip_worktree_requirement: { type: 'boolean' as const, default: false },
77
- };
78
-
79
- const completeTaskSchema = {
80
- task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
81
- summary: { type: 'string' as const },
82
- };
83
-
84
- const deleteTaskSchema = {
85
- task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
86
- };
87
-
88
- const addTaskReferenceSchema = {
89
- task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
90
- url: { type: 'string' as const, required: true as const },
91
- label: { type: 'string' as const },
92
- };
93
-
94
- const removeTaskReferenceSchema = {
95
- task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
96
- url: { type: 'string' as const, required: true as const },
97
- };
98
-
99
- const batchUpdateTasksSchema = {
100
- updates: { type: 'array' as const, required: true as const },
101
- };
102
-
103
- const batchCompleteTasksSchema = {
104
- completions: { type: 'array' as const, required: true as const },
105
- };
106
-
107
- const addSubtaskSchema = {
108
- parent_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
109
- title: { type: 'string' as const, required: true as const },
110
- description: { type: 'string' as const },
111
- priority: { type: 'number' as const, validate: priorityValidator },
112
- estimated_minutes: { type: 'number' as const, validate: minutesValidator },
113
- };
114
-
115
- const getSubtasksSchema = {
116
- parent_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
117
- status: { type: 'string' as const, validate: taskStatusValidator },
118
- limit: { type: 'number' as const, default: 20 },
119
- offset: { type: 'number' as const, default: 0 },
120
- };
121
-
122
- // ============================================================================
123
- // New Targeted Task Query Schemas
124
- // ============================================================================
125
-
126
- const getTaskSchema = {
127
- task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
128
- include_subtasks: { type: 'boolean' as const, default: false },
129
- include_milestones: { type: 'boolean' as const, default: false },
130
- };
131
-
132
- const searchTasksSchema = {
133
- project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
134
- query: { type: 'string' as const, required: true as const },
135
- status: { type: 'array' as const },
136
- limit: { type: 'number' as const, default: 10 },
137
- offset: { type: 'number' as const, default: 0 },
138
- };
139
-
140
- const getTasksByPrioritySchema = {
141
- project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
142
- priority: { type: 'number' as const, validate: priorityValidator },
143
- priority_max: { type: 'number' as const, validate: priorityValidator },
144
- status: { type: 'string' as const, validate: taskStatusValidator },
145
- limit: { type: 'number' as const, default: 10 },
146
- offset: { type: 'number' as const, default: 0 },
147
- };
148
-
149
- const getRecentTasksSchema = {
150
- project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
151
- order: { type: 'string' as const, validate: createEnumValidator(['newest', 'oldest']) },
152
- status: { type: 'string' as const, validate: taskStatusValidator },
153
- limit: { type: 'number' as const, default: 10 },
154
- offset: { type: 'number' as const, default: 0 },
155
- };
156
-
157
- const getTaskStatsSchema = {
158
- project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
159
- };
160
-
161
- // ============================================================================
162
- // Git workflow helpers (used by complete_task response)
163
- // ============================================================================
164
-
165
- interface GitWorkflowConfig {
166
- git_workflow: string;
167
- git_main_branch: string;
168
- git_develop_branch?: string | null;
169
- git_auto_branch?: boolean;
170
- }
171
-
172
- interface GitCompleteInstructions {
173
- steps: string[];
174
- pr_suggestion?: {
175
- title: string;
176
- body_template: string;
177
- };
178
- next_step: string;
179
- }
180
-
181
- interface GitMergeInstructions {
182
- target_branch: string;
183
- feature_branch: string;
184
- steps: string[];
185
- cleanup: string[];
186
- note: string;
187
- }
188
-
189
- function getTaskCompleteGitInstructions(
190
- gitWorkflow: string,
191
- gitMainBranch: string,
192
- gitDevelopBranch: string | undefined,
193
- taskBranch: string | undefined,
194
- taskTitle: string,
195
- taskId: string
196
- ): GitCompleteInstructions | undefined {
197
- if (gitWorkflow === 'none') {
198
- return undefined;
199
- }
200
-
201
- if (gitWorkflow === 'trunk-based') {
202
- return {
203
- steps: [`git add .`, `git commit -m "feat: ${taskTitle}"`, `git push origin ${gitMainBranch}`],
204
- next_step: 'Changes committed directly to main branch.',
205
- };
206
- }
207
-
208
- if (!taskBranch) {
209
- return {
210
- steps: ['No branch was tracked for this task.'],
211
- next_step: 'Consider creating a branch for future tasks using the git_branch parameter.',
212
- };
213
- }
214
-
215
- // github-flow or git-flow
216
- return {
217
- steps: [`git add .`, `git commit -m "feat: ${taskTitle}"`, `git push -u origin ${taskBranch}`],
218
- pr_suggestion: {
219
- title: taskTitle,
220
- body_template: `## Summary\n[Describe what was implemented]\n\n## Task Reference\nVibescope Task: ${taskId}\n\n## Testing\n- [ ] Tests pass\n- [ ] Manual testing done\n\n## Checklist\n- [ ] Code follows project conventions\n- [ ] No unnecessary changes included`,
221
- },
222
- next_step: 'Create PR and add link via add_task_reference. Merge happens AFTER validation approval.',
223
- };
224
- }
225
-
226
- export function getValidationApprovedGitInstructions(
227
- config: GitWorkflowConfig,
228
- taskBranch: string | undefined
229
- ): GitMergeInstructions | undefined {
230
- const { git_workflow, git_main_branch, git_develop_branch } = config;
231
-
232
- if (git_workflow === 'none' || git_workflow === 'trunk-based' || !taskBranch) {
233
- return undefined;
234
- }
235
-
236
- const targetBranch = git_workflow === 'git-flow' ? (git_develop_branch || 'develop') : git_main_branch;
237
-
238
- return {
239
- target_branch: targetBranch,
240
- feature_branch: taskBranch,
241
- steps: [
242
- 'Option 1: Merge via GitHub/GitLab PR UI (recommended)',
243
- `Option 2: Command line merge:`,
244
- ` git checkout ${targetBranch}`,
245
- ` git pull origin ${targetBranch}`,
246
- ` git merge ${taskBranch}`,
247
- ` git push origin ${targetBranch}`,
248
- ],
249
- cleanup: [`git branch -d ${taskBranch}`, `git push origin --delete ${taskBranch}`],
250
- note: 'Validation approved - safe to merge. Clean up branch after successful merge.',
251
- };
252
- }
253
-
254
- // ============================================================================
255
- // Task Handlers - Using API Client
256
- // ============================================================================
257
-
258
- export const getNextTask: Handler = async (args, ctx) => {
259
- const { project_id } = parseArgs(args, getNextTaskSchema);
260
-
261
- const api = getApiClient();
262
- const response = await api.getNextTask(project_id, ctx.session.currentSessionId || undefined);
263
-
264
- if (!response.ok) {
265
- return { result: { error: response.error || 'Failed to get next task' }, isError: true };
266
- }
267
-
268
- const data = response.data;
269
- if (!data) {
270
- return { result: { task: null, message: 'No response from server' } };
271
- }
272
-
273
- // Map API response to handler response format
274
- const result: Record<string, unknown> = {};
275
-
276
- if (data.task) {
277
- result.task = data.task;
278
- } else {
279
- result.task = null;
280
- }
281
-
282
- if (data.blocking_task) result.blocking_task = true;
283
- if (data.deployment_blocks_tasks) {
284
- result.deployment_blocks_tasks = true;
285
- result.deployment = data.deployment;
286
- result.action = data.action;
287
- }
288
- if (data.awaiting_validation) {
289
- result.awaiting_validation = data.awaiting_validation;
290
- result.validation_priority = data.validation_priority;
291
- result.suggested_activity = data.suggested_activity;
292
- }
293
- if (data.all_claimed) result.all_claimed = true;
294
- if (data.is_subtask) result.is_subtask = true;
295
- if (data.suggested_activity) result.suggested_activity = data.suggested_activity;
296
- if (data.directive) result.directive = data.directive;
297
- if (data.message) result.message = data.message;
298
-
299
- return { result };
300
- };
301
-
302
- export const addTask: Handler = async (args, ctx) => {
303
- const { project_id, title, description, priority, estimated_minutes, blocking, task_type } = parseArgs(args, addTaskSchema);
304
-
305
- const api = getApiClient();
306
- const response = await api.createTask(project_id, {
307
- title,
308
- description,
309
- priority,
310
- estimated_minutes,
311
- blocking,
312
- session_id: ctx.session.currentSessionId || undefined,
313
- task_type,
314
- });
315
-
316
- if (!response.ok) {
317
- return { result: { error: response.error || 'Failed to add task' }, isError: true };
318
- }
319
-
320
- const data = response.data;
321
- const result: Record<string, unknown> = {
322
- success: true,
323
- task_id: data?.task_id,
324
- title,
325
- };
326
-
327
- if (data?.blocking) {
328
- result.blocking = true;
329
- result.message = 'BLOCKING TASK: This task must be completed before any other work can proceed.';
330
- }
331
-
332
- return { result };
333
- };
334
-
335
- export const updateTask: Handler = async (args, ctx) => {
336
- 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);
337
- const updates = { title, description, priority, status, progress_percentage, estimated_minutes, git_branch, worktree_path, task_type };
338
-
339
- // Enforce worktree creation: require git_branch when marking task as in_progress
340
- // This ensures multi-agent collaboration works properly with isolated worktrees
341
- if (status === 'in_progress' && !git_branch && !skip_worktree_requirement) {
342
- return {
343
- result: {
344
- error: 'worktree_required',
345
- message: 'git_branch is required when marking a task as in_progress. Create a worktree first and provide the branch name.',
346
- 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.',
347
- worktree_example: {
348
- command: `git worktree add ../worktree-${task_id.substring(0, 8)} -b feature/${task_id.substring(0, 8)}-task develop`,
349
- then: `update_task(task_id: "${task_id}", status: "in_progress", git_branch: "feature/${task_id.substring(0, 8)}-task")`,
350
- },
351
- skip_option: 'If this project does not use git branching (trunk-based or no git workflow), pass skip_worktree_requirement: true',
352
- },
353
- };
354
- }
355
-
356
- const api = getApiClient();
357
- const response = await api.updateTask(task_id, {
358
- ...updates,
359
- progress_note,
360
- session_id: ctx.session.currentSessionId || undefined,
361
- });
362
-
363
- if (!response.ok) {
364
- // Check for specific error types
365
- if (response.error?.includes('agent_task_limit') || response.error?.includes('already has a task')) {
366
- return {
367
- result: {
368
- error: 'agent_task_limit',
369
- message: response.error,
370
- },
371
- };
372
- }
373
- if (response.error?.includes('task_claimed') || response.error?.includes('being worked on')) {
374
- return {
375
- result: {
376
- error: 'task_claimed',
377
- message: response.error,
378
- },
379
- };
380
- }
381
- if (response.error?.includes('invalid_status_transition')) {
382
- return {
383
- result: {
384
- error: 'invalid_status_transition',
385
- message: response.error,
386
- },
387
- };
388
- }
389
- if (response.error?.includes('branch_conflict')) {
390
- return {
391
- result: {
392
- error: 'branch_conflict',
393
- message: response.error,
394
- conflicting_task_id: (response.data as { conflicting_task_id?: string })?.conflicting_task_id,
395
- conflicting_task_title: (response.data as { conflicting_task_title?: string })?.conflicting_task_title,
396
- },
397
- };
398
- }
399
- return { result: { error: response.error || 'Failed to update task' }, isError: true };
400
- }
401
-
402
- // Build result - include git workflow info when transitioning to in_progress
403
- const data = response.data;
404
- const result: Record<string, unknown> = { success: true, task_id };
405
-
406
- if (data?.git_workflow) {
407
- result.git_workflow = data.git_workflow;
408
- }
409
- if (data?.worktree_setup) {
410
- result.worktree_setup = data.worktree_setup;
411
- }
412
- if (data?.next_step) {
413
- result.next_step = data.next_step;
414
- }
415
-
416
- // Add test reminder when starting work on a task
417
- if (status === 'in_progress') {
418
- result.test_reminder = {
419
- message: 'Remember to write tests for this task before marking it complete.',
420
- minimum_expectation: 'Basic tests that validate the task requirements are met',
421
- ideal: 'Tests that also cover edge cases and error handling',
422
- test_patterns: ['*.test.ts', '*.spec.ts', '*.test.js', '*.spec.js', '__tests__/*'],
423
- note: 'Validators will check for test file changes during review. Documentation-only or config changes may not require tests.',
424
- };
425
- }
426
-
427
- return { result };
428
- };
429
-
430
- export const completeTask: Handler = async (args, ctx) => {
431
- const { task_id, summary } = parseArgs(args, completeTaskSchema);
432
-
433
- const api = getApiClient();
434
- const response = await api.completeTask(task_id, {
435
- summary,
436
- session_id: ctx.session.currentSessionId || undefined,
437
- });
438
-
439
- if (!response.ok) {
440
- return { result: { error: response.error || 'Failed to complete task' }, isError: true };
441
- }
442
-
443
- const data = response.data;
444
- if (!data) {
445
- return { result: { error: 'No response data from complete task' }, isError: true };
446
- }
447
-
448
- // Build result matching expected format
449
- const result: Record<string, unknown> = {
450
- success: true,
451
- directive: data.directive,
452
- auto_continue: data.auto_continue,
453
- completed_task_id: data.completed_task_id,
454
- next_task: data.next_task,
455
- };
456
-
457
- if (data.context) {
458
- result.context = data.context;
459
- }
460
-
461
- // Pass through warnings (e.g., missing git_branch)
462
- if (data.warnings) {
463
- result.warnings = data.warnings;
464
- }
465
-
466
- // Git workflow instructions are already in API response but we need to fetch
467
- // task details if we want to include them (API should return these)
468
- result.next_action = data.next_action;
469
-
470
- return { result };
471
- };
472
-
473
- export const deleteTask: Handler = async (args, ctx) => {
474
- const { task_id } = parseArgs(args, deleteTaskSchema);
475
-
476
- const api = getApiClient();
477
- const response = await api.deleteTask(task_id);
478
-
479
- if (!response.ok) {
480
- return { result: { error: response.error || 'Failed to delete task' }, isError: true };
481
- }
482
-
483
- return { result: { success: true, deleted_id: task_id } };
484
- };
485
-
486
- export const addTaskReference: Handler = async (args, ctx) => {
487
- const { task_id, url, label } = parseArgs(args, addTaskReferenceSchema);
488
-
489
- const api = getApiClient();
490
- const response = await api.addTaskReference(task_id, url, label);
491
-
492
- if (!response.ok) {
493
- if (response.error?.includes('already exists')) {
494
- return { result: { success: false, error: 'Reference with this URL already exists' } };
495
- }
496
- return { result: { error: response.error || 'Failed to add reference' }, isError: true };
497
- }
498
-
499
- return {
500
- result: {
501
- success: true,
502
- reference: response.data?.reference,
503
- },
504
- };
505
- };
506
-
507
- export const removeTaskReference: Handler = async (args, ctx) => {
508
- const { task_id, url } = parseArgs(args, removeTaskReferenceSchema);
509
-
510
- const api = getApiClient();
511
- const response = await api.removeTaskReference(task_id, url);
512
-
513
- if (!response.ok) {
514
- if (response.error?.includes('not found')) {
515
- return { result: { success: false, error: 'Reference with this URL not found' } };
516
- }
517
- return { result: { error: response.error || 'Failed to remove reference' }, isError: true };
518
- }
519
-
520
- return { result: { success: true } };
521
- };
522
-
523
- export const batchUpdateTasks: Handler = async (args, ctx) => {
524
- const { updates } = parseArgs(args, batchUpdateTasksSchema);
525
-
526
- const typedUpdates = updates as Array<{
527
- task_id: string;
528
- status?: string;
529
- progress_percentage?: number;
530
- progress_note?: string;
531
- priority?: number;
532
- }>;
533
-
534
- if (!Array.isArray(typedUpdates) || typedUpdates.length === 0) {
535
- throw new ValidationError('updates must be a non-empty array', {
536
- field: 'updates',
537
- hint: 'Provide an array of task updates with at least one item',
538
- });
539
- }
540
-
541
- if (typedUpdates.length > 50) {
542
- throw new ValidationError('Too many updates. Maximum is 50 per batch.', {
543
- field: 'updates',
544
- hint: 'Split your updates into smaller batches',
545
- });
546
- }
547
-
548
- // Individual item validation happens at API level
549
- const api = getApiClient();
550
- const response = await api.batchUpdateTasks(typedUpdates);
551
-
552
- if (!response.ok) {
553
- return { result: { error: response.error || 'Failed to batch update tasks' }, isError: true };
554
- }
555
-
556
- return {
557
- result: {
558
- success: response.data?.success || false,
559
- total: typedUpdates.length,
560
- succeeded: response.data?.updated_count || 0,
561
- },
562
- };
563
- };
564
-
565
- export const batchCompleteTasks: Handler = async (args, ctx) => {
566
- const { completions } = parseArgs(args, batchCompleteTasksSchema);
567
-
568
- const typedCompletions = completions as Array<{
569
- task_id: string;
570
- summary?: string;
571
- }>;
572
-
573
- if (!Array.isArray(typedCompletions) || typedCompletions.length === 0) {
574
- throw new ValidationError('completions must be a non-empty array', {
575
- field: 'completions',
576
- hint: 'Provide an array of task completions with at least one item',
577
- });
578
- }
579
-
580
- if (typedCompletions.length > 50) {
581
- throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
582
- field: 'completions',
583
- hint: 'Split your completions into smaller batches',
584
- });
585
- }
586
-
587
- // Individual item validation happens at API level
588
-
589
- const api = getApiClient();
590
- const response = await api.batchCompleteTasks(typedCompletions);
591
-
592
- if (!response.ok) {
593
- return { result: { error: response.error || 'Failed to batch complete tasks' }, isError: true };
594
- }
595
-
596
- const data = response.data;
597
- return {
598
- result: {
599
- success: data?.success || false,
600
- total: typedCompletions.length,
601
- succeeded: data?.completed_count || 0,
602
- failed: typedCompletions.length - (data?.completed_count || 0),
603
- next_task: data?.next_task,
604
- },
605
- };
606
- };
607
-
608
- // ============================================================================
609
- // Subtask Handlers
610
- // ============================================================================
611
-
612
- export const addSubtask: Handler = async (args, ctx) => {
613
- const { parent_task_id, title, description, priority, estimated_minutes } = parseArgs(args, addSubtaskSchema);
614
-
615
- const api = getApiClient();
616
- const response = await api.addSubtask(parent_task_id, {
617
- title,
618
- description,
619
- priority,
620
- estimated_minutes,
621
- }, ctx.session.currentSessionId || undefined);
622
-
623
- if (!response.ok) {
624
- if (response.error?.includes('Cannot create subtask of a subtask')) {
625
- return {
626
- result: {
627
- success: false,
628
- error: 'Cannot create subtask of a subtask',
629
- hint: 'Subtasks cannot have their own subtasks. Add this task to the parent task instead.',
630
- },
631
- };
632
- }
633
- return { result: { error: response.error || 'Failed to add subtask' }, isError: true };
634
- }
635
-
636
- return {
637
- result: {
638
- success: true,
639
- subtask_id: response.data?.subtask_id,
640
- parent_task_id: response.data?.parent_task_id,
641
- },
642
- };
643
- };
644
-
645
- export const getSubtasks: Handler = async (args, ctx) => {
646
- const { parent_task_id, status } = parseArgs(args, getSubtasksSchema);
647
-
648
- const api = getApiClient();
649
- const response = await api.getSubtasks(parent_task_id, status);
650
-
651
- if (!response.ok) {
652
- return { result: { error: response.error || 'Failed to fetch subtasks' }, isError: true };
653
- }
654
-
655
- return {
656
- result: {
657
- subtasks: response.data?.subtasks || [],
658
- stats: response.data?.stats || {
659
- total: 0,
660
- completed: 0,
661
- progress_percentage: 0,
662
- },
663
- },
664
- };
665
- };
666
-
667
- // ============================================================================
668
- // New Targeted Task Query Handlers
669
- // ============================================================================
670
-
671
- /**
672
- * Get a single task by ID with optional subtasks and milestones
673
- */
674
- export const getTask: Handler = async (args, ctx) => {
675
- const { task_id, include_subtasks, include_milestones } = parseArgs(args, getTaskSchema);
676
-
677
- const api = getApiClient();
678
- const response = await api.getTaskById(task_id, {
679
- include_subtasks,
680
- include_milestones,
681
- });
682
-
683
- if (!response.ok) {
684
- return { result: { error: response.error || 'Failed to fetch task' }, isError: true };
685
- }
686
-
687
- const result: Record<string, unknown> = {
688
- task: response.data?.task,
689
- };
690
-
691
- if (include_subtasks && response.data?.subtasks) {
692
- result.subtasks = response.data.subtasks;
693
- }
694
-
695
- if (include_milestones && response.data?.milestones) {
696
- result.milestones = response.data.milestones;
697
- }
698
-
699
- return { result };
700
- };
701
-
702
- /**
703
- * Search tasks by text query with pagination
704
- */
705
- export const searchTasks: Handler = async (args, ctx) => {
706
- const { project_id, query, status, limit, offset } = parseArgs(args, searchTasksSchema);
707
-
708
- // Validate query length
709
- if (query.length < 2) {
710
- return {
711
- result: {
712
- error: 'query_too_short',
713
- message: 'Search query must be at least 2 characters',
714
- },
715
- };
716
- }
717
-
718
- // Cap limit at 20, safe offset
719
- const cappedLimit = Math.min(limit ?? 10, 20);
720
- const safeOffset = Math.max(0, offset ?? 0);
721
-
722
- const api = getApiClient();
723
- const response = await api.searchTasks(project_id, {
724
- query,
725
- status: status as string[] | undefined,
726
- limit: cappedLimit,
727
- offset: safeOffset,
728
- });
729
-
730
- if (!response.ok) {
731
- return { result: { error: response.error || 'Failed to search tasks' }, isError: true };
732
- }
733
-
734
- const tasks = response.data?.tasks || [];
735
- const totalMatches = response.data?.total_matches || 0;
736
-
737
- return {
738
- result: {
739
- tasks,
740
- total_matches: totalMatches,
741
- has_more: safeOffset + tasks.length < totalMatches,
742
- offset: safeOffset,
743
- limit: cappedLimit,
744
- },
745
- };
746
- };
747
-
748
- /**
749
- * Get tasks filtered by priority with pagination
750
- */
751
- export const getTasksByPriority: Handler = async (args, ctx) => {
752
- const { project_id, priority, priority_max, status, limit, offset } = parseArgs(args, getTasksByPrioritySchema);
753
-
754
- // Cap limit at 20, safe offset
755
- const cappedLimit = Math.min(limit ?? 10, 20);
756
- const safeOffset = Math.max(0, offset ?? 0);
757
-
758
- const api = getApiClient();
759
- const response = await api.getTasksByPriority(project_id, {
760
- priority,
761
- priority_max,
762
- status,
763
- limit: cappedLimit,
764
- offset: safeOffset,
765
- });
766
-
767
- if (!response.ok) {
768
- return { result: { error: response.error || 'Failed to fetch tasks by priority' }, isError: true };
769
- }
770
-
771
- const tasks = response.data?.tasks || [];
772
- const totalCount = response.data?.total_count || 0;
773
-
774
- return {
775
- result: {
776
- tasks,
777
- total_count: totalCount,
778
- has_more: safeOffset + tasks.length < totalCount,
779
- offset: safeOffset,
780
- limit: cappedLimit,
781
- },
782
- };
783
- };
784
-
785
- /**
786
- * Get recent tasks (newest or oldest) with pagination
787
- */
788
- export const getRecentTasks: Handler = async (args, ctx) => {
789
- const { project_id, order, status, limit, offset } = parseArgs(args, getRecentTasksSchema);
790
-
791
- // Cap limit at 20, safe offset
792
- const cappedLimit = Math.min(limit ?? 10, 20);
793
- const safeOffset = Math.max(0, offset ?? 0);
794
-
795
- const api = getApiClient();
796
- const response = await api.getRecentTasks(project_id, {
797
- order: order as 'newest' | 'oldest' | undefined,
798
- status,
799
- limit: cappedLimit,
800
- offset: safeOffset,
801
- });
802
-
803
- if (!response.ok) {
804
- return { result: { error: response.error || 'Failed to fetch recent tasks' }, isError: true };
805
- }
806
-
807
- const tasks = response.data?.tasks || [];
808
- const totalCount = response.data?.total_count || 0;
809
-
810
- return {
811
- result: {
812
- tasks,
813
- total_count: totalCount,
814
- has_more: safeOffset + tasks.length < totalCount,
815
- offset: safeOffset,
816
- limit: cappedLimit,
817
- },
818
- };
819
- };
820
-
821
- /**
822
- * Get task statistics for a project (aggregate counts only, minimal tokens)
823
- */
824
- export const getTaskStats: Handler = async (args, ctx) => {
825
- const { project_id } = parseArgs(args, getTaskStatsSchema);
826
-
827
- const api = getApiClient();
828
- const response = await api.getTaskStats(project_id);
829
-
830
- if (!response.ok) {
831
- return { result: { error: response.error || 'Failed to fetch task stats' }, isError: true };
832
- }
833
-
834
- return {
835
- result: {
836
- total: response.data?.total || 0,
837
- by_status: response.data?.by_status || {
838
- backlog: 0,
839
- pending: 0,
840
- in_progress: 0,
841
- completed: 0,
842
- cancelled: 0,
843
- },
844
- by_priority: response.data?.by_priority || { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
845
- awaiting_validation: response.data?.awaiting_validation || 0,
846
- oldest_pending_days: response.data?.oldest_pending_days ?? null,
847
- },
848
- };
849
- };
850
-
851
- // ============================================================================
852
- // Worktree Cleanup Handlers
853
- // ============================================================================
854
-
855
- const getStaleWorktreesSchema = {
856
- project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
857
- hostname: { type: 'string' as const }, // Machine hostname to filter worktrees
858
- limit: { type: 'number' as const, default: 20 },
859
- offset: { type: 'number' as const, default: 0 },
860
- };
861
-
862
- const clearWorktreePathSchema = {
863
- task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
864
- };
865
-
866
- export const getStaleWorktrees: Handler = async (args, ctx) => {
867
- const { project_id, hostname: providedHostname, limit, offset } = parseArgs(args, getStaleWorktreesSchema);
868
-
869
- // Use auto-detected hostname if not provided - filters to only worktrees on THIS machine
870
- const hostname = providedHostname || MACHINE_HOSTNAME;
871
-
872
- // Cap limit to prevent context bloating
873
- const cappedLimit = Math.min(limit ?? 20, 50);
874
-
875
- const api = getApiClient();
876
- const response = await api.getStaleWorktrees(project_id, { hostname, limit: cappedLimit, offset });
877
-
878
- if (!response.ok) {
879
- return { result: { error: response.error || 'Failed to get stale worktrees' }, isError: true };
880
- }
881
-
882
- const data = response.data;
883
- return {
884
- result: {
885
- project_id: data?.project_id,
886
- project_name: data?.project_name,
887
- hostname_filter: data?.hostname_filter,
888
- stale_worktrees: data?.stale_worktrees || [],
889
- count: data?.count || 0,
890
- local_count: data?.local_count || 0,
891
- remote_count: data?.remote_count || 0,
892
- total_count: data?.total_count || 0,
893
- has_more: data?.has_more || false,
894
- cleanup_instructions: data?.cleanup_instructions,
895
- remote_worktree_note: data?.remote_worktree_note,
896
- },
897
- };
898
- };
899
-
900
- export const clearWorktreePath: Handler = async (args, ctx) => {
901
- const { task_id } = parseArgs(args, clearWorktreePathSchema);
902
-
903
- const api = getApiClient();
904
- const response = await api.clearWorktreePath(task_id);
905
-
906
- if (!response.ok) {
907
- return { result: { error: response.error || 'Failed to clear worktree path' }, isError: true };
908
- }
909
-
910
- return {
911
- result: {
912
- success: true,
913
- task_id,
914
- message: 'Worktree path cleared. The worktree can now be safely removed if not already done.',
915
- },
916
- };
917
- };
918
-
919
- /**
920
- * Task handlers registry
921
- */
922
- export const taskHandlers: HandlerRegistry = {
923
- // Targeted task query endpoints (token-efficient)
924
- get_task: getTask,
925
- search_tasks: searchTasks,
926
- get_tasks_by_priority: getTasksByPriority,
927
- get_recent_tasks: getRecentTasks,
928
- get_task_stats: getTaskStats,
929
- // Core task operations
930
- get_next_task: getNextTask,
931
- add_task: addTask,
932
- update_task: updateTask,
933
- complete_task: completeTask,
934
- delete_task: deleteTask,
935
- add_task_reference: addTaskReference,
936
- remove_task_reference: removeTaskReference,
937
- batch_update_tasks: batchUpdateTasks,
938
- batch_complete_tasks: batchCompleteTasks,
939
- // Subtask handlers
940
- add_subtask: addSubtask,
941
- get_subtasks: getSubtasks,
942
- // Worktree cleanup handlers
943
- get_stale_worktrees: getStaleWorktrees,
944
- clear_worktree_path: clearWorktreePath,
945
- };
1
+ /**
2
+ * Task Handlers (Migrated to API Client)
3
+ *
4
+ * Handles task CRUD and management:
5
+ * - get_task (single task by ID)
6
+ * - search_tasks (text search)
7
+ * - get_tasks_by_priority (priority filter)
8
+ * - get_recent_tasks (by date)
9
+ * - get_task_stats (aggregate counts)
10
+ * - get_next_task
11
+ * - add_task
12
+ * - update_task
13
+ * - complete_task
14
+ * - delete_task
15
+ * - release_task
16
+ * - cancel_task
17
+ * - add_task_reference
18
+ * - remove_task_reference
19
+ * - batch_update_tasks
20
+ * - batch_complete_tasks
21
+ * - add_subtask
22
+ * - get_subtasks
23
+ */
24
+
25
+ import os from 'os';
26
+ import type { Handler, HandlerRegistry } from './types.js';
27
+ import {
28
+ parseArgs,
29
+ uuidValidator,
30
+ taskStatusValidator,
31
+ priorityValidator,
32
+ progressValidator,
33
+ minutesValidator,
34
+ createEnumValidator,
35
+ ValidationError,
36
+ } from '../validators.js';
37
+ import { getApiClient } from '../api-client.js';
38
+
39
+ // Auto-detect machine hostname for worktree tracking
40
+ const MACHINE_HOSTNAME = os.hostname();
41
+
42
+ // Valid task types
43
+ const VALID_TASK_TYPES = [
44
+ 'frontend', 'backend', 'database', 'feature', 'bugfix',
45
+ 'design', 'mcp', 'testing', 'docs', 'infra', 'other'
46
+ ] as const;
47
+
48
+ // ============================================================================
49
+ // Argument Schemas
50
+ // ============================================================================
51
+
52
+ const getNextTaskSchema = {
53
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
54
+ };
55
+
56
+ const addTaskSchema = {
57
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
58
+ title: { type: 'string' as const, required: true as const },
59
+ description: { type: 'string' as const },
60
+ priority: { type: 'number' as const, default: 3, validate: priorityValidator },
61
+ estimated_minutes: { type: 'number' as const, validate: minutesValidator },
62
+ blocking: { type: 'boolean' as const, default: false },
63
+ task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
64
+ };
65
+
66
+ const updateTaskSchema = {
67
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
68
+ title: { type: 'string' as const },
69
+ description: { type: 'string' as const },
70
+ priority: { type: 'number' as const, validate: priorityValidator },
71
+ status: { type: 'string' as const, validate: taskStatusValidator },
72
+ progress_percentage: { type: 'number' as const, validate: progressValidator },
73
+ progress_note: { type: 'string' as const },
74
+ estimated_minutes: { type: 'number' as const, validate: minutesValidator },
75
+ git_branch: { type: 'string' as const },
76
+ worktree_path: { type: 'string' as const },
77
+ task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
78
+ skip_worktree_requirement: { type: 'boolean' as const, default: false },
79
+ };
80
+
81
+ const completeTaskSchema = {
82
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
83
+ summary: { type: 'string' as const },
84
+ };
85
+
86
+ const deleteTaskSchema = {
87
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
88
+ };
89
+
90
+ const releaseTaskSchema = {
91
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
92
+ reason: { type: 'string' as const },
93
+ };
94
+
95
+ // Valid reasons for task cancellation
96
+ const VALID_CANCELLED_REASONS = [
97
+ 'pr_closed', 'superseded', 'user_cancelled', 'validation_failed', 'obsolete', 'blocked'
98
+ ] as const;
99
+
100
+ const cancelTaskSchema = {
101
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
102
+ cancelled_reason: { type: 'string' as const, validate: createEnumValidator(VALID_CANCELLED_REASONS) },
103
+ cancellation_note: { type: 'string' as const },
104
+ };
105
+
106
+ const addTaskReferenceSchema = {
107
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
108
+ url: { type: 'string' as const, required: true as const },
109
+ label: { type: 'string' as const },
110
+ };
111
+
112
+ const removeTaskReferenceSchema = {
113
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
114
+ url: { type: 'string' as const, required: true as const },
115
+ };
116
+
117
+ const batchUpdateTasksSchema = {
118
+ updates: { type: 'array' as const, required: true as const },
119
+ };
120
+
121
+ const batchCompleteTasksSchema = {
122
+ completions: { type: 'array' as const, required: true as const },
123
+ };
124
+
125
+ const addSubtaskSchema = {
126
+ parent_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
127
+ title: { type: 'string' as const, required: true as const },
128
+ description: { type: 'string' as const },
129
+ priority: { type: 'number' as const, validate: priorityValidator },
130
+ estimated_minutes: { type: 'number' as const, validate: minutesValidator },
131
+ };
132
+
133
+ const getSubtasksSchema = {
134
+ parent_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
135
+ status: { type: 'string' as const, validate: taskStatusValidator },
136
+ limit: { type: 'number' as const, default: 20 },
137
+ offset: { type: 'number' as const, default: 0 },
138
+ };
139
+
140
+ // ============================================================================
141
+ // New Targeted Task Query Schemas
142
+ // ============================================================================
143
+
144
+ const getTaskSchema = {
145
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
146
+ include_subtasks: { type: 'boolean' as const, default: false },
147
+ include_milestones: { type: 'boolean' as const, default: false },
148
+ };
149
+
150
+ const searchTasksSchema = {
151
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
152
+ query: { type: 'string' as const, required: true as const },
153
+ status: { type: 'array' as const },
154
+ limit: { type: 'number' as const, default: 10 },
155
+ offset: { type: 'number' as const, default: 0 },
156
+ };
157
+
158
+ const getTasksByPrioritySchema = {
159
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
160
+ priority: { type: 'number' as const, validate: priorityValidator },
161
+ priority_max: { type: 'number' as const, validate: priorityValidator },
162
+ status: { type: 'string' as const, validate: taskStatusValidator },
163
+ limit: { type: 'number' as const, default: 10 },
164
+ offset: { type: 'number' as const, default: 0 },
165
+ };
166
+
167
+ const getRecentTasksSchema = {
168
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
169
+ order: { type: 'string' as const, validate: createEnumValidator(['newest', 'oldest']) },
170
+ status: { type: 'string' as const, validate: taskStatusValidator },
171
+ limit: { type: 'number' as const, default: 10 },
172
+ offset: { type: 'number' as const, default: 0 },
173
+ };
174
+
175
+ const getTaskStatsSchema = {
176
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
177
+ };
178
+
179
+ // ============================================================================
180
+ // Git workflow helpers (used by complete_task response)
181
+ // ============================================================================
182
+
183
+ interface GitWorkflowConfig {
184
+ git_workflow: string;
185
+ git_main_branch: string;
186
+ git_develop_branch?: string | null;
187
+ git_auto_branch?: boolean;
188
+ }
189
+
190
+ interface GitCompleteInstructions {
191
+ steps: string[];
192
+ pr_suggestion?: {
193
+ title: string;
194
+ body_template: string;
195
+ };
196
+ next_step: string;
197
+ }
198
+
199
+ interface GitMergeInstructions {
200
+ target_branch: string;
201
+ feature_branch: string;
202
+ steps: string[];
203
+ cleanup: string[];
204
+ note: string;
205
+ }
206
+
207
+ function getTaskCompleteGitInstructions(
208
+ gitWorkflow: string,
209
+ gitMainBranch: string,
210
+ gitDevelopBranch: string | undefined,
211
+ taskBranch: string | undefined,
212
+ taskTitle: string,
213
+ taskId: string
214
+ ): GitCompleteInstructions | undefined {
215
+ if (gitWorkflow === 'none') {
216
+ return undefined;
217
+ }
218
+
219
+ if (gitWorkflow === 'trunk-based') {
220
+ return {
221
+ steps: [`git add .`, `git commit -m "feat: ${taskTitle}"`, `git push origin ${gitMainBranch}`],
222
+ next_step: 'Changes committed directly to main branch.',
223
+ };
224
+ }
225
+
226
+ if (!taskBranch) {
227
+ return {
228
+ steps: ['No branch was tracked for this task.'],
229
+ next_step: 'Consider creating a branch for future tasks using the git_branch parameter.',
230
+ };
231
+ }
232
+
233
+ // github-flow or git-flow
234
+ return {
235
+ steps: [`git add .`, `git commit -m "feat: ${taskTitle}"`, `git push -u origin ${taskBranch}`],
236
+ pr_suggestion: {
237
+ title: taskTitle,
238
+ body_template: `## Summary\n[Describe what was implemented]\n\n## Task Reference\nVibescope Task: ${taskId}\n\n## Testing\n- [ ] Tests pass\n- [ ] Manual testing done\n\n## Checklist\n- [ ] Code follows project conventions\n- [ ] No unnecessary changes included`,
239
+ },
240
+ next_step: 'Create PR and add link via add_task_reference. Merge happens AFTER validation approval.',
241
+ };
242
+ }
243
+
244
+ export function getValidationApprovedGitInstructions(
245
+ config: GitWorkflowConfig,
246
+ taskBranch: string | undefined
247
+ ): GitMergeInstructions | undefined {
248
+ const { git_workflow, git_main_branch, git_develop_branch } = config;
249
+
250
+ if (git_workflow === 'none' || git_workflow === 'trunk-based' || !taskBranch) {
251
+ return undefined;
252
+ }
253
+
254
+ const targetBranch = git_workflow === 'git-flow' ? (git_develop_branch || 'develop') : git_main_branch;
255
+
256
+ return {
257
+ target_branch: targetBranch,
258
+ feature_branch: taskBranch,
259
+ steps: [
260
+ 'Option 1: Merge via GitHub/GitLab PR UI (recommended)',
261
+ `Option 2: Command line merge:`,
262
+ ` git checkout ${targetBranch}`,
263
+ ` git pull origin ${targetBranch}`,
264
+ ` git merge ${taskBranch}`,
265
+ ` git push origin ${targetBranch}`,
266
+ ],
267
+ cleanup: [`git branch -d ${taskBranch}`, `git push origin --delete ${taskBranch}`],
268
+ note: 'Validation approved - safe to merge. Clean up branch after successful merge.',
269
+ };
270
+ }
271
+
272
+ // ============================================================================
273
+ // Task Handlers - Using API Client
274
+ // ============================================================================
275
+
276
+ export const getNextTask: Handler = async (args, ctx) => {
277
+ const { project_id } = parseArgs(args, getNextTaskSchema);
278
+
279
+ const api = getApiClient();
280
+ const response = await api.getNextTask(project_id, ctx.session.currentSessionId || undefined);
281
+
282
+ if (!response.ok) {
283
+ return { result: { error: response.error || 'Failed to get next task' }, isError: true };
284
+ }
285
+
286
+ const data = response.data;
287
+ if (!data) {
288
+ return { result: { task: null, message: 'No response from server' } };
289
+ }
290
+
291
+ // Map API response to handler response format
292
+ const result: Record<string, unknown> = {};
293
+
294
+ if (data.task) {
295
+ result.task = data.task;
296
+ } else {
297
+ result.task = null;
298
+ // Add IDLE_GUIDANCE when no tasks are available
299
+ result.IDLE_GUIDANCE = {
300
+ message: 'No tasks available. Follow these steps:',
301
+ steps: [
302
+ '1. Call signal_idle() to update dashboard immediately - shows you are available',
303
+ '2. Start a fallback_activity (code_review, security_review, test_coverage, etc.)',
304
+ '3. Never ask "what should I do?" - be autonomous',
305
+ ],
306
+ autonomy_rules: [
307
+ 'Never ask "should I continue?" → Just continue',
308
+ 'Never say "let me know what to do" → Use fallback activities',
309
+ 'When context grows large: /clear → start_work_session (don\'t ask, just do it)',
310
+ ],
311
+ next_action: `signal_idle() then start_fallback_activity(project_id: "${project_id}", activity: "code_review")`,
312
+ };
313
+ }
314
+
315
+ if (data.blocking_task) result.blocking_task = true;
316
+ if (data.deployment_blocks_tasks) {
317
+ result.deployment_blocks_tasks = true;
318
+ result.deployment = data.deployment;
319
+ result.action = data.action;
320
+ }
321
+ if (data.awaiting_validation) {
322
+ result.awaiting_validation = data.awaiting_validation;
323
+ result.validation_priority = data.validation_priority;
324
+ result.suggested_activity = data.suggested_activity;
325
+ }
326
+ if (data.all_claimed) result.all_claimed = true;
327
+ if (data.is_subtask) result.is_subtask = true;
328
+ if (data.suggested_activity) result.suggested_activity = data.suggested_activity;
329
+ if (data.directive) result.directive = data.directive;
330
+ if (data.message) result.message = data.message;
331
+
332
+ return { result };
333
+ };
334
+
335
+ export const addTask: Handler = async (args, ctx) => {
336
+ const { project_id, title, description, priority, estimated_minutes, blocking, task_type } = parseArgs(args, addTaskSchema);
337
+
338
+ const api = getApiClient();
339
+ const response = await api.createTask(project_id, {
340
+ title,
341
+ description,
342
+ priority,
343
+ estimated_minutes,
344
+ blocking,
345
+ session_id: ctx.session.currentSessionId || undefined,
346
+ task_type,
347
+ });
348
+
349
+ if (!response.ok) {
350
+ return { result: { error: response.error || 'Failed to add task' }, isError: true };
351
+ }
352
+
353
+ const data = response.data;
354
+ const result: Record<string, unknown> = {
355
+ success: true,
356
+ task_id: data?.task_id,
357
+ title,
358
+ };
359
+
360
+ if (data?.blocking) {
361
+ result.blocking = true;
362
+ result.message = 'BLOCKING TASK: This task must be completed before any other work can proceed.';
363
+ }
364
+
365
+ return { result };
366
+ };
367
+
368
+ export const updateTask: Handler = async (args, ctx) => {
369
+ 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);
370
+ const updates = { title, description, priority, status, progress_percentage, estimated_minutes, git_branch, worktree_path, task_type };
371
+
372
+ // Enforce worktree creation: require git_branch when marking task as in_progress
373
+ // This ensures multi-agent collaboration works properly with isolated worktrees
374
+ if (status === 'in_progress' && !git_branch && !skip_worktree_requirement) {
375
+ return {
376
+ result: {
377
+ error: 'worktree_required',
378
+ message: 'git_branch is required when marking a task as in_progress. Create a worktree first and provide the branch name.',
379
+ 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.',
380
+ worktree_example: {
381
+ command: `git worktree add ../worktree-${task_id.substring(0, 8)} -b feature/${task_id.substring(0, 8)}-task develop`,
382
+ then: `update_task(task_id: "${task_id}", status: "in_progress", git_branch: "feature/${task_id.substring(0, 8)}-task")`,
383
+ },
384
+ skip_option: 'If this project does not use git branching (trunk-based or no git workflow), pass skip_worktree_requirement: true',
385
+ },
386
+ };
387
+ }
388
+
389
+ const api = getApiClient();
390
+ const response = await api.updateTask(task_id, {
391
+ ...updates,
392
+ progress_note,
393
+ session_id: ctx.session.currentSessionId || undefined,
394
+ });
395
+
396
+ if (!response.ok) {
397
+ // Check for specific error types
398
+ if (response.error?.includes('agent_task_limit') || response.error?.includes('already has a task')) {
399
+ return {
400
+ result: {
401
+ error: 'agent_task_limit',
402
+ message: response.error,
403
+ },
404
+ };
405
+ }
406
+ if (response.error?.includes('task_claimed') || response.error?.includes('task_already_claimed') || response.error?.includes('being worked on') || response.error?.includes('already being worked on')) {
407
+ const data = response.data as { claimed_by?: string; claimed_session_id?: string; message?: string } | undefined;
408
+ return {
409
+ result: {
410
+ error: 'task_already_claimed',
411
+ message: data?.message || response.error || 'Task is already claimed by another agent',
412
+ claimed_by: data?.claimed_by,
413
+ claimed_session_id: data?.claimed_session_id,
414
+ suggestion: 'Use get_next_task() to get a different available task, or wait for the claiming agent to finish.',
415
+ },
416
+ };
417
+ }
418
+ if (response.error?.includes('invalid_status_transition')) {
419
+ return {
420
+ result: {
421
+ error: 'invalid_status_transition',
422
+ message: response.error,
423
+ },
424
+ };
425
+ }
426
+ if (response.error?.includes('branch_conflict')) {
427
+ return {
428
+ result: {
429
+ error: 'branch_conflict',
430
+ message: response.error,
431
+ conflicting_task_id: (response.data as { conflicting_task_id?: string })?.conflicting_task_id,
432
+ conflicting_task_title: (response.data as { conflicting_task_title?: string })?.conflicting_task_title,
433
+ },
434
+ };
435
+ }
436
+ return { result: { error: response.error || 'Failed to update task' }, isError: true };
437
+ }
438
+
439
+ // Build result - include git workflow info when transitioning to in_progress
440
+ const data = response.data;
441
+ const result: Record<string, unknown> = { success: true, task_id };
442
+
443
+ if (data?.git_workflow) {
444
+ result.git_workflow = data.git_workflow;
445
+ }
446
+ if (data?.worktree_setup) {
447
+ result.worktree_setup = data.worktree_setup;
448
+ }
449
+ if (data?.next_step) {
450
+ result.next_step = data.next_step;
451
+ }
452
+
453
+ // Add test reminder when starting work on a task
454
+ if (status === 'in_progress') {
455
+ result.test_reminder = {
456
+ message: 'Remember to write tests for this task before marking it complete.',
457
+ minimum_expectation: 'Basic tests that validate the task requirements are met',
458
+ ideal: 'Tests that also cover edge cases and error handling',
459
+ test_patterns: ['*.test.ts', '*.spec.ts', '*.test.js', '*.spec.js', '__tests__/*'],
460
+ note: 'Validators will check for test file changes during review. Documentation-only or config changes may not require tests.',
461
+ };
462
+
463
+ // Add comprehensive WORKTREE RULES for branching workflows
464
+ // This reminds agents of the critical workflow order
465
+ result.WORKTREE_RULES = {
466
+ mandatory: true,
467
+ rules: [
468
+ '1. Create worktree BEFORE any file edits - reading is fine, editing requires worktree first',
469
+ '2. Naming: ../PROJECT-PERSONA-short-desc (max 24 chars for description)',
470
+ '3. Command: git worktree add ../PROJECT-PERSONA-desc -b feature/TASKID-desc BASE_BRANCH',
471
+ '4. Report location: heartbeat(current_worktree_path: "...")',
472
+ '5. Store path: update_task(task_id, worktree_path: "...")',
473
+ '6. REBASE before PR: git fetch origin && git rebase origin/BASE_BRANCH && git push --force-with-lease',
474
+ ],
475
+ rebase_before_pr: {
476
+ mandatory: true,
477
+ why: 'Without rebasing, your branch may contain old versions of files that other agents modified. When merged, your old version overwrites their changes.',
478
+ commands: [
479
+ 'git fetch origin',
480
+ 'git rebase origin/develop # or origin/main for github-flow',
481
+ 'git push --force-with-lease',
482
+ ],
483
+ },
484
+ wrong_order: {
485
+ violation: 'Edit file → stash → create worktree → pop → commit',
486
+ why: 'Even if you eventually use a worktree, editing before creating one is a violation',
487
+ },
488
+ right_order: {
489
+ correct: 'Read to understand → create worktree → cd into it → THEN edit',
490
+ why: 'Worktrees must exist BEFORE any file modifications',
491
+ },
492
+ };
493
+
494
+ // Add HOTFIX_WORKFLOW guidance when branch name indicates hotfix
495
+ if (git_branch && git_branch.includes('hotfix/')) {
496
+ result.HOTFIX_WORKFLOW = {
497
+ message: 'HOTFIX detected - special workflow applies:',
498
+ steps: [
499
+ '1. Create worktree from MAIN (not develop): git worktree add ../PROJECT-PERSONA-hotfix-desc -b hotfix/TASKID-desc main',
500
+ '2. Work in worktree and make your fix',
501
+ '3. Commit: git add -A && git commit -m "fix: description"',
502
+ '4. Push: git push -u origin hotfix/TASKID-desc',
503
+ '5. Create PR targeting MAIN: gh pr create --base main --title "fix: ..." --body "Hotfix for production"',
504
+ '6. Remove worktree immediately after PR',
505
+ ],
506
+ important: 'Hotfixes go to MAIN, not develop. They are later merged to develop separately.',
507
+ worktree_required: true,
508
+ };
509
+ }
510
+
511
+ // Guidance for when investigation reveals fix already exists
512
+ result.FIX_ALREADY_EXISTS_GUIDANCE = {
513
+ message: 'If investigation reveals the fix already exists but needs deployment:',
514
+ steps: [
515
+ '1. Add finding: add_finding(project_id, title: "Fix exists, awaits deployment", category: "other", severity: "info", description: "...", related_task_id: task_id)',
516
+ '2. Complete task: complete_task(task_id, summary: "Fix already exists in codebase (PR #XXX). Needs deployment.")',
517
+ '3. Check deployment: check_deployment_status(project_id)',
518
+ '4. Request deployment if not pending: request_deployment(project_id, notes: "Includes fix for [issue]")',
519
+ ],
520
+ rationale: 'This prevents tasks from being blocked waiting for deployment when the actual work is done.',
521
+ };
522
+ }
523
+
524
+ return { result };
525
+ };
526
+
527
+ export const completeTask: Handler = async (args, ctx) => {
528
+ const { task_id, summary } = parseArgs(args, completeTaskSchema);
529
+
530
+ const api = getApiClient();
531
+ const response = await api.completeTask(task_id, {
532
+ summary,
533
+ session_id: ctx.session.currentSessionId || undefined,
534
+ });
535
+
536
+ if (!response.ok) {
537
+ return { result: { error: response.error || 'Failed to complete task' }, isError: true };
538
+ }
539
+
540
+ const data = response.data;
541
+ if (!data) {
542
+ return { result: { error: 'No response data from complete task' }, isError: true };
543
+ }
544
+
545
+ // Build result matching expected format
546
+ const result: Record<string, unknown> = {
547
+ success: true,
548
+ directive: data.directive,
549
+ auto_continue: data.auto_continue,
550
+ completed_task_id: data.completed_task_id,
551
+ next_task: data.next_task,
552
+ };
553
+
554
+ if (data.context) {
555
+ result.context = data.context;
556
+ }
557
+
558
+ // Pass through warnings (e.g., missing git_branch)
559
+ if (data.warnings) {
560
+ result.warnings = data.warnings;
561
+ }
562
+
563
+ // Git workflow instructions are already in API response but we need to fetch
564
+ // task details if we want to include them (API should return these)
565
+ result.next_action = data.next_action;
566
+
567
+ // Add mandatory action reminders for complete_task
568
+ result.MANDATORY_ACTIONS = {
569
+ message: 'Before marking task complete, ensure you have done the following:',
570
+ checklist: [
571
+ 'If you made code changes: Commit and push all changes to your branch',
572
+ 'REBASE before PR: git fetch origin && git rebase origin/BASE_BRANCH && git push --force-with-lease',
573
+ 'If project uses PR workflow: Create PR targeting correct branch (develop for git-flow, main for github-flow)',
574
+ 'If using worktree: Remove worktree IMMEDIATELY after PR is created',
575
+ ],
576
+ sequence: 'Commit Rebase Push PR created complete_task() → remove worktree → next task',
577
+ important: 'DO NOT wait for PR review/merge - validation handles that. Complete task immediately after PR.',
578
+ rebase_warning: 'Always rebase before creating PR to avoid overwriting other agents\' work.',
579
+ };
580
+
581
+ // Add worktree cleanup reminder if worktree was used
582
+ if (data.context?.worktree_path) {
583
+ result.worktree_cleanup = {
584
+ required: true,
585
+ path: data.context.worktree_path,
586
+ command: `git worktree remove ${data.context.worktree_path}`,
587
+ timing: 'Remove immediately after PR is created and complete_task is called',
588
+ };
589
+ }
590
+
591
+ return { result };
592
+ };
593
+
594
+ export const deleteTask: Handler = async (args, ctx) => {
595
+ const { task_id } = parseArgs(args, deleteTaskSchema);
596
+
597
+ const api = getApiClient();
598
+ const response = await api.deleteTask(task_id);
599
+
600
+ if (!response.ok) {
601
+ return { result: { error: response.error || 'Failed to delete task' }, isError: true };
602
+ }
603
+
604
+ return { result: { success: true, deleted_id: task_id } };
605
+ };
606
+
607
+ /**
608
+ * Release a task back to pending status.
609
+ * Use when an agent needs to give up a claimed task (context limits, conflicts, user request).
610
+ */
611
+ export const releaseTask: Handler = async (args, ctx) => {
612
+ const { task_id, reason } = parseArgs(args, releaseTaskSchema);
613
+
614
+ const api = getApiClient();
615
+ const response = await api.releaseTask(task_id, {
616
+ reason,
617
+ session_id: ctx.session.currentSessionId || undefined,
618
+ });
619
+
620
+ if (!response.ok) {
621
+ return { result: { error: response.error || 'Failed to release task' }, isError: true };
622
+ }
623
+
624
+ return {
625
+ result: {
626
+ success: true,
627
+ task_id,
628
+ message: response.data?.message || 'Task released and returned to pending status',
629
+ reason: reason || null,
630
+ hint: 'The task is now available for other agents to claim. Call get_next_task() to get a new task.',
631
+ },
632
+ };
633
+ };
634
+
635
+ export const cancelTask: Handler = async (args, ctx) => {
636
+ const { task_id, cancelled_reason, cancellation_note } = parseArgs(args, cancelTaskSchema);
637
+
638
+ const api = getApiClient();
639
+ // Cast cancelled_reason to the expected union type - validation already ensures it's valid
640
+ const response = await api.cancelTask(task_id, {
641
+ cancelled_reason: cancelled_reason as 'pr_closed' | 'superseded' | 'user_cancelled' | 'validation_failed' | 'obsolete' | 'blocked' | undefined,
642
+ cancellation_note,
643
+ session_id: ctx.session.currentSessionId || undefined,
644
+ });
645
+
646
+ if (!response.ok) {
647
+ return { result: { error: response.error || 'Failed to cancel task' }, isError: true };
648
+ }
649
+
650
+ return {
651
+ result: {
652
+ success: true,
653
+ task_id,
654
+ cancelled_reason: cancelled_reason || null,
655
+ message: response.data?.message || `Task cancelled${cancelled_reason ? ` (${cancelled_reason})` : ''}`,
656
+ },
657
+ };
658
+ };
659
+
660
+ export const addTaskReference: Handler = async (args, ctx) => {
661
+ const { task_id, url, label } = parseArgs(args, addTaskReferenceSchema);
662
+
663
+ const api = getApiClient();
664
+ const response = await api.addTaskReference(task_id, url, label);
665
+
666
+ if (!response.ok) {
667
+ if (response.error?.includes('already exists')) {
668
+ return { result: { success: false, error: 'Reference with this URL already exists' } };
669
+ }
670
+ return { result: { error: response.error || 'Failed to add reference' }, isError: true };
671
+ }
672
+
673
+ return {
674
+ result: {
675
+ success: true,
676
+ reference: response.data?.reference,
677
+ },
678
+ };
679
+ };
680
+
681
+ export const removeTaskReference: Handler = async (args, ctx) => {
682
+ const { task_id, url } = parseArgs(args, removeTaskReferenceSchema);
683
+
684
+ const api = getApiClient();
685
+ const response = await api.removeTaskReference(task_id, url);
686
+
687
+ if (!response.ok) {
688
+ if (response.error?.includes('not found')) {
689
+ return { result: { success: false, error: 'Reference with this URL not found' } };
690
+ }
691
+ return { result: { error: response.error || 'Failed to remove reference' }, isError: true };
692
+ }
693
+
694
+ return { result: { success: true } };
695
+ };
696
+
697
+ export const batchUpdateTasks: Handler = async (args, ctx) => {
698
+ const { updates } = parseArgs(args, batchUpdateTasksSchema);
699
+
700
+ const typedUpdates = updates as Array<{
701
+ task_id: string;
702
+ status?: string;
703
+ progress_percentage?: number;
704
+ progress_note?: string;
705
+ priority?: number;
706
+ }>;
707
+
708
+ if (!Array.isArray(typedUpdates) || typedUpdates.length === 0) {
709
+ throw new ValidationError('updates must be a non-empty array', {
710
+ field: 'updates',
711
+ hint: 'Provide an array of task updates with at least one item',
712
+ });
713
+ }
714
+
715
+ if (typedUpdates.length > 50) {
716
+ throw new ValidationError('Too many updates. Maximum is 50 per batch.', {
717
+ field: 'updates',
718
+ hint: 'Split your updates into smaller batches',
719
+ });
720
+ }
721
+
722
+ // Individual item validation happens at API level
723
+ const api = getApiClient();
724
+ const response = await api.batchUpdateTasks(typedUpdates);
725
+
726
+ if (!response.ok) {
727
+ return { result: { error: response.error || 'Failed to batch update tasks' }, isError: true };
728
+ }
729
+
730
+ return {
731
+ result: {
732
+ success: response.data?.success || false,
733
+ total: typedUpdates.length,
734
+ succeeded: response.data?.updated_count || 0,
735
+ },
736
+ };
737
+ };
738
+
739
+ export const batchCompleteTasks: Handler = async (args, ctx) => {
740
+ const { completions } = parseArgs(args, batchCompleteTasksSchema);
741
+
742
+ const typedCompletions = completions as Array<{
743
+ task_id: string;
744
+ summary?: string;
745
+ }>;
746
+
747
+ if (!Array.isArray(typedCompletions) || typedCompletions.length === 0) {
748
+ throw new ValidationError('completions must be a non-empty array', {
749
+ field: 'completions',
750
+ hint: 'Provide an array of task completions with at least one item',
751
+ });
752
+ }
753
+
754
+ if (typedCompletions.length > 50) {
755
+ throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
756
+ field: 'completions',
757
+ hint: 'Split your completions into smaller batches',
758
+ });
759
+ }
760
+
761
+ // Individual item validation happens at API level
762
+
763
+ const api = getApiClient();
764
+ const response = await api.batchCompleteTasks(typedCompletions);
765
+
766
+ if (!response.ok) {
767
+ return { result: { error: response.error || 'Failed to batch complete tasks' }, isError: true };
768
+ }
769
+
770
+ const data = response.data;
771
+ return {
772
+ result: {
773
+ success: data?.success || false,
774
+ total: typedCompletions.length,
775
+ succeeded: data?.completed_count || 0,
776
+ failed: typedCompletions.length - (data?.completed_count || 0),
777
+ next_task: data?.next_task,
778
+ },
779
+ };
780
+ };
781
+
782
+ // ============================================================================
783
+ // Subtask Handlers
784
+ // ============================================================================
785
+
786
+ export const addSubtask: Handler = async (args, ctx) => {
787
+ const { parent_task_id, title, description, priority, estimated_minutes } = parseArgs(args, addSubtaskSchema);
788
+
789
+ const api = getApiClient();
790
+ const response = await api.addSubtask(parent_task_id, {
791
+ title,
792
+ description,
793
+ priority,
794
+ estimated_minutes,
795
+ }, ctx.session.currentSessionId || undefined);
796
+
797
+ if (!response.ok) {
798
+ if (response.error?.includes('Cannot create subtask of a subtask')) {
799
+ return {
800
+ result: {
801
+ success: false,
802
+ error: 'Cannot create subtask of a subtask',
803
+ hint: 'Subtasks cannot have their own subtasks. Add this task to the parent task instead.',
804
+ },
805
+ };
806
+ }
807
+ return { result: { error: response.error || 'Failed to add subtask' }, isError: true };
808
+ }
809
+
810
+ return {
811
+ result: {
812
+ success: true,
813
+ subtask_id: response.data?.subtask_id,
814
+ parent_task_id: response.data?.parent_task_id,
815
+ },
816
+ };
817
+ };
818
+
819
+ export const getSubtasks: Handler = async (args, ctx) => {
820
+ const { parent_task_id, status } = parseArgs(args, getSubtasksSchema);
821
+
822
+ const api = getApiClient();
823
+ const response = await api.getSubtasks(parent_task_id, status);
824
+
825
+ if (!response.ok) {
826
+ return { result: { error: response.error || 'Failed to fetch subtasks' }, isError: true };
827
+ }
828
+
829
+ return {
830
+ result: {
831
+ subtasks: response.data?.subtasks || [],
832
+ stats: response.data?.stats || {
833
+ total: 0,
834
+ completed: 0,
835
+ progress_percentage: 0,
836
+ },
837
+ },
838
+ };
839
+ };
840
+
841
+ // ============================================================================
842
+ // New Targeted Task Query Handlers
843
+ // ============================================================================
844
+
845
+ /**
846
+ * Get a single task by ID with optional subtasks and milestones
847
+ */
848
+ export const getTask: Handler = async (args, ctx) => {
849
+ const { task_id, include_subtasks, include_milestones } = parseArgs(args, getTaskSchema);
850
+
851
+ const api = getApiClient();
852
+ const response = await api.getTaskById(task_id, {
853
+ include_subtasks,
854
+ include_milestones,
855
+ });
856
+
857
+ if (!response.ok) {
858
+ return { result: { error: response.error || 'Failed to fetch task' }, isError: true };
859
+ }
860
+
861
+ const result: Record<string, unknown> = {
862
+ task: response.data?.task,
863
+ };
864
+
865
+ if (include_subtasks && response.data?.subtasks) {
866
+ result.subtasks = response.data.subtasks;
867
+ }
868
+
869
+ if (include_milestones && response.data?.milestones) {
870
+ result.milestones = response.data.milestones;
871
+ }
872
+
873
+ return { result };
874
+ };
875
+
876
+ /**
877
+ * Search tasks by text query with pagination
878
+ */
879
+ export const searchTasks: Handler = async (args, ctx) => {
880
+ const { project_id, query, status, limit, offset } = parseArgs(args, searchTasksSchema);
881
+
882
+ // Validate query length
883
+ if (query.length < 2) {
884
+ return {
885
+ result: {
886
+ error: 'query_too_short',
887
+ message: 'Search query must be at least 2 characters',
888
+ },
889
+ };
890
+ }
891
+
892
+ // Cap limit at 20, safe offset
893
+ const cappedLimit = Math.min(limit ?? 10, 20);
894
+ const safeOffset = Math.max(0, offset ?? 0);
895
+
896
+ const api = getApiClient();
897
+ const response = await api.searchTasks(project_id, {
898
+ query,
899
+ status: status as string[] | undefined,
900
+ limit: cappedLimit,
901
+ offset: safeOffset,
902
+ });
903
+
904
+ if (!response.ok) {
905
+ return { result: { error: response.error || 'Failed to search tasks' }, isError: true };
906
+ }
907
+
908
+ const tasks = response.data?.tasks || [];
909
+ const totalMatches = response.data?.total_matches || 0;
910
+
911
+ return {
912
+ result: {
913
+ tasks,
914
+ total_matches: totalMatches,
915
+ has_more: safeOffset + tasks.length < totalMatches,
916
+ offset: safeOffset,
917
+ limit: cappedLimit,
918
+ },
919
+ };
920
+ };
921
+
922
+ /**
923
+ * Get tasks filtered by priority with pagination
924
+ */
925
+ export const getTasksByPriority: Handler = async (args, ctx) => {
926
+ const { project_id, priority, priority_max, status, limit, offset } = parseArgs(args, getTasksByPrioritySchema);
927
+
928
+ // Cap limit at 20, safe offset
929
+ const cappedLimit = Math.min(limit ?? 10, 20);
930
+ const safeOffset = Math.max(0, offset ?? 0);
931
+
932
+ const api = getApiClient();
933
+ const response = await api.getTasksByPriority(project_id, {
934
+ priority,
935
+ priority_max,
936
+ status,
937
+ limit: cappedLimit,
938
+ offset: safeOffset,
939
+ });
940
+
941
+ if (!response.ok) {
942
+ return { result: { error: response.error || 'Failed to fetch tasks by priority' }, isError: true };
943
+ }
944
+
945
+ const tasks = response.data?.tasks || [];
946
+ const totalCount = response.data?.total_count || 0;
947
+
948
+ return {
949
+ result: {
950
+ tasks,
951
+ total_count: totalCount,
952
+ has_more: safeOffset + tasks.length < totalCount,
953
+ offset: safeOffset,
954
+ limit: cappedLimit,
955
+ },
956
+ };
957
+ };
958
+
959
+ /**
960
+ * Get recent tasks (newest or oldest) with pagination
961
+ */
962
+ export const getRecentTasks: Handler = async (args, ctx) => {
963
+ const { project_id, order, status, limit, offset } = parseArgs(args, getRecentTasksSchema);
964
+
965
+ // Cap limit at 20, safe offset
966
+ const cappedLimit = Math.min(limit ?? 10, 20);
967
+ const safeOffset = Math.max(0, offset ?? 0);
968
+
969
+ const api = getApiClient();
970
+ const response = await api.getRecentTasks(project_id, {
971
+ order: order as 'newest' | 'oldest' | undefined,
972
+ status,
973
+ limit: cappedLimit,
974
+ offset: safeOffset,
975
+ });
976
+
977
+ if (!response.ok) {
978
+ return { result: { error: response.error || 'Failed to fetch recent tasks' }, isError: true };
979
+ }
980
+
981
+ const tasks = response.data?.tasks || [];
982
+ const totalCount = response.data?.total_count || 0;
983
+
984
+ return {
985
+ result: {
986
+ tasks,
987
+ total_count: totalCount,
988
+ has_more: safeOffset + tasks.length < totalCount,
989
+ offset: safeOffset,
990
+ limit: cappedLimit,
991
+ },
992
+ };
993
+ };
994
+
995
+ /**
996
+ * Get task statistics for a project (aggregate counts only, minimal tokens)
997
+ */
998
+ export const getTaskStats: Handler = async (args, ctx) => {
999
+ const { project_id } = parseArgs(args, getTaskStatsSchema);
1000
+
1001
+ const api = getApiClient();
1002
+ const response = await api.getTaskStats(project_id);
1003
+
1004
+ if (!response.ok) {
1005
+ return { result: { error: response.error || 'Failed to fetch task stats' }, isError: true };
1006
+ }
1007
+
1008
+ return {
1009
+ result: {
1010
+ total: response.data?.total || 0,
1011
+ by_status: response.data?.by_status || {
1012
+ backlog: 0,
1013
+ pending: 0,
1014
+ in_progress: 0,
1015
+ completed: 0,
1016
+ cancelled: 0,
1017
+ },
1018
+ by_priority: response.data?.by_priority || { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
1019
+ awaiting_validation: response.data?.awaiting_validation || 0,
1020
+ oldest_pending_days: response.data?.oldest_pending_days ?? null,
1021
+ },
1022
+ };
1023
+ };
1024
+
1025
+ // ============================================================================
1026
+ // Worktree Cleanup Handlers
1027
+ // ============================================================================
1028
+
1029
+ const getStaleWorktreesSchema = {
1030
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
1031
+ hostname: { type: 'string' as const }, // Machine hostname to filter worktrees
1032
+ limit: { type: 'number' as const, default: 20 },
1033
+ offset: { type: 'number' as const, default: 0 },
1034
+ };
1035
+
1036
+ const clearWorktreePathSchema = {
1037
+ task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
1038
+ };
1039
+
1040
+ export const getStaleWorktrees: Handler = async (args, ctx) => {
1041
+ const { project_id, hostname: providedHostname, limit, offset } = parseArgs(args, getStaleWorktreesSchema);
1042
+
1043
+ // Use auto-detected hostname if not provided - filters to only worktrees on THIS machine
1044
+ const hostname = providedHostname || MACHINE_HOSTNAME;
1045
+
1046
+ // Cap limit to prevent context bloating
1047
+ const cappedLimit = Math.min(limit ?? 20, 50);
1048
+
1049
+ const api = getApiClient();
1050
+ const response = await api.getStaleWorktrees(project_id, { hostname, limit: cappedLimit, offset });
1051
+
1052
+ if (!response.ok) {
1053
+ return { result: { error: response.error || 'Failed to get stale worktrees' }, isError: true };
1054
+ }
1055
+
1056
+ const data = response.data;
1057
+ return {
1058
+ result: {
1059
+ project_id: data?.project_id,
1060
+ project_name: data?.project_name,
1061
+ hostname_filter: data?.hostname_filter,
1062
+ stale_worktrees: data?.stale_worktrees || [],
1063
+ count: data?.count || 0,
1064
+ local_count: data?.local_count || 0,
1065
+ remote_count: data?.remote_count || 0,
1066
+ total_count: data?.total_count || 0,
1067
+ has_more: data?.has_more || false,
1068
+ cleanup_instructions: data?.cleanup_instructions,
1069
+ remote_worktree_note: data?.remote_worktree_note,
1070
+ },
1071
+ };
1072
+ };
1073
+
1074
+ export const clearWorktreePath: Handler = async (args, ctx) => {
1075
+ const { task_id } = parseArgs(args, clearWorktreePathSchema);
1076
+
1077
+ const api = getApiClient();
1078
+ const response = await api.clearWorktreePath(task_id);
1079
+
1080
+ if (!response.ok) {
1081
+ return { result: { error: response.error || 'Failed to clear worktree path' }, isError: true };
1082
+ }
1083
+
1084
+ return {
1085
+ result: {
1086
+ success: true,
1087
+ task_id,
1088
+ message: 'Worktree path cleared. The worktree can now be safely removed if not already done.',
1089
+ },
1090
+ };
1091
+ };
1092
+
1093
+ /**
1094
+ * Task handlers registry
1095
+ */
1096
+ export const taskHandlers: HandlerRegistry = {
1097
+ // Targeted task query endpoints (token-efficient)
1098
+ get_task: getTask,
1099
+ search_tasks: searchTasks,
1100
+ get_tasks_by_priority: getTasksByPriority,
1101
+ get_recent_tasks: getRecentTasks,
1102
+ get_task_stats: getTaskStats,
1103
+ // Core task operations
1104
+ get_next_task: getNextTask,
1105
+ add_task: addTask,
1106
+ update_task: updateTask,
1107
+ complete_task: completeTask,
1108
+ delete_task: deleteTask,
1109
+ release_task: releaseTask,
1110
+ cancel_task: cancelTask,
1111
+ add_task_reference: addTaskReference,
1112
+ remove_task_reference: removeTaskReference,
1113
+ batch_update_tasks: batchUpdateTasks,
1114
+ batch_complete_tasks: batchCompleteTasks,
1115
+ // Subtask handlers
1116
+ add_subtask: addSubtask,
1117
+ get_subtasks: getSubtasks,
1118
+ // Worktree cleanup handlers
1119
+ get_stale_worktrees: getStaleWorktrees,
1120
+ clear_worktree_path: clearWorktreePath,
1121
+ };