@vibescope/mcp-server 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +113 -98
  2. package/dist/api-client.d.ts +1169 -0
  3. package/dist/api-client.js +713 -0
  4. package/dist/cli.d.ts +1 -6
  5. package/dist/cli.js +39 -240
  6. package/dist/config/tool-categories.d.ts +31 -0
  7. package/dist/config/tool-categories.js +253 -0
  8. package/dist/handlers/blockers.js +57 -58
  9. package/dist/handlers/bodies-of-work.d.ts +2 -0
  10. package/dist/handlers/bodies-of-work.js +108 -477
  11. package/dist/handlers/cost.d.ts +1 -0
  12. package/dist/handlers/cost.js +35 -113
  13. package/dist/handlers/decisions.d.ts +2 -0
  14. package/dist/handlers/decisions.js +28 -27
  15. package/dist/handlers/deployment.js +113 -828
  16. package/dist/handlers/discovery.d.ts +3 -0
  17. package/dist/handlers/discovery.js +26 -627
  18. package/dist/handlers/fallback.d.ts +2 -0
  19. package/dist/handlers/fallback.js +56 -142
  20. package/dist/handlers/findings.d.ts +8 -1
  21. package/dist/handlers/findings.js +65 -68
  22. package/dist/handlers/git-issues.d.ts +9 -13
  23. package/dist/handlers/git-issues.js +80 -225
  24. package/dist/handlers/ideas.d.ts +3 -0
  25. package/dist/handlers/ideas.js +53 -134
  26. package/dist/handlers/index.d.ts +2 -0
  27. package/dist/handlers/index.js +6 -0
  28. package/dist/handlers/milestones.d.ts +2 -0
  29. package/dist/handlers/milestones.js +51 -98
  30. package/dist/handlers/organizations.js +79 -275
  31. package/dist/handlers/progress.d.ts +2 -0
  32. package/dist/handlers/progress.js +25 -123
  33. package/dist/handlers/project.js +42 -221
  34. package/dist/handlers/requests.d.ts +2 -0
  35. package/dist/handlers/requests.js +23 -83
  36. package/dist/handlers/session.js +119 -590
  37. package/dist/handlers/sprints.d.ts +32 -0
  38. package/dist/handlers/sprints.js +275 -0
  39. package/dist/handlers/tasks.d.ts +7 -10
  40. package/dist/handlers/tasks.js +245 -894
  41. package/dist/handlers/tool-docs.d.ts +9 -0
  42. package/dist/handlers/tool-docs.js +904 -0
  43. package/dist/handlers/types.d.ts +11 -3
  44. package/dist/handlers/validation.d.ts +1 -1
  45. package/dist/handlers/validation.js +38 -153
  46. package/dist/index.js +493 -162
  47. package/dist/knowledge.js +106 -9
  48. package/dist/tools.js +34 -4
  49. package/dist/validators.d.ts +21 -0
  50. package/dist/validators.js +91 -0
  51. package/package.json +2 -3
  52. package/src/api-client.ts +1822 -0
  53. package/src/cli.test.ts +128 -302
  54. package/src/cli.ts +41 -285
  55. package/src/handlers/__test-setup__.ts +215 -0
  56. package/src/handlers/__test-utils__.ts +4 -134
  57. package/src/handlers/blockers.test.ts +114 -124
  58. package/src/handlers/blockers.ts +68 -70
  59. package/src/handlers/bodies-of-work.test.ts +236 -831
  60. package/src/handlers/bodies-of-work.ts +210 -525
  61. package/src/handlers/cost.test.ts +149 -113
  62. package/src/handlers/cost.ts +44 -132
  63. package/src/handlers/decisions.test.ts +111 -209
  64. package/src/handlers/decisions.ts +35 -27
  65. package/src/handlers/deployment.test.ts +193 -239
  66. package/src/handlers/deployment.ts +143 -896
  67. package/src/handlers/discovery.test.ts +20 -67
  68. package/src/handlers/discovery.ts +29 -714
  69. package/src/handlers/fallback.test.ts +206 -361
  70. package/src/handlers/fallback.ts +81 -156
  71. package/src/handlers/findings.test.ts +229 -320
  72. package/src/handlers/findings.ts +76 -64
  73. package/src/handlers/git-issues.test.ts +623 -0
  74. package/src/handlers/git-issues.ts +174 -0
  75. package/src/handlers/ideas.test.ts +229 -343
  76. package/src/handlers/ideas.ts +69 -143
  77. package/src/handlers/index.ts +6 -0
  78. package/src/handlers/milestones.test.ts +167 -281
  79. package/src/handlers/milestones.ts +54 -93
  80. package/src/handlers/organizations.test.ts +275 -467
  81. package/src/handlers/organizations.ts +84 -294
  82. package/src/handlers/progress.test.ts +112 -218
  83. package/src/handlers/progress.ts +29 -142
  84. package/src/handlers/project.test.ts +203 -226
  85. package/src/handlers/project.ts +48 -238
  86. package/src/handlers/requests.test.ts +74 -342
  87. package/src/handlers/requests.ts +25 -83
  88. package/src/handlers/session.test.ts +276 -206
  89. package/src/handlers/session.ts +136 -662
  90. package/src/handlers/sprints.test.ts +711 -0
  91. package/src/handlers/sprints.ts +510 -0
  92. package/src/handlers/tasks.test.ts +669 -353
  93. package/src/handlers/tasks.ts +263 -1015
  94. package/src/handlers/tool-docs.ts +1024 -0
  95. package/src/handlers/types.ts +12 -4
  96. package/src/handlers/validation.test.ts +237 -568
  97. package/src/handlers/validation.ts +43 -167
  98. package/src/index.ts +493 -186
  99. package/src/tools.ts +2532 -0
  100. package/src/validators.test.ts +223 -223
  101. package/src/validators.ts +127 -0
  102. package/tsconfig.json +1 -1
  103. package/vitest.config.ts +14 -13
  104. package/dist/cli.test.d.ts +0 -1
  105. package/dist/cli.test.js +0 -367
  106. package/dist/handlers/__test-utils__.d.ts +0 -72
  107. package/dist/handlers/__test-utils__.js +0 -176
  108. package/dist/handlers/checkouts.d.ts +0 -37
  109. package/dist/handlers/checkouts.js +0 -377
  110. package/dist/handlers/knowledge-query.d.ts +0 -22
  111. package/dist/handlers/knowledge-query.js +0 -253
  112. package/dist/handlers/knowledge.d.ts +0 -12
  113. package/dist/handlers/knowledge.js +0 -108
  114. package/dist/handlers/roles.d.ts +0 -30
  115. package/dist/handlers/roles.js +0 -281
  116. package/dist/handlers/tasks.test.d.ts +0 -1
  117. package/dist/handlers/tasks.test.js +0 -431
  118. package/dist/utils.test.d.ts +0 -1
  119. package/dist/utils.test.js +0 -532
  120. package/dist/validators.test.d.ts +0 -1
  121. package/dist/validators.test.js +0 -176
  122. package/src/knowledge.ts +0 -132
  123. package/src/tmpclaude-0078-cwd +0 -1
  124. package/src/tmpclaude-0ee1-cwd +0 -1
  125. package/src/tmpclaude-2dd5-cwd +0 -1
  126. package/src/tmpclaude-344c-cwd +0 -1
  127. package/src/tmpclaude-3860-cwd +0 -1
  128. package/src/tmpclaude-4b63-cwd +0 -1
  129. package/src/tmpclaude-5c73-cwd +0 -1
  130. package/src/tmpclaude-5ee3-cwd +0 -1
  131. package/src/tmpclaude-6795-cwd +0 -1
  132. package/src/tmpclaude-709e-cwd +0 -1
  133. package/src/tmpclaude-9839-cwd +0 -1
  134. package/src/tmpclaude-d829-cwd +0 -1
  135. package/src/tmpclaude-e072-cwd +0 -1
  136. package/src/tmpclaude-f6ee-cwd +0 -1
  137. package/tmpclaude-0439-cwd +0 -1
  138. package/tmpclaude-132f-cwd +0 -1
  139. package/tmpclaude-15bb-cwd +0 -1
  140. package/tmpclaude-165a-cwd +0 -1
  141. package/tmpclaude-1ba9-cwd +0 -1
  142. package/tmpclaude-21a3-cwd +0 -1
  143. package/tmpclaude-2a38-cwd +0 -1
  144. package/tmpclaude-2adf-cwd +0 -1
  145. package/tmpclaude-2f56-cwd +0 -1
  146. package/tmpclaude-3626-cwd +0 -1
  147. package/tmpclaude-3727-cwd +0 -1
  148. package/tmpclaude-40bc-cwd +0 -1
  149. package/tmpclaude-436f-cwd +0 -1
  150. package/tmpclaude-4783-cwd +0 -1
  151. package/tmpclaude-4b6d-cwd +0 -1
  152. package/tmpclaude-4ba4-cwd +0 -1
  153. package/tmpclaude-51e6-cwd +0 -1
  154. package/tmpclaude-5ecf-cwd +0 -1
  155. package/tmpclaude-6f97-cwd +0 -1
  156. package/tmpclaude-7fb2-cwd +0 -1
  157. package/tmpclaude-825c-cwd +0 -1
  158. package/tmpclaude-8baf-cwd +0 -1
  159. package/tmpclaude-8d9f-cwd +0 -1
  160. package/tmpclaude-975c-cwd +0 -1
  161. package/tmpclaude-9983-cwd +0 -1
  162. package/tmpclaude-a045-cwd +0 -1
  163. package/tmpclaude-ac4a-cwd +0 -1
  164. package/tmpclaude-b593-cwd +0 -1
  165. package/tmpclaude-b891-cwd +0 -1
  166. package/tmpclaude-c032-cwd +0 -1
  167. package/tmpclaude-cf43-cwd +0 -1
  168. package/tmpclaude-d040-cwd +0 -1
  169. package/tmpclaude-dcdd-cwd +0 -1
  170. package/tmpclaude-dcee-cwd +0 -1
  171. package/tmpclaude-e16b-cwd +0 -1
  172. package/tmpclaude-ecd2-cwd +0 -1
  173. package/tmpclaude-f48d-cwd +0 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Task Handlers
2
+ * Task Handlers (Migrated to API Client)
3
3
  *
4
4
  * Handles task CRUD and management:
5
5
  * - get_tasks
@@ -12,39 +12,18 @@
12
12
  * - remove_task_reference
13
13
  * - batch_update_tasks
14
14
  * - batch_complete_tasks
15
+ * - add_subtask
16
+ * - get_subtasks
15
17
  */
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') {
18
+ import { validateRequired, validateUUID, validateTaskStatus, validatePriority, validateProgressPercentage, validateEstimatedMinutes, ValidationError, } from '../validators.js';
19
+ import { getApiClient } from '../api-client.js';
20
+ function getTaskCompleteGitInstructions(gitWorkflow, gitMainBranch, gitDevelopBranch, taskBranch, taskTitle, taskId) {
21
+ if (gitWorkflow === 'none') {
25
22
  return undefined;
26
23
  }
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') {
24
+ if (gitWorkflow === 'trunk-based') {
46
25
  return {
47
- steps: [`git add .`, `git commit -m "feat: ${taskTitle}"`, `git push origin ${git_main_branch}`],
26
+ steps: [`git add .`, `git commit -m "feat: ${taskTitle}"`, `git push origin ${gitMainBranch}`],
48
27
  next_step: 'Changes committed directly to main branch.',
49
28
  };
50
29
  }
@@ -64,7 +43,7 @@ function getTaskCompleteGitInstructions(config, taskBranch, taskTitle, taskId) {
64
43
  next_step: 'Create PR and add link via add_task_reference. Merge happens AFTER validation approval.',
65
44
  };
66
45
  }
67
- function getValidationApprovedGitInstructions(config, taskBranch) {
46
+ export function getValidationApprovedGitInstructions(config, taskBranch) {
68
47
  const { git_workflow, git_main_branch, git_develop_branch } = config;
69
48
  if (git_workflow === 'none' || git_workflow === 'trunk-based' || !taskBranch) {
70
49
  return undefined;
@@ -85,326 +64,105 @@ function getValidationApprovedGitInstructions(config, taskBranch) {
85
64
  note: 'Validation approved - safe to merge. Clean up branch after successful merge.',
86
65
  };
87
66
  }
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
- }
67
+ // ============================================================================
68
+ // Task Handlers - Using API Client
69
+ // ============================================================================
118
70
  export const getTasks = async (args, ctx) => {
119
- const { project_id, status, limit = 50, include_subtasks = false } = args;
71
+ const { project_id, status, limit = 50, offset = 0, search_query, include_subtasks = false, include_metadata = false } = args;
120
72
  validateRequired(project_id, 'project_id');
121
73
  validateUUID(project_id, 'project_id');
122
74
  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
- }
75
+ const api = getApiClient();
76
+ const response = await api.getTasks(project_id, {
77
+ status,
78
+ limit: Math.min(limit, 200),
79
+ offset,
80
+ include_subtasks,
81
+ search_query,
82
+ include_metadata,
83
+ });
84
+ if (!response.ok) {
85
+ throw new Error(`Failed to fetch tasks: ${response.error}`);
371
86
  }
372
- // All tasks (including subtasks) claimed
373
87
  return {
374
88
  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.',
89
+ tasks: response.data?.tasks || [],
90
+ total_count: response.data?.total_count || 0,
91
+ has_more: response.data?.has_more || false,
380
92
  },
381
93
  };
382
94
  };
95
+ export const getNextTask = async (args, ctx) => {
96
+ const { project_id } = args;
97
+ validateRequired(project_id, 'project_id');
98
+ validateUUID(project_id, 'project_id');
99
+ const api = getApiClient();
100
+ const response = await api.getNextTask(project_id, ctx.session.currentSessionId || undefined);
101
+ if (!response.ok) {
102
+ throw new Error(`Failed to get next task: ${response.error}`);
103
+ }
104
+ const data = response.data;
105
+ if (!data) {
106
+ return { result: { task: null, message: 'No response from server' } };
107
+ }
108
+ // Map API response to handler response format
109
+ const result = {};
110
+ if (data.task) {
111
+ result.task = data.task;
112
+ }
113
+ else {
114
+ result.task = null;
115
+ }
116
+ if (data.blocking_task)
117
+ result.blocking_task = true;
118
+ if (data.deployment_blocks_tasks) {
119
+ result.deployment_blocks_tasks = true;
120
+ result.deployment = data.deployment;
121
+ result.action = data.action;
122
+ }
123
+ if (data.awaiting_validation) {
124
+ result.awaiting_validation = data.awaiting_validation;
125
+ result.validation_priority = data.validation_priority;
126
+ result.suggested_activity = data.suggested_activity;
127
+ }
128
+ if (data.all_claimed)
129
+ result.all_claimed = true;
130
+ if (data.is_subtask)
131
+ result.is_subtask = true;
132
+ if (data.suggested_activity)
133
+ result.suggested_activity = data.suggested_activity;
134
+ if (data.directive)
135
+ result.directive = data.directive;
136
+ if (data.message)
137
+ result.message = data.message;
138
+ return { result };
139
+ };
383
140
  export const addTask = async (args, ctx) => {
384
- const { project_id, title, description, priority = 3, estimated_minutes, blocking = false } = args;
141
+ const { project_id, title, description, priority = 3, estimated_minutes, blocking = false, task_type } = args;
385
142
  validateRequired(project_id, 'project_id');
386
143
  validateRequired(title, 'title');
387
144
  validateUUID(project_id, 'project_id');
388
145
  validatePriority(priority);
389
146
  validateEstimatedMinutes(estimated_minutes);
390
- const { data, error } = await ctx.supabase
391
- .from('tasks')
392
- .insert({
393
- project_id,
147
+ const api = getApiClient();
148
+ const response = await api.createTask(project_id, {
394
149
  title,
395
- description: description || null,
150
+ description,
396
151
  priority,
397
- created_by: 'agent',
398
- created_by_session_id: ctx.session.currentSessionId,
399
- estimated_minutes: estimated_minutes || null,
152
+ estimated_minutes,
400
153
  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) {
154
+ session_id: ctx.session.currentSessionId || undefined,
155
+ });
156
+ if (!response.ok) {
157
+ throw new Error(`Failed to add task: ${response.error}`);
158
+ }
159
+ const data = response.data;
160
+ const result = {
161
+ success: true,
162
+ task_id: data?.task_id,
163
+ title,
164
+ };
165
+ if (data?.blocking) {
408
166
  result.blocking = true;
409
167
  result.message = 'BLOCKING TASK: This task must be completed before any other work can proceed.';
410
168
  }
@@ -412,329 +170,110 @@ export const addTask = async (args, ctx) => {
412
170
  };
413
171
  export const updateTask = async (args, ctx) => {
414
172
  const { task_id, progress_note, ...updates } = args;
415
- const { supabase, session } = ctx;
416
- const currentSessionId = session.currentSessionId;
417
173
  validateRequired(task_id, 'task_id');
418
174
  validateUUID(task_id, 'task_id');
419
175
  validateTaskStatus(updates.status);
420
176
  validatePriority(updates.priority);
421
177
  validateProgressPercentage(updates.progress_percentage);
422
178
  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) {
179
+ const api = getApiClient();
180
+ const response = await api.updateTask(task_id, {
181
+ ...updates,
182
+ progress_note,
183
+ session_id: ctx.session.currentSessionId || undefined,
184
+ });
185
+ if (!response.ok) {
186
+ // Check for specific error types
187
+ if (response.error?.includes('agent_task_limit') || response.error?.includes('already has a task')) {
433
188
  return {
434
189
  result: {
435
- error: 'invalid_status_transition',
436
- message: transition.reason,
190
+ error: 'agent_task_limit',
191
+ message: response.error,
437
192
  },
438
193
  };
439
194
  }
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) {
195
+ if (response.error?.includes('task_claimed') || response.error?.includes('being worked on')) {
454
196
  return {
455
197
  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,
198
+ error: 'task_claimed',
199
+ message: response.error,
460
200
  },
461
201
  };
462
202
  }
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);
203
+ if (response.error?.includes('invalid_status_transition')) {
204
+ return {
205
+ result: {
206
+ error: 'invalid_status_transition',
207
+ message: response.error,
208
+ },
209
+ };
480
210
  }
211
+ throw new Error(`Failed to update task: ${response.error}`);
481
212
  }
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);
213
+ // Build result - include git workflow info when transitioning to in_progress
214
+ const data = response.data;
215
+ const result = { success: true, task_id };
216
+ if (data?.git_workflow) {
217
+ result.git_workflow = data.git_workflow;
499
218
  }
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;
219
+ if (data?.worktree_setup) {
220
+ result.worktree_setup = data.worktree_setup;
505
221
  }
506
- // When cancelled, also release the task
507
- if (updates.status === 'cancelled') {
508
- updateData.working_agent_session_id = null;
222
+ if (data?.next_step) {
223
+ result.next_step = data.next_step;
509
224
  }
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
- }
225
+ // Warn if transitioning to in_progress without git_branch
226
+ if (updates.status === 'in_progress' && !updates.git_branch) {
227
+ result.warning = 'git_branch not set. For multi-agent collaboration, set git_branch when marking in_progress to track your worktree.';
228
+ result.hint = 'Call update_task again with git_branch parameter after creating your worktree.';
544
229
  }
545
230
  return { result };
546
231
  };
547
232
  export const completeTask = async (args, ctx) => {
548
233
  const { task_id, summary } = args;
549
- const { supabase, session } = ctx;
550
- const currentSessionId = session.currentSessionId;
551
234
  validateRequired(task_id, 'task_id');
552
235
  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);
236
+ const api = getApiClient();
237
+ const response = await api.completeTask(task_id, {
238
+ summary,
239
+ session_id: ctx.session.currentSessionId || undefined,
240
+ });
241
+ if (!response.ok) {
242
+ throw new Error(`Failed to complete task: ${response.error}`);
584
243
  }
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
- });
244
+ const data = response.data;
245
+ if (!data) {
246
+ throw new Error('No response data from complete task');
595
247
  }
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
248
+ // Build result matching expected format
641
249
  const result = {
642
250
  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,
251
+ directive: data.directive,
252
+ auto_continue: data.auto_continue,
253
+ completed_task_id: data.completed_task_id,
254
+ next_task: data.next_task,
647
255
  };
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
- };
256
+ if (data.context) {
257
+ result.context = data.context;
658
258
  }
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
- }
259
+ // Pass through warnings (e.g., missing git_branch)
260
+ if (data.warnings) {
261
+ result.warnings = data.warnings;
723
262
  }
724
- // REPEAT at end - agents weight last items heavily
725
- result.next_action = nextAction;
263
+ // Git workflow instructions are already in API response but we need to fetch
264
+ // task details if we want to include them (API should return these)
265
+ result.next_action = data.next_action;
726
266
  return { result };
727
267
  };
728
268
  export const deleteTask = async (args, ctx) => {
729
269
  const { task_id } = args;
730
270
  validateRequired(task_id, 'task_id');
731
271
  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}`);
272
+ const api = getApiClient();
273
+ const response = await api.deleteTask(task_id);
274
+ if (!response.ok) {
275
+ throw new Error(`Failed to delete task: ${response.error}`);
276
+ }
738
277
  return { result: { success: true, deleted_id: task_id } };
739
278
  };
740
279
  export const addTaskReference = async (args, ctx) => {
@@ -742,56 +281,38 @@ export const addTaskReference = async (args, ctx) => {
742
281
  validateRequired(task_id, 'task_id');
743
282
  validateUUID(task_id, 'task_id');
744
283
  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' } };
284
+ const api = getApiClient();
285
+ const response = await api.addTaskReference(task_id, url, label);
286
+ if (!response.ok) {
287
+ if (response.error?.includes('already exists')) {
288
+ return { result: { success: false, error: 'Reference with this URL already exists' } };
289
+ }
290
+ throw new Error(`Failed to add reference: ${response.error}`);
755
291
  }
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 } };
292
+ return {
293
+ result: {
294
+ success: true,
295
+ reference: response.data?.reference,
296
+ },
297
+ };
765
298
  };
766
299
  export const removeTaskReference = async (args, ctx) => {
767
300
  const { task_id, url } = args;
768
301
  validateRequired(task_id, 'task_id');
769
302
  validateUUID(task_id, 'task_id');
770
303
  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' } };
304
+ const api = getApiClient();
305
+ const response = await api.removeTaskReference(task_id, url);
306
+ if (!response.ok) {
307
+ if (response.error?.includes('not found')) {
308
+ return { result: { success: false, error: 'Reference with this URL not found' } };
309
+ }
310
+ throw new Error(`Failed to remove reference: ${response.error}`);
782
311
  }
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 } };
312
+ return { result: { success: true } };
790
313
  };
791
314
  export const batchUpdateTasks = async (args, ctx) => {
792
315
  const { updates } = args;
793
- const { supabase, session } = ctx;
794
- const currentSessionId = session.currentSessionId;
795
316
  if (!updates || !Array.isArray(updates) || updates.length === 0) {
796
317
  throw new ValidationError('updates must be a non-empty array', {
797
318
  field: 'updates',
@@ -804,107 +325,59 @@ export const batchUpdateTasks = async (args, ctx) => {
804
325
  hint: 'Split your updates into smaller batches',
805
326
  });
806
327
  }
807
- // Validate all inputs first (no DB queries)
808
- const taskIds = [];
328
+ // Validate all inputs first
809
329
  for (const update of updates) {
810
330
  validateRequired(update.task_id, 'task_id');
811
331
  validateUUID(update.task_id, 'task_id');
812
332
  validateTaskStatus(update.status);
813
333
  validatePriority(update.priority);
814
334
  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
335
  }
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);
336
+ const api = getApiClient();
337
+ const response = await api.batchUpdateTasks(updates);
338
+ if (!response.ok) {
339
+ throw new Error(`Failed to batch update tasks: ${response.error}`);
901
340
  }
902
- const successCount = results.filter(r => r.success).length;
903
341
  return {
904
342
  result: {
905
- success: successCount === updates.length,
343
+ success: response.data?.success || false,
906
344
  total: updates.length,
907
- succeeded: successCount,
345
+ succeeded: response.data?.updated_count || 0,
346
+ },
347
+ };
348
+ };
349
+ export const batchCompleteTasks = async (args, ctx) => {
350
+ const { completions } = args;
351
+ if (!completions || !Array.isArray(completions) || completions.length === 0) {
352
+ throw new ValidationError('completions must be a non-empty array', {
353
+ field: 'completions',
354
+ hint: 'Provide an array of task completions with at least one item',
355
+ });
356
+ }
357
+ if (completions.length > 50) {
358
+ throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
359
+ field: 'completions',
360
+ hint: 'Split your completions into smaller batches',
361
+ });
362
+ }
363
+ // Validate all inputs first
364
+ for (const completion of completions) {
365
+ validateRequired(completion.task_id, 'task_id');
366
+ validateUUID(completion.task_id, 'task_id');
367
+ }
368
+ const api = getApiClient();
369
+ const response = await api.batchCompleteTasks(completions);
370
+ if (!response.ok) {
371
+ throw new Error(`Failed to batch complete tasks: ${response.error}`);
372
+ }
373
+ const data = response.data;
374
+ return {
375
+ result: {
376
+ success: data?.success || false,
377
+ total: completions.length,
378
+ succeeded: data?.completed_count || 0,
379
+ failed: completions.length - (data?.completed_count || 0),
380
+ next_task: data?.next_task,
908
381
  },
909
382
  };
910
383
  };
@@ -913,8 +386,6 @@ export const batchUpdateTasks = async (args, ctx) => {
913
386
  // ============================================================================
914
387
  export const addSubtask = async (args, ctx) => {
915
388
  const { parent_task_id, title, description, priority, estimated_minutes } = args;
916
- const { supabase, session } = ctx;
917
- const currentSessionId = session.currentSessionId;
918
389
  validateRequired(parent_task_id, 'parent_task_id');
919
390
  validateUUID(parent_task_id, 'parent_task_id');
920
391
  validateRequired(title, 'title');
@@ -922,175 +393,55 @@ export const addSubtask = async (args, ctx) => {
922
393
  validatePriority(priority);
923
394
  if (estimated_minutes !== undefined)
924
395
  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,
396
+ const api = getApiClient();
397
+ const response = await api.addSubtask(parent_task_id, {
955
398
  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
- });
399
+ description,
400
+ priority,
401
+ estimated_minutes,
402
+ }, ctx.session.currentSessionId || undefined);
403
+ if (!response.ok) {
404
+ if (response.error?.includes('Cannot create subtask of a subtask')) {
405
+ return {
406
+ result: {
407
+ success: false,
408
+ error: 'Cannot create subtask of a subtask',
409
+ hint: 'Subtasks cannot have their own subtasks. Add this task to the parent task instead.',
410
+ },
411
+ };
412
+ }
413
+ throw new Error(`Failed to add subtask: ${response.error}`);
414
+ }
974
415
  return {
975
416
  result: {
976
417
  success: true,
977
- subtask_id: subtask.id,
978
- parent_task_id,
979
- title: subtask.title,
980
- priority: subtask.priority,
418
+ subtask_id: response.data?.subtask_id,
419
+ parent_task_id: response.data?.parent_task_id,
981
420
  },
982
421
  };
983
422
  };
984
423
  export const getSubtasks = async (args, ctx) => {
985
424
  const { parent_task_id, status } = args;
986
- const { supabase } = ctx;
987
425
  validateRequired(parent_task_id, 'parent_task_id');
988
426
  validateUUID(parent_task_id, 'parent_task_id');
989
427
  if (status)
990
428
  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);
429
+ const api = getApiClient();
430
+ const response = await api.getSubtasks(parent_task_id, status);
431
+ if (!response.ok) {
432
+ throw new Error(`Failed to fetch subtasks: ${response.error}`);
999
433
  }
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
434
  return {
1009
435
  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,
436
+ subtasks: response.data?.subtasks || [],
437
+ stats: response.data?.stats || {
438
+ total: 0,
439
+ completed: 0,
440
+ progress_percentage: 0,
1017
441
  },
1018
442
  },
1019
443
  };
1020
444
  };
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
445
  /**
1095
446
  * Task handlers registry
1096
447
  */