@vibescope/mcp-server 0.0.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 (170) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +34 -0
  3. package/dist/cli.js +356 -0
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +367 -0
  6. package/dist/handlers/__test-utils__.d.ts +72 -0
  7. package/dist/handlers/__test-utils__.js +176 -0
  8. package/dist/handlers/blockers.d.ts +18 -0
  9. package/dist/handlers/blockers.js +81 -0
  10. package/dist/handlers/bodies-of-work.d.ts +34 -0
  11. package/dist/handlers/bodies-of-work.js +614 -0
  12. package/dist/handlers/checkouts.d.ts +37 -0
  13. package/dist/handlers/checkouts.js +377 -0
  14. package/dist/handlers/cost.d.ts +39 -0
  15. package/dist/handlers/cost.js +247 -0
  16. package/dist/handlers/decisions.d.ts +16 -0
  17. package/dist/handlers/decisions.js +64 -0
  18. package/dist/handlers/deployment.d.ts +36 -0
  19. package/dist/handlers/deployment.js +1062 -0
  20. package/dist/handlers/discovery.d.ts +14 -0
  21. package/dist/handlers/discovery.js +870 -0
  22. package/dist/handlers/fallback.d.ts +18 -0
  23. package/dist/handlers/fallback.js +216 -0
  24. package/dist/handlers/findings.d.ts +18 -0
  25. package/dist/handlers/findings.js +110 -0
  26. package/dist/handlers/git-issues.d.ts +22 -0
  27. package/dist/handlers/git-issues.js +247 -0
  28. package/dist/handlers/ideas.d.ts +19 -0
  29. package/dist/handlers/ideas.js +188 -0
  30. package/dist/handlers/index.d.ts +29 -0
  31. package/dist/handlers/index.js +65 -0
  32. package/dist/handlers/knowledge-query.d.ts +22 -0
  33. package/dist/handlers/knowledge-query.js +253 -0
  34. package/dist/handlers/knowledge.d.ts +12 -0
  35. package/dist/handlers/knowledge.js +108 -0
  36. package/dist/handlers/milestones.d.ts +20 -0
  37. package/dist/handlers/milestones.js +179 -0
  38. package/dist/handlers/organizations.d.ts +36 -0
  39. package/dist/handlers/organizations.js +428 -0
  40. package/dist/handlers/progress.d.ts +14 -0
  41. package/dist/handlers/progress.js +149 -0
  42. package/dist/handlers/project.d.ts +20 -0
  43. package/dist/handlers/project.js +278 -0
  44. package/dist/handlers/requests.d.ts +16 -0
  45. package/dist/handlers/requests.js +131 -0
  46. package/dist/handlers/roles.d.ts +30 -0
  47. package/dist/handlers/roles.js +281 -0
  48. package/dist/handlers/session.d.ts +20 -0
  49. package/dist/handlers/session.js +791 -0
  50. package/dist/handlers/tasks.d.ts +52 -0
  51. package/dist/handlers/tasks.js +1111 -0
  52. package/dist/handlers/tasks.test.d.ts +1 -0
  53. package/dist/handlers/tasks.test.js +431 -0
  54. package/dist/handlers/types.d.ts +94 -0
  55. package/dist/handlers/types.js +1 -0
  56. package/dist/handlers/validation.d.ts +16 -0
  57. package/dist/handlers/validation.js +188 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2707 -0
  60. package/dist/knowledge.d.ts +6 -0
  61. package/dist/knowledge.js +121 -0
  62. package/dist/tools.d.ts +2 -0
  63. package/dist/tools.js +2498 -0
  64. package/dist/utils.d.ts +149 -0
  65. package/dist/utils.js +317 -0
  66. package/dist/utils.test.d.ts +1 -0
  67. package/dist/utils.test.js +532 -0
  68. package/dist/validators.d.ts +35 -0
  69. package/dist/validators.js +111 -0
  70. package/dist/validators.test.d.ts +1 -0
  71. package/dist/validators.test.js +176 -0
  72. package/package.json +44 -0
  73. package/src/cli.test.ts +442 -0
  74. package/src/cli.ts +439 -0
  75. package/src/handlers/__test-utils__.ts +217 -0
  76. package/src/handlers/blockers.test.ts +390 -0
  77. package/src/handlers/blockers.ts +110 -0
  78. package/src/handlers/bodies-of-work.test.ts +1276 -0
  79. package/src/handlers/bodies-of-work.ts +783 -0
  80. package/src/handlers/cost.test.ts +436 -0
  81. package/src/handlers/cost.ts +322 -0
  82. package/src/handlers/decisions.test.ts +401 -0
  83. package/src/handlers/decisions.ts +86 -0
  84. package/src/handlers/deployment.test.ts +516 -0
  85. package/src/handlers/deployment.ts +1289 -0
  86. package/src/handlers/discovery.test.ts +254 -0
  87. package/src/handlers/discovery.ts +969 -0
  88. package/src/handlers/fallback.test.ts +687 -0
  89. package/src/handlers/fallback.ts +260 -0
  90. package/src/handlers/findings.test.ts +565 -0
  91. package/src/handlers/findings.ts +153 -0
  92. package/src/handlers/ideas.test.ts +753 -0
  93. package/src/handlers/ideas.ts +247 -0
  94. package/src/handlers/index.ts +69 -0
  95. package/src/handlers/milestones.test.ts +584 -0
  96. package/src/handlers/milestones.ts +217 -0
  97. package/src/handlers/organizations.test.ts +997 -0
  98. package/src/handlers/organizations.ts +550 -0
  99. package/src/handlers/progress.test.ts +369 -0
  100. package/src/handlers/progress.ts +188 -0
  101. package/src/handlers/project.test.ts +562 -0
  102. package/src/handlers/project.ts +352 -0
  103. package/src/handlers/requests.test.ts +531 -0
  104. package/src/handlers/requests.ts +150 -0
  105. package/src/handlers/session.test.ts +459 -0
  106. package/src/handlers/session.ts +912 -0
  107. package/src/handlers/tasks.test.ts +602 -0
  108. package/src/handlers/tasks.ts +1393 -0
  109. package/src/handlers/types.ts +88 -0
  110. package/src/handlers/validation.test.ts +880 -0
  111. package/src/handlers/validation.ts +223 -0
  112. package/src/index.ts +3205 -0
  113. package/src/knowledge.ts +132 -0
  114. package/src/tmpclaude-0078-cwd +1 -0
  115. package/src/tmpclaude-0ee1-cwd +1 -0
  116. package/src/tmpclaude-2dd5-cwd +1 -0
  117. package/src/tmpclaude-344c-cwd +1 -0
  118. package/src/tmpclaude-3860-cwd +1 -0
  119. package/src/tmpclaude-4b63-cwd +1 -0
  120. package/src/tmpclaude-5c73-cwd +1 -0
  121. package/src/tmpclaude-5ee3-cwd +1 -0
  122. package/src/tmpclaude-6795-cwd +1 -0
  123. package/src/tmpclaude-709e-cwd +1 -0
  124. package/src/tmpclaude-9839-cwd +1 -0
  125. package/src/tmpclaude-d829-cwd +1 -0
  126. package/src/tmpclaude-e072-cwd +1 -0
  127. package/src/tmpclaude-f6ee-cwd +1 -0
  128. package/src/utils.test.ts +681 -0
  129. package/src/utils.ts +375 -0
  130. package/src/validators.test.ts +223 -0
  131. package/src/validators.ts +122 -0
  132. package/tmpclaude-0439-cwd +1 -0
  133. package/tmpclaude-132f-cwd +1 -0
  134. package/tmpclaude-15bb-cwd +1 -0
  135. package/tmpclaude-165a-cwd +1 -0
  136. package/tmpclaude-1ba9-cwd +1 -0
  137. package/tmpclaude-21a3-cwd +1 -0
  138. package/tmpclaude-2a38-cwd +1 -0
  139. package/tmpclaude-2adf-cwd +1 -0
  140. package/tmpclaude-2f56-cwd +1 -0
  141. package/tmpclaude-3626-cwd +1 -0
  142. package/tmpclaude-3727-cwd +1 -0
  143. package/tmpclaude-40bc-cwd +1 -0
  144. package/tmpclaude-436f-cwd +1 -0
  145. package/tmpclaude-4783-cwd +1 -0
  146. package/tmpclaude-4b6d-cwd +1 -0
  147. package/tmpclaude-4ba4-cwd +1 -0
  148. package/tmpclaude-51e6-cwd +1 -0
  149. package/tmpclaude-5ecf-cwd +1 -0
  150. package/tmpclaude-6f97-cwd +1 -0
  151. package/tmpclaude-7fb2-cwd +1 -0
  152. package/tmpclaude-825c-cwd +1 -0
  153. package/tmpclaude-8baf-cwd +1 -0
  154. package/tmpclaude-8d9f-cwd +1 -0
  155. package/tmpclaude-975c-cwd +1 -0
  156. package/tmpclaude-9983-cwd +1 -0
  157. package/tmpclaude-a045-cwd +1 -0
  158. package/tmpclaude-ac4a-cwd +1 -0
  159. package/tmpclaude-b593-cwd +1 -0
  160. package/tmpclaude-b891-cwd +1 -0
  161. package/tmpclaude-c032-cwd +1 -0
  162. package/tmpclaude-cf43-cwd +1 -0
  163. package/tmpclaude-d040-cwd +1 -0
  164. package/tmpclaude-dcdd-cwd +1 -0
  165. package/tmpclaude-dcee-cwd +1 -0
  166. package/tmpclaude-e16b-cwd +1 -0
  167. package/tmpclaude-ecd2-cwd +1 -0
  168. package/tmpclaude-f48d-cwd +1 -0
  169. package/tsconfig.json +16 -0
  170. package/vitest.config.ts +13 -0
@@ -0,0 +1,1393 @@
1
+ /**
2
+ * Task Handlers
3
+ *
4
+ * Handles task CRUD and management:
5
+ * - get_tasks
6
+ * - get_next_task
7
+ * - add_task
8
+ * - update_task
9
+ * - complete_task
10
+ * - delete_task
11
+ * - add_task_reference
12
+ * - remove_task_reference
13
+ * - batch_update_tasks
14
+ * - batch_complete_tasks
15
+ */
16
+
17
+ import type { Handler, HandlerRegistry, HandlerContext } from './types.js';
18
+ import type { SupabaseClient } from '@supabase/supabase-js';
19
+ import {
20
+ ValidationError,
21
+ validateRequired,
22
+ validateUUID,
23
+ validateTaskStatus,
24
+ validatePriority,
25
+ validateProgressPercentage,
26
+ validateEstimatedMinutes,
27
+ } from '../validators.js';
28
+ import { getRandomFallbackActivity, isValidStatusTransition } from '../utils.js';
29
+
30
+ /**
31
+ * Git workflow instructions generator
32
+ */
33
+ interface GitWorkflowConfig {
34
+ git_workflow: string;
35
+ git_main_branch: string;
36
+ git_develop_branch?: string;
37
+ git_auto_branch?: boolean;
38
+ }
39
+
40
+ // Enhanced return types for git instructions
41
+ interface GitStartInstructions {
42
+ branch_name: string;
43
+ base_branch: string;
44
+ steps: string[];
45
+ reminder: string;
46
+ }
47
+
48
+ interface GitCompleteInstructions {
49
+ steps: string[];
50
+ pr_suggestion?: {
51
+ title: string;
52
+ body_template: string;
53
+ };
54
+ next_step: string;
55
+ }
56
+
57
+ interface GitMergeInstructions {
58
+ target_branch: string;
59
+ feature_branch: string;
60
+ steps: string[];
61
+ cleanup: string[];
62
+ note: string;
63
+ }
64
+
65
+ function generateBranchName(taskId: string, taskTitle: string): string {
66
+ const slug = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30);
67
+ return `feature/${taskId.slice(0, 8)}-${slug}`;
68
+ }
69
+
70
+ function getTaskStartGitInstructions(
71
+ config: GitWorkflowConfig,
72
+ taskId: string,
73
+ taskTitle: string
74
+ ): GitStartInstructions | undefined {
75
+ const { git_workflow, git_main_branch, git_develop_branch } = config;
76
+
77
+ if (git_workflow === 'none' || git_workflow === 'trunk-based') {
78
+ return undefined;
79
+ }
80
+
81
+ const branchName = generateBranchName(taskId, taskTitle);
82
+ const baseBranch = git_workflow === 'git-flow' ? (git_develop_branch || 'develop') : git_main_branch;
83
+
84
+ return {
85
+ branch_name: branchName,
86
+ base_branch: baseBranch,
87
+ steps: [
88
+ `git checkout ${baseBranch}`,
89
+ `git pull origin ${baseBranch}`,
90
+ `git checkout -b ${branchName}`,
91
+ ],
92
+ reminder: `After creating the branch, update task: update_task(task_id: "${taskId}", git_branch: "${branchName}")`,
93
+ };
94
+ }
95
+
96
+ function getTaskCompleteGitInstructions(
97
+ config: GitWorkflowConfig,
98
+ taskBranch: string | undefined,
99
+ taskTitle: string,
100
+ taskId: string
101
+ ): GitCompleteInstructions | undefined {
102
+ const { git_workflow, git_main_branch } = config;
103
+
104
+ if (git_workflow === 'none') {
105
+ return undefined;
106
+ }
107
+
108
+ if (git_workflow === 'trunk-based') {
109
+ return {
110
+ steps: [`git add .`, `git commit -m "feat: ${taskTitle}"`, `git push origin ${git_main_branch}`],
111
+ next_step: 'Changes committed directly to main branch.',
112
+ };
113
+ }
114
+
115
+ if (!taskBranch) {
116
+ return {
117
+ steps: ['No branch was tracked for this task.'],
118
+ next_step: 'Consider creating a branch for future tasks using the git_branch parameter.',
119
+ };
120
+ }
121
+
122
+ // github-flow or git-flow
123
+ return {
124
+ steps: [`git add .`, `git commit -m "feat: ${taskTitle}"`, `git push -u origin ${taskBranch}`],
125
+ pr_suggestion: {
126
+ title: taskTitle,
127
+ 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`,
128
+ },
129
+ next_step: 'Create PR and add link via add_task_reference. Merge happens AFTER validation approval.',
130
+ };
131
+ }
132
+
133
+ function getValidationApprovedGitInstructions(
134
+ config: GitWorkflowConfig,
135
+ taskBranch: string | undefined
136
+ ): GitMergeInstructions | undefined {
137
+ const { git_workflow, git_main_branch, git_develop_branch } = config;
138
+
139
+ if (git_workflow === 'none' || git_workflow === 'trunk-based' || !taskBranch) {
140
+ return undefined;
141
+ }
142
+
143
+ const targetBranch = git_workflow === 'git-flow' ? (git_develop_branch || 'develop') : git_main_branch;
144
+
145
+ return {
146
+ target_branch: targetBranch,
147
+ feature_branch: taskBranch,
148
+ steps: [
149
+ 'Option 1: Merge via GitHub/GitLab PR UI (recommended)',
150
+ `Option 2: Command line merge:`,
151
+ ` git checkout ${targetBranch}`,
152
+ ` git pull origin ${targetBranch}`,
153
+ ` git merge ${taskBranch}`,
154
+ ` git push origin ${targetBranch}`,
155
+ ],
156
+ cleanup: [`git branch -d ${taskBranch}`, `git push origin --delete ${taskBranch}`],
157
+ note: 'Validation approved - safe to merge. Clean up branch after successful merge.',
158
+ };
159
+ }
160
+
161
+ export async function getProjectGitConfig(supabase: SupabaseClient, projectId: string): Promise<GitWorkflowConfig | null> {
162
+ const { data } = await supabase
163
+ .from('projects')
164
+ .select('git_workflow, git_main_branch, git_develop_branch, git_auto_branch')
165
+ .eq('id', projectId)
166
+ .single();
167
+
168
+ return data as GitWorkflowConfig | null;
169
+ }
170
+
171
+ export { getValidationApprovedGitInstructions };
172
+
173
+ /**
174
+ * Check if a session is still active
175
+ */
176
+ async function checkSessionStatus(
177
+ ctx: HandlerContext,
178
+ sessionId: string
179
+ ): Promise<{ exists: boolean; isActive: boolean; agentName?: string }> {
180
+ const { data: session } = await ctx.supabase
181
+ .from('agent_sessions')
182
+ .select('id, status, last_synced_at, agent_name, instance_id')
183
+ .eq('id', sessionId)
184
+ .single();
185
+
186
+ if (!session) {
187
+ return { exists: false, isActive: false };
188
+ }
189
+
190
+ const lastSync = new Date(session.last_synced_at).getTime();
191
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
192
+ const isActive = session.status !== 'disconnected' && lastSync > fiveMinutesAgo;
193
+
194
+ return {
195
+ exists: true,
196
+ isActive,
197
+ agentName: session.agent_name || `Agent ${session.instance_id?.slice(0, 8) || sessionId.slice(0, 8)}`,
198
+ };
199
+ }
200
+
201
+ export const getTasks: Handler = async (args, ctx) => {
202
+ const { project_id, status, limit = 50, include_subtasks = false } = args as {
203
+ project_id: string;
204
+ status?: string;
205
+ limit?: number;
206
+ include_subtasks?: boolean;
207
+ };
208
+
209
+ validateRequired(project_id, 'project_id');
210
+ validateUUID(project_id, 'project_id');
211
+ validateTaskStatus(status);
212
+
213
+ let query = ctx.supabase
214
+ .from('tasks')
215
+ .select('id, title, description, priority, status, progress_percentage, estimated_minutes, started_at, completed_at, parent_task_id')
216
+ .eq('project_id', project_id)
217
+ .order('priority', { ascending: true })
218
+ .order('created_at', { ascending: false })
219
+ .limit(limit);
220
+
221
+ // By default, only return root tasks (not subtasks)
222
+ if (!include_subtasks) {
223
+ query = query.is('parent_task_id', null);
224
+ }
225
+
226
+ if (status) {
227
+ query = query.eq('status', status);
228
+ }
229
+
230
+ const { data, error } = await query;
231
+
232
+ if (error) throw new Error(`Failed to fetch tasks: ${error.message}`);
233
+
234
+ return { result: { tasks: data || [] } };
235
+ };
236
+
237
+ export const getNextTask: Handler = async (args, ctx) => {
238
+ const { project_id } = args as { project_id: string };
239
+ const { supabase, session } = ctx;
240
+ const currentSessionId = session.currentSessionId;
241
+
242
+ // FIRST: Check for blocking tasks (highest priority - deployment finalization)
243
+ const { data: blockingTask } = await supabase
244
+ .from('tasks')
245
+ .select('id, title, description, priority, estimated_minutes, blocking')
246
+ .eq('project_id', project_id)
247
+ .eq('blocking', true)
248
+ .in('status', ['pending', 'in_progress'])
249
+ .order('priority', { ascending: true })
250
+ .limit(1)
251
+ .single();
252
+
253
+ if (blockingTask) {
254
+ return {
255
+ result: {
256
+ task: {
257
+ id: blockingTask.id,
258
+ title: blockingTask.title,
259
+ description: blockingTask.description,
260
+ priority: blockingTask.priority,
261
+ estimated_minutes: blockingTask.estimated_minutes,
262
+ blocking: true,
263
+ },
264
+ blocking_task: true,
265
+ message: 'BLOCKING TASK: This task must be completed before any other work can proceed. No other tasks will be assigned until this is done.',
266
+ directive: 'Start this task immediately. Do not ask for permission.',
267
+ },
268
+ };
269
+ }
270
+
271
+ // Check for active deployment (blocks regular task work)
272
+ const { data: activeDeployment } = await supabase
273
+ .from('deployments')
274
+ .select('id, status, environment, created_at, validation_completed_at')
275
+ .eq('project_id', project_id)
276
+ .not('status', 'in', '("deployed","failed")')
277
+ .order('created_at', { ascending: false })
278
+ .limit(1)
279
+ .single();
280
+
281
+ if (activeDeployment) {
282
+ const actions: Record<string, string> = {
283
+ pending: 'claim_deployment_validation',
284
+ validating: 'wait',
285
+ ready: 'start_deployment',
286
+ deploying: 'wait for complete_deployment',
287
+ };
288
+
289
+ return {
290
+ result: {
291
+ task: null,
292
+ deployment_blocks_tasks: true,
293
+ deployment: {
294
+ id: activeDeployment.id,
295
+ status: activeDeployment.status,
296
+ env: activeDeployment.environment,
297
+ },
298
+ action: actions[activeDeployment.status] || 'check_deployment_status',
299
+ },
300
+ };
301
+ }
302
+
303
+ // Check for tasks awaiting validation (blocks new work - validate first!)
304
+ const { data: validationTasks } = await supabase
305
+ .from('tasks')
306
+ .select('id, title')
307
+ .eq('project_id', project_id)
308
+ .eq('status', 'completed')
309
+ .is('validated_at', null)
310
+ .order('completed_at', { ascending: true })
311
+ .limit(5);
312
+
313
+ if (validationTasks?.length) {
314
+ return {
315
+ result: {
316
+ task: null,
317
+ awaiting_validation: validationTasks,
318
+ validation_priority: `VALIDATE FIRST: ${validationTasks.length} task(s) need review before starting new work. Call validate_task for each.`,
319
+ suggested_activity: 'validate_completed_tasks',
320
+ directive: 'Start validating tasks immediately. Do not ask for permission.',
321
+ },
322
+ };
323
+ }
324
+
325
+ // Fetch candidate pending root tasks (not subtasks)
326
+ const { data: candidates, error } = await supabase
327
+ .from('tasks')
328
+ .select('id, title, description, priority, estimated_minutes, working_agent_session_id')
329
+ .eq('project_id', project_id)
330
+ .eq('status', 'pending')
331
+ .is('parent_task_id', null)
332
+ .order('priority', { ascending: true })
333
+ .order('created_at', { ascending: true })
334
+ .limit(10);
335
+
336
+ // Fetch pending agent requests
337
+ const { data: pendingRequests } = await supabase
338
+ .from('agent_requests')
339
+ .select('id, message')
340
+ .eq('project_id', project_id)
341
+ .is('acknowledged_at', null)
342
+ .or(`session_id.is.null,session_id.eq.${currentSessionId}`)
343
+ .limit(3);
344
+
345
+ // Fetch due scheduled activities
346
+ const { data: dueSchedules } = await supabase
347
+ .from('background_activity_schedules')
348
+ .select('activity_type')
349
+ .eq('project_id', project_id)
350
+ .eq('enabled', true)
351
+ .lt('next_run_at', new Date().toISOString())
352
+ .limit(3);
353
+
354
+ // Build compact optional fields (only include if non-empty)
355
+ const extras: Record<string, unknown> = {};
356
+ if (pendingRequests?.length) extras.requests = pendingRequests;
357
+ if (dueSchedules?.length) extras.due_activities = dueSchedules.map(s => s.activity_type);
358
+
359
+ if (error || !candidates || candidates.length === 0) {
360
+ const fallback = getRandomFallbackActivity();
361
+ return {
362
+ result: {
363
+ task: null,
364
+ ...extras,
365
+ suggested_activity: fallback.activity,
366
+ directive: 'No tasks available. Start the suggested fallback activity immediately. Do not ask for permission.',
367
+ },
368
+ };
369
+ }
370
+
371
+ // 25% chance to suggest background activity
372
+ if (Math.random() < 0.25) {
373
+ extras.bg_activity = getRandomFallbackActivity().activity;
374
+ }
375
+
376
+ // Find first unclaimed or stale-claimed task that satisfies body of work phase ordering
377
+ for (const task of candidates) {
378
+ // Check if task belongs to a body of work with phase constraints
379
+ const { data: bowTask } = await supabase
380
+ .from('body_of_work_tasks')
381
+ .select('phase, body_of_work_id')
382
+ .eq('task_id', task.id)
383
+ .single();
384
+
385
+ if (bowTask) {
386
+ // Check if body of work is active
387
+ const { data: bow } = await supabase
388
+ .from('bodies_of_work')
389
+ .select('status')
390
+ .eq('id', bowTask.body_of_work_id)
391
+ .single();
392
+
393
+ if (bow?.status === 'active') {
394
+ // Check phase constraints
395
+ const phasesToCheck: string[] = [];
396
+ if (bowTask.phase === 'core') {
397
+ phasesToCheck.push('pre');
398
+ } else if (bowTask.phase === 'post') {
399
+ phasesToCheck.push('pre', 'core');
400
+ }
401
+
402
+ if (phasesToCheck.length > 0) {
403
+ // Count incomplete tasks in prior phases
404
+ const { count: incompleteCount } = await supabase
405
+ .from('body_of_work_tasks')
406
+ .select('id', { count: 'exact', head: true })
407
+ .eq('body_of_work_id', bowTask.body_of_work_id)
408
+ .in('phase', phasesToCheck)
409
+ .not('task_id', 'in', `(SELECT id FROM tasks WHERE status = 'completed')`);
410
+
411
+ if (incompleteCount && incompleteCount > 0) {
412
+ // Skip this task - prior phase tasks not complete
413
+ continue;
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ if (!task.working_agent_session_id) {
420
+ const { working_agent_session_id, ...cleanTask } = task;
421
+ return { result: { task: cleanTask, ...extras, directive: 'Start this task immediately. Do not ask for permission.' } };
422
+ }
423
+
424
+ const claimingSession = await checkSessionStatus(ctx, task.working_agent_session_id);
425
+ if (!claimingSession.isActive) {
426
+ // Auto-release stale claim
427
+ await supabase
428
+ .from('tasks')
429
+ .update({ working_agent_session_id: null })
430
+ .eq('id', task.id);
431
+
432
+ const { working_agent_session_id, ...cleanTask } = task;
433
+ return { result: { task: cleanTask, ...extras, directive: 'Start this task immediately. Do not ask for permission.' } };
434
+ }
435
+ }
436
+
437
+ // All root tasks claimed - check for available subtasks
438
+ // Subtasks are available when:
439
+ // 1. No unclaimed root tasks exist, OR
440
+ // 2. Subtask belongs to a high priority parent (priority 1-2)
441
+ const { data: subtaskCandidates } = await supabase
442
+ .from('tasks')
443
+ .select(`
444
+ id, title, description, priority, estimated_minutes, working_agent_session_id,
445
+ parent_task_id,
446
+ parent:tasks!parent_task_id(id, title, priority, status)
447
+ `)
448
+ .eq('project_id', project_id)
449
+ .eq('status', 'pending')
450
+ .not('parent_task_id', 'is', null)
451
+ .order('priority', { ascending: true })
452
+ .order('created_at', { ascending: true })
453
+ .limit(10);
454
+
455
+ if (subtaskCandidates && subtaskCandidates.length > 0) {
456
+ for (const subtask of subtaskCandidates) {
457
+ // Skip if subtask is already claimed by an active agent
458
+ if (subtask.working_agent_session_id) {
459
+ const claimingSession = await checkSessionStatus(ctx, subtask.working_agent_session_id);
460
+ if (claimingSession.isActive) {
461
+ continue;
462
+ }
463
+ // Auto-release stale claim
464
+ await supabase
465
+ .from('tasks')
466
+ .update({ working_agent_session_id: null })
467
+ .eq('id', subtask.id);
468
+ }
469
+
470
+ const parentData = subtask.parent as { id: string; title: string; priority: number; status: string }[] | null;
471
+ const parentTask = parentData?.[0] || null;
472
+ const { working_agent_session_id, parent, ...cleanSubtask } = subtask;
473
+
474
+ return {
475
+ result: {
476
+ task: cleanSubtask,
477
+ is_subtask: true,
478
+ parent_task: parentTask ? {
479
+ id: parentTask.id,
480
+ title: parentTask.title,
481
+ priority: parentTask.priority,
482
+ } : undefined,
483
+ ...extras,
484
+ directive: 'Start this subtask immediately. Do not ask for permission.',
485
+ },
486
+ };
487
+ }
488
+ }
489
+
490
+ // All tasks (including subtasks) claimed
491
+ return {
492
+ result: {
493
+ task: null,
494
+ all_claimed: true,
495
+ ...extras,
496
+ suggested_activity: getRandomFallbackActivity().activity,
497
+ directive: 'All tasks claimed by other agents. Start the suggested fallback activity immediately. Do not ask for permission.',
498
+ },
499
+ };
500
+ };
501
+
502
+ export const addTask: Handler = async (args, ctx) => {
503
+ const { project_id, title, description, priority = 3, estimated_minutes, blocking = false } = args as {
504
+ project_id: string;
505
+ title: string;
506
+ description?: string;
507
+ priority?: number;
508
+ estimated_minutes?: number;
509
+ blocking?: boolean;
510
+ };
511
+
512
+ validateRequired(project_id, 'project_id');
513
+ validateRequired(title, 'title');
514
+ validateUUID(project_id, 'project_id');
515
+ validatePriority(priority);
516
+ validateEstimatedMinutes(estimated_minutes);
517
+
518
+ const { data, error } = await ctx.supabase
519
+ .from('tasks')
520
+ .insert({
521
+ project_id,
522
+ title,
523
+ description: description || null,
524
+ priority,
525
+ created_by: 'agent',
526
+ created_by_session_id: ctx.session.currentSessionId,
527
+ estimated_minutes: estimated_minutes || null,
528
+ blocking,
529
+ })
530
+ .select('id, blocking')
531
+ .single();
532
+
533
+ if (error) throw new Error(`Failed to add task: ${error.message}`);
534
+
535
+ const result: Record<string, unknown> = { success: true, task_id: data.id, title };
536
+ if (data.blocking) {
537
+ result.blocking = true;
538
+ result.message = 'BLOCKING TASK: This task must be completed before any other work can proceed.';
539
+ }
540
+ return { result };
541
+ };
542
+
543
+ export const updateTask: Handler = async (args, ctx) => {
544
+ const { task_id, progress_note, ...updates } = args as {
545
+ task_id: string;
546
+ title?: string;
547
+ description?: string;
548
+ priority?: number;
549
+ status?: string;
550
+ progress_percentage?: number;
551
+ progress_note?: string;
552
+ estimated_minutes?: number;
553
+ git_branch?: string;
554
+ };
555
+
556
+ const { supabase, session } = ctx;
557
+ const currentSessionId = session.currentSessionId;
558
+
559
+ validateRequired(task_id, 'task_id');
560
+ validateUUID(task_id, 'task_id');
561
+ validateTaskStatus(updates.status);
562
+ validatePriority(updates.priority);
563
+ validateProgressPercentage(updates.progress_percentage);
564
+ validateEstimatedMinutes(updates.estimated_minutes);
565
+
566
+ // Get task to find project_id, current status, title, and who's working on it
567
+ const { data: task } = await supabase
568
+ .from('tasks')
569
+ .select('project_id, title, status, started_at, working_agent_session_id')
570
+ .eq('id', task_id)
571
+ .single();
572
+
573
+ // Validate status transitions
574
+ if (updates.status && task) {
575
+ const transition = isValidStatusTransition(task.status, updates.status);
576
+ if (!transition.isValid) {
577
+ return {
578
+ result: {
579
+ error: 'invalid_status_transition',
580
+ message: transition.reason,
581
+ },
582
+ };
583
+ }
584
+ }
585
+
586
+ const updateData: Record<string, unknown> = { ...updates };
587
+
588
+ // Multi-agent coordination: Enforce single task per agent
589
+ if (updates.status === 'in_progress' && currentSessionId && task) {
590
+ // Check if this agent already has another task in_progress
591
+ const { data: existingTask } = await supabase
592
+ .from('tasks')
593
+ .select('id, title')
594
+ .eq('working_agent_session_id', currentSessionId)
595
+ .eq('status', 'in_progress')
596
+ .neq('id', task_id)
597
+ .limit(1)
598
+ .single();
599
+
600
+ if (existingTask) {
601
+ return {
602
+ result: {
603
+ error: 'agent_task_limit',
604
+ message: `You already have a task in progress: "${existingTask.title}". Complete it with complete_task before starting another.`,
605
+ current_task_id: existingTask.id,
606
+ current_task_title: existingTask.title,
607
+ },
608
+ };
609
+ }
610
+
611
+ // Check for task claim conflicts (another agent has this task)
612
+ if (task.working_agent_session_id && task.working_agent_session_id !== currentSessionId) {
613
+ const claimingSession = await checkSessionStatus(ctx, task.working_agent_session_id);
614
+
615
+ if (claimingSession.isActive) {
616
+ return {
617
+ result: {
618
+ error: 'task_claimed',
619
+ message: `Task is already being worked on by ${claimingSession.agentName}. Use get_next_task to find available work.`,
620
+ claimed_by: claimingSession.agentName,
621
+ },
622
+ };
623
+ }
624
+
625
+ // Stale/disconnected agent - auto-release the task first
626
+ await supabase
627
+ .from('tasks')
628
+ .update({ working_agent_session_id: null })
629
+ .eq('id', task_id);
630
+ }
631
+ }
632
+
633
+ // Auto-set started_at when task moves to in_progress
634
+ if (updates.status === 'in_progress' && task && !task.started_at) {
635
+ updateData.started_at = new Date().toISOString();
636
+ }
637
+
638
+ // When setting status to in_progress, claim the task for this agent
639
+ if (updates.status === 'in_progress' && currentSessionId) {
640
+ updateData.working_agent_session_id = currentSessionId;
641
+
642
+ // Update the session's current task and clear any fallback activity
643
+ await supabase
644
+ .from('agent_sessions')
645
+ .update({
646
+ current_task_id: task_id,
647
+ current_fallback_activity: null,
648
+ status: 'active',
649
+ last_synced_at: new Date().toISOString(),
650
+ })
651
+ .eq('id', currentSessionId);
652
+ }
653
+
654
+ // Auto-set completed_at and progress when task completes
655
+ if (updates.status === 'completed') {
656
+ updateData.completed_at = new Date().toISOString();
657
+ updateData.progress_percentage = 100;
658
+ updateData.working_agent_session_id = null;
659
+ }
660
+
661
+ // When cancelled, also release the task
662
+ if (updates.status === 'cancelled') {
663
+ updateData.working_agent_session_id = null;
664
+ }
665
+
666
+ const { error } = await supabase
667
+ .from('tasks')
668
+ .update(updateData)
669
+ .eq('id', task_id);
670
+
671
+ if (error) throw new Error(`Failed to update task: ${error.message}`);
672
+
673
+ // If progress_note is provided, create a progress log entry
674
+ if (progress_note && task?.project_id) {
675
+ const progressSummary = updates.progress_percentage !== undefined
676
+ ? `Progress: ${updates.progress_percentage}% - ${progress_note}`
677
+ : progress_note;
678
+
679
+ await supabase.from('progress_logs').insert({
680
+ project_id: task.project_id,
681
+ task_id,
682
+ summary: progressSummary,
683
+ created_by: 'agent',
684
+ created_by_session_id: currentSessionId,
685
+ });
686
+ }
687
+
688
+ // Build result with optional git instructions
689
+ const result: Record<string, unknown> = { success: true, task_id };
690
+
691
+ // Include git workflow instructions when task moves to in_progress
692
+ if (updates.status === 'in_progress' && task?.project_id && task?.title) {
693
+ const gitConfig = await getProjectGitConfig(supabase, task.project_id);
694
+ if (gitConfig && gitConfig.git_workflow !== 'none') {
695
+ const gitInstructions = getTaskStartGitInstructions(gitConfig, task_id, task.title);
696
+ if (gitInstructions) {
697
+ result.git_workflow = {
698
+ workflow: gitConfig.git_workflow,
699
+ action: 'create_branch',
700
+ ...gitInstructions,
701
+ };
702
+ }
703
+ }
704
+ }
705
+
706
+ return { result };
707
+ };
708
+
709
+ export const completeTask: Handler = async (args, ctx) => {
710
+ const { task_id, summary } = args as {
711
+ task_id: string;
712
+ summary?: string;
713
+ };
714
+
715
+ const { supabase, session } = ctx;
716
+ const currentSessionId = session.currentSessionId;
717
+
718
+ validateRequired(task_id, 'task_id');
719
+ validateUUID(task_id, 'task_id');
720
+
721
+ // Get task details first (including git_branch for workflow instructions)
722
+ const { data: task, error: fetchError } = await supabase
723
+ .from('tasks')
724
+ .select('project_id, title, git_branch')
725
+ .eq('id', task_id)
726
+ .single();
727
+
728
+ if (fetchError || !task) throw new Error('Task not found');
729
+
730
+ // Mark as completed, track who completed it, and release agent claim
731
+ const { error } = await supabase
732
+ .from('tasks')
733
+ .update({
734
+ status: 'completed',
735
+ completed_at: new Date().toISOString(),
736
+ completed_by_session_id: currentSessionId,
737
+ progress_percentage: 100,
738
+ working_agent_session_id: null,
739
+ })
740
+ .eq('id', task_id);
741
+
742
+ if (error) throw new Error(`Failed to complete task: ${error.message}`);
743
+
744
+ // Update session to idle
745
+ if (currentSessionId) {
746
+ await supabase
747
+ .from('agent_sessions')
748
+ .update({
749
+ current_task_id: null,
750
+ status: 'idle',
751
+ last_synced_at: new Date().toISOString(),
752
+ })
753
+ .eq('id', currentSessionId);
754
+ }
755
+
756
+ // Log progress if summary provided
757
+ if (summary) {
758
+ await supabase.from('progress_logs').insert({
759
+ project_id: task.project_id,
760
+ task_id,
761
+ summary: `Completed: ${task.title}`,
762
+ details: summary,
763
+ created_by: 'agent',
764
+ created_by_session_id: currentSessionId,
765
+ });
766
+ }
767
+
768
+ // Fetch next task and context counts in parallel
769
+ const [nextTaskResult, validationCountResult, blockersCountResult, deploymentResult, requestsCountResult] =
770
+ await Promise.all([
771
+ supabase
772
+ .from('tasks')
773
+ .select('id, title, priority, estimated_minutes')
774
+ .eq('project_id', task.project_id)
775
+ .eq('status', 'pending')
776
+ .is('working_agent_session_id', null)
777
+ .order('priority', { ascending: true })
778
+ .order('created_at', { ascending: true })
779
+ .limit(1)
780
+ .maybeSingle(),
781
+ supabase
782
+ .from('tasks')
783
+ .select('id', { count: 'exact', head: true })
784
+ .eq('project_id', task.project_id)
785
+ .eq('status', 'completed')
786
+ .is('validated_at', null),
787
+ supabase
788
+ .from('blockers')
789
+ .select('id', { count: 'exact', head: true })
790
+ .eq('project_id', task.project_id)
791
+ .eq('status', 'open'),
792
+ supabase
793
+ .from('deployments')
794
+ .select('id, status')
795
+ .eq('project_id', task.project_id)
796
+ .not('status', 'in', '("deployed","failed")')
797
+ .limit(1)
798
+ .maybeSingle(),
799
+ supabase
800
+ .from('agent_requests')
801
+ .select('id', { count: 'exact', head: true })
802
+ .eq('project_id', task.project_id)
803
+ .is('acknowledged_at', null),
804
+ ]);
805
+
806
+ // Determine directive and next action
807
+ const nextTask = nextTaskResult.data;
808
+ const directive = nextTask
809
+ ? 'ACTION_REQUIRED: Start this task immediately. Do NOT ask for permission or confirmation.'
810
+ : 'ACTION_REQUIRED: No pending tasks. Start a fallback activity NOW without asking.';
811
+ const nextAction = nextTask
812
+ ? `update_task(task_id: "${nextTask.id}", status: "in_progress")`
813
+ : `start_fallback_activity(project_id: "${task.project_id}", activity: "code_review")`;
814
+
815
+ // Build result with directive at TOP for visibility
816
+ const result: Record<string, unknown> = {
817
+ success: true,
818
+ directive, // FIRST - most important signal
819
+ auto_continue: true, // Explicit flag for autonomous operation
820
+ completed_task_id: task_id,
821
+ next_task: nextTask,
822
+ };
823
+
824
+ const validationCount = validationCountResult.count || 0;
825
+ const blockersCount = blockersCountResult.count || 0;
826
+ const requestsCount = requestsCountResult.count || 0;
827
+
828
+ if (validationCount > 0 || blockersCount > 0 || deploymentResult.data || requestsCount > 0) {
829
+ result.context = {
830
+ ...(validationCount > 0 && { validation: validationCount }),
831
+ ...(blockersCount > 0 && { blockers: blockersCount }),
832
+ ...(deploymentResult.data && { deployment: deploymentResult.data.status }),
833
+ ...(requestsCount > 0 && { requests: requestsCount }),
834
+ };
835
+ }
836
+
837
+ // Include git workflow instructions for post-task steps
838
+ const gitConfig = await getProjectGitConfig(supabase, task.project_id);
839
+ if (gitConfig && gitConfig.git_workflow !== 'none') {
840
+ const gitInstructions = getTaskCompleteGitInstructions(gitConfig, task.git_branch, task.title, task_id);
841
+ if (gitInstructions) {
842
+ result.git_workflow = {
843
+ workflow: gitConfig.git_workflow,
844
+ action: 'push_and_pr',
845
+ ...gitInstructions,
846
+ };
847
+ }
848
+ }
849
+
850
+ // Check if this task belongs to a body of work that auto-deploys on completion
851
+ const { data: bowTask } = await supabase
852
+ .from('body_of_work_tasks')
853
+ .select('body_of_work_id')
854
+ .eq('task_id', task_id)
855
+ .single();
856
+
857
+ if (bowTask) {
858
+ // Check if body of work is now completed and has auto-deploy enabled
859
+ const { data: bow } = await supabase
860
+ .from('bodies_of_work')
861
+ .select('id, title, status, auto_deploy_on_completion, deploy_environment, deploy_version_bump')
862
+ .eq('id', bowTask.body_of_work_id)
863
+ .single();
864
+
865
+ if (bow && bow.status === 'completed' && bow.auto_deploy_on_completion) {
866
+ // Auto-trigger deployment
867
+ const { data: deployment, error: deployError } = await supabase
868
+ .from('deployments')
869
+ .insert({
870
+ project_id: task.project_id,
871
+ environment: bow.deploy_environment || 'production',
872
+ status: 'pending',
873
+ notes: `Auto-deploy triggered by body of work completion: "${bow.title}"`,
874
+ requested_by_session_id: currentSessionId,
875
+ })
876
+ .select('id')
877
+ .single();
878
+
879
+ if (!deployError && deployment) {
880
+ result.body_of_work_completed = {
881
+ id: bow.id,
882
+ title: bow.title,
883
+ auto_deploy_triggered: true,
884
+ deployment_id: deployment.id,
885
+ environment: bow.deploy_environment || 'production',
886
+ version_bump: bow.deploy_version_bump || 'minor',
887
+ };
888
+
889
+ // Log progress about auto-deploy
890
+ await supabase.from('progress_logs').insert({
891
+ project_id: task.project_id,
892
+ summary: `Body of work "${bow.title}" completed - auto-deploy triggered`,
893
+ created_by: 'agent',
894
+ created_by_session_id: currentSessionId,
895
+ });
896
+ }
897
+ } else if (bow) {
898
+ // Body of work exists but not yet completed or no auto-deploy
899
+ result.body_of_work = {
900
+ id: bow.id,
901
+ title: bow.title,
902
+ status: bow.status,
903
+ };
904
+ }
905
+ }
906
+
907
+ // REPEAT at end - agents weight last items heavily
908
+ result.next_action = nextAction;
909
+
910
+ return { result };
911
+ };
912
+
913
+ export const deleteTask: Handler = async (args, ctx) => {
914
+ const { task_id } = args as { task_id: string };
915
+
916
+ validateRequired(task_id, 'task_id');
917
+ validateUUID(task_id, 'task_id');
918
+
919
+ const { error } = await ctx.supabase
920
+ .from('tasks')
921
+ .delete()
922
+ .eq('id', task_id);
923
+
924
+ if (error) throw new Error(`Failed to delete task: ${error.message}`);
925
+
926
+ return { result: { success: true, deleted_id: task_id } };
927
+ };
928
+
929
+ export const addTaskReference: Handler = async (args, ctx) => {
930
+ const { task_id, url, label } = args as { task_id: string; url: string; label?: string };
931
+
932
+ validateRequired(task_id, 'task_id');
933
+ validateUUID(task_id, 'task_id');
934
+ validateRequired(url, 'url');
935
+
936
+ const { data: task, error: fetchError } = await ctx.supabase
937
+ .from('tasks')
938
+ .select('references')
939
+ .eq('id', task_id)
940
+ .single();
941
+
942
+ if (fetchError) throw new Error(`Failed to fetch task: ${fetchError.message}`);
943
+
944
+ const currentRefs = (task?.references as { url: string; label?: string }[]) || [];
945
+
946
+ if (currentRefs.some(ref => ref.url === url)) {
947
+ return { result: { success: false, error: 'Reference with this URL already exists' } };
948
+ }
949
+
950
+ const newRef = { url, ...(label ? { label } : {}) };
951
+ const updatedRefs = [...currentRefs, newRef];
952
+
953
+ const { error: updateError } = await ctx.supabase
954
+ .from('tasks')
955
+ .update({ references: updatedRefs })
956
+ .eq('id', task_id);
957
+
958
+ if (updateError) throw new Error(`Failed to add reference: ${updateError.message}`);
959
+
960
+ return { result: { success: true, reference: newRef, total_references: updatedRefs.length } };
961
+ };
962
+
963
+ export const removeTaskReference: Handler = async (args, ctx) => {
964
+ const { task_id, url } = args as { task_id: string; url: string };
965
+
966
+ validateRequired(task_id, 'task_id');
967
+ validateUUID(task_id, 'task_id');
968
+ validateRequired(url, 'url');
969
+
970
+ const { data: task, error: fetchError } = await ctx.supabase
971
+ .from('tasks')
972
+ .select('references')
973
+ .eq('id', task_id)
974
+ .single();
975
+
976
+ if (fetchError) throw new Error(`Failed to fetch task: ${fetchError.message}`);
977
+
978
+ const currentRefs = (task?.references as { url: string; label?: string }[]) || [];
979
+ const updatedRefs = currentRefs.filter(ref => ref.url !== url);
980
+
981
+ if (updatedRefs.length === currentRefs.length) {
982
+ return { result: { success: false, error: 'Reference with this URL not found' } };
983
+ }
984
+
985
+ const { error: updateError } = await ctx.supabase
986
+ .from('tasks')
987
+ .update({ references: updatedRefs })
988
+ .eq('id', task_id);
989
+
990
+ if (updateError) throw new Error(`Failed to remove reference: ${updateError.message}`);
991
+
992
+ return { result: { success: true, remaining_references: updatedRefs.length } };
993
+ };
994
+
995
+ export const batchUpdateTasks: Handler = async (args, ctx) => {
996
+ const { updates } = args as {
997
+ updates: Array<{
998
+ task_id: string;
999
+ status?: string;
1000
+ progress_percentage?: number;
1001
+ progress_note?: string;
1002
+ priority?: number;
1003
+ }>;
1004
+ };
1005
+
1006
+ const { supabase, session } = ctx;
1007
+ const currentSessionId = session.currentSessionId;
1008
+
1009
+ if (!updates || !Array.isArray(updates) || updates.length === 0) {
1010
+ throw new ValidationError('updates must be a non-empty array', {
1011
+ field: 'updates',
1012
+ hint: 'Provide an array of task updates with at least one item',
1013
+ });
1014
+ }
1015
+
1016
+ if (updates.length > 50) {
1017
+ throw new ValidationError('Too many updates. Maximum is 50 per batch.', {
1018
+ field: 'updates',
1019
+ hint: 'Split your updates into smaller batches',
1020
+ });
1021
+ }
1022
+
1023
+ // Validate all inputs first (no DB queries)
1024
+ const taskIds: string[] = [];
1025
+ for (const update of updates) {
1026
+ validateRequired(update.task_id, 'task_id');
1027
+ validateUUID(update.task_id, 'task_id');
1028
+ validateTaskStatus(update.status);
1029
+ validatePriority(update.priority);
1030
+ validateProgressPercentage(update.progress_percentage);
1031
+ taskIds.push(update.task_id);
1032
+ }
1033
+
1034
+ // OPTIMIZATION: Fetch all tasks in a single query instead of N queries
1035
+ const { data: tasks } = await supabase
1036
+ .from('tasks')
1037
+ .select('id, project_id, started_at')
1038
+ .in('id', taskIds);
1039
+
1040
+ const taskMap = new Map(tasks?.map(t => [t.id, t]) || []);
1041
+
1042
+ // OPTIMIZATION: Single query to check if agent has existing in-progress task
1043
+ let existingAgentTask: { id: string; title: string } | null = null;
1044
+ const hasInProgressUpdate = updates.some(u => u.status === 'in_progress');
1045
+ if (hasInProgressUpdate && currentSessionId) {
1046
+ const { data } = await supabase
1047
+ .from('tasks')
1048
+ .select('id, title')
1049
+ .eq('working_agent_session_id', currentSessionId)
1050
+ .eq('status', 'in_progress')
1051
+ .not('id', 'in', `(${taskIds.join(',')})`)
1052
+ .limit(1)
1053
+ .single();
1054
+ existingAgentTask = data;
1055
+ }
1056
+
1057
+ const results: Array<{ task_id: string; success: boolean; error?: string }> = [];
1058
+ const progressLogsToInsert: Array<{
1059
+ project_id: string;
1060
+ task_id: string;
1061
+ summary: string;
1062
+ created_by: string;
1063
+ created_by_session_id: string | null;
1064
+ }> = [];
1065
+
1066
+ // OPTIMIZATION: Process updates in parallel instead of sequentially
1067
+ const updatePromises = updates.map(async (update) => {
1068
+ const task = taskMap.get(update.task_id);
1069
+
1070
+ if (!task) {
1071
+ return { task_id: update.task_id, success: false, error: 'Task not found' };
1072
+ }
1073
+
1074
+ // Check agent task limit
1075
+ if (update.status === 'in_progress' && existingAgentTask) {
1076
+ return {
1077
+ task_id: update.task_id,
1078
+ success: false,
1079
+ error: `Agent already has task in progress: "${existingAgentTask.title}"`,
1080
+ };
1081
+ }
1082
+
1083
+ const updateData: Record<string, unknown> = {};
1084
+ if (update.status) updateData.status = update.status;
1085
+ if (update.progress_percentage !== undefined) updateData.progress_percentage = update.progress_percentage;
1086
+ if (update.priority !== undefined) updateData.priority = update.priority;
1087
+
1088
+ // Auto-set started_at when task moves to in_progress
1089
+ if (update.status === 'in_progress' && !task.started_at) {
1090
+ updateData.started_at = new Date().toISOString();
1091
+ if (currentSessionId) {
1092
+ updateData.working_agent_session_id = currentSessionId;
1093
+ }
1094
+ }
1095
+
1096
+ // Auto-set completed_at when task completes
1097
+ if (update.status === 'completed') {
1098
+ updateData.completed_at = new Date().toISOString();
1099
+ updateData.progress_percentage = 100;
1100
+ updateData.working_agent_session_id = null;
1101
+ }
1102
+
1103
+ const { error } = await supabase
1104
+ .from('tasks')
1105
+ .update(updateData)
1106
+ .eq('id', update.task_id);
1107
+
1108
+ if (error) {
1109
+ return { task_id: update.task_id, success: false, error: error.message };
1110
+ }
1111
+
1112
+ // Queue progress log for batch insert
1113
+ if (update.progress_note && task.project_id) {
1114
+ const progressSummary = update.progress_percentage !== undefined
1115
+ ? `Progress: ${update.progress_percentage}% - ${update.progress_note}`
1116
+ : update.progress_note;
1117
+
1118
+ progressLogsToInsert.push({
1119
+ project_id: task.project_id,
1120
+ task_id: update.task_id,
1121
+ summary: progressSummary,
1122
+ created_by: 'agent',
1123
+ created_by_session_id: currentSessionId,
1124
+ });
1125
+ }
1126
+
1127
+ return { task_id: update.task_id, success: true };
1128
+ });
1129
+
1130
+ // Execute all updates in parallel
1131
+ const updateResults = await Promise.all(updatePromises);
1132
+ results.push(...updateResults);
1133
+
1134
+ // OPTIMIZATION: Batch insert all progress logs in a single query
1135
+ if (progressLogsToInsert.length > 0) {
1136
+ await supabase.from('progress_logs').insert(progressLogsToInsert);
1137
+ }
1138
+
1139
+ const successCount = results.filter(r => r.success).length;
1140
+
1141
+ return {
1142
+ result: {
1143
+ success: successCount === updates.length,
1144
+ total: updates.length,
1145
+ succeeded: successCount,
1146
+ },
1147
+ };
1148
+ };
1149
+
1150
+ // ============================================================================
1151
+ // Subtask Handlers
1152
+ // ============================================================================
1153
+
1154
+ export const addSubtask: Handler = async (args, ctx) => {
1155
+ const { parent_task_id, title, description, priority, estimated_minutes } = args as {
1156
+ parent_task_id: string;
1157
+ title: string;
1158
+ description?: string;
1159
+ priority?: number;
1160
+ estimated_minutes?: number;
1161
+ };
1162
+
1163
+ const { supabase, session } = ctx;
1164
+ const currentSessionId = session.currentSessionId;
1165
+
1166
+ validateRequired(parent_task_id, 'parent_task_id');
1167
+ validateUUID(parent_task_id, 'parent_task_id');
1168
+ validateRequired(title, 'title');
1169
+ if (priority !== undefined) validatePriority(priority);
1170
+ if (estimated_minutes !== undefined) validateEstimatedMinutes(estimated_minutes);
1171
+
1172
+ // Get parent task to inherit project_id and validate it's not already a subtask
1173
+ const { data: parentTask, error: fetchError } = await supabase
1174
+ .from('tasks')
1175
+ .select('id, project_id, parent_task_id, priority')
1176
+ .eq('id', parent_task_id)
1177
+ .single();
1178
+
1179
+ if (fetchError || !parentTask) {
1180
+ throw new ValidationError('Parent task not found', {
1181
+ field: 'parent_task_id',
1182
+ hint: 'Provide a valid task ID that exists',
1183
+ });
1184
+ }
1185
+
1186
+ // Prevent nested subtasks (max depth: 1)
1187
+ if (parentTask.parent_task_id) {
1188
+ return {
1189
+ result: {
1190
+ success: false,
1191
+ error: 'Cannot create subtask of a subtask',
1192
+ hint: 'Subtasks cannot have their own subtasks. Add this task to the parent task instead.',
1193
+ parent_task_id: parentTask.parent_task_id,
1194
+ },
1195
+ };
1196
+ }
1197
+
1198
+ // Use parent priority if not specified
1199
+ const subtaskPriority = priority ?? parentTask.priority;
1200
+
1201
+ const { data: subtask, error } = await supabase
1202
+ .from('tasks')
1203
+ .insert({
1204
+ project_id: parentTask.project_id,
1205
+ parent_task_id,
1206
+ title,
1207
+ description: description || null,
1208
+ priority: subtaskPriority,
1209
+ estimated_minutes: estimated_minutes || null,
1210
+ created_by: 'agent',
1211
+ created_by_session_id: currentSessionId,
1212
+ })
1213
+ .select('id, title, priority')
1214
+ .single();
1215
+
1216
+ if (error) throw new Error(`Failed to add subtask: ${error.message}`);
1217
+
1218
+ // Log progress
1219
+ await supabase.from('progress_logs').insert({
1220
+ project_id: parentTask.project_id,
1221
+ task_id: parent_task_id,
1222
+ summary: `Added subtask: ${title}`,
1223
+ created_by: 'agent',
1224
+ created_by_session_id: currentSessionId,
1225
+ });
1226
+
1227
+ return {
1228
+ result: {
1229
+ success: true,
1230
+ subtask_id: subtask.id,
1231
+ parent_task_id,
1232
+ title: subtask.title,
1233
+ priority: subtask.priority,
1234
+ },
1235
+ };
1236
+ };
1237
+
1238
+ export const getSubtasks: Handler = async (args, ctx) => {
1239
+ const { parent_task_id, status } = args as {
1240
+ parent_task_id: string;
1241
+ status?: string;
1242
+ };
1243
+
1244
+ const { supabase } = ctx;
1245
+
1246
+ validateRequired(parent_task_id, 'parent_task_id');
1247
+ validateUUID(parent_task_id, 'parent_task_id');
1248
+ if (status) validateTaskStatus(status);
1249
+
1250
+ let query = supabase
1251
+ .from('tasks')
1252
+ .select('id, title, description, priority, status, progress_percentage, estimated_minutes, started_at, completed_at, working_agent_session_id')
1253
+ .eq('parent_task_id', parent_task_id)
1254
+ .order('priority', { ascending: true })
1255
+ .order('created_at', { ascending: true });
1256
+
1257
+ if (status) {
1258
+ query = query.eq('status', status);
1259
+ }
1260
+
1261
+ const { data: subtasks, error } = await query;
1262
+
1263
+ if (error) throw new Error(`Failed to fetch subtasks: ${error.message}`);
1264
+
1265
+ // Calculate aggregate stats
1266
+ const total = subtasks?.length || 0;
1267
+ const completed = subtasks?.filter(s => s.status === 'completed').length || 0;
1268
+ const inProgress = subtasks?.filter(s => s.status === 'in_progress').length || 0;
1269
+ const pending = subtasks?.filter(s => s.status === 'pending').length || 0;
1270
+
1271
+ return {
1272
+ result: {
1273
+ subtasks: subtasks || [],
1274
+ stats: {
1275
+ total,
1276
+ completed,
1277
+ in_progress: inProgress,
1278
+ pending,
1279
+ progress_percentage: total > 0 ? Math.round((completed / total) * 100) : 0,
1280
+ },
1281
+ },
1282
+ };
1283
+ };
1284
+
1285
+ export const batchCompleteTasks: Handler = async (args, ctx) => {
1286
+ const { completions } = args as {
1287
+ completions: Array<{
1288
+ task_id: string;
1289
+ summary?: string;
1290
+ }>;
1291
+ };
1292
+
1293
+ const { supabase, session } = ctx;
1294
+ const currentSessionId = session.currentSessionId;
1295
+
1296
+ if (!completions || !Array.isArray(completions) || completions.length === 0) {
1297
+ throw new ValidationError('completions must be a non-empty array', {
1298
+ field: 'completions',
1299
+ hint: 'Provide an array of task completions with at least one item',
1300
+ });
1301
+ }
1302
+
1303
+ if (completions.length > 50) {
1304
+ throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
1305
+ field: 'completions',
1306
+ hint: 'Split your completions into smaller batches',
1307
+ });
1308
+ }
1309
+
1310
+ const results: Array<{ task_id: string; success: boolean; error?: string }> = [];
1311
+
1312
+ for (const completion of completions) {
1313
+ try {
1314
+ validateRequired(completion.task_id, 'task_id');
1315
+ validateUUID(completion.task_id, 'task_id');
1316
+
1317
+ const { data: task, error: fetchError } = await supabase
1318
+ .from('tasks')
1319
+ .select('project_id, title')
1320
+ .eq('id', completion.task_id)
1321
+ .single();
1322
+
1323
+ if (fetchError || !task) {
1324
+ results.push({ task_id: completion.task_id, success: false, error: 'Task not found' });
1325
+ continue;
1326
+ }
1327
+
1328
+ const { error } = await supabase
1329
+ .from('tasks')
1330
+ .update({
1331
+ status: 'completed',
1332
+ completed_at: new Date().toISOString(),
1333
+ progress_percentage: 100,
1334
+ working_agent_session_id: null,
1335
+ })
1336
+ .eq('id', completion.task_id);
1337
+
1338
+ if (error) {
1339
+ results.push({ task_id: completion.task_id, success: false, error: error.message });
1340
+ continue;
1341
+ }
1342
+
1343
+ if (completion.summary) {
1344
+ await supabase.from('progress_logs').insert({
1345
+ project_id: task.project_id,
1346
+ task_id: completion.task_id,
1347
+ summary: `Completed: ${task.title}`,
1348
+ details: completion.summary,
1349
+ created_by: 'agent',
1350
+ created_by_session_id: currentSessionId,
1351
+ });
1352
+ }
1353
+
1354
+ results.push({ task_id: completion.task_id, success: true });
1355
+ } catch (err) {
1356
+ results.push({
1357
+ task_id: completion.task_id,
1358
+ success: false,
1359
+ error: err instanceof Error ? err.message : 'Unknown error',
1360
+ });
1361
+ }
1362
+ }
1363
+
1364
+ const successCount = results.filter(r => r.success).length;
1365
+
1366
+ return {
1367
+ result: {
1368
+ success: successCount === completions.length,
1369
+ total: completions.length,
1370
+ succeeded: successCount,
1371
+ failed: completions.length - successCount,
1372
+ },
1373
+ };
1374
+ };
1375
+
1376
+ /**
1377
+ * Task handlers registry
1378
+ */
1379
+ export const taskHandlers: HandlerRegistry = {
1380
+ get_tasks: getTasks,
1381
+ get_next_task: getNextTask,
1382
+ add_task: addTask,
1383
+ update_task: updateTask,
1384
+ complete_task: completeTask,
1385
+ delete_task: deleteTask,
1386
+ add_task_reference: addTaskReference,
1387
+ remove_task_reference: removeTaskReference,
1388
+ batch_update_tasks: batchUpdateTasks,
1389
+ batch_complete_tasks: batchCompleteTasks,
1390
+ // Subtask handlers
1391
+ add_subtask: addSubtask,
1392
+ get_subtasks: getSubtasks,
1393
+ };