@vibescope/mcp-server 0.0.1
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 +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bodies of Work Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles grouping tasks into bodies of work with phases:
|
|
5
|
+
* - create_body_of_work
|
|
6
|
+
* - update_body_of_work
|
|
7
|
+
* - get_body_of_work
|
|
8
|
+
* - get_bodies_of_work
|
|
9
|
+
* - delete_body_of_work
|
|
10
|
+
* - add_task_to_body_of_work
|
|
11
|
+
* - remove_task_from_body_of_work
|
|
12
|
+
* - activate_body_of_work
|
|
13
|
+
* - add_task_dependency
|
|
14
|
+
* - remove_task_dependency
|
|
15
|
+
* - get_task_dependencies
|
|
16
|
+
* - get_next_body_of_work_task
|
|
17
|
+
*/
|
|
18
|
+
import { validateRequired, validateUUID } from '../validators.js';
|
|
19
|
+
export const createBodyOfWork = async (args, ctx) => {
|
|
20
|
+
const { project_id, title, description, auto_deploy_on_completion, deploy_environment, deploy_version_bump, deploy_trigger, } = args;
|
|
21
|
+
validateRequired(project_id, 'project_id');
|
|
22
|
+
validateUUID(project_id, 'project_id');
|
|
23
|
+
validateRequired(title, 'title');
|
|
24
|
+
const { supabase, session } = ctx;
|
|
25
|
+
const { data, error } = await supabase
|
|
26
|
+
.from('bodies_of_work')
|
|
27
|
+
.insert({
|
|
28
|
+
project_id,
|
|
29
|
+
title,
|
|
30
|
+
description: description || null,
|
|
31
|
+
auto_deploy_on_completion: auto_deploy_on_completion || false,
|
|
32
|
+
deploy_environment: deploy_environment || 'production',
|
|
33
|
+
deploy_version_bump: deploy_version_bump || 'minor',
|
|
34
|
+
deploy_trigger: deploy_trigger || 'all_completed_validated',
|
|
35
|
+
created_by: 'agent',
|
|
36
|
+
created_by_session_id: session.currentSessionId,
|
|
37
|
+
})
|
|
38
|
+
.select('id')
|
|
39
|
+
.single();
|
|
40
|
+
if (error)
|
|
41
|
+
throw new Error(`Failed to create body of work: ${error.message}`);
|
|
42
|
+
return {
|
|
43
|
+
result: {
|
|
44
|
+
success: true,
|
|
45
|
+
body_of_work_id: data.id,
|
|
46
|
+
title,
|
|
47
|
+
status: 'draft',
|
|
48
|
+
message: 'Body of work created. Add tasks with add_task_to_body_of_work, then activate with activate_body_of_work.',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export const updateBodyOfWork = async (args, ctx) => {
|
|
53
|
+
const { body_of_work_id, title, description, auto_deploy_on_completion, deploy_environment, deploy_version_bump, deploy_trigger, } = args;
|
|
54
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
55
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
56
|
+
const { supabase } = ctx;
|
|
57
|
+
// Build update object
|
|
58
|
+
const updates = {};
|
|
59
|
+
if (title !== undefined)
|
|
60
|
+
updates.title = title;
|
|
61
|
+
if (description !== undefined)
|
|
62
|
+
updates.description = description;
|
|
63
|
+
if (auto_deploy_on_completion !== undefined)
|
|
64
|
+
updates.auto_deploy_on_completion = auto_deploy_on_completion;
|
|
65
|
+
if (deploy_environment !== undefined)
|
|
66
|
+
updates.deploy_environment = deploy_environment;
|
|
67
|
+
if (deploy_version_bump !== undefined)
|
|
68
|
+
updates.deploy_version_bump = deploy_version_bump;
|
|
69
|
+
if (deploy_trigger !== undefined)
|
|
70
|
+
updates.deploy_trigger = deploy_trigger;
|
|
71
|
+
if (Object.keys(updates).length === 0) {
|
|
72
|
+
return { result: { success: true, message: 'No updates provided' } };
|
|
73
|
+
}
|
|
74
|
+
const { error } = await supabase
|
|
75
|
+
.from('bodies_of_work')
|
|
76
|
+
.update(updates)
|
|
77
|
+
.eq('id', body_of_work_id);
|
|
78
|
+
if (error)
|
|
79
|
+
throw new Error(`Failed to update body of work: ${error.message}`);
|
|
80
|
+
return { result: { success: true, body_of_work_id } };
|
|
81
|
+
};
|
|
82
|
+
export const getBodyOfWork = async (args, ctx) => {
|
|
83
|
+
const { body_of_work_id } = args;
|
|
84
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
85
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
86
|
+
const { supabase } = ctx;
|
|
87
|
+
// Get body of work
|
|
88
|
+
const { data: bow, error: bowError } = await supabase
|
|
89
|
+
.from('bodies_of_work')
|
|
90
|
+
.select('*')
|
|
91
|
+
.eq('id', body_of_work_id)
|
|
92
|
+
.single();
|
|
93
|
+
if (bowError || !bow) {
|
|
94
|
+
throw new Error(`Body of work not found: ${body_of_work_id}`);
|
|
95
|
+
}
|
|
96
|
+
// Get tasks with their phases
|
|
97
|
+
const { data: taskLinks, error: taskError } = await supabase
|
|
98
|
+
.from('body_of_work_tasks')
|
|
99
|
+
.select(`
|
|
100
|
+
phase,
|
|
101
|
+
order_index,
|
|
102
|
+
tasks (
|
|
103
|
+
id,
|
|
104
|
+
title,
|
|
105
|
+
status,
|
|
106
|
+
priority,
|
|
107
|
+
progress_percentage
|
|
108
|
+
)
|
|
109
|
+
`)
|
|
110
|
+
.eq('body_of_work_id', body_of_work_id)
|
|
111
|
+
.order('order_index');
|
|
112
|
+
if (taskError)
|
|
113
|
+
throw new Error(`Failed to fetch tasks: ${taskError.message}`);
|
|
114
|
+
// Organize tasks by phase
|
|
115
|
+
const preTasks = [];
|
|
116
|
+
const coreTasks = [];
|
|
117
|
+
const postTasks = [];
|
|
118
|
+
for (const link of taskLinks || []) {
|
|
119
|
+
const task = link.tasks;
|
|
120
|
+
if (!task)
|
|
121
|
+
continue;
|
|
122
|
+
const taskWithPhase = { ...task, order_index: link.order_index };
|
|
123
|
+
switch (link.phase) {
|
|
124
|
+
case 'pre':
|
|
125
|
+
preTasks.push(taskWithPhase);
|
|
126
|
+
break;
|
|
127
|
+
case 'core':
|
|
128
|
+
coreTasks.push(taskWithPhase);
|
|
129
|
+
break;
|
|
130
|
+
case 'post':
|
|
131
|
+
postTasks.push(taskWithPhase);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
result: {
|
|
137
|
+
...bow,
|
|
138
|
+
pre_tasks: preTasks,
|
|
139
|
+
core_tasks: coreTasks,
|
|
140
|
+
post_tasks: postTasks,
|
|
141
|
+
total_tasks: preTasks.length + coreTasks.length + postTasks.length,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
export const getBodiesOfWork = async (args, ctx) => {
|
|
146
|
+
const { project_id, status } = args;
|
|
147
|
+
validateRequired(project_id, 'project_id');
|
|
148
|
+
validateUUID(project_id, 'project_id');
|
|
149
|
+
const { supabase } = ctx;
|
|
150
|
+
let query = supabase
|
|
151
|
+
.from('bodies_of_work')
|
|
152
|
+
.select(`
|
|
153
|
+
id,
|
|
154
|
+
title,
|
|
155
|
+
description,
|
|
156
|
+
status,
|
|
157
|
+
progress_percentage,
|
|
158
|
+
auto_deploy_on_completion,
|
|
159
|
+
deploy_environment,
|
|
160
|
+
deploy_version_bump,
|
|
161
|
+
created_at,
|
|
162
|
+
activated_at,
|
|
163
|
+
completed_at
|
|
164
|
+
`)
|
|
165
|
+
.eq('project_id', project_id);
|
|
166
|
+
if (status) {
|
|
167
|
+
query = query.eq('status', status);
|
|
168
|
+
}
|
|
169
|
+
const { data, error } = await query.order('created_at', { ascending: false });
|
|
170
|
+
if (error)
|
|
171
|
+
throw new Error(`Failed to fetch bodies of work: ${error.message}`);
|
|
172
|
+
const bodies = data || [];
|
|
173
|
+
if (bodies.length === 0) {
|
|
174
|
+
return { result: { bodies_of_work: [] } };
|
|
175
|
+
}
|
|
176
|
+
// Batch query: get all task links for all bodies of work in a single query
|
|
177
|
+
const bodyIds = bodies.map((b) => b.id);
|
|
178
|
+
const { data: allTaskLinks } = await supabase
|
|
179
|
+
.from('body_of_work_tasks')
|
|
180
|
+
.select('body_of_work_id, phase, tasks!inner(status)')
|
|
181
|
+
.in('body_of_work_id', bodyIds);
|
|
182
|
+
// Group task links by body_of_work_id
|
|
183
|
+
const taskLinksByBody = new Map();
|
|
184
|
+
for (const link of allTaskLinks || []) {
|
|
185
|
+
const existing = taskLinksByBody.get(link.body_of_work_id) || [];
|
|
186
|
+
existing.push(link);
|
|
187
|
+
taskLinksByBody.set(link.body_of_work_id, existing);
|
|
188
|
+
}
|
|
189
|
+
// Build response with task counts
|
|
190
|
+
const bodiesWithCounts = bodies.map((bow) => {
|
|
191
|
+
const taskLinks = taskLinksByBody.get(bow.id) || [];
|
|
192
|
+
const taskCounts = {
|
|
193
|
+
pre: { total: 0, completed: 0 },
|
|
194
|
+
core: { total: 0, completed: 0 },
|
|
195
|
+
post: { total: 0, completed: 0 },
|
|
196
|
+
};
|
|
197
|
+
for (const link of taskLinks) {
|
|
198
|
+
const phase = link.phase;
|
|
199
|
+
taskCounts[phase].total++;
|
|
200
|
+
const taskData = link.tasks;
|
|
201
|
+
if (taskData?.status === 'completed') {
|
|
202
|
+
taskCounts[phase].completed++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
...bow,
|
|
207
|
+
task_counts: taskCounts,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
return { result: { bodies_of_work: bodiesWithCounts } };
|
|
211
|
+
};
|
|
212
|
+
export const deleteBodyOfWork = async (args, ctx) => {
|
|
213
|
+
const { body_of_work_id } = args;
|
|
214
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
215
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
216
|
+
const { error } = await ctx.supabase
|
|
217
|
+
.from('bodies_of_work')
|
|
218
|
+
.delete()
|
|
219
|
+
.eq('id', body_of_work_id);
|
|
220
|
+
if (error)
|
|
221
|
+
throw new Error(`Failed to delete body of work: ${error.message}`);
|
|
222
|
+
return { result: { success: true, message: 'Body of work deleted. Tasks are preserved.' } };
|
|
223
|
+
};
|
|
224
|
+
export const addTaskToBodyOfWork = async (args, ctx) => {
|
|
225
|
+
const { body_of_work_id, task_id, phase, order_index } = args;
|
|
226
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
227
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
228
|
+
validateRequired(task_id, 'task_id');
|
|
229
|
+
validateUUID(task_id, 'task_id');
|
|
230
|
+
const { supabase } = ctx;
|
|
231
|
+
const taskPhase = phase || 'core';
|
|
232
|
+
// Check if body of work exists and is in draft/active status
|
|
233
|
+
const { data: bow, error: bowError } = await supabase
|
|
234
|
+
.from('bodies_of_work')
|
|
235
|
+
.select('status')
|
|
236
|
+
.eq('id', body_of_work_id)
|
|
237
|
+
.single();
|
|
238
|
+
if (bowError || !bow) {
|
|
239
|
+
throw new Error(`Body of work not found: ${body_of_work_id}`);
|
|
240
|
+
}
|
|
241
|
+
if (bow.status === 'completed' || bow.status === 'cancelled') {
|
|
242
|
+
throw new Error(`Cannot add tasks to ${bow.status} body of work`);
|
|
243
|
+
}
|
|
244
|
+
// Check if task is already in a body of work
|
|
245
|
+
const { data: existingLink } = await supabase
|
|
246
|
+
.from('body_of_work_tasks')
|
|
247
|
+
.select('body_of_work_id')
|
|
248
|
+
.eq('task_id', task_id)
|
|
249
|
+
.single();
|
|
250
|
+
if (existingLink) {
|
|
251
|
+
throw new Error('Task is already assigned to a body of work. Remove it first.');
|
|
252
|
+
}
|
|
253
|
+
// Get the next order index if not provided
|
|
254
|
+
let finalOrderIndex = order_index;
|
|
255
|
+
if (finalOrderIndex === undefined) {
|
|
256
|
+
const { data: maxOrder } = await supabase
|
|
257
|
+
.from('body_of_work_tasks')
|
|
258
|
+
.select('order_index')
|
|
259
|
+
.eq('body_of_work_id', body_of_work_id)
|
|
260
|
+
.eq('phase', taskPhase)
|
|
261
|
+
.order('order_index', { ascending: false })
|
|
262
|
+
.limit(1)
|
|
263
|
+
.single();
|
|
264
|
+
finalOrderIndex = maxOrder ? maxOrder.order_index + 1 : 0;
|
|
265
|
+
}
|
|
266
|
+
const { error } = await supabase
|
|
267
|
+
.from('body_of_work_tasks')
|
|
268
|
+
.insert({
|
|
269
|
+
body_of_work_id,
|
|
270
|
+
task_id,
|
|
271
|
+
phase: taskPhase,
|
|
272
|
+
order_index: finalOrderIndex,
|
|
273
|
+
});
|
|
274
|
+
if (error)
|
|
275
|
+
throw new Error(`Failed to add task to body of work: ${error.message}`);
|
|
276
|
+
return {
|
|
277
|
+
result: {
|
|
278
|
+
success: true,
|
|
279
|
+
body_of_work_id,
|
|
280
|
+
task_id,
|
|
281
|
+
phase: taskPhase,
|
|
282
|
+
order_index: finalOrderIndex,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
export const removeTaskFromBodyOfWork = async (args, ctx) => {
|
|
287
|
+
const { task_id } = args;
|
|
288
|
+
validateRequired(task_id, 'task_id');
|
|
289
|
+
validateUUID(task_id, 'task_id');
|
|
290
|
+
const { supabase } = ctx;
|
|
291
|
+
// Check if link exists
|
|
292
|
+
const { data: link } = await supabase
|
|
293
|
+
.from('body_of_work_tasks')
|
|
294
|
+
.select('body_of_work_id')
|
|
295
|
+
.eq('task_id', task_id)
|
|
296
|
+
.single();
|
|
297
|
+
if (!link) {
|
|
298
|
+
return { result: { success: true, message: 'Task is not in any body of work' } };
|
|
299
|
+
}
|
|
300
|
+
// Check if body of work is completed
|
|
301
|
+
const { data: bow } = await supabase
|
|
302
|
+
.from('bodies_of_work')
|
|
303
|
+
.select('status')
|
|
304
|
+
.eq('id', link.body_of_work_id)
|
|
305
|
+
.single();
|
|
306
|
+
if (bow?.status === 'completed') {
|
|
307
|
+
throw new Error('Cannot remove tasks from a completed body of work');
|
|
308
|
+
}
|
|
309
|
+
const { error } = await supabase
|
|
310
|
+
.from('body_of_work_tasks')
|
|
311
|
+
.delete()
|
|
312
|
+
.eq('task_id', task_id);
|
|
313
|
+
if (error)
|
|
314
|
+
throw new Error(`Failed to remove task from body of work: ${error.message}`);
|
|
315
|
+
return { result: { success: true, body_of_work_id: link.body_of_work_id } };
|
|
316
|
+
};
|
|
317
|
+
export const activateBodyOfWork = async (args, ctx) => {
|
|
318
|
+
const { body_of_work_id } = args;
|
|
319
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
320
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
321
|
+
const { supabase } = ctx;
|
|
322
|
+
// Check current status
|
|
323
|
+
const { data: bow, error: bowError } = await supabase
|
|
324
|
+
.from('bodies_of_work')
|
|
325
|
+
.select('status, title')
|
|
326
|
+
.eq('id', body_of_work_id)
|
|
327
|
+
.single();
|
|
328
|
+
if (bowError || !bow) {
|
|
329
|
+
throw new Error(`Body of work not found: ${body_of_work_id}`);
|
|
330
|
+
}
|
|
331
|
+
if (bow.status !== 'draft') {
|
|
332
|
+
throw new Error(`Can only activate draft bodies of work. Current status: ${bow.status}`);
|
|
333
|
+
}
|
|
334
|
+
// Check if there are any tasks
|
|
335
|
+
const { count } = await supabase
|
|
336
|
+
.from('body_of_work_tasks')
|
|
337
|
+
.select('id', { count: 'exact', head: true })
|
|
338
|
+
.eq('body_of_work_id', body_of_work_id);
|
|
339
|
+
if (!count || count === 0) {
|
|
340
|
+
throw new Error('Cannot activate body of work with no tasks. Add tasks first.');
|
|
341
|
+
}
|
|
342
|
+
const { error } = await supabase
|
|
343
|
+
.from('bodies_of_work')
|
|
344
|
+
.update({
|
|
345
|
+
status: 'active',
|
|
346
|
+
activated_at: new Date().toISOString(),
|
|
347
|
+
})
|
|
348
|
+
.eq('id', body_of_work_id);
|
|
349
|
+
if (error)
|
|
350
|
+
throw new Error(`Failed to activate body of work: ${error.message}`);
|
|
351
|
+
return {
|
|
352
|
+
result: {
|
|
353
|
+
success: true,
|
|
354
|
+
body_of_work_id,
|
|
355
|
+
title: bow.title,
|
|
356
|
+
status: 'active',
|
|
357
|
+
message: 'Body of work activated. Tasks can now be worked on following phase order.',
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
export const addTaskDependency = async (args, ctx) => {
|
|
362
|
+
const { body_of_work_id, task_id, depends_on_task_id } = args;
|
|
363
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
364
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
365
|
+
validateRequired(task_id, 'task_id');
|
|
366
|
+
validateUUID(task_id, 'task_id');
|
|
367
|
+
validateRequired(depends_on_task_id, 'depends_on_task_id');
|
|
368
|
+
validateUUID(depends_on_task_id, 'depends_on_task_id');
|
|
369
|
+
if (task_id === depends_on_task_id) {
|
|
370
|
+
throw new Error('A task cannot depend on itself');
|
|
371
|
+
}
|
|
372
|
+
const { supabase } = ctx;
|
|
373
|
+
// Verify both tasks belong to the same body of work
|
|
374
|
+
const { data: taskLinks, error: taskError } = await supabase
|
|
375
|
+
.from('body_of_work_tasks')
|
|
376
|
+
.select('task_id')
|
|
377
|
+
.eq('body_of_work_id', body_of_work_id)
|
|
378
|
+
.in('task_id', [task_id, depends_on_task_id]);
|
|
379
|
+
if (taskError)
|
|
380
|
+
throw new Error(`Failed to verify tasks: ${taskError.message}`);
|
|
381
|
+
if (!taskLinks || taskLinks.length !== 2) {
|
|
382
|
+
throw new Error('Both tasks must belong to the specified body of work');
|
|
383
|
+
}
|
|
384
|
+
// Check for circular dependencies by traversing the dependency graph
|
|
385
|
+
const visited = new Set();
|
|
386
|
+
const checkCircular = async (currentTaskId) => {
|
|
387
|
+
if (currentTaskId === task_id)
|
|
388
|
+
return true; // Found cycle
|
|
389
|
+
if (visited.has(currentTaskId))
|
|
390
|
+
return false;
|
|
391
|
+
visited.add(currentTaskId);
|
|
392
|
+
const { data: deps } = await supabase
|
|
393
|
+
.from('body_of_work_task_dependencies')
|
|
394
|
+
.select('depends_on_task_id')
|
|
395
|
+
.eq('task_id', currentTaskId)
|
|
396
|
+
.eq('body_of_work_id', body_of_work_id);
|
|
397
|
+
for (const dep of deps || []) {
|
|
398
|
+
if (await checkCircular(dep.depends_on_task_id)) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
};
|
|
404
|
+
if (await checkCircular(depends_on_task_id)) {
|
|
405
|
+
throw new Error('Cannot add dependency: would create a circular dependency');
|
|
406
|
+
}
|
|
407
|
+
const { error } = await supabase
|
|
408
|
+
.from('body_of_work_task_dependencies')
|
|
409
|
+
.insert({
|
|
410
|
+
body_of_work_id,
|
|
411
|
+
task_id,
|
|
412
|
+
depends_on_task_id,
|
|
413
|
+
});
|
|
414
|
+
if (error) {
|
|
415
|
+
if (error.message.includes('duplicate')) {
|
|
416
|
+
throw new Error('This dependency already exists');
|
|
417
|
+
}
|
|
418
|
+
throw new Error(`Failed to add dependency: ${error.message}`);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
result: {
|
|
422
|
+
success: true,
|
|
423
|
+
body_of_work_id,
|
|
424
|
+
task_id,
|
|
425
|
+
depends_on_task_id,
|
|
426
|
+
message: `Task now depends on completion of the specified task`,
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
export const removeTaskDependency = async (args, ctx) => {
|
|
431
|
+
const { task_id, depends_on_task_id } = args;
|
|
432
|
+
validateRequired(task_id, 'task_id');
|
|
433
|
+
validateUUID(task_id, 'task_id');
|
|
434
|
+
validateRequired(depends_on_task_id, 'depends_on_task_id');
|
|
435
|
+
validateUUID(depends_on_task_id, 'depends_on_task_id');
|
|
436
|
+
const { supabase } = ctx;
|
|
437
|
+
const { error } = await supabase
|
|
438
|
+
.from('body_of_work_task_dependencies')
|
|
439
|
+
.delete()
|
|
440
|
+
.eq('task_id', task_id)
|
|
441
|
+
.eq('depends_on_task_id', depends_on_task_id);
|
|
442
|
+
if (error)
|
|
443
|
+
throw new Error(`Failed to remove dependency: ${error.message}`);
|
|
444
|
+
return { result: { success: true, task_id, depends_on_task_id } };
|
|
445
|
+
};
|
|
446
|
+
export const getTaskDependencies = async (args, ctx) => {
|
|
447
|
+
const { body_of_work_id, task_id } = args;
|
|
448
|
+
if (!body_of_work_id && !task_id) {
|
|
449
|
+
throw new Error('Either body_of_work_id or task_id is required');
|
|
450
|
+
}
|
|
451
|
+
if (body_of_work_id)
|
|
452
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
453
|
+
if (task_id)
|
|
454
|
+
validateUUID(task_id, 'task_id');
|
|
455
|
+
const { supabase } = ctx;
|
|
456
|
+
let query = supabase
|
|
457
|
+
.from('body_of_work_task_dependencies')
|
|
458
|
+
.select(`
|
|
459
|
+
id,
|
|
460
|
+
task_id,
|
|
461
|
+
depends_on_task_id,
|
|
462
|
+
created_at
|
|
463
|
+
`);
|
|
464
|
+
if (body_of_work_id) {
|
|
465
|
+
query = query.eq('body_of_work_id', body_of_work_id);
|
|
466
|
+
}
|
|
467
|
+
if (task_id) {
|
|
468
|
+
query = query.eq('task_id', task_id);
|
|
469
|
+
}
|
|
470
|
+
const { data, error } = await query;
|
|
471
|
+
if (error)
|
|
472
|
+
throw new Error(`Failed to fetch dependencies: ${error.message}`);
|
|
473
|
+
return { result: { dependencies: data || [] } };
|
|
474
|
+
};
|
|
475
|
+
export const getNextBodyOfWorkTask = async (args, ctx) => {
|
|
476
|
+
const { body_of_work_id } = args;
|
|
477
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
478
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
479
|
+
const { supabase } = ctx;
|
|
480
|
+
// Get body of work status
|
|
481
|
+
const { data: bow, error: bowError } = await supabase
|
|
482
|
+
.from('bodies_of_work')
|
|
483
|
+
.select('status, title')
|
|
484
|
+
.eq('id', body_of_work_id)
|
|
485
|
+
.single();
|
|
486
|
+
if (bowError || !bow) {
|
|
487
|
+
throw new Error(`Body of work not found: ${body_of_work_id}`);
|
|
488
|
+
}
|
|
489
|
+
if (bow.status !== 'active') {
|
|
490
|
+
return {
|
|
491
|
+
result: {
|
|
492
|
+
next_task: null,
|
|
493
|
+
message: `Body of work is ${bow.status}, not active`,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
// Get all tasks with their status and phase
|
|
498
|
+
const { data: taskLinks, error: taskError } = await supabase
|
|
499
|
+
.from('body_of_work_tasks')
|
|
500
|
+
.select(`
|
|
501
|
+
task_id,
|
|
502
|
+
phase,
|
|
503
|
+
order_index,
|
|
504
|
+
tasks (
|
|
505
|
+
id,
|
|
506
|
+
title,
|
|
507
|
+
status,
|
|
508
|
+
priority,
|
|
509
|
+
claimed_by_session_id
|
|
510
|
+
)
|
|
511
|
+
`)
|
|
512
|
+
.eq('body_of_work_id', body_of_work_id)
|
|
513
|
+
.order('order_index');
|
|
514
|
+
if (taskError)
|
|
515
|
+
throw new Error(`Failed to fetch tasks: ${taskError.message}`);
|
|
516
|
+
// Get all dependencies
|
|
517
|
+
const { data: dependencies } = await supabase
|
|
518
|
+
.from('body_of_work_task_dependencies')
|
|
519
|
+
.select('task_id, depends_on_task_id')
|
|
520
|
+
.eq('body_of_work_id', body_of_work_id);
|
|
521
|
+
const depMap = new Map();
|
|
522
|
+
for (const dep of dependencies || []) {
|
|
523
|
+
const existing = depMap.get(dep.task_id) || [];
|
|
524
|
+
existing.push(dep.depends_on_task_id);
|
|
525
|
+
depMap.set(dep.task_id, existing);
|
|
526
|
+
}
|
|
527
|
+
// Build task status map
|
|
528
|
+
const taskStatusMap = new Map();
|
|
529
|
+
for (const link of taskLinks || []) {
|
|
530
|
+
const task = link.tasks;
|
|
531
|
+
if (task) {
|
|
532
|
+
taskStatusMap.set(task.id, task.status);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Check if all dependencies are completed
|
|
536
|
+
const areDependenciesComplete = (taskId) => {
|
|
537
|
+
const deps = depMap.get(taskId) || [];
|
|
538
|
+
return deps.every((depId) => taskStatusMap.get(depId) === 'completed');
|
|
539
|
+
};
|
|
540
|
+
// Phase order: pre -> core -> post
|
|
541
|
+
const phaseOrder = ['pre', 'core', 'post'];
|
|
542
|
+
// Check if all tasks in a phase are completed
|
|
543
|
+
const isPhaseComplete = (phase) => {
|
|
544
|
+
return (taskLinks || [])
|
|
545
|
+
.filter((link) => link.phase === phase)
|
|
546
|
+
.every((link) => {
|
|
547
|
+
const task = link.tasks;
|
|
548
|
+
return task?.status === 'completed';
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
// Find the next available task
|
|
552
|
+
for (const phase of phaseOrder) {
|
|
553
|
+
// Check if we can work on this phase
|
|
554
|
+
const prevPhaseIndex = phaseOrder.indexOf(phase) - 1;
|
|
555
|
+
if (prevPhaseIndex >= 0 && !isPhaseComplete(phaseOrder[prevPhaseIndex])) {
|
|
556
|
+
continue; // Previous phase not complete, skip this phase
|
|
557
|
+
}
|
|
558
|
+
const phaseTasks = (taskLinks || [])
|
|
559
|
+
.filter((link) => link.phase === phase)
|
|
560
|
+
.sort((a, b) => a.order_index - b.order_index);
|
|
561
|
+
for (const link of phaseTasks) {
|
|
562
|
+
const task = link.tasks;
|
|
563
|
+
if (!task)
|
|
564
|
+
continue;
|
|
565
|
+
// Skip completed, cancelled, or in-progress tasks
|
|
566
|
+
if (task.status !== 'pending')
|
|
567
|
+
continue;
|
|
568
|
+
// Skip tasks claimed by another session
|
|
569
|
+
if (task.claimed_by_session_id)
|
|
570
|
+
continue;
|
|
571
|
+
// Check dependencies
|
|
572
|
+
if (!areDependenciesComplete(task.id))
|
|
573
|
+
continue;
|
|
574
|
+
return {
|
|
575
|
+
result: {
|
|
576
|
+
next_task: {
|
|
577
|
+
id: task.id,
|
|
578
|
+
title: task.title,
|
|
579
|
+
phase: link.phase,
|
|
580
|
+
priority: task.priority,
|
|
581
|
+
},
|
|
582
|
+
body_of_work: {
|
|
583
|
+
id: body_of_work_id,
|
|
584
|
+
title: bow.title,
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// No available tasks
|
|
591
|
+
return {
|
|
592
|
+
result: {
|
|
593
|
+
next_task: null,
|
|
594
|
+
message: 'No available tasks - all are completed, in progress, or blocked by dependencies',
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* Bodies of Work handlers registry
|
|
600
|
+
*/
|
|
601
|
+
export const bodiesOfWorkHandlers = {
|
|
602
|
+
create_body_of_work: createBodyOfWork,
|
|
603
|
+
update_body_of_work: updateBodyOfWork,
|
|
604
|
+
get_body_of_work: getBodyOfWork,
|
|
605
|
+
get_bodies_of_work: getBodiesOfWork,
|
|
606
|
+
delete_body_of_work: deleteBodyOfWork,
|
|
607
|
+
add_task_to_body_of_work: addTaskToBodyOfWork,
|
|
608
|
+
remove_task_from_body_of_work: removeTaskFromBodyOfWork,
|
|
609
|
+
activate_body_of_work: activateBodyOfWork,
|
|
610
|
+
add_task_dependency: addTaskDependency,
|
|
611
|
+
remove_task_dependency: removeTaskDependency,
|
|
612
|
+
get_task_dependencies: getTaskDependencies,
|
|
613
|
+
get_next_body_of_work_task: getNextBodyOfWorkTask,
|
|
614
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Checkout Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles file checkout/checkin for multi-agent coordination:
|
|
5
|
+
* - checkout_file: Reserve a file for editing
|
|
6
|
+
* - checkin_file: Release a file after editing
|
|
7
|
+
* - get_file_checkouts: List active and recent checkouts
|
|
8
|
+
* - abandon_checkout: Force-release a checkout
|
|
9
|
+
*/
|
|
10
|
+
import type { Handler, HandlerRegistry } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Checkout a file for editing
|
|
13
|
+
* Prevents other agents from editing the same file
|
|
14
|
+
*/
|
|
15
|
+
export declare const checkoutFile: Handler;
|
|
16
|
+
/**
|
|
17
|
+
* Checkin a file after editing
|
|
18
|
+
* Releases the file for other agents to edit
|
|
19
|
+
*/
|
|
20
|
+
export declare const checkinFile: Handler;
|
|
21
|
+
/**
|
|
22
|
+
* Get file checkouts for a project
|
|
23
|
+
*/
|
|
24
|
+
export declare const getFileCheckouts: Handler;
|
|
25
|
+
/**
|
|
26
|
+
* Abandon a checkout (force release)
|
|
27
|
+
* Use when the original agent session died or is stuck
|
|
28
|
+
*/
|
|
29
|
+
export declare const abandonCheckout: Handler;
|
|
30
|
+
/**
|
|
31
|
+
* Check if a file is available for checkout
|
|
32
|
+
*/
|
|
33
|
+
export declare const isFileAvailable: Handler;
|
|
34
|
+
/**
|
|
35
|
+
* Checkout handlers registry
|
|
36
|
+
*/
|
|
37
|
+
export declare const checkoutHandlers: HandlerRegistry;
|