@vibescope/mcp-server 0.5.0 → 0.5.2

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