@vibescope/mcp-server 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1114 -0
- package/dist/api-client.js +698 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +106 -476
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +112 -828
- package/dist/handlers/discovery.js +31 -0
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +39 -134
- package/dist/handlers/findings.js +43 -67
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +99 -585
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +274 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +230 -900
- package/dist/handlers/tool-docs.d.ts +8 -0
- package/dist/handlers/tool-docs.js +657 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +26 -153
- package/dist/index.js +473 -160
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +4 -0
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1752 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +210 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +194 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +140 -895
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +32 -0
- package/src/handlers/fallback.test.ts +128 -361
- package/src/handlers/fallback.ts +62 -148
- package/src/handlers/findings.test.ts +127 -345
- package/src/handlers/findings.ts +49 -66
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +241 -206
- package/src/handlers/session.ts +110 -657
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +497 -0
- package/src/handlers/tasks.test.ts +608 -353
- package/src/handlers/tasks.ts +248 -1025
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +189 -572
- package/src/handlers/validation.ts +29 -166
- package/src/index.ts +473 -184
- package/src/knowledge.ts +107 -9
- package/src/tools.ts +2506 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- package/tmpclaude-f48d-cwd +0 -1
|
@@ -14,35 +14,38 @@
|
|
|
14
14
|
* - remove_task_dependency
|
|
15
15
|
* - get_task_dependencies
|
|
16
16
|
* - get_next_body_of_work_task
|
|
17
|
+
*
|
|
18
|
+
* MIGRATED: Uses Vibescope API client instead of direct Supabase
|
|
17
19
|
*/
|
|
18
20
|
import { validateRequired, validateUUID } from '../validators.js';
|
|
21
|
+
import { getApiClient } from '../api-client.js';
|
|
19
22
|
export const createBodyOfWork = async (args, ctx) => {
|
|
20
23
|
const { project_id, title, description, auto_deploy_on_completion, deploy_environment, deploy_version_bump, deploy_trigger, } = args;
|
|
21
24
|
validateRequired(project_id, 'project_id');
|
|
22
25
|
validateUUID(project_id, 'project_id');
|
|
23
26
|
validateRequired(title, 'title');
|
|
24
|
-
const {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
.insert({
|
|
27
|
+
const { session } = ctx;
|
|
28
|
+
const apiClient = getApiClient();
|
|
29
|
+
const response = await apiClient.proxy('create_body_of_work', {
|
|
28
30
|
project_id,
|
|
29
31
|
title,
|
|
30
|
-
description
|
|
31
|
-
auto_deploy_on_completion
|
|
32
|
-
deploy_environment
|
|
33
|
-
deploy_version_bump
|
|
34
|
-
deploy_trigger
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
throw new Error(`Failed to create body of work: ${error
|
|
32
|
+
description,
|
|
33
|
+
auto_deploy_on_completion,
|
|
34
|
+
deploy_environment,
|
|
35
|
+
deploy_version_bump,
|
|
36
|
+
deploy_trigger
|
|
37
|
+
}, {
|
|
38
|
+
session_id: session.currentSessionId,
|
|
39
|
+
persona: session.currentPersona,
|
|
40
|
+
instance_id: session.instanceId
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`Failed to create body of work: ${response.error}`);
|
|
44
|
+
}
|
|
42
45
|
return {
|
|
43
46
|
result: {
|
|
44
47
|
success: true,
|
|
45
|
-
body_of_work_id: data
|
|
48
|
+
body_of_work_id: response.data?.body_of_work_id,
|
|
46
49
|
title,
|
|
47
50
|
status: 'draft',
|
|
48
51
|
message: 'Body of work created. Add tasks with add_task_to_body_of_work, then activate with activate_body_of_work.',
|
|
@@ -53,172 +56,65 @@ export const updateBodyOfWork = async (args, ctx) => {
|
|
|
53
56
|
const { body_of_work_id, title, description, auto_deploy_on_completion, deploy_environment, deploy_version_bump, deploy_trigger, } = args;
|
|
54
57
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
55
58
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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) {
|
|
59
|
+
// Check if any updates provided
|
|
60
|
+
if (title === undefined && description === undefined && auto_deploy_on_completion === undefined &&
|
|
61
|
+
deploy_environment === undefined && deploy_version_bump === undefined && deploy_trigger === undefined) {
|
|
72
62
|
return { result: { success: true, message: 'No updates provided' } };
|
|
73
63
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
const apiClient = getApiClient();
|
|
65
|
+
const response = await apiClient.proxy('update_body_of_work', {
|
|
66
|
+
body_of_work_id,
|
|
67
|
+
title,
|
|
68
|
+
description,
|
|
69
|
+
auto_deploy_on_completion,
|
|
70
|
+
deploy_environment,
|
|
71
|
+
deploy_version_bump,
|
|
72
|
+
deploy_trigger
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Failed to update body of work: ${response.error}`);
|
|
76
|
+
}
|
|
80
77
|
return { result: { success: true, body_of_work_id } };
|
|
81
78
|
};
|
|
82
79
|
export const getBodyOfWork = async (args, ctx) => {
|
|
83
80
|
const { body_of_work_id } = args;
|
|
84
81
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
85
82
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
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
|
-
}
|
|
83
|
+
const apiClient = getApiClient();
|
|
84
|
+
const response = await apiClient.proxy('get_body_of_work', { body_of_work_id });
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`Failed to get body of work: ${response.error}`);
|
|
134
87
|
}
|
|
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
|
-
};
|
|
88
|
+
return { result: response.data };
|
|
144
89
|
};
|
|
145
90
|
export const getBodiesOfWork = async (args, ctx) => {
|
|
146
|
-
const { project_id, status } = args;
|
|
91
|
+
const { project_id, status, limit = 50, offset = 0, search_query } = args;
|
|
147
92
|
validateRequired(project_id, 'project_id');
|
|
148
93
|
validateUUID(project_id, 'project_id');
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
};
|
|
94
|
+
const apiClient = getApiClient();
|
|
95
|
+
const response = await apiClient.proxy('get_bodies_of_work', {
|
|
96
|
+
project_id,
|
|
97
|
+
status,
|
|
98
|
+
limit: Math.min(limit, 100),
|
|
99
|
+
offset,
|
|
100
|
+
search_query
|
|
209
101
|
});
|
|
210
|
-
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`Failed to fetch bodies of work: ${response.error}`);
|
|
104
|
+
}
|
|
105
|
+
return { result: response.data };
|
|
211
106
|
};
|
|
212
107
|
export const deleteBodyOfWork = async (args, ctx) => {
|
|
213
108
|
const { body_of_work_id } = args;
|
|
214
109
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
215
110
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
throw new Error(`Failed to delete body of work: ${error
|
|
111
|
+
const apiClient = getApiClient();
|
|
112
|
+
const response = await apiClient.proxy('delete_body_of_work', {
|
|
113
|
+
body_of_work_id
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`Failed to delete body of work: ${response.error}`);
|
|
117
|
+
}
|
|
222
118
|
return { result: { success: true, message: 'Body of work deleted. Tasks are preserved.' } };
|
|
223
119
|
};
|
|
224
120
|
export const addTaskToBodyOfWork = async (args, ctx) => {
|
|
@@ -227,136 +123,39 @@ export const addTaskToBodyOfWork = async (args, ctx) => {
|
|
|
227
123
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
228
124
|
validateRequired(task_id, 'task_id');
|
|
229
125
|
validateUUID(task_id, 'task_id');
|
|
230
|
-
const
|
|
231
|
-
const
|
|
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({
|
|
126
|
+
const apiClient = getApiClient();
|
|
127
|
+
const response = await apiClient.proxy('add_task_to_body_of_work', {
|
|
269
128
|
body_of_work_id,
|
|
270
129
|
task_id,
|
|
271
|
-
phase:
|
|
272
|
-
order_index
|
|
130
|
+
phase: phase || 'core',
|
|
131
|
+
order_index
|
|
273
132
|
});
|
|
274
|
-
if (
|
|
275
|
-
throw new Error(`Failed to add task to body of work: ${error
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
success: true,
|
|
279
|
-
body_of_work_id,
|
|
280
|
-
task_id,
|
|
281
|
-
phase: taskPhase,
|
|
282
|
-
order_index: finalOrderIndex,
|
|
283
|
-
},
|
|
284
|
-
};
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`Failed to add task to body of work: ${response.error}`);
|
|
135
|
+
}
|
|
136
|
+
return { result: response.data };
|
|
285
137
|
};
|
|
286
138
|
export const removeTaskFromBodyOfWork = async (args, ctx) => {
|
|
287
139
|
const { task_id } = args;
|
|
288
140
|
validateRequired(task_id, 'task_id');
|
|
289
141
|
validateUUID(task_id, 'task_id');
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
.
|
|
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');
|
|
142
|
+
const apiClient = getApiClient();
|
|
143
|
+
const response = await apiClient.proxy('remove_task_from_body_of_work', { task_id });
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(`Failed to remove task from body of work: ${response.error}`);
|
|
308
146
|
}
|
|
309
|
-
|
|
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 } };
|
|
147
|
+
return { result: response.data };
|
|
316
148
|
};
|
|
317
149
|
export const activateBodyOfWork = async (args, ctx) => {
|
|
318
150
|
const { body_of_work_id } = args;
|
|
319
151
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
320
152
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
.
|
|
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}`);
|
|
153
|
+
const apiClient = getApiClient();
|
|
154
|
+
const response = await apiClient.proxy('activate_body_of_work', { body_of_work_id });
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error(`Failed to activate body of work: ${response.error}`);
|
|
333
157
|
}
|
|
334
|
-
|
|
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
|
-
};
|
|
158
|
+
return { result: response.data };
|
|
360
159
|
};
|
|
361
160
|
export const addTaskDependency = async (args, ctx) => {
|
|
362
161
|
const { body_of_work_id, task_id, depends_on_task_id } = args;
|
|
@@ -369,63 +168,16 @@ export const addTaskDependency = async (args, ctx) => {
|
|
|
369
168
|
if (task_id === depends_on_task_id) {
|
|
370
169
|
throw new Error('A task cannot depend on itself');
|
|
371
170
|
}
|
|
372
|
-
const
|
|
373
|
-
|
|
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({
|
|
171
|
+
const apiClient = getApiClient();
|
|
172
|
+
const response = await apiClient.proxy('add_task_dependency', {
|
|
410
173
|
body_of_work_id,
|
|
411
174
|
task_id,
|
|
412
|
-
depends_on_task_id
|
|
175
|
+
depends_on_task_id
|
|
413
176
|
});
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
throw new Error('This dependency already exists');
|
|
417
|
-
}
|
|
418
|
-
throw new Error(`Failed to add dependency: ${error.message}`);
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`Failed to add task dependency: ${response.error}`);
|
|
419
179
|
}
|
|
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
|
-
};
|
|
180
|
+
return { result: response.data };
|
|
429
181
|
};
|
|
430
182
|
export const removeTaskDependency = async (args, ctx) => {
|
|
431
183
|
const { task_id, depends_on_task_id } = args;
|
|
@@ -433,15 +185,15 @@ export const removeTaskDependency = async (args, ctx) => {
|
|
|
433
185
|
validateUUID(task_id, 'task_id');
|
|
434
186
|
validateRequired(depends_on_task_id, 'depends_on_task_id');
|
|
435
187
|
validateUUID(depends_on_task_id, 'depends_on_task_id');
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return { result:
|
|
188
|
+
const apiClient = getApiClient();
|
|
189
|
+
const response = await apiClient.proxy('remove_task_dependency', {
|
|
190
|
+
task_id,
|
|
191
|
+
depends_on_task_id
|
|
192
|
+
});
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(`Failed to remove task dependency: ${response.error}`);
|
|
195
|
+
}
|
|
196
|
+
return { result: response.data };
|
|
445
197
|
};
|
|
446
198
|
export const getTaskDependencies = async (args, ctx) => {
|
|
447
199
|
const { body_of_work_id, task_id } = args;
|
|
@@ -452,148 +204,26 @@ export const getTaskDependencies = async (args, ctx) => {
|
|
|
452
204
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
453
205
|
if (task_id)
|
|
454
206
|
validateUUID(task_id, 'task_id');
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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);
|
|
207
|
+
const apiClient = getApiClient();
|
|
208
|
+
const response = await apiClient.proxy('get_task_dependencies', {
|
|
209
|
+
body_of_work_id,
|
|
210
|
+
task_id
|
|
211
|
+
});
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
throw new Error(`Failed to fetch task dependencies: ${response.error}`);
|
|
469
214
|
}
|
|
470
|
-
|
|
471
|
-
if (error)
|
|
472
|
-
throw new Error(`Failed to fetch dependencies: ${error.message}`);
|
|
473
|
-
return { result: { dependencies: data || [] } };
|
|
215
|
+
return { result: response.data };
|
|
474
216
|
};
|
|
475
217
|
export const getNextBodyOfWorkTask = async (args, ctx) => {
|
|
476
218
|
const { body_of_work_id } = args;
|
|
477
219
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
478
220
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
.
|
|
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
|
-
}
|
|
221
|
+
const apiClient = getApiClient();
|
|
222
|
+
const response = await apiClient.proxy('get_next_body_of_work_task', { body_of_work_id });
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
throw new Error(`Failed to get next body of work task: ${response.error}`);
|
|
534
225
|
}
|
|
535
|
-
|
|
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
|
-
};
|
|
226
|
+
return { result: response.data };
|
|
597
227
|
};
|
|
598
228
|
/**
|
|
599
229
|
* Bodies of Work handlers registry
|