@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,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
- };
175
+ if (!response.ok) {
176
+ throw new Error(`Failed to get next task: ${response.error}`);
323
177
  }
324
178
 
325
- // Fetch candidate pending root tasks (not subtasks)
326
- const { data: candidates, error } = await supabase
327
- .from('tasks')
328
- .select('id, title, description, priority, estimated_minutes, working_agent_session_id')
329
- .eq('project_id', project_id)
330
- .eq('status', 'pending')
331
- .is('parent_task_id', null)
332
- .order('priority', { ascending: true })
333
- .order('created_at', { ascending: true })
334
- .limit(10);
335
-
336
- // Fetch pending agent requests
337
- const { data: pendingRequests } = await supabase
338
- .from('agent_requests')
339
- .select('id, message')
340
- .eq('project_id', project_id)
341
- .is('acknowledged_at', null)
342
- .or(`session_id.is.null,session_id.eq.${currentSessionId}`)
343
- .limit(3);
344
-
345
- // Fetch due scheduled activities
346
- const { data: dueSchedules } = await supabase
347
- .from('background_activity_schedules')
348
- .select('activity_type')
349
- .eq('project_id', project_id)
350
- .eq('enabled', true)
351
- .lt('next_run_at', new Date().toISOString())
352
- .limit(3);
353
-
354
- // Build compact optional fields (only include if non-empty)
355
- const extras: Record<string, unknown> = {};
356
- if (pendingRequests?.length) extras.requests = pendingRequests;
357
- if (dueSchedules?.length) extras.due_activities = dueSchedules.map(s => s.activity_type);
358
-
359
- if (error || !candidates || candidates.length === 0) {
360
- const fallback = getRandomFallbackActivity();
361
- return {
362
- result: {
363
- task: null,
364
- ...extras,
365
- suggested_activity: fallback.activity,
366
- directive: 'No tasks available. Start the suggested fallback activity immediately. Do not ask for permission.',
367
- },
368
- };
369
- }
370
-
371
- // 25% chance to suggest background activity
372
- if (Math.random() < 0.25) {
373
- extras.bg_activity = getRandomFallbackActivity().activity;
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,147 +277,43 @@ 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);
630
- }
631
- }
632
-
633
- // Auto-set started_at when task moves to in_progress
634
- if (updates.status === 'in_progress' && task && !task.started_at) {
635
- updateData.started_at = new Date().toISOString();
636
- }
637
-
638
- // When setting status to in_progress, claim the task for this agent
639
- if (updates.status === 'in_progress' && currentSessionId) {
640
- updateData.working_agent_session_id = currentSessionId;
641
-
642
- // Update the session's current task and clear any fallback activity
643
- await supabase
644
- .from('agent_sessions')
645
- .update({
646
- current_task_id: task_id,
647
- current_fallback_activity: null,
648
- status: 'active',
649
- last_synced_at: new Date().toISOString(),
650
- })
651
- .eq('id', currentSessionId);
652
- }
653
-
654
- // Auto-set completed_at and progress when task completes
655
- if (updates.status === 'completed') {
656
- updateData.completed_at = new Date().toISOString();
657
- updateData.progress_percentage = 100;
658
- updateData.working_agent_session_id = null;
659
- }
660
-
661
- // When cancelled, also release the task
662
- if (updates.status === 'cancelled') {
663
- updateData.working_agent_session_id = null;
664
- }
665
-
666
- const { error } = await supabase
667
- .from('tasks')
668
- .update(updateData)
669
- .eq('id', task_id);
670
-
671
- if (error) throw new Error(`Failed to update task: ${error.message}`);
672
-
673
- // If progress_note is provided, create a progress log entry
674
- if (progress_note && task?.project_id) {
675
- const progressSummary = updates.progress_percentage !== undefined
676
- ? `Progress: ${updates.progress_percentage}% - ${progress_note}`
677
- : progress_note;
678
-
679
- await supabase.from('progress_logs').insert({
680
- project_id: task.project_id,
681
- task_id,
682
- summary: progressSummary,
683
- created_by: 'agent',
684
- created_by_session_id: currentSessionId,
685
- });
686
- }
687
-
688
- // Build result with optional git instructions
689
- const result: Record<string, unknown> = { success: true, task_id };
690
-
691
- // Include git workflow instructions when task moves to in_progress
692
- if (updates.status === 'in_progress' && task?.project_id && task?.title) {
693
- const gitConfig = await getProjectGitConfig(supabase, task.project_id);
694
- if (gitConfig && gitConfig.git_workflow !== 'none') {
695
- const gitInstructions = getTaskStartGitInstructions(gitConfig, task_id, task.title);
696
- if (gitInstructions) {
697
- result.git_workflow = {
698
- workflow: gitConfig.git_workflow,
699
- action: 'create_branch',
700
- ...gitInstructions,
701
- };
702
- }
305
+ if (response.error?.includes('invalid_status_transition')) {
306
+ return {
307
+ result: {
308
+ error: 'invalid_status_transition',
309
+ message: response.error,
310
+ },
311
+ };
703
312
  }
313
+ throw new Error(`Failed to update task: ${response.error}`);
704
314
  }
705
315
 
706
- return { result };
316
+ return { result: { success: true, task_id } };
707
317
  };
708
318
 
709
319
  export const completeTask: Handler = async (args, ctx) => {
@@ -712,200 +322,40 @@ export const completeTask: Handler = async (args, ctx) => {
712
322
  summary?: string;
713
323
  };
714
324
 
715
- const { supabase, session } = ctx;
716
- const currentSessionId = session.currentSessionId;
717
-
718
325
  validateRequired(task_id, 'task_id');
719
326
  validateUUID(task_id, 'task_id');
720
327
 
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);
328
+ const api = getApiClient();
329
+ const response = await api.completeTask(task_id, {
330
+ summary,
331
+ session_id: ctx.session.currentSessionId || undefined,
332
+ });
333
+
334
+ if (!response.ok) {
335
+ throw new Error(`Failed to complete task: ${response.error}`);
754
336
  }
755
337
 
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
- });
338
+ const data = response.data;
339
+ if (!data) {
340
+ throw new Error('No response data from complete task');
766
341
  }
767
342
 
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
343
+ // Build result matching expected format
816
344
  const result: Record<string, unknown> = {
817
345
  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,
346
+ directive: data.directive,
347
+ auto_continue: data.auto_continue,
348
+ completed_task_id: data.completed_task_id,
349
+ next_task: data.next_task,
822
350
  };
823
351
 
824
- const validationCount = validationCountResult.count || 0;
825
- const blockersCount = blockersCountResult.count || 0;
826
- const requestsCount = requestsCountResult.count || 0;
827
-
828
- if (validationCount > 0 || blockersCount > 0 || deploymentResult.data || requestsCount > 0) {
829
- result.context = {
830
- ...(validationCount > 0 && { validation: validationCount }),
831
- ...(blockersCount > 0 && { blockers: blockersCount }),
832
- ...(deploymentResult.data && { deployment: deploymentResult.data.status }),
833
- ...(requestsCount > 0 && { requests: requestsCount }),
834
- };
835
- }
836
-
837
- // Include git workflow instructions for post-task steps
838
- const gitConfig = await getProjectGitConfig(supabase, task.project_id);
839
- if (gitConfig && gitConfig.git_workflow !== 'none') {
840
- const gitInstructions = getTaskCompleteGitInstructions(gitConfig, task.git_branch, task.title, task_id);
841
- if (gitInstructions) {
842
- result.git_workflow = {
843
- workflow: gitConfig.git_workflow,
844
- action: 'push_and_pr',
845
- ...gitInstructions,
846
- };
847
- }
848
- }
849
-
850
- // Check if this task belongs to a body of work that auto-deploys on completion
851
- const { data: bowTask } = await supabase
852
- .from('body_of_work_tasks')
853
- .select('body_of_work_id')
854
- .eq('task_id', task_id)
855
- .single();
856
-
857
- if (bowTask) {
858
- // Check if body of work is now completed and has auto-deploy enabled
859
- const { data: bow } = await supabase
860
- .from('bodies_of_work')
861
- .select('id, title, status, auto_deploy_on_completion, deploy_environment, deploy_version_bump')
862
- .eq('id', bowTask.body_of_work_id)
863
- .single();
864
-
865
- if (bow && bow.status === 'completed' && bow.auto_deploy_on_completion) {
866
- // Auto-trigger deployment
867
- const { data: deployment, error: deployError } = await supabase
868
- .from('deployments')
869
- .insert({
870
- project_id: task.project_id,
871
- environment: bow.deploy_environment || 'production',
872
- status: 'pending',
873
- notes: `Auto-deploy triggered by body of work completion: "${bow.title}"`,
874
- requested_by_session_id: currentSessionId,
875
- })
876
- .select('id')
877
- .single();
878
-
879
- if (!deployError && deployment) {
880
- result.body_of_work_completed = {
881
- id: bow.id,
882
- title: bow.title,
883
- auto_deploy_triggered: true,
884
- deployment_id: deployment.id,
885
- environment: bow.deploy_environment || 'production',
886
- version_bump: bow.deploy_version_bump || 'minor',
887
- };
888
-
889
- // Log progress about auto-deploy
890
- await supabase.from('progress_logs').insert({
891
- project_id: task.project_id,
892
- summary: `Body of work "${bow.title}" completed - auto-deploy triggered`,
893
- created_by: 'agent',
894
- created_by_session_id: currentSessionId,
895
- });
896
- }
897
- } else if (bow) {
898
- // Body of work exists but not yet completed or no auto-deploy
899
- result.body_of_work = {
900
- id: bow.id,
901
- title: bow.title,
902
- status: bow.status,
903
- };
904
- }
352
+ if (data.context) {
353
+ result.context = data.context;
905
354
  }
906
355
 
907
- // REPEAT at end - agents weight last items heavily
908
- result.next_action = nextAction;
356
+ // Git workflow instructions are already in API response but we need to fetch
357
+ // task details if we want to include them (API should return these)
358
+ result.next_action = data.next_action;
909
359
 
910
360
  return { result };
911
361
  };
@@ -916,12 +366,12 @@ export const deleteTask: Handler = async (args, ctx) => {
916
366
  validateRequired(task_id, 'task_id');
917
367
  validateUUID(task_id, 'task_id');
918
368
 
919
- const { error } = await ctx.supabase
920
- .from('tasks')
921
- .delete()
922
- .eq('id', task_id);
369
+ const api = getApiClient();
370
+ const response = await api.deleteTask(task_id);
923
371
 
924
- if (error) throw new Error(`Failed to delete task: ${error.message}`);
372
+ if (!response.ok) {
373
+ throw new Error(`Failed to delete task: ${response.error}`);
374
+ }
925
375
 
926
376
  return { result: { success: true, deleted_id: task_id } };
927
377
  };
@@ -933,31 +383,22 @@ export const addTaskReference: Handler = async (args, ctx) => {
933
383
  validateUUID(task_id, 'task_id');
934
384
  validateRequired(url, 'url');
935
385
 
936
- const { data: task, error: fetchError } = await ctx.supabase
937
- .from('tasks')
938
- .select('references')
939
- .eq('id', task_id)
940
- .single();
941
-
942
- if (fetchError) throw new Error(`Failed to fetch task: ${fetchError.message}`);
386
+ const api = getApiClient();
387
+ const response = await api.addTaskReference(task_id, url, label);
943
388
 
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' } };
389
+ if (!response.ok) {
390
+ if (response.error?.includes('already exists')) {
391
+ return { result: { success: false, error: 'Reference with this URL already exists' } };
392
+ }
393
+ throw new Error(`Failed to add reference: ${response.error}`);
948
394
  }
949
395
 
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 } };
396
+ return {
397
+ result: {
398
+ success: true,
399
+ reference: response.data?.reference,
400
+ },
401
+ };
961
402
  };
962
403
 
963
404
  export const removeTaskReference: Handler = async (args, ctx) => {
@@ -967,29 +408,17 @@ export const removeTaskReference: Handler = async (args, ctx) => {
967
408
  validateUUID(task_id, 'task_id');
968
409
  validateRequired(url, 'url');
969
410
 
970
- const { data: task, error: fetchError } = await ctx.supabase
971
- .from('tasks')
972
- .select('references')
973
- .eq('id', task_id)
974
- .single();
975
-
976
- if (fetchError) throw new Error(`Failed to fetch task: ${fetchError.message}`);
977
-
978
- const currentRefs = (task?.references as { url: string; label?: string }[]) || [];
979
- const updatedRefs = currentRefs.filter(ref => ref.url !== url);
411
+ const api = getApiClient();
412
+ const response = await api.removeTaskReference(task_id, url);
980
413
 
981
- if (updatedRefs.length === currentRefs.length) {
982
- return { result: { success: false, error: 'Reference with this URL not found' } };
414
+ if (!response.ok) {
415
+ if (response.error?.includes('not found')) {
416
+ return { result: { success: false, error: 'Reference with this URL not found' } };
417
+ }
418
+ throw new Error(`Failed to remove reference: ${response.error}`);
983
419
  }
984
420
 
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 } };
421
+ return { result: { success: true } };
993
422
  };
994
423
 
995
424
  export const batchUpdateTasks: Handler = async (args, ctx) => {
@@ -1003,9 +432,6 @@ export const batchUpdateTasks: Handler = async (args, ctx) => {
1003
432
  }>;
1004
433
  };
1005
434
 
1006
- const { supabase, session } = ctx;
1007
- const currentSessionId = session.currentSessionId;
1008
-
1009
435
  if (!updates || !Array.isArray(updates) || updates.length === 0) {
1010
436
  throw new ValidationError('updates must be a non-empty array', {
1011
437
  field: 'updates',
@@ -1020,129 +446,74 @@ export const batchUpdateTasks: Handler = async (args, ctx) => {
1020
446
  });
1021
447
  }
1022
448
 
1023
- // Validate all inputs first (no DB queries)
1024
- const taskIds: string[] = [];
449
+ // Validate all inputs first
1025
450
  for (const update of updates) {
1026
451
  validateRequired(update.task_id, 'task_id');
1027
452
  validateUUID(update.task_id, 'task_id');
1028
453
  validateTaskStatus(update.status);
1029
454
  validatePriority(update.priority);
1030
455
  validateProgressPercentage(update.progress_percentage);
1031
- taskIds.push(update.task_id);
1032
456
  }
1033
457
 
1034
- // OPTIMIZATION: Fetch all tasks in a single query instead of N queries
1035
- const { data: tasks } = await supabase
1036
- .from('tasks')
1037
- .select('id, project_id, started_at')
1038
- .in('id', taskIds);
1039
-
1040
- const taskMap = new Map(tasks?.map(t => [t.id, t]) || []);
1041
-
1042
- // OPTIMIZATION: Single query to check if agent has existing in-progress task
1043
- let existingAgentTask: { id: string; title: string } | null = null;
1044
- const hasInProgressUpdate = updates.some(u => u.status === 'in_progress');
1045
- if (hasInProgressUpdate && currentSessionId) {
1046
- const { data } = await supabase
1047
- .from('tasks')
1048
- .select('id, title')
1049
- .eq('working_agent_session_id', currentSessionId)
1050
- .eq('status', 'in_progress')
1051
- .not('id', 'in', `(${taskIds.join(',')})`)
1052
- .limit(1)
1053
- .single();
1054
- existingAgentTask = data;
1055
- }
1056
-
1057
- const results: Array<{ task_id: string; success: boolean; error?: string }> = [];
1058
- const progressLogsToInsert: Array<{
1059
- project_id: string;
1060
- task_id: string;
1061
- summary: string;
1062
- created_by: string;
1063
- created_by_session_id: string | null;
1064
- }> = [];
1065
-
1066
- // OPTIMIZATION: Process updates in parallel instead of sequentially
1067
- const updatePromises = updates.map(async (update) => {
1068
- const task = taskMap.get(update.task_id);
1069
-
1070
- if (!task) {
1071
- return { task_id: update.task_id, success: false, error: 'Task not found' };
1072
- }
1073
-
1074
- // Check agent task limit
1075
- if (update.status === 'in_progress' && existingAgentTask) {
1076
- return {
1077
- task_id: update.task_id,
1078
- success: false,
1079
- error: `Agent already has task in progress: "${existingAgentTask.title}"`,
1080
- };
1081
- }
458
+ const api = getApiClient();
459
+ const response = await api.batchUpdateTasks(updates);
1082
460
 
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
- }
461
+ if (!response.ok) {
462
+ throw new Error(`Failed to batch update tasks: ${response.error}`);
463
+ }
1095
464
 
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
- }
465
+ return {
466
+ result: {
467
+ success: response.data?.success || false,
468
+ total: updates.length,
469
+ succeeded: response.data?.updated_count || 0,
470
+ },
471
+ };
472
+ };
1102
473
 
1103
- const { error } = await supabase
1104
- .from('tasks')
1105
- .update(updateData)
1106
- .eq('id', update.task_id);
474
+ export const batchCompleteTasks: Handler = async (args, ctx) => {
475
+ const { completions } = args as {
476
+ completions: Array<{
477
+ task_id: string;
478
+ summary?: string;
479
+ }>;
480
+ };
1107
481
 
1108
- if (error) {
1109
- return { task_id: update.task_id, success: false, error: error.message };
1110
- }
482
+ if (!completions || !Array.isArray(completions) || completions.length === 0) {
483
+ throw new ValidationError('completions must be a non-empty array', {
484
+ field: 'completions',
485
+ hint: 'Provide an array of task completions with at least one item',
486
+ });
487
+ }
1111
488
 
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
- }
489
+ if (completions.length > 50) {
490
+ throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
491
+ field: 'completions',
492
+ hint: 'Split your completions into smaller batches',
493
+ });
494
+ }
1126
495
 
1127
- return { task_id: update.task_id, success: true };
1128
- });
496
+ // Validate all inputs first
497
+ for (const completion of completions) {
498
+ validateRequired(completion.task_id, 'task_id');
499
+ validateUUID(completion.task_id, 'task_id');
500
+ }
1129
501
 
1130
- // Execute all updates in parallel
1131
- const updateResults = await Promise.all(updatePromises);
1132
- results.push(...updateResults);
502
+ const api = getApiClient();
503
+ const response = await api.batchCompleteTasks(completions);
1133
504
 
1134
- // OPTIMIZATION: Batch insert all progress logs in a single query
1135
- if (progressLogsToInsert.length > 0) {
1136
- await supabase.from('progress_logs').insert(progressLogsToInsert);
505
+ if (!response.ok) {
506
+ throw new Error(`Failed to batch complete tasks: ${response.error}`);
1137
507
  }
1138
508
 
1139
- const successCount = results.filter(r => r.success).length;
1140
-
509
+ const data = response.data;
1141
510
  return {
1142
511
  result: {
1143
- success: successCount === updates.length,
1144
- total: updates.length,
1145
- succeeded: successCount,
512
+ success: data?.success || false,
513
+ total: completions.length,
514
+ succeeded: data?.completed_count || 0,
515
+ failed: completions.length - (data?.completed_count || 0),
516
+ next_task: data?.next_task,
1146
517
  },
1147
518
  };
1148
519
  };
@@ -1160,77 +531,38 @@ export const addSubtask: Handler = async (args, ctx) => {
1160
531
  estimated_minutes?: number;
1161
532
  };
1162
533
 
1163
- const { supabase, session } = ctx;
1164
- const currentSessionId = session.currentSessionId;
1165
-
1166
534
  validateRequired(parent_task_id, 'parent_task_id');
1167
535
  validateUUID(parent_task_id, 'parent_task_id');
1168
536
  validateRequired(title, 'title');
1169
537
  if (priority !== undefined) validatePriority(priority);
1170
538
  if (estimated_minutes !== undefined) validateEstimatedMinutes(estimated_minutes);
1171
539
 
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
- }
540
+ const api = getApiClient();
541
+ const response = await api.addSubtask(parent_task_id, {
542
+ title,
543
+ description,
544
+ priority,
545
+ estimated_minutes,
546
+ }, ctx.session.currentSessionId || undefined);
1185
547
 
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
- };
548
+ if (!response.ok) {
549
+ if (response.error?.includes('Cannot create subtask of a subtask')) {
550
+ return {
551
+ result: {
552
+ success: false,
553
+ error: 'Cannot create subtask of a subtask',
554
+ hint: 'Subtasks cannot have their own subtasks. Add this task to the parent task instead.',
555
+ },
556
+ };
557
+ }
558
+ throw new Error(`Failed to add subtask: ${response.error}`);
1196
559
  }
1197
560
 
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
561
  return {
1228
562
  result: {
1229
563
  success: true,
1230
- subtask_id: subtask.id,
1231
- parent_task_id,
1232
- title: subtask.title,
1233
- priority: subtask.priority,
564
+ subtask_id: response.data?.subtask_id,
565
+ parent_task_id: response.data?.parent_task_id,
1234
566
  },
1235
567
  };
1236
568
  };
@@ -1241,138 +573,29 @@ export const getSubtasks: Handler = async (args, ctx) => {
1241
573
  status?: string;
1242
574
  };
1243
575
 
1244
- const { supabase } = ctx;
1245
-
1246
576
  validateRequired(parent_task_id, 'parent_task_id');
1247
577
  validateUUID(parent_task_id, 'parent_task_id');
1248
578
  if (status) validateTaskStatus(status);
1249
579
 
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 });
580
+ const api = getApiClient();
581
+ const response = await api.getSubtasks(parent_task_id, status);
1256
582
 
1257
- if (status) {
1258
- query = query.eq('status', status);
583
+ if (!response.ok) {
584
+ throw new Error(`Failed to fetch subtasks: ${response.error}`);
1259
585
  }
1260
586
 
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
587
  return {
1272
588
  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,
589
+ subtasks: response.data?.subtasks || [],
590
+ stats: response.data?.stats || {
591
+ total: 0,
592
+ completed: 0,
593
+ progress_percentage: 0,
1280
594
  },
1281
595
  },
1282
596
  };
1283
597
  };
1284
598
 
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
599
  /**
1377
600
  * Task handlers registry
1378
601
  */