@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,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;