@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,783 @@
|
|
|
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
|
+
|
|
19
|
+
import type { Handler, HandlerRegistry } from './types.js';
|
|
20
|
+
import { validateRequired, validateUUID } from '../validators.js';
|
|
21
|
+
|
|
22
|
+
type BodyOfWorkStatus = 'draft' | 'active' | 'completed' | 'cancelled';
|
|
23
|
+
type TaskPhase = 'pre' | 'core' | 'post';
|
|
24
|
+
type DeployEnvironment = 'development' | 'staging' | 'production';
|
|
25
|
+
type VersionBump = 'patch' | 'minor' | 'major';
|
|
26
|
+
type DeployTrigger = 'all_completed' | 'all_completed_validated';
|
|
27
|
+
|
|
28
|
+
export const createBodyOfWork: Handler = async (args, ctx) => {
|
|
29
|
+
const {
|
|
30
|
+
project_id,
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
auto_deploy_on_completion,
|
|
34
|
+
deploy_environment,
|
|
35
|
+
deploy_version_bump,
|
|
36
|
+
deploy_trigger,
|
|
37
|
+
} = args as {
|
|
38
|
+
project_id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
auto_deploy_on_completion?: boolean;
|
|
42
|
+
deploy_environment?: DeployEnvironment;
|
|
43
|
+
deploy_version_bump?: VersionBump;
|
|
44
|
+
deploy_trigger?: DeployTrigger;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
validateRequired(project_id, 'project_id');
|
|
48
|
+
validateUUID(project_id, 'project_id');
|
|
49
|
+
validateRequired(title, 'title');
|
|
50
|
+
|
|
51
|
+
const { supabase, session } = ctx;
|
|
52
|
+
|
|
53
|
+
const { data, error } = await supabase
|
|
54
|
+
.from('bodies_of_work')
|
|
55
|
+
.insert({
|
|
56
|
+
project_id,
|
|
57
|
+
title,
|
|
58
|
+
description: description || null,
|
|
59
|
+
auto_deploy_on_completion: auto_deploy_on_completion || false,
|
|
60
|
+
deploy_environment: deploy_environment || 'production',
|
|
61
|
+
deploy_version_bump: deploy_version_bump || 'minor',
|
|
62
|
+
deploy_trigger: deploy_trigger || 'all_completed_validated',
|
|
63
|
+
created_by: 'agent',
|
|
64
|
+
created_by_session_id: session.currentSessionId,
|
|
65
|
+
})
|
|
66
|
+
.select('id')
|
|
67
|
+
.single();
|
|
68
|
+
|
|
69
|
+
if (error) throw new Error(`Failed to create body of work: ${error.message}`);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
result: {
|
|
73
|
+
success: true,
|
|
74
|
+
body_of_work_id: data.id,
|
|
75
|
+
title,
|
|
76
|
+
status: 'draft',
|
|
77
|
+
message: 'Body of work created. Add tasks with add_task_to_body_of_work, then activate with activate_body_of_work.',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const updateBodyOfWork: Handler = async (args, ctx) => {
|
|
83
|
+
const {
|
|
84
|
+
body_of_work_id,
|
|
85
|
+
title,
|
|
86
|
+
description,
|
|
87
|
+
auto_deploy_on_completion,
|
|
88
|
+
deploy_environment,
|
|
89
|
+
deploy_version_bump,
|
|
90
|
+
deploy_trigger,
|
|
91
|
+
} = args as {
|
|
92
|
+
body_of_work_id: string;
|
|
93
|
+
title?: string;
|
|
94
|
+
description?: string;
|
|
95
|
+
auto_deploy_on_completion?: boolean;
|
|
96
|
+
deploy_environment?: DeployEnvironment;
|
|
97
|
+
deploy_version_bump?: VersionBump;
|
|
98
|
+
deploy_trigger?: DeployTrigger;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
102
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
103
|
+
|
|
104
|
+
const { supabase } = ctx;
|
|
105
|
+
|
|
106
|
+
// Build update object
|
|
107
|
+
const updates: Record<string, unknown> = {};
|
|
108
|
+
if (title !== undefined) updates.title = title;
|
|
109
|
+
if (description !== undefined) updates.description = description;
|
|
110
|
+
if (auto_deploy_on_completion !== undefined) updates.auto_deploy_on_completion = auto_deploy_on_completion;
|
|
111
|
+
if (deploy_environment !== undefined) updates.deploy_environment = deploy_environment;
|
|
112
|
+
if (deploy_version_bump !== undefined) updates.deploy_version_bump = deploy_version_bump;
|
|
113
|
+
if (deploy_trigger !== undefined) updates.deploy_trigger = deploy_trigger;
|
|
114
|
+
|
|
115
|
+
if (Object.keys(updates).length === 0) {
|
|
116
|
+
return { result: { success: true, message: 'No updates provided' } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { error } = await supabase
|
|
120
|
+
.from('bodies_of_work')
|
|
121
|
+
.update(updates)
|
|
122
|
+
.eq('id', body_of_work_id);
|
|
123
|
+
|
|
124
|
+
if (error) throw new Error(`Failed to update body of work: ${error.message}`);
|
|
125
|
+
|
|
126
|
+
return { result: { success: true, body_of_work_id } };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const getBodyOfWork: Handler = async (args, ctx) => {
|
|
130
|
+
const { body_of_work_id } = args as { body_of_work_id: string };
|
|
131
|
+
|
|
132
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
133
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
134
|
+
|
|
135
|
+
const { supabase } = ctx;
|
|
136
|
+
|
|
137
|
+
// Get body of work
|
|
138
|
+
const { data: bow, error: bowError } = await supabase
|
|
139
|
+
.from('bodies_of_work')
|
|
140
|
+
.select('*')
|
|
141
|
+
.eq('id', body_of_work_id)
|
|
142
|
+
.single();
|
|
143
|
+
|
|
144
|
+
if (bowError || !bow) {
|
|
145
|
+
throw new Error(`Body of work not found: ${body_of_work_id}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Get tasks with their phases
|
|
149
|
+
const { data: taskLinks, error: taskError } = await supabase
|
|
150
|
+
.from('body_of_work_tasks')
|
|
151
|
+
.select(`
|
|
152
|
+
phase,
|
|
153
|
+
order_index,
|
|
154
|
+
tasks (
|
|
155
|
+
id,
|
|
156
|
+
title,
|
|
157
|
+
status,
|
|
158
|
+
priority,
|
|
159
|
+
progress_percentage
|
|
160
|
+
)
|
|
161
|
+
`)
|
|
162
|
+
.eq('body_of_work_id', body_of_work_id)
|
|
163
|
+
.order('order_index');
|
|
164
|
+
|
|
165
|
+
if (taskError) throw new Error(`Failed to fetch tasks: ${taskError.message}`);
|
|
166
|
+
|
|
167
|
+
// Organize tasks by phase
|
|
168
|
+
const preTasks: unknown[] = [];
|
|
169
|
+
const coreTasks: unknown[] = [];
|
|
170
|
+
const postTasks: unknown[] = [];
|
|
171
|
+
|
|
172
|
+
for (const link of taskLinks || []) {
|
|
173
|
+
const task = link.tasks;
|
|
174
|
+
if (!task) continue;
|
|
175
|
+
|
|
176
|
+
const taskWithPhase = { ...task, order_index: link.order_index };
|
|
177
|
+
|
|
178
|
+
switch (link.phase) {
|
|
179
|
+
case 'pre':
|
|
180
|
+
preTasks.push(taskWithPhase);
|
|
181
|
+
break;
|
|
182
|
+
case 'core':
|
|
183
|
+
coreTasks.push(taskWithPhase);
|
|
184
|
+
break;
|
|
185
|
+
case 'post':
|
|
186
|
+
postTasks.push(taskWithPhase);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
result: {
|
|
193
|
+
...bow,
|
|
194
|
+
pre_tasks: preTasks,
|
|
195
|
+
core_tasks: coreTasks,
|
|
196
|
+
post_tasks: postTasks,
|
|
197
|
+
total_tasks: preTasks.length + coreTasks.length + postTasks.length,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const getBodiesOfWork: Handler = async (args, ctx) => {
|
|
203
|
+
const { project_id, status } = args as {
|
|
204
|
+
project_id: string;
|
|
205
|
+
status?: BodyOfWorkStatus;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
validateRequired(project_id, 'project_id');
|
|
209
|
+
validateUUID(project_id, 'project_id');
|
|
210
|
+
|
|
211
|
+
const { supabase } = ctx;
|
|
212
|
+
|
|
213
|
+
let query = supabase
|
|
214
|
+
.from('bodies_of_work')
|
|
215
|
+
.select(`
|
|
216
|
+
id,
|
|
217
|
+
title,
|
|
218
|
+
description,
|
|
219
|
+
status,
|
|
220
|
+
progress_percentage,
|
|
221
|
+
auto_deploy_on_completion,
|
|
222
|
+
deploy_environment,
|
|
223
|
+
deploy_version_bump,
|
|
224
|
+
created_at,
|
|
225
|
+
activated_at,
|
|
226
|
+
completed_at
|
|
227
|
+
`)
|
|
228
|
+
.eq('project_id', project_id);
|
|
229
|
+
|
|
230
|
+
if (status) {
|
|
231
|
+
query = query.eq('status', status);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const { data, error } = await query.order('created_at', { ascending: false });
|
|
235
|
+
|
|
236
|
+
if (error) throw new Error(`Failed to fetch bodies of work: ${error.message}`);
|
|
237
|
+
|
|
238
|
+
const bodies = data || [];
|
|
239
|
+
if (bodies.length === 0) {
|
|
240
|
+
return { result: { bodies_of_work: [] } };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Batch query: get all task links for all bodies of work in a single query
|
|
244
|
+
const bodyIds = bodies.map((b) => b.id);
|
|
245
|
+
const { data: allTaskLinks } = await supabase
|
|
246
|
+
.from('body_of_work_tasks')
|
|
247
|
+
.select('body_of_work_id, phase, tasks!inner(status)')
|
|
248
|
+
.in('body_of_work_id', bodyIds);
|
|
249
|
+
|
|
250
|
+
// Group task links by body_of_work_id
|
|
251
|
+
const taskLinksByBody = new Map<string, typeof allTaskLinks>();
|
|
252
|
+
for (const link of allTaskLinks || []) {
|
|
253
|
+
const existing = taskLinksByBody.get(link.body_of_work_id) || [];
|
|
254
|
+
existing.push(link);
|
|
255
|
+
taskLinksByBody.set(link.body_of_work_id, existing);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Build response with task counts
|
|
259
|
+
const bodiesWithCounts = bodies.map((bow) => {
|
|
260
|
+
const taskLinks = taskLinksByBody.get(bow.id) || [];
|
|
261
|
+
const taskCounts = {
|
|
262
|
+
pre: { total: 0, completed: 0 },
|
|
263
|
+
core: { total: 0, completed: 0 },
|
|
264
|
+
post: { total: 0, completed: 0 },
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
for (const link of taskLinks) {
|
|
268
|
+
const phase = link.phase as TaskPhase;
|
|
269
|
+
taskCounts[phase].total++;
|
|
270
|
+
const taskData = link.tasks as unknown as { status: string } | null;
|
|
271
|
+
if (taskData?.status === 'completed') {
|
|
272
|
+
taskCounts[phase].completed++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
...bow,
|
|
278
|
+
task_counts: taskCounts,
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return { result: { bodies_of_work: bodiesWithCounts } };
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const deleteBodyOfWork: Handler = async (args, ctx) => {
|
|
286
|
+
const { body_of_work_id } = args as { body_of_work_id: string };
|
|
287
|
+
|
|
288
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
289
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
290
|
+
|
|
291
|
+
const { error } = await ctx.supabase
|
|
292
|
+
.from('bodies_of_work')
|
|
293
|
+
.delete()
|
|
294
|
+
.eq('id', body_of_work_id);
|
|
295
|
+
|
|
296
|
+
if (error) throw new Error(`Failed to delete body of work: ${error.message}`);
|
|
297
|
+
|
|
298
|
+
return { result: { success: true, message: 'Body of work deleted. Tasks are preserved.' } };
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const addTaskToBodyOfWork: Handler = async (args, ctx) => {
|
|
302
|
+
const { body_of_work_id, task_id, phase, order_index } = args as {
|
|
303
|
+
body_of_work_id: string;
|
|
304
|
+
task_id: string;
|
|
305
|
+
phase?: TaskPhase;
|
|
306
|
+
order_index?: number;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
310
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
311
|
+
validateRequired(task_id, 'task_id');
|
|
312
|
+
validateUUID(task_id, 'task_id');
|
|
313
|
+
|
|
314
|
+
const { supabase } = ctx;
|
|
315
|
+
const taskPhase = phase || 'core';
|
|
316
|
+
|
|
317
|
+
// Check if body of work exists and is in draft/active status
|
|
318
|
+
const { data: bow, error: bowError } = await supabase
|
|
319
|
+
.from('bodies_of_work')
|
|
320
|
+
.select('status')
|
|
321
|
+
.eq('id', body_of_work_id)
|
|
322
|
+
.single();
|
|
323
|
+
|
|
324
|
+
if (bowError || !bow) {
|
|
325
|
+
throw new Error(`Body of work not found: ${body_of_work_id}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (bow.status === 'completed' || bow.status === 'cancelled') {
|
|
329
|
+
throw new Error(`Cannot add tasks to ${bow.status} body of work`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check if task is already in a body of work
|
|
333
|
+
const { data: existingLink } = await supabase
|
|
334
|
+
.from('body_of_work_tasks')
|
|
335
|
+
.select('body_of_work_id')
|
|
336
|
+
.eq('task_id', task_id)
|
|
337
|
+
.single();
|
|
338
|
+
|
|
339
|
+
if (existingLink) {
|
|
340
|
+
throw new Error('Task is already assigned to a body of work. Remove it first.');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Get the next order index if not provided
|
|
344
|
+
let finalOrderIndex = order_index;
|
|
345
|
+
if (finalOrderIndex === undefined) {
|
|
346
|
+
const { data: maxOrder } = await supabase
|
|
347
|
+
.from('body_of_work_tasks')
|
|
348
|
+
.select('order_index')
|
|
349
|
+
.eq('body_of_work_id', body_of_work_id)
|
|
350
|
+
.eq('phase', taskPhase)
|
|
351
|
+
.order('order_index', { ascending: false })
|
|
352
|
+
.limit(1)
|
|
353
|
+
.single();
|
|
354
|
+
|
|
355
|
+
finalOrderIndex = maxOrder ? maxOrder.order_index + 1 : 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const { error } = await supabase
|
|
359
|
+
.from('body_of_work_tasks')
|
|
360
|
+
.insert({
|
|
361
|
+
body_of_work_id,
|
|
362
|
+
task_id,
|
|
363
|
+
phase: taskPhase,
|
|
364
|
+
order_index: finalOrderIndex,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (error) throw new Error(`Failed to add task to body of work: ${error.message}`);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
result: {
|
|
371
|
+
success: true,
|
|
372
|
+
body_of_work_id,
|
|
373
|
+
task_id,
|
|
374
|
+
phase: taskPhase,
|
|
375
|
+
order_index: finalOrderIndex,
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export const removeTaskFromBodyOfWork: Handler = async (args, ctx) => {
|
|
381
|
+
const { task_id } = args as { task_id: string };
|
|
382
|
+
|
|
383
|
+
validateRequired(task_id, 'task_id');
|
|
384
|
+
validateUUID(task_id, 'task_id');
|
|
385
|
+
|
|
386
|
+
const { supabase } = ctx;
|
|
387
|
+
|
|
388
|
+
// Check if link exists
|
|
389
|
+
const { data: link } = await supabase
|
|
390
|
+
.from('body_of_work_tasks')
|
|
391
|
+
.select('body_of_work_id')
|
|
392
|
+
.eq('task_id', task_id)
|
|
393
|
+
.single();
|
|
394
|
+
|
|
395
|
+
if (!link) {
|
|
396
|
+
return { result: { success: true, message: 'Task is not in any body of work' } };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check if body of work is completed
|
|
400
|
+
const { data: bow } = await supabase
|
|
401
|
+
.from('bodies_of_work')
|
|
402
|
+
.select('status')
|
|
403
|
+
.eq('id', link.body_of_work_id)
|
|
404
|
+
.single();
|
|
405
|
+
|
|
406
|
+
if (bow?.status === 'completed') {
|
|
407
|
+
throw new Error('Cannot remove tasks from a completed body of work');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const { error } = await supabase
|
|
411
|
+
.from('body_of_work_tasks')
|
|
412
|
+
.delete()
|
|
413
|
+
.eq('task_id', task_id);
|
|
414
|
+
|
|
415
|
+
if (error) throw new Error(`Failed to remove task from body of work: ${error.message}`);
|
|
416
|
+
|
|
417
|
+
return { result: { success: true, body_of_work_id: link.body_of_work_id } };
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export const activateBodyOfWork: Handler = async (args, ctx) => {
|
|
421
|
+
const { body_of_work_id } = args as { body_of_work_id: string };
|
|
422
|
+
|
|
423
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
424
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
425
|
+
|
|
426
|
+
const { supabase } = ctx;
|
|
427
|
+
|
|
428
|
+
// Check current status
|
|
429
|
+
const { data: bow, error: bowError } = await supabase
|
|
430
|
+
.from('bodies_of_work')
|
|
431
|
+
.select('status, title')
|
|
432
|
+
.eq('id', body_of_work_id)
|
|
433
|
+
.single();
|
|
434
|
+
|
|
435
|
+
if (bowError || !bow) {
|
|
436
|
+
throw new Error(`Body of work not found: ${body_of_work_id}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (bow.status !== 'draft') {
|
|
440
|
+
throw new Error(`Can only activate draft bodies of work. Current status: ${bow.status}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check if there are any tasks
|
|
444
|
+
const { count } = await supabase
|
|
445
|
+
.from('body_of_work_tasks')
|
|
446
|
+
.select('id', { count: 'exact', head: true })
|
|
447
|
+
.eq('body_of_work_id', body_of_work_id);
|
|
448
|
+
|
|
449
|
+
if (!count || count === 0) {
|
|
450
|
+
throw new Error('Cannot activate body of work with no tasks. Add tasks first.');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const { error } = await supabase
|
|
454
|
+
.from('bodies_of_work')
|
|
455
|
+
.update({
|
|
456
|
+
status: 'active',
|
|
457
|
+
activated_at: new Date().toISOString(),
|
|
458
|
+
})
|
|
459
|
+
.eq('id', body_of_work_id);
|
|
460
|
+
|
|
461
|
+
if (error) throw new Error(`Failed to activate body of work: ${error.message}`);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
result: {
|
|
465
|
+
success: true,
|
|
466
|
+
body_of_work_id,
|
|
467
|
+
title: bow.title,
|
|
468
|
+
status: 'active',
|
|
469
|
+
message: 'Body of work activated. Tasks can now be worked on following phase order.',
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export const addTaskDependency: Handler = async (args, ctx) => {
|
|
475
|
+
const { body_of_work_id, task_id, depends_on_task_id } = args as {
|
|
476
|
+
body_of_work_id: string;
|
|
477
|
+
task_id: string;
|
|
478
|
+
depends_on_task_id: string;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
482
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
483
|
+
validateRequired(task_id, 'task_id');
|
|
484
|
+
validateUUID(task_id, 'task_id');
|
|
485
|
+
validateRequired(depends_on_task_id, 'depends_on_task_id');
|
|
486
|
+
validateUUID(depends_on_task_id, 'depends_on_task_id');
|
|
487
|
+
|
|
488
|
+
if (task_id === depends_on_task_id) {
|
|
489
|
+
throw new Error('A task cannot depend on itself');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const { supabase } = ctx;
|
|
493
|
+
|
|
494
|
+
// Verify both tasks belong to the same body of work
|
|
495
|
+
const { data: taskLinks, error: taskError } = await supabase
|
|
496
|
+
.from('body_of_work_tasks')
|
|
497
|
+
.select('task_id')
|
|
498
|
+
.eq('body_of_work_id', body_of_work_id)
|
|
499
|
+
.in('task_id', [task_id, depends_on_task_id]);
|
|
500
|
+
|
|
501
|
+
if (taskError) throw new Error(`Failed to verify tasks: ${taskError.message}`);
|
|
502
|
+
|
|
503
|
+
if (!taskLinks || taskLinks.length !== 2) {
|
|
504
|
+
throw new Error('Both tasks must belong to the specified body of work');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Check for circular dependencies by traversing the dependency graph
|
|
508
|
+
const visited = new Set<string>();
|
|
509
|
+
const checkCircular = async (currentTaskId: string): Promise<boolean> => {
|
|
510
|
+
if (currentTaskId === task_id) return true; // Found cycle
|
|
511
|
+
if (visited.has(currentTaskId)) return false;
|
|
512
|
+
visited.add(currentTaskId);
|
|
513
|
+
|
|
514
|
+
const { data: deps } = await supabase
|
|
515
|
+
.from('body_of_work_task_dependencies')
|
|
516
|
+
.select('depends_on_task_id')
|
|
517
|
+
.eq('task_id', currentTaskId)
|
|
518
|
+
.eq('body_of_work_id', body_of_work_id);
|
|
519
|
+
|
|
520
|
+
for (const dep of deps || []) {
|
|
521
|
+
if (await checkCircular(dep.depends_on_task_id)) {
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
if (await checkCircular(depends_on_task_id)) {
|
|
529
|
+
throw new Error('Cannot add dependency: would create a circular dependency');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const { error } = await supabase
|
|
533
|
+
.from('body_of_work_task_dependencies')
|
|
534
|
+
.insert({
|
|
535
|
+
body_of_work_id,
|
|
536
|
+
task_id,
|
|
537
|
+
depends_on_task_id,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (error) {
|
|
541
|
+
if (error.message.includes('duplicate')) {
|
|
542
|
+
throw new Error('This dependency already exists');
|
|
543
|
+
}
|
|
544
|
+
throw new Error(`Failed to add dependency: ${error.message}`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
result: {
|
|
549
|
+
success: true,
|
|
550
|
+
body_of_work_id,
|
|
551
|
+
task_id,
|
|
552
|
+
depends_on_task_id,
|
|
553
|
+
message: `Task now depends on completion of the specified task`,
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
export const removeTaskDependency: Handler = async (args, ctx) => {
|
|
559
|
+
const { task_id, depends_on_task_id } = args as {
|
|
560
|
+
task_id: string;
|
|
561
|
+
depends_on_task_id: string;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
validateRequired(task_id, 'task_id');
|
|
565
|
+
validateUUID(task_id, 'task_id');
|
|
566
|
+
validateRequired(depends_on_task_id, 'depends_on_task_id');
|
|
567
|
+
validateUUID(depends_on_task_id, 'depends_on_task_id');
|
|
568
|
+
|
|
569
|
+
const { supabase } = ctx;
|
|
570
|
+
|
|
571
|
+
const { error } = await supabase
|
|
572
|
+
.from('body_of_work_task_dependencies')
|
|
573
|
+
.delete()
|
|
574
|
+
.eq('task_id', task_id)
|
|
575
|
+
.eq('depends_on_task_id', depends_on_task_id);
|
|
576
|
+
|
|
577
|
+
if (error) throw new Error(`Failed to remove dependency: ${error.message}`);
|
|
578
|
+
|
|
579
|
+
return { result: { success: true, task_id, depends_on_task_id } };
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
export const getTaskDependencies: Handler = async (args, ctx) => {
|
|
583
|
+
const { body_of_work_id, task_id } = args as {
|
|
584
|
+
body_of_work_id?: string;
|
|
585
|
+
task_id?: string;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
if (!body_of_work_id && !task_id) {
|
|
589
|
+
throw new Error('Either body_of_work_id or task_id is required');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (body_of_work_id) validateUUID(body_of_work_id, 'body_of_work_id');
|
|
593
|
+
if (task_id) validateUUID(task_id, 'task_id');
|
|
594
|
+
|
|
595
|
+
const { supabase } = ctx;
|
|
596
|
+
|
|
597
|
+
let query = supabase
|
|
598
|
+
.from('body_of_work_task_dependencies')
|
|
599
|
+
.select(`
|
|
600
|
+
id,
|
|
601
|
+
task_id,
|
|
602
|
+
depends_on_task_id,
|
|
603
|
+
created_at
|
|
604
|
+
`);
|
|
605
|
+
|
|
606
|
+
if (body_of_work_id) {
|
|
607
|
+
query = query.eq('body_of_work_id', body_of_work_id);
|
|
608
|
+
}
|
|
609
|
+
if (task_id) {
|
|
610
|
+
query = query.eq('task_id', task_id);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const { data, error } = await query;
|
|
614
|
+
|
|
615
|
+
if (error) throw new Error(`Failed to fetch dependencies: ${error.message}`);
|
|
616
|
+
|
|
617
|
+
return { result: { dependencies: data || [] } };
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export const getNextBodyOfWorkTask: Handler = async (args, ctx) => {
|
|
621
|
+
const { body_of_work_id } = args as { body_of_work_id: string };
|
|
622
|
+
|
|
623
|
+
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
624
|
+
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
625
|
+
|
|
626
|
+
const { supabase } = ctx;
|
|
627
|
+
|
|
628
|
+
// Get body of work status
|
|
629
|
+
const { data: bow, error: bowError } = await supabase
|
|
630
|
+
.from('bodies_of_work')
|
|
631
|
+
.select('status, title')
|
|
632
|
+
.eq('id', body_of_work_id)
|
|
633
|
+
.single();
|
|
634
|
+
|
|
635
|
+
if (bowError || !bow) {
|
|
636
|
+
throw new Error(`Body of work not found: ${body_of_work_id}`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (bow.status !== 'active') {
|
|
640
|
+
return {
|
|
641
|
+
result: {
|
|
642
|
+
next_task: null,
|
|
643
|
+
message: `Body of work is ${bow.status}, not active`,
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Get all tasks with their status and phase
|
|
649
|
+
const { data: taskLinks, error: taskError } = await supabase
|
|
650
|
+
.from('body_of_work_tasks')
|
|
651
|
+
.select(`
|
|
652
|
+
task_id,
|
|
653
|
+
phase,
|
|
654
|
+
order_index,
|
|
655
|
+
tasks (
|
|
656
|
+
id,
|
|
657
|
+
title,
|
|
658
|
+
status,
|
|
659
|
+
priority,
|
|
660
|
+
claimed_by_session_id
|
|
661
|
+
)
|
|
662
|
+
`)
|
|
663
|
+
.eq('body_of_work_id', body_of_work_id)
|
|
664
|
+
.order('order_index');
|
|
665
|
+
|
|
666
|
+
if (taskError) throw new Error(`Failed to fetch tasks: ${taskError.message}`);
|
|
667
|
+
|
|
668
|
+
// Get all dependencies
|
|
669
|
+
const { data: dependencies } = await supabase
|
|
670
|
+
.from('body_of_work_task_dependencies')
|
|
671
|
+
.select('task_id, depends_on_task_id')
|
|
672
|
+
.eq('body_of_work_id', body_of_work_id);
|
|
673
|
+
|
|
674
|
+
const depMap = new Map<string, string[]>();
|
|
675
|
+
for (const dep of dependencies || []) {
|
|
676
|
+
const existing = depMap.get(dep.task_id) || [];
|
|
677
|
+
existing.push(dep.depends_on_task_id);
|
|
678
|
+
depMap.set(dep.task_id, existing);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Build task status map
|
|
682
|
+
const taskStatusMap = new Map<string, string>();
|
|
683
|
+
for (const link of taskLinks || []) {
|
|
684
|
+
const task = link.tasks as unknown as { id: string; status: string } | null;
|
|
685
|
+
if (task) {
|
|
686
|
+
taskStatusMap.set(task.id, task.status);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Check if all dependencies are completed
|
|
691
|
+
const areDependenciesComplete = (taskId: string): boolean => {
|
|
692
|
+
const deps = depMap.get(taskId) || [];
|
|
693
|
+
return deps.every((depId) => taskStatusMap.get(depId) === 'completed');
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// Phase order: pre -> core -> post
|
|
697
|
+
const phaseOrder = ['pre', 'core', 'post'];
|
|
698
|
+
|
|
699
|
+
// Check if all tasks in a phase are completed
|
|
700
|
+
const isPhaseComplete = (phase: string): boolean => {
|
|
701
|
+
return (taskLinks || [])
|
|
702
|
+
.filter((link) => link.phase === phase)
|
|
703
|
+
.every((link) => {
|
|
704
|
+
const task = link.tasks as unknown as { status: string } | null;
|
|
705
|
+
return task?.status === 'completed';
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// Find the next available task
|
|
710
|
+
for (const phase of phaseOrder) {
|
|
711
|
+
// Check if we can work on this phase
|
|
712
|
+
const prevPhaseIndex = phaseOrder.indexOf(phase) - 1;
|
|
713
|
+
if (prevPhaseIndex >= 0 && !isPhaseComplete(phaseOrder[prevPhaseIndex])) {
|
|
714
|
+
continue; // Previous phase not complete, skip this phase
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const phaseTasks = (taskLinks || [])
|
|
718
|
+
.filter((link) => link.phase === phase)
|
|
719
|
+
.sort((a, b) => a.order_index - b.order_index);
|
|
720
|
+
|
|
721
|
+
for (const link of phaseTasks) {
|
|
722
|
+
const task = link.tasks as unknown as {
|
|
723
|
+
id: string;
|
|
724
|
+
title: string;
|
|
725
|
+
status: string;
|
|
726
|
+
priority: number;
|
|
727
|
+
claimed_by_session_id: string | null;
|
|
728
|
+
} | null;
|
|
729
|
+
|
|
730
|
+
if (!task) continue;
|
|
731
|
+
|
|
732
|
+
// Skip completed, cancelled, or in-progress tasks
|
|
733
|
+
if (task.status !== 'pending') continue;
|
|
734
|
+
|
|
735
|
+
// Skip tasks claimed by another session
|
|
736
|
+
if (task.claimed_by_session_id) continue;
|
|
737
|
+
|
|
738
|
+
// Check dependencies
|
|
739
|
+
if (!areDependenciesComplete(task.id)) continue;
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
result: {
|
|
743
|
+
next_task: {
|
|
744
|
+
id: task.id,
|
|
745
|
+
title: task.title,
|
|
746
|
+
phase: link.phase,
|
|
747
|
+
priority: task.priority,
|
|
748
|
+
},
|
|
749
|
+
body_of_work: {
|
|
750
|
+
id: body_of_work_id,
|
|
751
|
+
title: bow.title,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// No available tasks
|
|
759
|
+
return {
|
|
760
|
+
result: {
|
|
761
|
+
next_task: null,
|
|
762
|
+
message: 'No available tasks - all are completed, in progress, or blocked by dependencies',
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Bodies of Work handlers registry
|
|
769
|
+
*/
|
|
770
|
+
export const bodiesOfWorkHandlers: HandlerRegistry = {
|
|
771
|
+
create_body_of_work: createBodyOfWork,
|
|
772
|
+
update_body_of_work: updateBodyOfWork,
|
|
773
|
+
get_body_of_work: getBodyOfWork,
|
|
774
|
+
get_bodies_of_work: getBodiesOfWork,
|
|
775
|
+
delete_body_of_work: deleteBodyOfWork,
|
|
776
|
+
add_task_to_body_of_work: addTaskToBodyOfWork,
|
|
777
|
+
remove_task_from_body_of_work: removeTaskFromBodyOfWork,
|
|
778
|
+
activate_body_of_work: activateBodyOfWork,
|
|
779
|
+
add_task_dependency: addTaskDependency,
|
|
780
|
+
remove_task_dependency: removeTaskDependency,
|
|
781
|
+
get_task_dependencies: getTaskDependencies,
|
|
782
|
+
get_next_body_of_work_task: getNextBodyOfWorkTask,
|
|
783
|
+
};
|