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