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