@vibescope/mcp-server 0.0.1 → 0.1.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 (170) hide show
  1. package/README.md +113 -98
  2. package/dist/api-client.d.ts +1114 -0
  3. package/dist/api-client.js +698 -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 +106 -476
  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 +112 -828
  16. package/dist/handlers/discovery.js +31 -0
  17. package/dist/handlers/fallback.d.ts +2 -0
  18. package/dist/handlers/fallback.js +39 -134
  19. package/dist/handlers/findings.js +43 -67
  20. package/dist/handlers/git-issues.d.ts +9 -13
  21. package/dist/handlers/git-issues.js +80 -225
  22. package/dist/handlers/ideas.d.ts +3 -0
  23. package/dist/handlers/ideas.js +53 -134
  24. package/dist/handlers/index.d.ts +2 -0
  25. package/dist/handlers/index.js +6 -0
  26. package/dist/handlers/milestones.d.ts +2 -0
  27. package/dist/handlers/milestones.js +51 -98
  28. package/dist/handlers/organizations.js +79 -275
  29. package/dist/handlers/progress.d.ts +2 -0
  30. package/dist/handlers/progress.js +25 -123
  31. package/dist/handlers/project.js +42 -221
  32. package/dist/handlers/requests.d.ts +2 -0
  33. package/dist/handlers/requests.js +23 -83
  34. package/dist/handlers/session.js +99 -585
  35. package/dist/handlers/sprints.d.ts +32 -0
  36. package/dist/handlers/sprints.js +274 -0
  37. package/dist/handlers/tasks.d.ts +7 -10
  38. package/dist/handlers/tasks.js +230 -900
  39. package/dist/handlers/tool-docs.d.ts +8 -0
  40. package/dist/handlers/tool-docs.js +657 -0
  41. package/dist/handlers/types.d.ts +11 -3
  42. package/dist/handlers/validation.d.ts +1 -1
  43. package/dist/handlers/validation.js +26 -153
  44. package/dist/index.js +473 -160
  45. package/dist/knowledge.js +106 -9
  46. package/dist/tools.js +4 -0
  47. package/dist/validators.d.ts +21 -0
  48. package/dist/validators.js +91 -0
  49. package/package.json +2 -3
  50. package/src/api-client.ts +1752 -0
  51. package/src/cli.test.ts +128 -302
  52. package/src/cli.ts +41 -285
  53. package/src/handlers/__test-setup__.ts +210 -0
  54. package/src/handlers/__test-utils__.ts +4 -134
  55. package/src/handlers/blockers.test.ts +114 -124
  56. package/src/handlers/blockers.ts +68 -70
  57. package/src/handlers/bodies-of-work.test.ts +236 -831
  58. package/src/handlers/bodies-of-work.ts +194 -525
  59. package/src/handlers/cost.test.ts +149 -113
  60. package/src/handlers/cost.ts +44 -132
  61. package/src/handlers/decisions.test.ts +111 -209
  62. package/src/handlers/decisions.ts +35 -27
  63. package/src/handlers/deployment.test.ts +193 -239
  64. package/src/handlers/deployment.ts +140 -895
  65. package/src/handlers/discovery.test.ts +20 -67
  66. package/src/handlers/discovery.ts +32 -0
  67. package/src/handlers/fallback.test.ts +128 -361
  68. package/src/handlers/fallback.ts +62 -148
  69. package/src/handlers/findings.test.ts +127 -345
  70. package/src/handlers/findings.ts +49 -66
  71. package/src/handlers/git-issues.test.ts +623 -0
  72. package/src/handlers/git-issues.ts +174 -0
  73. package/src/handlers/ideas.test.ts +229 -343
  74. package/src/handlers/ideas.ts +69 -143
  75. package/src/handlers/index.ts +6 -0
  76. package/src/handlers/milestones.test.ts +167 -281
  77. package/src/handlers/milestones.ts +54 -93
  78. package/src/handlers/organizations.test.ts +275 -467
  79. package/src/handlers/organizations.ts +84 -294
  80. package/src/handlers/progress.test.ts +112 -218
  81. package/src/handlers/progress.ts +29 -142
  82. package/src/handlers/project.test.ts +203 -226
  83. package/src/handlers/project.ts +48 -238
  84. package/src/handlers/requests.test.ts +74 -342
  85. package/src/handlers/requests.ts +25 -83
  86. package/src/handlers/session.test.ts +241 -206
  87. package/src/handlers/session.ts +110 -657
  88. package/src/handlers/sprints.test.ts +711 -0
  89. package/src/handlers/sprints.ts +497 -0
  90. package/src/handlers/tasks.test.ts +608 -353
  91. package/src/handlers/tasks.ts +248 -1025
  92. package/src/handlers/types.ts +12 -4
  93. package/src/handlers/validation.test.ts +189 -572
  94. package/src/handlers/validation.ts +29 -166
  95. package/src/index.ts +473 -184
  96. package/src/knowledge.ts +107 -9
  97. package/src/tools.ts +2506 -0
  98. package/src/validators.test.ts +223 -223
  99. package/src/validators.ts +127 -0
  100. package/tsconfig.json +1 -1
  101. package/vitest.config.ts +14 -13
  102. package/dist/cli.test.d.ts +0 -1
  103. package/dist/cli.test.js +0 -367
  104. package/dist/handlers/__test-utils__.d.ts +0 -72
  105. package/dist/handlers/__test-utils__.js +0 -176
  106. package/dist/handlers/checkouts.d.ts +0 -37
  107. package/dist/handlers/checkouts.js +0 -377
  108. package/dist/handlers/knowledge-query.d.ts +0 -22
  109. package/dist/handlers/knowledge-query.js +0 -253
  110. package/dist/handlers/knowledge.d.ts +0 -12
  111. package/dist/handlers/knowledge.js +0 -108
  112. package/dist/handlers/roles.d.ts +0 -30
  113. package/dist/handlers/roles.js +0 -281
  114. package/dist/handlers/tasks.test.d.ts +0 -1
  115. package/dist/handlers/tasks.test.js +0 -431
  116. package/dist/utils.test.d.ts +0 -1
  117. package/dist/utils.test.js +0 -532
  118. package/dist/validators.test.d.ts +0 -1
  119. package/dist/validators.test.js +0 -176
  120. package/src/tmpclaude-0078-cwd +0 -1
  121. package/src/tmpclaude-0ee1-cwd +0 -1
  122. package/src/tmpclaude-2dd5-cwd +0 -1
  123. package/src/tmpclaude-344c-cwd +0 -1
  124. package/src/tmpclaude-3860-cwd +0 -1
  125. package/src/tmpclaude-4b63-cwd +0 -1
  126. package/src/tmpclaude-5c73-cwd +0 -1
  127. package/src/tmpclaude-5ee3-cwd +0 -1
  128. package/src/tmpclaude-6795-cwd +0 -1
  129. package/src/tmpclaude-709e-cwd +0 -1
  130. package/src/tmpclaude-9839-cwd +0 -1
  131. package/src/tmpclaude-d829-cwd +0 -1
  132. package/src/tmpclaude-e072-cwd +0 -1
  133. package/src/tmpclaude-f6ee-cwd +0 -1
  134. package/tmpclaude-0439-cwd +0 -1
  135. package/tmpclaude-132f-cwd +0 -1
  136. package/tmpclaude-15bb-cwd +0 -1
  137. package/tmpclaude-165a-cwd +0 -1
  138. package/tmpclaude-1ba9-cwd +0 -1
  139. package/tmpclaude-21a3-cwd +0 -1
  140. package/tmpclaude-2a38-cwd +0 -1
  141. package/tmpclaude-2adf-cwd +0 -1
  142. package/tmpclaude-2f56-cwd +0 -1
  143. package/tmpclaude-3626-cwd +0 -1
  144. package/tmpclaude-3727-cwd +0 -1
  145. package/tmpclaude-40bc-cwd +0 -1
  146. package/tmpclaude-436f-cwd +0 -1
  147. package/tmpclaude-4783-cwd +0 -1
  148. package/tmpclaude-4b6d-cwd +0 -1
  149. package/tmpclaude-4ba4-cwd +0 -1
  150. package/tmpclaude-51e6-cwd +0 -1
  151. package/tmpclaude-5ecf-cwd +0 -1
  152. package/tmpclaude-6f97-cwd +0 -1
  153. package/tmpclaude-7fb2-cwd +0 -1
  154. package/tmpclaude-825c-cwd +0 -1
  155. package/tmpclaude-8baf-cwd +0 -1
  156. package/tmpclaude-8d9f-cwd +0 -1
  157. package/tmpclaude-975c-cwd +0 -1
  158. package/tmpclaude-9983-cwd +0 -1
  159. package/tmpclaude-a045-cwd +0 -1
  160. package/tmpclaude-ac4a-cwd +0 -1
  161. package/tmpclaude-b593-cwd +0 -1
  162. package/tmpclaude-b891-cwd +0 -1
  163. package/tmpclaude-c032-cwd +0 -1
  164. package/tmpclaude-cf43-cwd +0 -1
  165. package/tmpclaude-d040-cwd +0 -1
  166. package/tmpclaude-dcdd-cwd +0 -1
  167. package/tmpclaude-dcee-cwd +0 -1
  168. package/tmpclaude-e16b-cwd +0 -1
  169. package/tmpclaude-ecd2-cwd +0 -1
  170. 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,89 @@ 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);
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
- }
203
+ if (response.error?.includes('invalid_status_transition')) {
204
+ return {
205
+ result: {
206
+ error: 'invalid_status_transition',
207
+ message: response.error,
208
+ },
209
+ };
543
210
  }
211
+ throw new Error(`Failed to update task: ${response.error}`);
544
212
  }
545
- return { result };
213
+ return { result: { success: true, task_id } };
546
214
  };
547
215
  export const completeTask = async (args, ctx) => {
548
216
  const { task_id, summary } = args;
549
- const { supabase, session } = ctx;
550
- const currentSessionId = session.currentSessionId;
551
217
  validateRequired(task_id, 'task_id');
552
218
  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);
219
+ const api = getApiClient();
220
+ const response = await api.completeTask(task_id, {
221
+ summary,
222
+ session_id: ctx.session.currentSessionId || undefined,
223
+ });
224
+ if (!response.ok) {
225
+ throw new Error(`Failed to complete task: ${response.error}`);
584
226
  }
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
- });
227
+ const data = response.data;
228
+ if (!data) {
229
+ throw new Error('No response data from complete task');
595
230
  }
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
231
+ // Build result matching expected format
641
232
  const result = {
642
233
  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,
234
+ directive: data.directive,
235
+ auto_continue: data.auto_continue,
236
+ completed_task_id: data.completed_task_id,
237
+ next_task: data.next_task,
647
238
  };
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
- };
239
+ if (data.context) {
240
+ result.context = data.context;
658
241
  }
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;
242
+ // Git workflow instructions are already in API response but we need to fetch
243
+ // task details if we want to include them (API should return these)
244
+ result.next_action = data.next_action;
726
245
  return { result };
727
246
  };
728
247
  export const deleteTask = async (args, ctx) => {
729
248
  const { task_id } = args;
730
249
  validateRequired(task_id, 'task_id');
731
250
  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}`);
251
+ const api = getApiClient();
252
+ const response = await api.deleteTask(task_id);
253
+ if (!response.ok) {
254
+ throw new Error(`Failed to delete task: ${response.error}`);
255
+ }
738
256
  return { result: { success: true, deleted_id: task_id } };
739
257
  };
740
258
  export const addTaskReference = async (args, ctx) => {
@@ -742,56 +260,38 @@ export const addTaskReference = async (args, ctx) => {
742
260
  validateRequired(task_id, 'task_id');
743
261
  validateUUID(task_id, 'task_id');
744
262
  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' } };
263
+ const api = getApiClient();
264
+ const response = await api.addTaskReference(task_id, url, label);
265
+ if (!response.ok) {
266
+ if (response.error?.includes('already exists')) {
267
+ return { result: { success: false, error: 'Reference with this URL already exists' } };
268
+ }
269
+ throw new Error(`Failed to add reference: ${response.error}`);
755
270
  }
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 } };
271
+ return {
272
+ result: {
273
+ success: true,
274
+ reference: response.data?.reference,
275
+ },
276
+ };
765
277
  };
766
278
  export const removeTaskReference = async (args, ctx) => {
767
279
  const { task_id, url } = args;
768
280
  validateRequired(task_id, 'task_id');
769
281
  validateUUID(task_id, 'task_id');
770
282
  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' } };
283
+ const api = getApiClient();
284
+ const response = await api.removeTaskReference(task_id, url);
285
+ if (!response.ok) {
286
+ if (response.error?.includes('not found')) {
287
+ return { result: { success: false, error: 'Reference with this URL not found' } };
288
+ }
289
+ throw new Error(`Failed to remove reference: ${response.error}`);
782
290
  }
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 } };
291
+ return { result: { success: true } };
790
292
  };
791
293
  export const batchUpdateTasks = async (args, ctx) => {
792
294
  const { updates } = args;
793
- const { supabase, session } = ctx;
794
- const currentSessionId = session.currentSessionId;
795
295
  if (!updates || !Array.isArray(updates) || updates.length === 0) {
796
296
  throw new ValidationError('updates must be a non-empty array', {
797
297
  field: 'updates',
@@ -804,107 +304,59 @@ export const batchUpdateTasks = async (args, ctx) => {
804
304
  hint: 'Split your updates into smaller batches',
805
305
  });
806
306
  }
807
- // Validate all inputs first (no DB queries)
808
- const taskIds = [];
307
+ // Validate all inputs first
809
308
  for (const update of updates) {
810
309
  validateRequired(update.task_id, 'task_id');
811
310
  validateUUID(update.task_id, 'task_id');
812
311
  validateTaskStatus(update.status);
813
312
  validatePriority(update.priority);
814
313
  validateProgressPercentage(update.progress_percentage);
815
- taskIds.push(update.task_id);
816
314
  }
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;
315
+ const api = getApiClient();
316
+ const response = await api.batchUpdateTasks(updates);
317
+ if (!response.ok) {
318
+ throw new Error(`Failed to batch update tasks: ${response.error}`);
836
319
  }
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
320
  return {
904
321
  result: {
905
- success: successCount === updates.length,
322
+ success: response.data?.success || false,
906
323
  total: updates.length,
907
- succeeded: successCount,
324
+ succeeded: response.data?.updated_count || 0,
325
+ },
326
+ };
327
+ };
328
+ export const batchCompleteTasks = async (args, ctx) => {
329
+ const { completions } = args;
330
+ if (!completions || !Array.isArray(completions) || completions.length === 0) {
331
+ throw new ValidationError('completions must be a non-empty array', {
332
+ field: 'completions',
333
+ hint: 'Provide an array of task completions with at least one item',
334
+ });
335
+ }
336
+ if (completions.length > 50) {
337
+ throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
338
+ field: 'completions',
339
+ hint: 'Split your completions into smaller batches',
340
+ });
341
+ }
342
+ // Validate all inputs first
343
+ for (const completion of completions) {
344
+ validateRequired(completion.task_id, 'task_id');
345
+ validateUUID(completion.task_id, 'task_id');
346
+ }
347
+ const api = getApiClient();
348
+ const response = await api.batchCompleteTasks(completions);
349
+ if (!response.ok) {
350
+ throw new Error(`Failed to batch complete tasks: ${response.error}`);
351
+ }
352
+ const data = response.data;
353
+ return {
354
+ result: {
355
+ success: data?.success || false,
356
+ total: completions.length,
357
+ succeeded: data?.completed_count || 0,
358
+ failed: completions.length - (data?.completed_count || 0),
359
+ next_task: data?.next_task,
908
360
  },
909
361
  };
910
362
  };
@@ -913,8 +365,6 @@ export const batchUpdateTasks = async (args, ctx) => {
913
365
  // ============================================================================
914
366
  export const addSubtask = async (args, ctx) => {
915
367
  const { parent_task_id, title, description, priority, estimated_minutes } = args;
916
- const { supabase, session } = ctx;
917
- const currentSessionId = session.currentSessionId;
918
368
  validateRequired(parent_task_id, 'parent_task_id');
919
369
  validateUUID(parent_task_id, 'parent_task_id');
920
370
  validateRequired(title, 'title');
@@ -922,175 +372,55 @@ export const addSubtask = async (args, ctx) => {
922
372
  validatePriority(priority);
923
373
  if (estimated_minutes !== undefined)
924
374
  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,
375
+ const api = getApiClient();
376
+ const response = await api.addSubtask(parent_task_id, {
955
377
  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
- });
378
+ description,
379
+ priority,
380
+ estimated_minutes,
381
+ }, ctx.session.currentSessionId || undefined);
382
+ if (!response.ok) {
383
+ if (response.error?.includes('Cannot create subtask of a subtask')) {
384
+ return {
385
+ result: {
386
+ success: false,
387
+ error: 'Cannot create subtask of a subtask',
388
+ hint: 'Subtasks cannot have their own subtasks. Add this task to the parent task instead.',
389
+ },
390
+ };
391
+ }
392
+ throw new Error(`Failed to add subtask: ${response.error}`);
393
+ }
974
394
  return {
975
395
  result: {
976
396
  success: true,
977
- subtask_id: subtask.id,
978
- parent_task_id,
979
- title: subtask.title,
980
- priority: subtask.priority,
397
+ subtask_id: response.data?.subtask_id,
398
+ parent_task_id: response.data?.parent_task_id,
981
399
  },
982
400
  };
983
401
  };
984
402
  export const getSubtasks = async (args, ctx) => {
985
403
  const { parent_task_id, status } = args;
986
- const { supabase } = ctx;
987
404
  validateRequired(parent_task_id, 'parent_task_id');
988
405
  validateUUID(parent_task_id, 'parent_task_id');
989
406
  if (status)
990
407
  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);
408
+ const api = getApiClient();
409
+ const response = await api.getSubtasks(parent_task_id, status);
410
+ if (!response.ok) {
411
+ throw new Error(`Failed to fetch subtasks: ${response.error}`);
999
412
  }
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
413
  return {
1009
414
  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,
415
+ subtasks: response.data?.subtasks || [],
416
+ stats: response.data?.stats || {
417
+ total: 0,
418
+ completed: 0,
419
+ progress_percentage: 0,
1017
420
  },
1018
421
  },
1019
422
  };
1020
423
  };
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
424
  /**
1095
425
  * Task handlers registry
1096
426
  */