@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.
Files changed (170) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +34 -0
  3. package/dist/cli.js +356 -0
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +367 -0
  6. package/dist/handlers/__test-utils__.d.ts +72 -0
  7. package/dist/handlers/__test-utils__.js +176 -0
  8. package/dist/handlers/blockers.d.ts +18 -0
  9. package/dist/handlers/blockers.js +81 -0
  10. package/dist/handlers/bodies-of-work.d.ts +34 -0
  11. package/dist/handlers/bodies-of-work.js +614 -0
  12. package/dist/handlers/checkouts.d.ts +37 -0
  13. package/dist/handlers/checkouts.js +377 -0
  14. package/dist/handlers/cost.d.ts +39 -0
  15. package/dist/handlers/cost.js +247 -0
  16. package/dist/handlers/decisions.d.ts +16 -0
  17. package/dist/handlers/decisions.js +64 -0
  18. package/dist/handlers/deployment.d.ts +36 -0
  19. package/dist/handlers/deployment.js +1062 -0
  20. package/dist/handlers/discovery.d.ts +14 -0
  21. package/dist/handlers/discovery.js +870 -0
  22. package/dist/handlers/fallback.d.ts +18 -0
  23. package/dist/handlers/fallback.js +216 -0
  24. package/dist/handlers/findings.d.ts +18 -0
  25. package/dist/handlers/findings.js +110 -0
  26. package/dist/handlers/git-issues.d.ts +22 -0
  27. package/dist/handlers/git-issues.js +247 -0
  28. package/dist/handlers/ideas.d.ts +19 -0
  29. package/dist/handlers/ideas.js +188 -0
  30. package/dist/handlers/index.d.ts +29 -0
  31. package/dist/handlers/index.js +65 -0
  32. package/dist/handlers/knowledge-query.d.ts +22 -0
  33. package/dist/handlers/knowledge-query.js +253 -0
  34. package/dist/handlers/knowledge.d.ts +12 -0
  35. package/dist/handlers/knowledge.js +108 -0
  36. package/dist/handlers/milestones.d.ts +20 -0
  37. package/dist/handlers/milestones.js +179 -0
  38. package/dist/handlers/organizations.d.ts +36 -0
  39. package/dist/handlers/organizations.js +428 -0
  40. package/dist/handlers/progress.d.ts +14 -0
  41. package/dist/handlers/progress.js +149 -0
  42. package/dist/handlers/project.d.ts +20 -0
  43. package/dist/handlers/project.js +278 -0
  44. package/dist/handlers/requests.d.ts +16 -0
  45. package/dist/handlers/requests.js +131 -0
  46. package/dist/handlers/roles.d.ts +30 -0
  47. package/dist/handlers/roles.js +281 -0
  48. package/dist/handlers/session.d.ts +20 -0
  49. package/dist/handlers/session.js +791 -0
  50. package/dist/handlers/tasks.d.ts +52 -0
  51. package/dist/handlers/tasks.js +1111 -0
  52. package/dist/handlers/tasks.test.d.ts +1 -0
  53. package/dist/handlers/tasks.test.js +431 -0
  54. package/dist/handlers/types.d.ts +94 -0
  55. package/dist/handlers/types.js +1 -0
  56. package/dist/handlers/validation.d.ts +16 -0
  57. package/dist/handlers/validation.js +188 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2707 -0
  60. package/dist/knowledge.d.ts +6 -0
  61. package/dist/knowledge.js +121 -0
  62. package/dist/tools.d.ts +2 -0
  63. package/dist/tools.js +2498 -0
  64. package/dist/utils.d.ts +149 -0
  65. package/dist/utils.js +317 -0
  66. package/dist/utils.test.d.ts +1 -0
  67. package/dist/utils.test.js +532 -0
  68. package/dist/validators.d.ts +35 -0
  69. package/dist/validators.js +111 -0
  70. package/dist/validators.test.d.ts +1 -0
  71. package/dist/validators.test.js +176 -0
  72. package/package.json +44 -0
  73. package/src/cli.test.ts +442 -0
  74. package/src/cli.ts +439 -0
  75. package/src/handlers/__test-utils__.ts +217 -0
  76. package/src/handlers/blockers.test.ts +390 -0
  77. package/src/handlers/blockers.ts +110 -0
  78. package/src/handlers/bodies-of-work.test.ts +1276 -0
  79. package/src/handlers/bodies-of-work.ts +783 -0
  80. package/src/handlers/cost.test.ts +436 -0
  81. package/src/handlers/cost.ts +322 -0
  82. package/src/handlers/decisions.test.ts +401 -0
  83. package/src/handlers/decisions.ts +86 -0
  84. package/src/handlers/deployment.test.ts +516 -0
  85. package/src/handlers/deployment.ts +1289 -0
  86. package/src/handlers/discovery.test.ts +254 -0
  87. package/src/handlers/discovery.ts +969 -0
  88. package/src/handlers/fallback.test.ts +687 -0
  89. package/src/handlers/fallback.ts +260 -0
  90. package/src/handlers/findings.test.ts +565 -0
  91. package/src/handlers/findings.ts +153 -0
  92. package/src/handlers/ideas.test.ts +753 -0
  93. package/src/handlers/ideas.ts +247 -0
  94. package/src/handlers/index.ts +69 -0
  95. package/src/handlers/milestones.test.ts +584 -0
  96. package/src/handlers/milestones.ts +217 -0
  97. package/src/handlers/organizations.test.ts +997 -0
  98. package/src/handlers/organizations.ts +550 -0
  99. package/src/handlers/progress.test.ts +369 -0
  100. package/src/handlers/progress.ts +188 -0
  101. package/src/handlers/project.test.ts +562 -0
  102. package/src/handlers/project.ts +352 -0
  103. package/src/handlers/requests.test.ts +531 -0
  104. package/src/handlers/requests.ts +150 -0
  105. package/src/handlers/session.test.ts +459 -0
  106. package/src/handlers/session.ts +912 -0
  107. package/src/handlers/tasks.test.ts +602 -0
  108. package/src/handlers/tasks.ts +1393 -0
  109. package/src/handlers/types.ts +88 -0
  110. package/src/handlers/validation.test.ts +880 -0
  111. package/src/handlers/validation.ts +223 -0
  112. package/src/index.ts +3205 -0
  113. package/src/knowledge.ts +132 -0
  114. package/src/tmpclaude-0078-cwd +1 -0
  115. package/src/tmpclaude-0ee1-cwd +1 -0
  116. package/src/tmpclaude-2dd5-cwd +1 -0
  117. package/src/tmpclaude-344c-cwd +1 -0
  118. package/src/tmpclaude-3860-cwd +1 -0
  119. package/src/tmpclaude-4b63-cwd +1 -0
  120. package/src/tmpclaude-5c73-cwd +1 -0
  121. package/src/tmpclaude-5ee3-cwd +1 -0
  122. package/src/tmpclaude-6795-cwd +1 -0
  123. package/src/tmpclaude-709e-cwd +1 -0
  124. package/src/tmpclaude-9839-cwd +1 -0
  125. package/src/tmpclaude-d829-cwd +1 -0
  126. package/src/tmpclaude-e072-cwd +1 -0
  127. package/src/tmpclaude-f6ee-cwd +1 -0
  128. package/src/utils.test.ts +681 -0
  129. package/src/utils.ts +375 -0
  130. package/src/validators.test.ts +223 -0
  131. package/src/validators.ts +122 -0
  132. package/tmpclaude-0439-cwd +1 -0
  133. package/tmpclaude-132f-cwd +1 -0
  134. package/tmpclaude-15bb-cwd +1 -0
  135. package/tmpclaude-165a-cwd +1 -0
  136. package/tmpclaude-1ba9-cwd +1 -0
  137. package/tmpclaude-21a3-cwd +1 -0
  138. package/tmpclaude-2a38-cwd +1 -0
  139. package/tmpclaude-2adf-cwd +1 -0
  140. package/tmpclaude-2f56-cwd +1 -0
  141. package/tmpclaude-3626-cwd +1 -0
  142. package/tmpclaude-3727-cwd +1 -0
  143. package/tmpclaude-40bc-cwd +1 -0
  144. package/tmpclaude-436f-cwd +1 -0
  145. package/tmpclaude-4783-cwd +1 -0
  146. package/tmpclaude-4b6d-cwd +1 -0
  147. package/tmpclaude-4ba4-cwd +1 -0
  148. package/tmpclaude-51e6-cwd +1 -0
  149. package/tmpclaude-5ecf-cwd +1 -0
  150. package/tmpclaude-6f97-cwd +1 -0
  151. package/tmpclaude-7fb2-cwd +1 -0
  152. package/tmpclaude-825c-cwd +1 -0
  153. package/tmpclaude-8baf-cwd +1 -0
  154. package/tmpclaude-8d9f-cwd +1 -0
  155. package/tmpclaude-975c-cwd +1 -0
  156. package/tmpclaude-9983-cwd +1 -0
  157. package/tmpclaude-a045-cwd +1 -0
  158. package/tmpclaude-ac4a-cwd +1 -0
  159. package/tmpclaude-b593-cwd +1 -0
  160. package/tmpclaude-b891-cwd +1 -0
  161. package/tmpclaude-c032-cwd +1 -0
  162. package/tmpclaude-cf43-cwd +1 -0
  163. package/tmpclaude-d040-cwd +1 -0
  164. package/tmpclaude-dcdd-cwd +1 -0
  165. package/tmpclaude-dcee-cwd +1 -0
  166. package/tmpclaude-e16b-cwd +1 -0
  167. package/tmpclaude-ecd2-cwd +1 -0
  168. package/tmpclaude-f48d-cwd +1 -0
  169. package/tsconfig.json +16 -0
  170. 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
+ };