@vibescope/mcp-server 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) 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/tasks.js +7 -1
  7. package/dist/handlers/tool-docs.js +1216 -1216
  8. package/dist/index.js +73 -73
  9. package/dist/templates/agent-guidelines.d.ts +1 -1
  10. package/dist/templates/agent-guidelines.js +205 -205
  11. package/dist/templates/help-content.js +1621 -1621
  12. package/dist/tools/bodies-of-work.js +6 -6
  13. package/dist/tools/cloud-agents.js +22 -22
  14. package/dist/tools/milestones.js +2 -2
  15. package/dist/tools/requests.js +1 -1
  16. package/dist/tools/session.js +11 -11
  17. package/dist/tools/sprints.js +9 -9
  18. package/dist/tools/tasks.js +43 -35
  19. package/dist/tools/worktrees.js +14 -14
  20. package/dist/utils.js +11 -11
  21. package/docs/TOOLS.md +2687 -2685
  22. package/package.json +53 -53
  23. package/scripts/generate-docs.ts +212 -212
  24. package/scripts/version-bump.ts +203 -203
  25. package/src/api-client/blockers.ts +86 -86
  26. package/src/api-client/bodies-of-work.ts +194 -194
  27. package/src/api-client/chat.ts +50 -50
  28. package/src/api-client/connectors.ts +152 -152
  29. package/src/api-client/cost.ts +185 -185
  30. package/src/api-client/decisions.ts +87 -87
  31. package/src/api-client/deployment.ts +313 -313
  32. package/src/api-client/discovery.ts +81 -81
  33. package/src/api-client/fallback.ts +52 -52
  34. package/src/api-client/file-checkouts.ts +115 -115
  35. package/src/api-client/findings.ts +100 -100
  36. package/src/api-client/git-issues.ts +88 -88
  37. package/src/api-client/ideas.ts +112 -112
  38. package/src/api-client/index.ts +592 -592
  39. package/src/api-client/milestones.ts +83 -83
  40. package/src/api-client/organizations.ts +185 -185
  41. package/src/api-client/progress.ts +94 -94
  42. package/src/api-client/project.ts +181 -181
  43. package/src/api-client/requests.ts +54 -54
  44. package/src/api-client/session.ts +220 -220
  45. package/src/api-client/sprints.ts +227 -227
  46. package/src/api-client/subtasks.ts +57 -57
  47. package/src/api-client/tasks.ts +451 -450
  48. package/src/api-client/types.ts +32 -32
  49. package/src/api-client/validation.ts +60 -60
  50. package/src/api-client/worktrees.ts +53 -53
  51. package/src/api-client.test.ts +847 -847
  52. package/src/api-client.ts +2728 -2728
  53. package/src/cli-init.ts +558 -558
  54. package/src/cli.test.ts +284 -284
  55. package/src/cli.ts +204 -204
  56. package/src/handlers/__test-setup__.ts +240 -240
  57. package/src/handlers/__test-utils__.ts +89 -89
  58. package/src/handlers/blockers.test.ts +468 -468
  59. package/src/handlers/blockers.ts +172 -172
  60. package/src/handlers/bodies-of-work.test.ts +704 -704
  61. package/src/handlers/bodies-of-work.ts +526 -526
  62. package/src/handlers/chat.test.ts +185 -185
  63. package/src/handlers/chat.ts +101 -101
  64. package/src/handlers/cloud-agents.test.ts +438 -438
  65. package/src/handlers/cloud-agents.ts +156 -156
  66. package/src/handlers/connectors.test.ts +834 -834
  67. package/src/handlers/connectors.ts +229 -229
  68. package/src/handlers/cost.test.ts +462 -462
  69. package/src/handlers/cost.ts +285 -285
  70. package/src/handlers/decisions.test.ts +382 -382
  71. package/src/handlers/decisions.ts +153 -153
  72. package/src/handlers/deployment.test.ts +551 -551
  73. package/src/handlers/deployment.ts +570 -570
  74. package/src/handlers/discovery.test.ts +206 -206
  75. package/src/handlers/discovery.ts +433 -433
  76. package/src/handlers/fallback.test.ts +537 -537
  77. package/src/handlers/fallback.ts +194 -194
  78. package/src/handlers/file-checkouts.test.ts +750 -750
  79. package/src/handlers/file-checkouts.ts +185 -185
  80. package/src/handlers/findings.test.ts +633 -633
  81. package/src/handlers/findings.ts +239 -239
  82. package/src/handlers/git-issues.test.ts +631 -631
  83. package/src/handlers/git-issues.ts +136 -136
  84. package/src/handlers/ideas.test.ts +644 -644
  85. package/src/handlers/ideas.ts +207 -207
  86. package/src/handlers/index.ts +93 -93
  87. package/src/handlers/milestones.test.ts +475 -475
  88. package/src/handlers/milestones.ts +180 -180
  89. package/src/handlers/organizations.test.ts +826 -826
  90. package/src/handlers/organizations.ts +315 -315
  91. package/src/handlers/progress.test.ts +269 -269
  92. package/src/handlers/progress.ts +77 -77
  93. package/src/handlers/project.test.ts +546 -546
  94. package/src/handlers/project.ts +245 -245
  95. package/src/handlers/requests.test.ts +303 -303
  96. package/src/handlers/requests.ts +99 -99
  97. package/src/handlers/roles.test.ts +305 -305
  98. package/src/handlers/roles.ts +219 -219
  99. package/src/handlers/session.test.ts +998 -998
  100. package/src/handlers/session.ts +1105 -1105
  101. package/src/handlers/sprints.test.ts +732 -732
  102. package/src/handlers/sprints.ts +537 -537
  103. package/src/handlers/tasks.test.ts +931 -931
  104. package/src/handlers/tasks.ts +1144 -1137
  105. package/src/handlers/tool-categories.test.ts +66 -66
  106. package/src/handlers/tool-docs.test.ts +511 -511
  107. package/src/handlers/tool-docs.ts +1595 -1595
  108. package/src/handlers/types.test.ts +259 -259
  109. package/src/handlers/types.ts +176 -176
  110. package/src/handlers/validation.test.ts +582 -582
  111. package/src/handlers/validation.ts +164 -164
  112. package/src/handlers/version.ts +63 -63
  113. package/src/index.test.ts +674 -674
  114. package/src/index.ts +884 -884
  115. package/src/setup.test.ts +243 -243
  116. package/src/setup.ts +410 -410
  117. package/src/templates/agent-guidelines.ts +233 -233
  118. package/src/templates/help-content.ts +1751 -1751
  119. package/src/token-tracking.test.ts +463 -463
  120. package/src/token-tracking.ts +167 -167
  121. package/src/tools/blockers.ts +122 -122
  122. package/src/tools/bodies-of-work.ts +283 -283
  123. package/src/tools/chat.ts +72 -72
  124. package/src/tools/cloud-agents.ts +101 -101
  125. package/src/tools/connectors.ts +191 -191
  126. package/src/tools/cost.ts +111 -111
  127. package/src/tools/decisions.ts +111 -111
  128. package/src/tools/deployment.ts +455 -455
  129. package/src/tools/discovery.ts +76 -76
  130. package/src/tools/fallback.ts +111 -111
  131. package/src/tools/features.ts +154 -154
  132. package/src/tools/file-checkouts.ts +145 -145
  133. package/src/tools/findings.ts +101 -101
  134. package/src/tools/git-issues.ts +130 -130
  135. package/src/tools/ideas.ts +162 -162
  136. package/src/tools/index.ts +145 -145
  137. package/src/tools/milestones.ts +118 -118
  138. package/src/tools/organizations.ts +224 -224
  139. package/src/tools/persona-templates.ts +25 -25
  140. package/src/tools/progress.ts +73 -73
  141. package/src/tools/project.ts +210 -210
  142. package/src/tools/requests.ts +68 -68
  143. package/src/tools/roles.ts +112 -112
  144. package/src/tools/session.ts +181 -181
  145. package/src/tools/sprints.ts +298 -298
  146. package/src/tools/tasks.ts +583 -575
  147. package/src/tools/tools.test.ts +222 -222
  148. package/src/tools/types.ts +9 -9
  149. package/src/tools/validation.ts +75 -75
  150. package/src/tools/version.ts +34 -34
  151. package/src/tools/worktrees.ts +66 -66
  152. package/src/tools.test.ts +416 -416
  153. package/src/utils.test.ts +1014 -1014
  154. package/src/utils.ts +586 -586
  155. package/src/validators.test.ts +223 -223
  156. package/src/validators.ts +249 -249
  157. package/src/version.ts +162 -162
  158. package/tsconfig.json +16 -16
  159. package/vitest.config.ts +14 -14
  160. package/dist/tools.d.ts +0 -2
  161. 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
+ };