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