@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.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1114 -0
- package/dist/api-client.js +698 -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 +106 -476
- 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 +112 -828
- package/dist/handlers/discovery.js +31 -0
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +39 -134
- package/dist/handlers/findings.js +43 -67
- 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 +99 -585
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +274 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +230 -900
- package/dist/handlers/tool-docs.d.ts +8 -0
- package/dist/handlers/tool-docs.js +657 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +26 -153
- package/dist/index.js +473 -160
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +4 -0
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1752 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +210 -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 +194 -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 +140 -895
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +32 -0
- package/src/handlers/fallback.test.ts +128 -361
- package/src/handlers/fallback.ts +62 -148
- package/src/handlers/findings.test.ts +127 -345
- package/src/handlers/findings.ts +49 -66
- 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 +241 -206
- package/src/handlers/session.ts +110 -657
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +497 -0
- package/src/handlers/tasks.test.ts +608 -353
- package/src/handlers/tasks.ts +248 -1025
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +189 -572
- package/src/handlers/validation.ts +29 -166
- package/src/index.ts +473 -184
- package/src/knowledge.ts +107 -9
- package/src/tools.ts +2506 -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/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,89 @@ export const addTask = async (args, ctx) => {
|
|
|
412
170
|
};
|
|
413
171
|
export const updateTask = async (args, ctx) => {
|
|
414
172
|
const { task_id, progress_note, ...updates } = args;
|
|
415
|
-
const { supabase, session } = ctx;
|
|
416
|
-
const currentSessionId = session.currentSessionId;
|
|
417
173
|
validateRequired(task_id, 'task_id');
|
|
418
174
|
validateUUID(task_id, 'task_id');
|
|
419
175
|
validateTaskStatus(updates.status);
|
|
420
176
|
validatePriority(updates.priority);
|
|
421
177
|
validateProgressPercentage(updates.progress_percentage);
|
|
422
178
|
validateEstimatedMinutes(updates.estimated_minutes);
|
|
423
|
-
|
|
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);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
// Auto-set started_at when task moves to in_progress
|
|
483
|
-
if (updates.status === 'in_progress' && task && !task.started_at) {
|
|
484
|
-
updateData.started_at = new Date().toISOString();
|
|
485
|
-
}
|
|
486
|
-
// When setting status to in_progress, claim the task for this agent
|
|
487
|
-
if (updates.status === 'in_progress' && currentSessionId) {
|
|
488
|
-
updateData.working_agent_session_id = currentSessionId;
|
|
489
|
-
// Update the session's current task and clear any fallback activity
|
|
490
|
-
await supabase
|
|
491
|
-
.from('agent_sessions')
|
|
492
|
-
.update({
|
|
493
|
-
current_task_id: task_id,
|
|
494
|
-
current_fallback_activity: null,
|
|
495
|
-
status: 'active',
|
|
496
|
-
last_synced_at: new Date().toISOString(),
|
|
497
|
-
})
|
|
498
|
-
.eq('id', currentSessionId);
|
|
499
|
-
}
|
|
500
|
-
// Auto-set completed_at and progress when task completes
|
|
501
|
-
if (updates.status === 'completed') {
|
|
502
|
-
updateData.completed_at = new Date().toISOString();
|
|
503
|
-
updateData.progress_percentage = 100;
|
|
504
|
-
updateData.working_agent_session_id = null;
|
|
505
|
-
}
|
|
506
|
-
// When cancelled, also release the task
|
|
507
|
-
if (updates.status === 'cancelled') {
|
|
508
|
-
updateData.working_agent_session_id = null;
|
|
509
|
-
}
|
|
510
|
-
const { error } = await supabase
|
|
511
|
-
.from('tasks')
|
|
512
|
-
.update(updateData)
|
|
513
|
-
.eq('id', task_id);
|
|
514
|
-
if (error)
|
|
515
|
-
throw new Error(`Failed to update task: ${error.message}`);
|
|
516
|
-
// If progress_note is provided, create a progress log entry
|
|
517
|
-
if (progress_note && task?.project_id) {
|
|
518
|
-
const progressSummary = updates.progress_percentage !== undefined
|
|
519
|
-
? `Progress: ${updates.progress_percentage}% - ${progress_note}`
|
|
520
|
-
: progress_note;
|
|
521
|
-
await supabase.from('progress_logs').insert({
|
|
522
|
-
project_id: task.project_id,
|
|
523
|
-
task_id,
|
|
524
|
-
summary: progressSummary,
|
|
525
|
-
created_by: 'agent',
|
|
526
|
-
created_by_session_id: currentSessionId,
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
// Build result with optional git instructions
|
|
530
|
-
const result = { success: true, task_id };
|
|
531
|
-
// Include git workflow instructions when task moves to in_progress
|
|
532
|
-
if (updates.status === 'in_progress' && task?.project_id && task?.title) {
|
|
533
|
-
const gitConfig = await getProjectGitConfig(supabase, task.project_id);
|
|
534
|
-
if (gitConfig && gitConfig.git_workflow !== 'none') {
|
|
535
|
-
const gitInstructions = getTaskStartGitInstructions(gitConfig, task_id, task.title);
|
|
536
|
-
if (gitInstructions) {
|
|
537
|
-
result.git_workflow = {
|
|
538
|
-
workflow: gitConfig.git_workflow,
|
|
539
|
-
action: 'create_branch',
|
|
540
|
-
...gitInstructions,
|
|
541
|
-
};
|
|
542
|
-
}
|
|
203
|
+
if (response.error?.includes('invalid_status_transition')) {
|
|
204
|
+
return {
|
|
205
|
+
result: {
|
|
206
|
+
error: 'invalid_status_transition',
|
|
207
|
+
message: response.error,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
543
210
|
}
|
|
211
|
+
throw new Error(`Failed to update task: ${response.error}`);
|
|
544
212
|
}
|
|
545
|
-
return { result };
|
|
213
|
+
return { result: { success: true, task_id } };
|
|
546
214
|
};
|
|
547
215
|
export const completeTask = async (args, ctx) => {
|
|
548
216
|
const { task_id, summary } = args;
|
|
549
|
-
const { supabase, session } = ctx;
|
|
550
|
-
const currentSessionId = session.currentSessionId;
|
|
551
217
|
validateRequired(task_id, 'task_id');
|
|
552
218
|
validateUUID(task_id, 'task_id');
|
|
553
|
-
|
|
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);
|
|
219
|
+
const api = getApiClient();
|
|
220
|
+
const response = await api.completeTask(task_id, {
|
|
221
|
+
summary,
|
|
222
|
+
session_id: ctx.session.currentSessionId || undefined,
|
|
223
|
+
});
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`Failed to complete task: ${response.error}`);
|
|
584
226
|
}
|
|
585
|
-
|
|
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
|
-
});
|
|
227
|
+
const data = response.data;
|
|
228
|
+
if (!data) {
|
|
229
|
+
throw new Error('No response data from complete task');
|
|
595
230
|
}
|
|
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
|
|
231
|
+
// Build result matching expected format
|
|
641
232
|
const result = {
|
|
642
233
|
success: true,
|
|
643
|
-
directive,
|
|
644
|
-
auto_continue:
|
|
645
|
-
completed_task_id:
|
|
646
|
-
next_task:
|
|
234
|
+
directive: data.directive,
|
|
235
|
+
auto_continue: data.auto_continue,
|
|
236
|
+
completed_task_id: data.completed_task_id,
|
|
237
|
+
next_task: data.next_task,
|
|
647
238
|
};
|
|
648
|
-
|
|
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
|
-
};
|
|
239
|
+
if (data.context) {
|
|
240
|
+
result.context = data.context;
|
|
658
241
|
}
|
|
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
|
-
}
|
|
723
|
-
}
|
|
724
|
-
// REPEAT at end - agents weight last items heavily
|
|
725
|
-
result.next_action = nextAction;
|
|
242
|
+
// Git workflow instructions are already in API response but we need to fetch
|
|
243
|
+
// task details if we want to include them (API should return these)
|
|
244
|
+
result.next_action = data.next_action;
|
|
726
245
|
return { result };
|
|
727
246
|
};
|
|
728
247
|
export const deleteTask = async (args, ctx) => {
|
|
729
248
|
const { task_id } = args;
|
|
730
249
|
validateRequired(task_id, 'task_id');
|
|
731
250
|
validateUUID(task_id, 'task_id');
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
throw new Error(`Failed to delete task: ${error.message}`);
|
|
251
|
+
const api = getApiClient();
|
|
252
|
+
const response = await api.deleteTask(task_id);
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
throw new Error(`Failed to delete task: ${response.error}`);
|
|
255
|
+
}
|
|
738
256
|
return { result: { success: true, deleted_id: task_id } };
|
|
739
257
|
};
|
|
740
258
|
export const addTaskReference = async (args, ctx) => {
|
|
@@ -742,56 +260,38 @@ export const addTaskReference = async (args, ctx) => {
|
|
|
742
260
|
validateRequired(task_id, 'task_id');
|
|
743
261
|
validateUUID(task_id, 'task_id');
|
|
744
262
|
validateRequired(url, 'url');
|
|
745
|
-
const
|
|
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' } };
|
|
263
|
+
const api = getApiClient();
|
|
264
|
+
const response = await api.addTaskReference(task_id, url, label);
|
|
265
|
+
if (!response.ok) {
|
|
266
|
+
if (response.error?.includes('already exists')) {
|
|
267
|
+
return { result: { success: false, error: 'Reference with this URL already exists' } };
|
|
268
|
+
}
|
|
269
|
+
throw new Error(`Failed to add reference: ${response.error}`);
|
|
755
270
|
}
|
|
756
|
-
|
|
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 } };
|
|
271
|
+
return {
|
|
272
|
+
result: {
|
|
273
|
+
success: true,
|
|
274
|
+
reference: response.data?.reference,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
765
277
|
};
|
|
766
278
|
export const removeTaskReference = async (args, ctx) => {
|
|
767
279
|
const { task_id, url } = args;
|
|
768
280
|
validateRequired(task_id, 'task_id');
|
|
769
281
|
validateUUID(task_id, 'task_id');
|
|
770
282
|
validateRequired(url, 'url');
|
|
771
|
-
const
|
|
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' } };
|
|
283
|
+
const api = getApiClient();
|
|
284
|
+
const response = await api.removeTaskReference(task_id, url);
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
if (response.error?.includes('not found')) {
|
|
287
|
+
return { result: { success: false, error: 'Reference with this URL not found' } };
|
|
288
|
+
}
|
|
289
|
+
throw new Error(`Failed to remove reference: ${response.error}`);
|
|
782
290
|
}
|
|
783
|
-
|
|
784
|
-
.from('tasks')
|
|
785
|
-
.update({ references: updatedRefs })
|
|
786
|
-
.eq('id', task_id);
|
|
787
|
-
if (updateError)
|
|
788
|
-
throw new Error(`Failed to remove reference: ${updateError.message}`);
|
|
789
|
-
return { result: { success: true, remaining_references: updatedRefs.length } };
|
|
291
|
+
return { result: { success: true } };
|
|
790
292
|
};
|
|
791
293
|
export const batchUpdateTasks = async (args, ctx) => {
|
|
792
294
|
const { updates } = args;
|
|
793
|
-
const { supabase, session } = ctx;
|
|
794
|
-
const currentSessionId = session.currentSessionId;
|
|
795
295
|
if (!updates || !Array.isArray(updates) || updates.length === 0) {
|
|
796
296
|
throw new ValidationError('updates must be a non-empty array', {
|
|
797
297
|
field: 'updates',
|
|
@@ -804,107 +304,59 @@ export const batchUpdateTasks = async (args, ctx) => {
|
|
|
804
304
|
hint: 'Split your updates into smaller batches',
|
|
805
305
|
});
|
|
806
306
|
}
|
|
807
|
-
// Validate all inputs first
|
|
808
|
-
const taskIds = [];
|
|
307
|
+
// Validate all inputs first
|
|
809
308
|
for (const update of updates) {
|
|
810
309
|
validateRequired(update.task_id, 'task_id');
|
|
811
310
|
validateUUID(update.task_id, 'task_id');
|
|
812
311
|
validateTaskStatus(update.status);
|
|
813
312
|
validatePriority(update.priority);
|
|
814
313
|
validateProgressPercentage(update.progress_percentage);
|
|
815
|
-
taskIds.push(update.task_id);
|
|
816
314
|
}
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
.in('id', taskIds);
|
|
822
|
-
const taskMap = new Map(tasks?.map(t => [t.id, t]) || []);
|
|
823
|
-
// OPTIMIZATION: Single query to check if agent has existing in-progress task
|
|
824
|
-
let existingAgentTask = null;
|
|
825
|
-
const hasInProgressUpdate = updates.some(u => u.status === 'in_progress');
|
|
826
|
-
if (hasInProgressUpdate && currentSessionId) {
|
|
827
|
-
const { data } = await supabase
|
|
828
|
-
.from('tasks')
|
|
829
|
-
.select('id, title')
|
|
830
|
-
.eq('working_agent_session_id', currentSessionId)
|
|
831
|
-
.eq('status', 'in_progress')
|
|
832
|
-
.not('id', 'in', `(${taskIds.join(',')})`)
|
|
833
|
-
.limit(1)
|
|
834
|
-
.single();
|
|
835
|
-
existingAgentTask = data;
|
|
315
|
+
const api = getApiClient();
|
|
316
|
+
const response = await api.batchUpdateTasks(updates);
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
throw new Error(`Failed to batch update tasks: ${response.error}`);
|
|
836
319
|
}
|
|
837
|
-
const results = [];
|
|
838
|
-
const progressLogsToInsert = [];
|
|
839
|
-
// OPTIMIZATION: Process updates in parallel instead of sequentially
|
|
840
|
-
const updatePromises = updates.map(async (update) => {
|
|
841
|
-
const task = taskMap.get(update.task_id);
|
|
842
|
-
if (!task) {
|
|
843
|
-
return { task_id: update.task_id, success: false, error: 'Task not found' };
|
|
844
|
-
}
|
|
845
|
-
// Check agent task limit
|
|
846
|
-
if (update.status === 'in_progress' && existingAgentTask) {
|
|
847
|
-
return {
|
|
848
|
-
task_id: update.task_id,
|
|
849
|
-
success: false,
|
|
850
|
-
error: `Agent already has task in progress: "${existingAgentTask.title}"`,
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
const updateData = {};
|
|
854
|
-
if (update.status)
|
|
855
|
-
updateData.status = update.status;
|
|
856
|
-
if (update.progress_percentage !== undefined)
|
|
857
|
-
updateData.progress_percentage = update.progress_percentage;
|
|
858
|
-
if (update.priority !== undefined)
|
|
859
|
-
updateData.priority = update.priority;
|
|
860
|
-
// Auto-set started_at when task moves to in_progress
|
|
861
|
-
if (update.status === 'in_progress' && !task.started_at) {
|
|
862
|
-
updateData.started_at = new Date().toISOString();
|
|
863
|
-
if (currentSessionId) {
|
|
864
|
-
updateData.working_agent_session_id = currentSessionId;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
// Auto-set completed_at when task completes
|
|
868
|
-
if (update.status === 'completed') {
|
|
869
|
-
updateData.completed_at = new Date().toISOString();
|
|
870
|
-
updateData.progress_percentage = 100;
|
|
871
|
-
updateData.working_agent_session_id = null;
|
|
872
|
-
}
|
|
873
|
-
const { error } = await supabase
|
|
874
|
-
.from('tasks')
|
|
875
|
-
.update(updateData)
|
|
876
|
-
.eq('id', update.task_id);
|
|
877
|
-
if (error) {
|
|
878
|
-
return { task_id: update.task_id, success: false, error: error.message };
|
|
879
|
-
}
|
|
880
|
-
// Queue progress log for batch insert
|
|
881
|
-
if (update.progress_note && task.project_id) {
|
|
882
|
-
const progressSummary = update.progress_percentage !== undefined
|
|
883
|
-
? `Progress: ${update.progress_percentage}% - ${update.progress_note}`
|
|
884
|
-
: update.progress_note;
|
|
885
|
-
progressLogsToInsert.push({
|
|
886
|
-
project_id: task.project_id,
|
|
887
|
-
task_id: update.task_id,
|
|
888
|
-
summary: progressSummary,
|
|
889
|
-
created_by: 'agent',
|
|
890
|
-
created_by_session_id: currentSessionId,
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
return { task_id: update.task_id, success: true };
|
|
894
|
-
});
|
|
895
|
-
// Execute all updates in parallel
|
|
896
|
-
const updateResults = await Promise.all(updatePromises);
|
|
897
|
-
results.push(...updateResults);
|
|
898
|
-
// OPTIMIZATION: Batch insert all progress logs in a single query
|
|
899
|
-
if (progressLogsToInsert.length > 0) {
|
|
900
|
-
await supabase.from('progress_logs').insert(progressLogsToInsert);
|
|
901
|
-
}
|
|
902
|
-
const successCount = results.filter(r => r.success).length;
|
|
903
320
|
return {
|
|
904
321
|
result: {
|
|
905
|
-
success:
|
|
322
|
+
success: response.data?.success || false,
|
|
906
323
|
total: updates.length,
|
|
907
|
-
succeeded:
|
|
324
|
+
succeeded: response.data?.updated_count || 0,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
export const batchCompleteTasks = async (args, ctx) => {
|
|
329
|
+
const { completions } = args;
|
|
330
|
+
if (!completions || !Array.isArray(completions) || completions.length === 0) {
|
|
331
|
+
throw new ValidationError('completions must be a non-empty array', {
|
|
332
|
+
field: 'completions',
|
|
333
|
+
hint: 'Provide an array of task completions with at least one item',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (completions.length > 50) {
|
|
337
|
+
throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
|
|
338
|
+
field: 'completions',
|
|
339
|
+
hint: 'Split your completions into smaller batches',
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// Validate all inputs first
|
|
343
|
+
for (const completion of completions) {
|
|
344
|
+
validateRequired(completion.task_id, 'task_id');
|
|
345
|
+
validateUUID(completion.task_id, 'task_id');
|
|
346
|
+
}
|
|
347
|
+
const api = getApiClient();
|
|
348
|
+
const response = await api.batchCompleteTasks(completions);
|
|
349
|
+
if (!response.ok) {
|
|
350
|
+
throw new Error(`Failed to batch complete tasks: ${response.error}`);
|
|
351
|
+
}
|
|
352
|
+
const data = response.data;
|
|
353
|
+
return {
|
|
354
|
+
result: {
|
|
355
|
+
success: data?.success || false,
|
|
356
|
+
total: completions.length,
|
|
357
|
+
succeeded: data?.completed_count || 0,
|
|
358
|
+
failed: completions.length - (data?.completed_count || 0),
|
|
359
|
+
next_task: data?.next_task,
|
|
908
360
|
},
|
|
909
361
|
};
|
|
910
362
|
};
|
|
@@ -913,8 +365,6 @@ export const batchUpdateTasks = async (args, ctx) => {
|
|
|
913
365
|
// ============================================================================
|
|
914
366
|
export const addSubtask = async (args, ctx) => {
|
|
915
367
|
const { parent_task_id, title, description, priority, estimated_minutes } = args;
|
|
916
|
-
const { supabase, session } = ctx;
|
|
917
|
-
const currentSessionId = session.currentSessionId;
|
|
918
368
|
validateRequired(parent_task_id, 'parent_task_id');
|
|
919
369
|
validateUUID(parent_task_id, 'parent_task_id');
|
|
920
370
|
validateRequired(title, 'title');
|
|
@@ -922,175 +372,55 @@ export const addSubtask = async (args, ctx) => {
|
|
|
922
372
|
validatePriority(priority);
|
|
923
373
|
if (estimated_minutes !== undefined)
|
|
924
374
|
validateEstimatedMinutes(estimated_minutes);
|
|
925
|
-
|
|
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,
|
|
375
|
+
const api = getApiClient();
|
|
376
|
+
const response = await api.addSubtask(parent_task_id, {
|
|
955
377
|
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
|
-
});
|
|
378
|
+
description,
|
|
379
|
+
priority,
|
|
380
|
+
estimated_minutes,
|
|
381
|
+
}, ctx.session.currentSessionId || undefined);
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
if (response.error?.includes('Cannot create subtask of a subtask')) {
|
|
384
|
+
return {
|
|
385
|
+
result: {
|
|
386
|
+
success: false,
|
|
387
|
+
error: 'Cannot create subtask of a subtask',
|
|
388
|
+
hint: 'Subtasks cannot have their own subtasks. Add this task to the parent task instead.',
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
throw new Error(`Failed to add subtask: ${response.error}`);
|
|
393
|
+
}
|
|
974
394
|
return {
|
|
975
395
|
result: {
|
|
976
396
|
success: true,
|
|
977
|
-
subtask_id:
|
|
978
|
-
parent_task_id,
|
|
979
|
-
title: subtask.title,
|
|
980
|
-
priority: subtask.priority,
|
|
397
|
+
subtask_id: response.data?.subtask_id,
|
|
398
|
+
parent_task_id: response.data?.parent_task_id,
|
|
981
399
|
},
|
|
982
400
|
};
|
|
983
401
|
};
|
|
984
402
|
export const getSubtasks = async (args, ctx) => {
|
|
985
403
|
const { parent_task_id, status } = args;
|
|
986
|
-
const { supabase } = ctx;
|
|
987
404
|
validateRequired(parent_task_id, 'parent_task_id');
|
|
988
405
|
validateUUID(parent_task_id, 'parent_task_id');
|
|
989
406
|
if (status)
|
|
990
407
|
validateTaskStatus(status);
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
.order('priority', { ascending: true })
|
|
996
|
-
.order('created_at', { ascending: true });
|
|
997
|
-
if (status) {
|
|
998
|
-
query = query.eq('status', status);
|
|
408
|
+
const api = getApiClient();
|
|
409
|
+
const response = await api.getSubtasks(parent_task_id, status);
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
throw new Error(`Failed to fetch subtasks: ${response.error}`);
|
|
999
412
|
}
|
|
1000
|
-
const { data: subtasks, error } = await query;
|
|
1001
|
-
if (error)
|
|
1002
|
-
throw new Error(`Failed to fetch subtasks: ${error.message}`);
|
|
1003
|
-
// Calculate aggregate stats
|
|
1004
|
-
const total = subtasks?.length || 0;
|
|
1005
|
-
const completed = subtasks?.filter(s => s.status === 'completed').length || 0;
|
|
1006
|
-
const inProgress = subtasks?.filter(s => s.status === 'in_progress').length || 0;
|
|
1007
|
-
const pending = subtasks?.filter(s => s.status === 'pending').length || 0;
|
|
1008
413
|
return {
|
|
1009
414
|
result: {
|
|
1010
|
-
subtasks: subtasks || [],
|
|
1011
|
-
stats: {
|
|
1012
|
-
total,
|
|
1013
|
-
completed,
|
|
1014
|
-
|
|
1015
|
-
pending,
|
|
1016
|
-
progress_percentage: total > 0 ? Math.round((completed / total) * 100) : 0,
|
|
415
|
+
subtasks: response.data?.subtasks || [],
|
|
416
|
+
stats: response.data?.stats || {
|
|
417
|
+
total: 0,
|
|
418
|
+
completed: 0,
|
|
419
|
+
progress_percentage: 0,
|
|
1017
420
|
},
|
|
1018
421
|
},
|
|
1019
422
|
};
|
|
1020
423
|
};
|
|
1021
|
-
export const batchCompleteTasks = async (args, ctx) => {
|
|
1022
|
-
const { completions } = args;
|
|
1023
|
-
const { supabase, session } = ctx;
|
|
1024
|
-
const currentSessionId = session.currentSessionId;
|
|
1025
|
-
if (!completions || !Array.isArray(completions) || completions.length === 0) {
|
|
1026
|
-
throw new ValidationError('completions must be a non-empty array', {
|
|
1027
|
-
field: 'completions',
|
|
1028
|
-
hint: 'Provide an array of task completions with at least one item',
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
if (completions.length > 50) {
|
|
1032
|
-
throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
|
|
1033
|
-
field: 'completions',
|
|
1034
|
-
hint: 'Split your completions into smaller batches',
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
const results = [];
|
|
1038
|
-
for (const completion of completions) {
|
|
1039
|
-
try {
|
|
1040
|
-
validateRequired(completion.task_id, 'task_id');
|
|
1041
|
-
validateUUID(completion.task_id, 'task_id');
|
|
1042
|
-
const { data: task, error: fetchError } = await supabase
|
|
1043
|
-
.from('tasks')
|
|
1044
|
-
.select('project_id, title')
|
|
1045
|
-
.eq('id', completion.task_id)
|
|
1046
|
-
.single();
|
|
1047
|
-
if (fetchError || !task) {
|
|
1048
|
-
results.push({ task_id: completion.task_id, success: false, error: 'Task not found' });
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
const { error } = await supabase
|
|
1052
|
-
.from('tasks')
|
|
1053
|
-
.update({
|
|
1054
|
-
status: 'completed',
|
|
1055
|
-
completed_at: new Date().toISOString(),
|
|
1056
|
-
progress_percentage: 100,
|
|
1057
|
-
working_agent_session_id: null,
|
|
1058
|
-
})
|
|
1059
|
-
.eq('id', completion.task_id);
|
|
1060
|
-
if (error) {
|
|
1061
|
-
results.push({ task_id: completion.task_id, success: false, error: error.message });
|
|
1062
|
-
continue;
|
|
1063
|
-
}
|
|
1064
|
-
if (completion.summary) {
|
|
1065
|
-
await supabase.from('progress_logs').insert({
|
|
1066
|
-
project_id: task.project_id,
|
|
1067
|
-
task_id: completion.task_id,
|
|
1068
|
-
summary: `Completed: ${task.title}`,
|
|
1069
|
-
details: completion.summary,
|
|
1070
|
-
created_by: 'agent',
|
|
1071
|
-
created_by_session_id: currentSessionId,
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
results.push({ task_id: completion.task_id, success: true });
|
|
1075
|
-
}
|
|
1076
|
-
catch (err) {
|
|
1077
|
-
results.push({
|
|
1078
|
-
task_id: completion.task_id,
|
|
1079
|
-
success: false,
|
|
1080
|
-
error: err instanceof Error ? err.message : 'Unknown error',
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
const successCount = results.filter(r => r.success).length;
|
|
1085
|
-
return {
|
|
1086
|
-
result: {
|
|
1087
|
-
success: successCount === completions.length,
|
|
1088
|
-
total: completions.length,
|
|
1089
|
-
succeeded: successCount,
|
|
1090
|
-
failed: completions.length - successCount,
|
|
1091
|
-
},
|
|
1092
|
-
};
|
|
1093
|
-
};
|
|
1094
424
|
/**
|
|
1095
425
|
* Task handlers registry
|
|
1096
426
|
*/
|