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