@vibescope/mcp-server 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1169 -0
- package/dist/api-client.js +713 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +108 -477
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +113 -828
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +26 -627
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +56 -142
- package/dist/handlers/findings.d.ts +8 -1
- package/dist/handlers/findings.js +65 -68
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +119 -590
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +275 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +245 -894
- package/dist/handlers/tool-docs.d.ts +9 -0
- package/dist/handlers/tool-docs.js +904 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +38 -153
- package/dist/index.js +493 -162
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +34 -4
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1822 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +215 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +210 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +143 -896
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +29 -714
- package/src/handlers/fallback.test.ts +206 -361
- package/src/handlers/fallback.ts +81 -156
- package/src/handlers/findings.test.ts +229 -320
- package/src/handlers/findings.ts +76 -64
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +276 -206
- package/src/handlers/session.ts +136 -662
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +510 -0
- package/src/handlers/tasks.test.ts +669 -353
- package/src/handlers/tasks.ts +263 -1015
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +237 -568
- package/src/handlers/validation.ts +43 -167
- package/src/index.ts +493 -186
- package/src/tools.ts +2532 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/knowledge.ts +0 -132
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- package/tmpclaude-f48d-cwd +0 -1
package/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
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Fetch candidate pending root tasks (not subtasks)
|
|
326
|
-
const { data: candidates, error } = await supabase
|
|
327
|
-
.from('tasks')
|
|
328
|
-
.select('id, title, description, priority, estimated_minutes, working_agent_session_id')
|
|
329
|
-
.eq('project_id', project_id)
|
|
330
|
-
.eq('status', 'pending')
|
|
331
|
-
.is('parent_task_id', null)
|
|
332
|
-
.order('priority', { ascending: true })
|
|
333
|
-
.order('created_at', { ascending: true })
|
|
334
|
-
.limit(10);
|
|
335
|
-
|
|
336
|
-
// Fetch pending agent requests
|
|
337
|
-
const { data: pendingRequests } = await supabase
|
|
338
|
-
.from('agent_requests')
|
|
339
|
-
.select('id, message')
|
|
340
|
-
.eq('project_id', project_id)
|
|
341
|
-
.is('acknowledged_at', null)
|
|
342
|
-
.or(`session_id.is.null,session_id.eq.${currentSessionId}`)
|
|
343
|
-
.limit(3);
|
|
344
|
-
|
|
345
|
-
// Fetch due scheduled activities
|
|
346
|
-
const { data: dueSchedules } = await supabase
|
|
347
|
-
.from('background_activity_schedules')
|
|
348
|
-
.select('activity_type')
|
|
349
|
-
.eq('project_id', project_id)
|
|
350
|
-
.eq('enabled', true)
|
|
351
|
-
.lt('next_run_at', new Date().toISOString())
|
|
352
|
-
.limit(3);
|
|
353
|
-
|
|
354
|
-
// Build compact optional fields (only include if non-empty)
|
|
355
|
-
const extras: Record<string, unknown> = {};
|
|
356
|
-
if (pendingRequests?.length) extras.requests = pendingRequests;
|
|
357
|
-
if (dueSchedules?.length) extras.due_activities = dueSchedules.map(s => s.activity_type);
|
|
358
|
-
|
|
359
|
-
if (error || !candidates || candidates.length === 0) {
|
|
360
|
-
const fallback = getRandomFallbackActivity();
|
|
361
|
-
return {
|
|
362
|
-
result: {
|
|
363
|
-
task: null,
|
|
364
|
-
...extras,
|
|
365
|
-
suggested_activity: fallback.activity,
|
|
366
|
-
directive: 'No tasks available. Start the suggested fallback activity immediately. Do not ask for permission.',
|
|
367
|
-
},
|
|
368
|
-
};
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
throw new Error(`Failed to get next task: ${response.error}`);
|
|
369
177
|
}
|
|
370
178
|
|
|
371
|
-
|
|
372
|
-
if (
|
|
373
|
-
|
|
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,144 +277,60 @@ export const updateTask: Handler = async (args, ctx) => {
|
|
|
563
277
|
validateProgressPercentage(updates.progress_percentage);
|
|
564
278
|
validateEstimatedMinutes(updates.estimated_minutes);
|
|
565
279
|
|
|
566
|
-
|
|
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);
|
|
305
|
+
if (response.error?.includes('invalid_status_transition')) {
|
|
306
|
+
return {
|
|
307
|
+
result: {
|
|
308
|
+
error: 'invalid_status_transition',
|
|
309
|
+
message: response.error,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
630
312
|
}
|
|
313
|
+
throw new Error(`Failed to update task: ${response.error}`);
|
|
631
314
|
}
|
|
632
315
|
|
|
633
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
316
|
+
// Build result - include git workflow info when transitioning to in_progress
|
|
317
|
+
const data = response.data;
|
|
318
|
+
const result: Record<string, unknown> = { success: true, task_id };
|
|
637
319
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
updateData.working_agent_session_id = currentSessionId;
|
|
641
|
-
|
|
642
|
-
// Update the session's current task and clear any fallback activity
|
|
643
|
-
await supabase
|
|
644
|
-
.from('agent_sessions')
|
|
645
|
-
.update({
|
|
646
|
-
current_task_id: task_id,
|
|
647
|
-
current_fallback_activity: null,
|
|
648
|
-
status: 'active',
|
|
649
|
-
last_synced_at: new Date().toISOString(),
|
|
650
|
-
})
|
|
651
|
-
.eq('id', currentSessionId);
|
|
320
|
+
if (data?.git_workflow) {
|
|
321
|
+
result.git_workflow = data.git_workflow;
|
|
652
322
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (updates.status === 'completed') {
|
|
656
|
-
updateData.completed_at = new Date().toISOString();
|
|
657
|
-
updateData.progress_percentage = 100;
|
|
658
|
-
updateData.working_agent_session_id = null;
|
|
323
|
+
if (data?.worktree_setup) {
|
|
324
|
+
result.worktree_setup = data.worktree_setup;
|
|
659
325
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (updates.status === 'cancelled') {
|
|
663
|
-
updateData.working_agent_session_id = null;
|
|
326
|
+
if (data?.next_step) {
|
|
327
|
+
result.next_step = data.next_step;
|
|
664
328
|
}
|
|
665
329
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
.
|
|
669
|
-
.
|
|
670
|
-
|
|
671
|
-
if (error) throw new Error(`Failed to update task: ${error.message}`);
|
|
672
|
-
|
|
673
|
-
// If progress_note is provided, create a progress log entry
|
|
674
|
-
if (progress_note && task?.project_id) {
|
|
675
|
-
const progressSummary = updates.progress_percentage !== undefined
|
|
676
|
-
? `Progress: ${updates.progress_percentage}% - ${progress_note}`
|
|
677
|
-
: progress_note;
|
|
678
|
-
|
|
679
|
-
await supabase.from('progress_logs').insert({
|
|
680
|
-
project_id: task.project_id,
|
|
681
|
-
task_id,
|
|
682
|
-
summary: progressSummary,
|
|
683
|
-
created_by: 'agent',
|
|
684
|
-
created_by_session_id: currentSessionId,
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Build result with optional git instructions
|
|
689
|
-
const result: Record<string, unknown> = { success: true, task_id };
|
|
690
|
-
|
|
691
|
-
// Include git workflow instructions when task moves to in_progress
|
|
692
|
-
if (updates.status === 'in_progress' && task?.project_id && task?.title) {
|
|
693
|
-
const gitConfig = await getProjectGitConfig(supabase, task.project_id);
|
|
694
|
-
if (gitConfig && gitConfig.git_workflow !== 'none') {
|
|
695
|
-
const gitInstructions = getTaskStartGitInstructions(gitConfig, task_id, task.title);
|
|
696
|
-
if (gitInstructions) {
|
|
697
|
-
result.git_workflow = {
|
|
698
|
-
workflow: gitConfig.git_workflow,
|
|
699
|
-
action: 'create_branch',
|
|
700
|
-
...gitInstructions,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
}
|
|
330
|
+
// Warn if transitioning to in_progress without git_branch
|
|
331
|
+
if (updates.status === 'in_progress' && !updates.git_branch) {
|
|
332
|
+
result.warning = 'git_branch not set. For multi-agent collaboration, set git_branch when marking in_progress to track your worktree.';
|
|
333
|
+
result.hint = 'Call update_task again with git_branch parameter after creating your worktree.';
|
|
704
334
|
}
|
|
705
335
|
|
|
706
336
|
return { result };
|
|
@@ -712,200 +342,45 @@ export const completeTask: Handler = async (args, ctx) => {
|
|
|
712
342
|
summary?: string;
|
|
713
343
|
};
|
|
714
344
|
|
|
715
|
-
const { supabase, session } = ctx;
|
|
716
|
-
const currentSessionId = session.currentSessionId;
|
|
717
|
-
|
|
718
345
|
validateRequired(task_id, 'task_id');
|
|
719
346
|
validateUUID(task_id, 'task_id');
|
|
720
347
|
|
|
721
|
-
|
|
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);
|
|
348
|
+
const api = getApiClient();
|
|
349
|
+
const response = await api.completeTask(task_id, {
|
|
350
|
+
summary,
|
|
351
|
+
session_id: ctx.session.currentSessionId || undefined,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
throw new Error(`Failed to complete task: ${response.error}`);
|
|
754
356
|
}
|
|
755
357
|
|
|
756
|
-
|
|
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
|
-
});
|
|
358
|
+
const data = response.data;
|
|
359
|
+
if (!data) {
|
|
360
|
+
throw new Error('No response data from complete task');
|
|
766
361
|
}
|
|
767
362
|
|
|
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
|
|
363
|
+
// Build result matching expected format
|
|
816
364
|
const result: Record<string, unknown> = {
|
|
817
365
|
success: true,
|
|
818
|
-
directive,
|
|
819
|
-
auto_continue:
|
|
820
|
-
completed_task_id:
|
|
821
|
-
next_task:
|
|
366
|
+
directive: data.directive,
|
|
367
|
+
auto_continue: data.auto_continue,
|
|
368
|
+
completed_task_id: data.completed_task_id,
|
|
369
|
+
next_task: data.next_task,
|
|
822
370
|
};
|
|
823
371
|
|
|
824
|
-
|
|
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
|
-
}
|
|
372
|
+
if (data.context) {
|
|
373
|
+
result.context = data.context;
|
|
848
374
|
}
|
|
849
375
|
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
.
|
|
853
|
-
.select('body_of_work_id')
|
|
854
|
-
.eq('task_id', task_id)
|
|
855
|
-
.single();
|
|
856
|
-
|
|
857
|
-
if (bowTask) {
|
|
858
|
-
// Check if body of work is now completed and has auto-deploy enabled
|
|
859
|
-
const { data: bow } = await supabase
|
|
860
|
-
.from('bodies_of_work')
|
|
861
|
-
.select('id, title, status, auto_deploy_on_completion, deploy_environment, deploy_version_bump')
|
|
862
|
-
.eq('id', bowTask.body_of_work_id)
|
|
863
|
-
.single();
|
|
864
|
-
|
|
865
|
-
if (bow && bow.status === 'completed' && bow.auto_deploy_on_completion) {
|
|
866
|
-
// Auto-trigger deployment
|
|
867
|
-
const { data: deployment, error: deployError } = await supabase
|
|
868
|
-
.from('deployments')
|
|
869
|
-
.insert({
|
|
870
|
-
project_id: task.project_id,
|
|
871
|
-
environment: bow.deploy_environment || 'production',
|
|
872
|
-
status: 'pending',
|
|
873
|
-
notes: `Auto-deploy triggered by body of work completion: "${bow.title}"`,
|
|
874
|
-
requested_by_session_id: currentSessionId,
|
|
875
|
-
})
|
|
876
|
-
.select('id')
|
|
877
|
-
.single();
|
|
878
|
-
|
|
879
|
-
if (!deployError && deployment) {
|
|
880
|
-
result.body_of_work_completed = {
|
|
881
|
-
id: bow.id,
|
|
882
|
-
title: bow.title,
|
|
883
|
-
auto_deploy_triggered: true,
|
|
884
|
-
deployment_id: deployment.id,
|
|
885
|
-
environment: bow.deploy_environment || 'production',
|
|
886
|
-
version_bump: bow.deploy_version_bump || 'minor',
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
// Log progress about auto-deploy
|
|
890
|
-
await supabase.from('progress_logs').insert({
|
|
891
|
-
project_id: task.project_id,
|
|
892
|
-
summary: `Body of work "${bow.title}" completed - auto-deploy triggered`,
|
|
893
|
-
created_by: 'agent',
|
|
894
|
-
created_by_session_id: currentSessionId,
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
} else if (bow) {
|
|
898
|
-
// Body of work exists but not yet completed or no auto-deploy
|
|
899
|
-
result.body_of_work = {
|
|
900
|
-
id: bow.id,
|
|
901
|
-
title: bow.title,
|
|
902
|
-
status: bow.status,
|
|
903
|
-
};
|
|
904
|
-
}
|
|
376
|
+
// Pass through warnings (e.g., missing git_branch)
|
|
377
|
+
if (data.warnings) {
|
|
378
|
+
result.warnings = data.warnings;
|
|
905
379
|
}
|
|
906
380
|
|
|
907
|
-
//
|
|
908
|
-
|
|
381
|
+
// Git workflow instructions are already in API response but we need to fetch
|
|
382
|
+
// task details if we want to include them (API should return these)
|
|
383
|
+
result.next_action = data.next_action;
|
|
909
384
|
|
|
910
385
|
return { result };
|
|
911
386
|
};
|
|
@@ -916,12 +391,12 @@ export const deleteTask: Handler = async (args, ctx) => {
|
|
|
916
391
|
validateRequired(task_id, 'task_id');
|
|
917
392
|
validateUUID(task_id, 'task_id');
|
|
918
393
|
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
.delete()
|
|
922
|
-
.eq('id', task_id);
|
|
394
|
+
const api = getApiClient();
|
|
395
|
+
const response = await api.deleteTask(task_id);
|
|
923
396
|
|
|
924
|
-
if (
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
throw new Error(`Failed to delete task: ${response.error}`);
|
|
399
|
+
}
|
|
925
400
|
|
|
926
401
|
return { result: { success: true, deleted_id: task_id } };
|
|
927
402
|
};
|
|
@@ -933,31 +408,22 @@ export const addTaskReference: Handler = async (args, ctx) => {
|
|
|
933
408
|
validateUUID(task_id, 'task_id');
|
|
934
409
|
validateRequired(url, 'url');
|
|
935
410
|
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
.select('references')
|
|
939
|
-
.eq('id', task_id)
|
|
940
|
-
.single();
|
|
411
|
+
const api = getApiClient();
|
|
412
|
+
const response = await api.addTaskReference(task_id, url, label);
|
|
941
413
|
|
|
942
|
-
if (
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
return { result: { success: false, error: 'Reference with this URL already exists' } };
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
if (response.error?.includes('already exists')) {
|
|
416
|
+
return { result: { success: false, error: 'Reference with this URL already exists' } };
|
|
417
|
+
}
|
|
418
|
+
throw new Error(`Failed to add reference: ${response.error}`);
|
|
948
419
|
}
|
|
949
420
|
|
|
950
|
-
|
|
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 } };
|
|
421
|
+
return {
|
|
422
|
+
result: {
|
|
423
|
+
success: true,
|
|
424
|
+
reference: response.data?.reference,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
961
427
|
};
|
|
962
428
|
|
|
963
429
|
export const removeTaskReference: Handler = async (args, ctx) => {
|
|
@@ -967,29 +433,17 @@ export const removeTaskReference: Handler = async (args, ctx) => {
|
|
|
967
433
|
validateUUID(task_id, 'task_id');
|
|
968
434
|
validateRequired(url, 'url');
|
|
969
435
|
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
.select('references')
|
|
973
|
-
.eq('id', task_id)
|
|
974
|
-
.single();
|
|
975
|
-
|
|
976
|
-
if (fetchError) throw new Error(`Failed to fetch task: ${fetchError.message}`);
|
|
436
|
+
const api = getApiClient();
|
|
437
|
+
const response = await api.removeTaskReference(task_id, url);
|
|
977
438
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
439
|
+
if (!response.ok) {
|
|
440
|
+
if (response.error?.includes('not found')) {
|
|
441
|
+
return { result: { success: false, error: 'Reference with this URL not found' } };
|
|
442
|
+
}
|
|
443
|
+
throw new Error(`Failed to remove reference: ${response.error}`);
|
|
983
444
|
}
|
|
984
445
|
|
|
985
|
-
|
|
986
|
-
.from('tasks')
|
|
987
|
-
.update({ references: updatedRefs })
|
|
988
|
-
.eq('id', task_id);
|
|
989
|
-
|
|
990
|
-
if (updateError) throw new Error(`Failed to remove reference: ${updateError.message}`);
|
|
991
|
-
|
|
992
|
-
return { result: { success: true, remaining_references: updatedRefs.length } };
|
|
446
|
+
return { result: { success: true } };
|
|
993
447
|
};
|
|
994
448
|
|
|
995
449
|
export const batchUpdateTasks: Handler = async (args, ctx) => {
|
|
@@ -1003,9 +457,6 @@ export const batchUpdateTasks: Handler = async (args, ctx) => {
|
|
|
1003
457
|
}>;
|
|
1004
458
|
};
|
|
1005
459
|
|
|
1006
|
-
const { supabase, session } = ctx;
|
|
1007
|
-
const currentSessionId = session.currentSessionId;
|
|
1008
|
-
|
|
1009
460
|
if (!updates || !Array.isArray(updates) || updates.length === 0) {
|
|
1010
461
|
throw new ValidationError('updates must be a non-empty array', {
|
|
1011
462
|
field: 'updates',
|
|
@@ -1020,129 +471,74 @@ export const batchUpdateTasks: Handler = async (args, ctx) => {
|
|
|
1020
471
|
});
|
|
1021
472
|
}
|
|
1022
473
|
|
|
1023
|
-
// Validate all inputs first
|
|
1024
|
-
const taskIds: string[] = [];
|
|
474
|
+
// Validate all inputs first
|
|
1025
475
|
for (const update of updates) {
|
|
1026
476
|
validateRequired(update.task_id, 'task_id');
|
|
1027
477
|
validateUUID(update.task_id, 'task_id');
|
|
1028
478
|
validateTaskStatus(update.status);
|
|
1029
479
|
validatePriority(update.priority);
|
|
1030
480
|
validateProgressPercentage(update.progress_percentage);
|
|
1031
|
-
taskIds.push(update.task_id);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// OPTIMIZATION: Fetch all tasks in a single query instead of N queries
|
|
1035
|
-
const { data: tasks } = await supabase
|
|
1036
|
-
.from('tasks')
|
|
1037
|
-
.select('id, project_id, started_at')
|
|
1038
|
-
.in('id', taskIds);
|
|
1039
|
-
|
|
1040
|
-
const taskMap = new Map(tasks?.map(t => [t.id, t]) || []);
|
|
1041
|
-
|
|
1042
|
-
// OPTIMIZATION: Single query to check if agent has existing in-progress task
|
|
1043
|
-
let existingAgentTask: { id: string; title: string } | null = null;
|
|
1044
|
-
const hasInProgressUpdate = updates.some(u => u.status === 'in_progress');
|
|
1045
|
-
if (hasInProgressUpdate && currentSessionId) {
|
|
1046
|
-
const { data } = await supabase
|
|
1047
|
-
.from('tasks')
|
|
1048
|
-
.select('id, title')
|
|
1049
|
-
.eq('working_agent_session_id', currentSessionId)
|
|
1050
|
-
.eq('status', 'in_progress')
|
|
1051
|
-
.not('id', 'in', `(${taskIds.join(',')})`)
|
|
1052
|
-
.limit(1)
|
|
1053
|
-
.single();
|
|
1054
|
-
existingAgentTask = data;
|
|
1055
481
|
}
|
|
1056
482
|
|
|
1057
|
-
const
|
|
1058
|
-
const
|
|
1059
|
-
project_id: string;
|
|
1060
|
-
task_id: string;
|
|
1061
|
-
summary: string;
|
|
1062
|
-
created_by: string;
|
|
1063
|
-
created_by_session_id: string | null;
|
|
1064
|
-
}> = [];
|
|
1065
|
-
|
|
1066
|
-
// OPTIMIZATION: Process updates in parallel instead of sequentially
|
|
1067
|
-
const updatePromises = updates.map(async (update) => {
|
|
1068
|
-
const task = taskMap.get(update.task_id);
|
|
1069
|
-
|
|
1070
|
-
if (!task) {
|
|
1071
|
-
return { task_id: update.task_id, success: false, error: 'Task not found' };
|
|
1072
|
-
}
|
|
483
|
+
const api = getApiClient();
|
|
484
|
+
const response = await api.batchUpdateTasks(updates);
|
|
1073
485
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
task_id: update.task_id,
|
|
1078
|
-
success: false,
|
|
1079
|
-
error: `Agent already has task in progress: "${existingAgentTask.title}"`,
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
const updateData: Record<string, unknown> = {};
|
|
1084
|
-
if (update.status) updateData.status = update.status;
|
|
1085
|
-
if (update.progress_percentage !== undefined) updateData.progress_percentage = update.progress_percentage;
|
|
1086
|
-
if (update.priority !== undefined) updateData.priority = update.priority;
|
|
1087
|
-
|
|
1088
|
-
// Auto-set started_at when task moves to in_progress
|
|
1089
|
-
if (update.status === 'in_progress' && !task.started_at) {
|
|
1090
|
-
updateData.started_at = new Date().toISOString();
|
|
1091
|
-
if (currentSessionId) {
|
|
1092
|
-
updateData.working_agent_session_id = currentSessionId;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
486
|
+
if (!response.ok) {
|
|
487
|
+
throw new Error(`Failed to batch update tasks: ${response.error}`);
|
|
488
|
+
}
|
|
1095
489
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
}
|
|
490
|
+
return {
|
|
491
|
+
result: {
|
|
492
|
+
success: response.data?.success || false,
|
|
493
|
+
total: updates.length,
|
|
494
|
+
succeeded: response.data?.updated_count || 0,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
};
|
|
1102
498
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
499
|
+
export const batchCompleteTasks: Handler = async (args, ctx) => {
|
|
500
|
+
const { completions } = args as {
|
|
501
|
+
completions: Array<{
|
|
502
|
+
task_id: string;
|
|
503
|
+
summary?: string;
|
|
504
|
+
}>;
|
|
505
|
+
};
|
|
1107
506
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
507
|
+
if (!completions || !Array.isArray(completions) || completions.length === 0) {
|
|
508
|
+
throw new ValidationError('completions must be a non-empty array', {
|
|
509
|
+
field: 'completions',
|
|
510
|
+
hint: 'Provide an array of task completions with at least one item',
|
|
511
|
+
});
|
|
512
|
+
}
|
|
1111
513
|
|
|
1112
|
-
|
|
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
|
-
}
|
|
514
|
+
if (completions.length > 50) {
|
|
515
|
+
throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
|
|
516
|
+
field: 'completions',
|
|
517
|
+
hint: 'Split your completions into smaller batches',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
1126
520
|
|
|
1127
|
-
|
|
1128
|
-
|
|
521
|
+
// Validate all inputs first
|
|
522
|
+
for (const completion of completions) {
|
|
523
|
+
validateRequired(completion.task_id, 'task_id');
|
|
524
|
+
validateUUID(completion.task_id, 'task_id');
|
|
525
|
+
}
|
|
1129
526
|
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
results.push(...updateResults);
|
|
527
|
+
const api = getApiClient();
|
|
528
|
+
const response = await api.batchCompleteTasks(completions);
|
|
1133
529
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
await supabase.from('progress_logs').insert(progressLogsToInsert);
|
|
530
|
+
if (!response.ok) {
|
|
531
|
+
throw new Error(`Failed to batch complete tasks: ${response.error}`);
|
|
1137
532
|
}
|
|
1138
533
|
|
|
1139
|
-
const
|
|
1140
|
-
|
|
534
|
+
const data = response.data;
|
|
1141
535
|
return {
|
|
1142
536
|
result: {
|
|
1143
|
-
success:
|
|
1144
|
-
total:
|
|
1145
|
-
succeeded:
|
|
537
|
+
success: data?.success || false,
|
|
538
|
+
total: completions.length,
|
|
539
|
+
succeeded: data?.completed_count || 0,
|
|
540
|
+
failed: completions.length - (data?.completed_count || 0),
|
|
541
|
+
next_task: data?.next_task,
|
|
1146
542
|
},
|
|
1147
543
|
};
|
|
1148
544
|
};
|
|
@@ -1160,77 +556,38 @@ export const addSubtask: Handler = async (args, ctx) => {
|
|
|
1160
556
|
estimated_minutes?: number;
|
|
1161
557
|
};
|
|
1162
558
|
|
|
1163
|
-
const { supabase, session } = ctx;
|
|
1164
|
-
const currentSessionId = session.currentSessionId;
|
|
1165
|
-
|
|
1166
559
|
validateRequired(parent_task_id, 'parent_task_id');
|
|
1167
560
|
validateUUID(parent_task_id, 'parent_task_id');
|
|
1168
561
|
validateRequired(title, 'title');
|
|
1169
562
|
if (priority !== undefined) validatePriority(priority);
|
|
1170
563
|
if (estimated_minutes !== undefined) validateEstimatedMinutes(estimated_minutes);
|
|
1171
564
|
|
|
1172
|
-
|
|
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
|
-
}
|
|
565
|
+
const api = getApiClient();
|
|
566
|
+
const response = await api.addSubtask(parent_task_id, {
|
|
567
|
+
title,
|
|
568
|
+
description,
|
|
569
|
+
priority,
|
|
570
|
+
estimated_minutes,
|
|
571
|
+
}, ctx.session.currentSessionId || undefined);
|
|
1185
572
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
573
|
+
if (!response.ok) {
|
|
574
|
+
if (response.error?.includes('Cannot create subtask of a subtask')) {
|
|
575
|
+
return {
|
|
576
|
+
result: {
|
|
577
|
+
success: false,
|
|
578
|
+
error: 'Cannot create subtask of a subtask',
|
|
579
|
+
hint: 'Subtasks cannot have their own subtasks. Add this task to the parent task instead.',
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
throw new Error(`Failed to add subtask: ${response.error}`);
|
|
1196
584
|
}
|
|
1197
585
|
|
|
1198
|
-
// Use parent priority if not specified
|
|
1199
|
-
const subtaskPriority = priority ?? parentTask.priority;
|
|
1200
|
-
|
|
1201
|
-
const { data: subtask, error } = await supabase
|
|
1202
|
-
.from('tasks')
|
|
1203
|
-
.insert({
|
|
1204
|
-
project_id: parentTask.project_id,
|
|
1205
|
-
parent_task_id,
|
|
1206
|
-
title,
|
|
1207
|
-
description: description || null,
|
|
1208
|
-
priority: subtaskPriority,
|
|
1209
|
-
estimated_minutes: estimated_minutes || null,
|
|
1210
|
-
created_by: 'agent',
|
|
1211
|
-
created_by_session_id: currentSessionId,
|
|
1212
|
-
})
|
|
1213
|
-
.select('id, title, priority')
|
|
1214
|
-
.single();
|
|
1215
|
-
|
|
1216
|
-
if (error) throw new Error(`Failed to add subtask: ${error.message}`);
|
|
1217
|
-
|
|
1218
|
-
// Log progress
|
|
1219
|
-
await supabase.from('progress_logs').insert({
|
|
1220
|
-
project_id: parentTask.project_id,
|
|
1221
|
-
task_id: parent_task_id,
|
|
1222
|
-
summary: `Added subtask: ${title}`,
|
|
1223
|
-
created_by: 'agent',
|
|
1224
|
-
created_by_session_id: currentSessionId,
|
|
1225
|
-
});
|
|
1226
|
-
|
|
1227
586
|
return {
|
|
1228
587
|
result: {
|
|
1229
588
|
success: true,
|
|
1230
|
-
subtask_id:
|
|
1231
|
-
parent_task_id,
|
|
1232
|
-
title: subtask.title,
|
|
1233
|
-
priority: subtask.priority,
|
|
589
|
+
subtask_id: response.data?.subtask_id,
|
|
590
|
+
parent_task_id: response.data?.parent_task_id,
|
|
1234
591
|
},
|
|
1235
592
|
};
|
|
1236
593
|
};
|
|
@@ -1241,138 +598,29 @@ export const getSubtasks: Handler = async (args, ctx) => {
|
|
|
1241
598
|
status?: string;
|
|
1242
599
|
};
|
|
1243
600
|
|
|
1244
|
-
const { supabase } = ctx;
|
|
1245
|
-
|
|
1246
601
|
validateRequired(parent_task_id, 'parent_task_id');
|
|
1247
602
|
validateUUID(parent_task_id, 'parent_task_id');
|
|
1248
603
|
if (status) validateTaskStatus(status);
|
|
1249
604
|
|
|
1250
|
-
|
|
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 });
|
|
605
|
+
const api = getApiClient();
|
|
606
|
+
const response = await api.getSubtasks(parent_task_id, status);
|
|
1256
607
|
|
|
1257
|
-
if (
|
|
1258
|
-
|
|
608
|
+
if (!response.ok) {
|
|
609
|
+
throw new Error(`Failed to fetch subtasks: ${response.error}`);
|
|
1259
610
|
}
|
|
1260
611
|
|
|
1261
|
-
const { data: subtasks, error } = await query;
|
|
1262
|
-
|
|
1263
|
-
if (error) throw new Error(`Failed to fetch subtasks: ${error.message}`);
|
|
1264
|
-
|
|
1265
|
-
// Calculate aggregate stats
|
|
1266
|
-
const total = subtasks?.length || 0;
|
|
1267
|
-
const completed = subtasks?.filter(s => s.status === 'completed').length || 0;
|
|
1268
|
-
const inProgress = subtasks?.filter(s => s.status === 'in_progress').length || 0;
|
|
1269
|
-
const pending = subtasks?.filter(s => s.status === 'pending').length || 0;
|
|
1270
|
-
|
|
1271
612
|
return {
|
|
1272
613
|
result: {
|
|
1273
|
-
subtasks: subtasks || [],
|
|
1274
|
-
stats: {
|
|
1275
|
-
total,
|
|
1276
|
-
completed,
|
|
1277
|
-
|
|
1278
|
-
pending,
|
|
1279
|
-
progress_percentage: total > 0 ? Math.round((completed / total) * 100) : 0,
|
|
614
|
+
subtasks: response.data?.subtasks || [],
|
|
615
|
+
stats: response.data?.stats || {
|
|
616
|
+
total: 0,
|
|
617
|
+
completed: 0,
|
|
618
|
+
progress_percentage: 0,
|
|
1280
619
|
},
|
|
1281
620
|
},
|
|
1282
621
|
};
|
|
1283
622
|
};
|
|
1284
623
|
|
|
1285
|
-
export const batchCompleteTasks: Handler = async (args, ctx) => {
|
|
1286
|
-
const { completions } = args as {
|
|
1287
|
-
completions: Array<{
|
|
1288
|
-
task_id: string;
|
|
1289
|
-
summary?: string;
|
|
1290
|
-
}>;
|
|
1291
|
-
};
|
|
1292
|
-
|
|
1293
|
-
const { supabase, session } = ctx;
|
|
1294
|
-
const currentSessionId = session.currentSessionId;
|
|
1295
|
-
|
|
1296
|
-
if (!completions || !Array.isArray(completions) || completions.length === 0) {
|
|
1297
|
-
throw new ValidationError('completions must be a non-empty array', {
|
|
1298
|
-
field: 'completions',
|
|
1299
|
-
hint: 'Provide an array of task completions with at least one item',
|
|
1300
|
-
});
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
if (completions.length > 50) {
|
|
1304
|
-
throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
|
|
1305
|
-
field: 'completions',
|
|
1306
|
-
hint: 'Split your completions into smaller batches',
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const results: Array<{ task_id: string; success: boolean; error?: string }> = [];
|
|
1311
|
-
|
|
1312
|
-
for (const completion of completions) {
|
|
1313
|
-
try {
|
|
1314
|
-
validateRequired(completion.task_id, 'task_id');
|
|
1315
|
-
validateUUID(completion.task_id, 'task_id');
|
|
1316
|
-
|
|
1317
|
-
const { data: task, error: fetchError } = await supabase
|
|
1318
|
-
.from('tasks')
|
|
1319
|
-
.select('project_id, title')
|
|
1320
|
-
.eq('id', completion.task_id)
|
|
1321
|
-
.single();
|
|
1322
|
-
|
|
1323
|
-
if (fetchError || !task) {
|
|
1324
|
-
results.push({ task_id: completion.task_id, success: false, error: 'Task not found' });
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const { error } = await supabase
|
|
1329
|
-
.from('tasks')
|
|
1330
|
-
.update({
|
|
1331
|
-
status: 'completed',
|
|
1332
|
-
completed_at: new Date().toISOString(),
|
|
1333
|
-
progress_percentage: 100,
|
|
1334
|
-
working_agent_session_id: null,
|
|
1335
|
-
})
|
|
1336
|
-
.eq('id', completion.task_id);
|
|
1337
|
-
|
|
1338
|
-
if (error) {
|
|
1339
|
-
results.push({ task_id: completion.task_id, success: false, error: error.message });
|
|
1340
|
-
continue;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
if (completion.summary) {
|
|
1344
|
-
await supabase.from('progress_logs').insert({
|
|
1345
|
-
project_id: task.project_id,
|
|
1346
|
-
task_id: completion.task_id,
|
|
1347
|
-
summary: `Completed: ${task.title}`,
|
|
1348
|
-
details: completion.summary,
|
|
1349
|
-
created_by: 'agent',
|
|
1350
|
-
created_by_session_id: currentSessionId,
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
results.push({ task_id: completion.task_id, success: true });
|
|
1355
|
-
} catch (err) {
|
|
1356
|
-
results.push({
|
|
1357
|
-
task_id: completion.task_id,
|
|
1358
|
-
success: false,
|
|
1359
|
-
error: err instanceof Error ? err.message : 'Unknown error',
|
|
1360
|
-
});
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
const successCount = results.filter(r => r.success).length;
|
|
1365
|
-
|
|
1366
|
-
return {
|
|
1367
|
-
result: {
|
|
1368
|
-
success: successCount === completions.length,
|
|
1369
|
-
total: completions.length,
|
|
1370
|
-
succeeded: successCount,
|
|
1371
|
-
failed: completions.length - successCount,
|
|
1372
|
-
},
|
|
1373
|
-
};
|
|
1374
|
-
};
|
|
1375
|
-
|
|
1376
624
|
/**
|
|
1377
625
|
* Task handlers registry
|
|
1378
626
|
*/
|