@vibescope/mcp-server 0.0.1 → 0.2.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.
Files changed (173) hide show
  1. package/README.md +113 -98
  2. package/dist/api-client.d.ts +1169 -0
  3. package/dist/api-client.js +713 -0
  4. package/dist/cli.d.ts +1 -6
  5. package/dist/cli.js +39 -240
  6. package/dist/config/tool-categories.d.ts +31 -0
  7. package/dist/config/tool-categories.js +253 -0
  8. package/dist/handlers/blockers.js +57 -58
  9. package/dist/handlers/bodies-of-work.d.ts +2 -0
  10. package/dist/handlers/bodies-of-work.js +108 -477
  11. package/dist/handlers/cost.d.ts +1 -0
  12. package/dist/handlers/cost.js +35 -113
  13. package/dist/handlers/decisions.d.ts +2 -0
  14. package/dist/handlers/decisions.js +28 -27
  15. package/dist/handlers/deployment.js +113 -828
  16. package/dist/handlers/discovery.d.ts +3 -0
  17. package/dist/handlers/discovery.js +26 -627
  18. package/dist/handlers/fallback.d.ts +2 -0
  19. package/dist/handlers/fallback.js +56 -142
  20. package/dist/handlers/findings.d.ts +8 -1
  21. package/dist/handlers/findings.js +65 -68
  22. package/dist/handlers/git-issues.d.ts +9 -13
  23. package/dist/handlers/git-issues.js +80 -225
  24. package/dist/handlers/ideas.d.ts +3 -0
  25. package/dist/handlers/ideas.js +53 -134
  26. package/dist/handlers/index.d.ts +2 -0
  27. package/dist/handlers/index.js +6 -0
  28. package/dist/handlers/milestones.d.ts +2 -0
  29. package/dist/handlers/milestones.js +51 -98
  30. package/dist/handlers/organizations.js +79 -275
  31. package/dist/handlers/progress.d.ts +2 -0
  32. package/dist/handlers/progress.js +25 -123
  33. package/dist/handlers/project.js +42 -221
  34. package/dist/handlers/requests.d.ts +2 -0
  35. package/dist/handlers/requests.js +23 -83
  36. package/dist/handlers/session.js +119 -590
  37. package/dist/handlers/sprints.d.ts +32 -0
  38. package/dist/handlers/sprints.js +275 -0
  39. package/dist/handlers/tasks.d.ts +7 -10
  40. package/dist/handlers/tasks.js +245 -894
  41. package/dist/handlers/tool-docs.d.ts +9 -0
  42. package/dist/handlers/tool-docs.js +904 -0
  43. package/dist/handlers/types.d.ts +11 -3
  44. package/dist/handlers/validation.d.ts +1 -1
  45. package/dist/handlers/validation.js +38 -153
  46. package/dist/index.js +493 -162
  47. package/dist/knowledge.js +106 -9
  48. package/dist/tools.js +34 -4
  49. package/dist/validators.d.ts +21 -0
  50. package/dist/validators.js +91 -0
  51. package/package.json +2 -3
  52. package/src/api-client.ts +1822 -0
  53. package/src/cli.test.ts +128 -302
  54. package/src/cli.ts +41 -285
  55. package/src/handlers/__test-setup__.ts +215 -0
  56. package/src/handlers/__test-utils__.ts +4 -134
  57. package/src/handlers/blockers.test.ts +114 -124
  58. package/src/handlers/blockers.ts +68 -70
  59. package/src/handlers/bodies-of-work.test.ts +236 -831
  60. package/src/handlers/bodies-of-work.ts +210 -525
  61. package/src/handlers/cost.test.ts +149 -113
  62. package/src/handlers/cost.ts +44 -132
  63. package/src/handlers/decisions.test.ts +111 -209
  64. package/src/handlers/decisions.ts +35 -27
  65. package/src/handlers/deployment.test.ts +193 -239
  66. package/src/handlers/deployment.ts +143 -896
  67. package/src/handlers/discovery.test.ts +20 -67
  68. package/src/handlers/discovery.ts +29 -714
  69. package/src/handlers/fallback.test.ts +206 -361
  70. package/src/handlers/fallback.ts +81 -156
  71. package/src/handlers/findings.test.ts +229 -320
  72. package/src/handlers/findings.ts +76 -64
  73. package/src/handlers/git-issues.test.ts +623 -0
  74. package/src/handlers/git-issues.ts +174 -0
  75. package/src/handlers/ideas.test.ts +229 -343
  76. package/src/handlers/ideas.ts +69 -143
  77. package/src/handlers/index.ts +6 -0
  78. package/src/handlers/milestones.test.ts +167 -281
  79. package/src/handlers/milestones.ts +54 -93
  80. package/src/handlers/organizations.test.ts +275 -467
  81. package/src/handlers/organizations.ts +84 -294
  82. package/src/handlers/progress.test.ts +112 -218
  83. package/src/handlers/progress.ts +29 -142
  84. package/src/handlers/project.test.ts +203 -226
  85. package/src/handlers/project.ts +48 -238
  86. package/src/handlers/requests.test.ts +74 -342
  87. package/src/handlers/requests.ts +25 -83
  88. package/src/handlers/session.test.ts +276 -206
  89. package/src/handlers/session.ts +136 -662
  90. package/src/handlers/sprints.test.ts +711 -0
  91. package/src/handlers/sprints.ts +510 -0
  92. package/src/handlers/tasks.test.ts +669 -353
  93. package/src/handlers/tasks.ts +263 -1015
  94. package/src/handlers/tool-docs.ts +1024 -0
  95. package/src/handlers/types.ts +12 -4
  96. package/src/handlers/validation.test.ts +237 -568
  97. package/src/handlers/validation.ts +43 -167
  98. package/src/index.ts +493 -186
  99. package/src/tools.ts +2532 -0
  100. package/src/validators.test.ts +223 -223
  101. package/src/validators.ts +127 -0
  102. package/tsconfig.json +1 -1
  103. package/vitest.config.ts +14 -13
  104. package/dist/cli.test.d.ts +0 -1
  105. package/dist/cli.test.js +0 -367
  106. package/dist/handlers/__test-utils__.d.ts +0 -72
  107. package/dist/handlers/__test-utils__.js +0 -176
  108. package/dist/handlers/checkouts.d.ts +0 -37
  109. package/dist/handlers/checkouts.js +0 -377
  110. package/dist/handlers/knowledge-query.d.ts +0 -22
  111. package/dist/handlers/knowledge-query.js +0 -253
  112. package/dist/handlers/knowledge.d.ts +0 -12
  113. package/dist/handlers/knowledge.js +0 -108
  114. package/dist/handlers/roles.d.ts +0 -30
  115. package/dist/handlers/roles.js +0 -281
  116. package/dist/handlers/tasks.test.d.ts +0 -1
  117. package/dist/handlers/tasks.test.js +0 -431
  118. package/dist/utils.test.d.ts +0 -1
  119. package/dist/utils.test.js +0 -532
  120. package/dist/validators.test.d.ts +0 -1
  121. package/dist/validators.test.js +0 -176
  122. package/src/knowledge.ts +0 -132
  123. package/src/tmpclaude-0078-cwd +0 -1
  124. package/src/tmpclaude-0ee1-cwd +0 -1
  125. package/src/tmpclaude-2dd5-cwd +0 -1
  126. package/src/tmpclaude-344c-cwd +0 -1
  127. package/src/tmpclaude-3860-cwd +0 -1
  128. package/src/tmpclaude-4b63-cwd +0 -1
  129. package/src/tmpclaude-5c73-cwd +0 -1
  130. package/src/tmpclaude-5ee3-cwd +0 -1
  131. package/src/tmpclaude-6795-cwd +0 -1
  132. package/src/tmpclaude-709e-cwd +0 -1
  133. package/src/tmpclaude-9839-cwd +0 -1
  134. package/src/tmpclaude-d829-cwd +0 -1
  135. package/src/tmpclaude-e072-cwd +0 -1
  136. package/src/tmpclaude-f6ee-cwd +0 -1
  137. package/tmpclaude-0439-cwd +0 -1
  138. package/tmpclaude-132f-cwd +0 -1
  139. package/tmpclaude-15bb-cwd +0 -1
  140. package/tmpclaude-165a-cwd +0 -1
  141. package/tmpclaude-1ba9-cwd +0 -1
  142. package/tmpclaude-21a3-cwd +0 -1
  143. package/tmpclaude-2a38-cwd +0 -1
  144. package/tmpclaude-2adf-cwd +0 -1
  145. package/tmpclaude-2f56-cwd +0 -1
  146. package/tmpclaude-3626-cwd +0 -1
  147. package/tmpclaude-3727-cwd +0 -1
  148. package/tmpclaude-40bc-cwd +0 -1
  149. package/tmpclaude-436f-cwd +0 -1
  150. package/tmpclaude-4783-cwd +0 -1
  151. package/tmpclaude-4b6d-cwd +0 -1
  152. package/tmpclaude-4ba4-cwd +0 -1
  153. package/tmpclaude-51e6-cwd +0 -1
  154. package/tmpclaude-5ecf-cwd +0 -1
  155. package/tmpclaude-6f97-cwd +0 -1
  156. package/tmpclaude-7fb2-cwd +0 -1
  157. package/tmpclaude-825c-cwd +0 -1
  158. package/tmpclaude-8baf-cwd +0 -1
  159. package/tmpclaude-8d9f-cwd +0 -1
  160. package/tmpclaude-975c-cwd +0 -1
  161. package/tmpclaude-9983-cwd +0 -1
  162. package/tmpclaude-a045-cwd +0 -1
  163. package/tmpclaude-ac4a-cwd +0 -1
  164. package/tmpclaude-b593-cwd +0 -1
  165. package/tmpclaude-b891-cwd +0 -1
  166. package/tmpclaude-c032-cwd +0 -1
  167. package/tmpclaude-cf43-cwd +0 -1
  168. package/tmpclaude-d040-cwd +0 -1
  169. package/tmpclaude-dcdd-cwd +0 -1
  170. package/tmpclaude-dcee-cwd +0 -1
  171. package/tmpclaude-e16b-cwd +0 -1
  172. package/tmpclaude-ecd2-cwd +0 -1
  173. package/tmpclaude-f48d-cwd +0 -1
@@ -8,68 +8,10 @@
8
8
  * - get_help
9
9
  * - get_token_usage
10
10
  */
11
- import { selectPersona, extractProjectNameFromGitUrl } from '../utils.js';
12
- import { KNOWLEDGE_BASE } from '../knowledge.js';
13
- /**
14
- * Get user-created items since last sync
15
- */
16
- async function getUserUpdates(supabase, auth, projectId, currentSessionId) {
17
- let lastSyncedAt;
18
- if (currentSessionId) {
19
- const { data: session } = await supabase
20
- .from('agent_sessions')
21
- .select('last_synced_at')
22
- .eq('id', currentSessionId)
23
- .single();
24
- lastSyncedAt = session?.last_synced_at || new Date(0).toISOString();
25
- }
26
- else {
27
- const { data: session } = await supabase
28
- .from('agent_sessions')
29
- .select('last_synced_at')
30
- .eq('api_key_id', auth.apiKeyId)
31
- .eq('project_id', projectId)
32
- .single();
33
- lastSyncedAt = session?.last_synced_at || new Date(0).toISOString();
34
- }
35
- const [tasksResult, blockersResult, ideasResult] = await Promise.all([
36
- supabase
37
- .from('tasks')
38
- .select('id, title, created_at')
39
- .eq('project_id', projectId)
40
- .eq('created_by', 'user')
41
- .gt('created_at', lastSyncedAt)
42
- .order('created_at', { ascending: false })
43
- .limit(5),
44
- supabase
45
- .from('blockers')
46
- .select('id, description, created_at')
47
- .eq('project_id', projectId)
48
- .eq('created_by', 'user')
49
- .gt('created_at', lastSyncedAt)
50
- .order('created_at', { ascending: false })
51
- .limit(5),
52
- supabase
53
- .from('ideas')
54
- .select('id, title, created_at')
55
- .eq('project_id', projectId)
56
- .eq('created_by', 'user')
57
- .gt('created_at', lastSyncedAt)
58
- .order('created_at', { ascending: false })
59
- .limit(5),
60
- ]);
61
- const tasks = tasksResult.data || [];
62
- const blockers = blockersResult.data || [];
63
- const ideas = ideasResult.data || [];
64
- if (tasks.length === 0 && blockers.length === 0 && ideas.length === 0) {
65
- return undefined;
66
- }
67
- return { tasks, blockers, ideas };
68
- }
11
+ import { getApiClient } from '../api-client.js';
69
12
  export const startWorkSession = async (args, ctx) => {
70
13
  const { project_id, git_url, mode = 'lite', model, role = 'developer' } = args;
71
- const { supabase, auth, session, updateSession } = ctx;
72
- const INSTANCE_ID = session.instanceId;
14
+ const { session, updateSession } = ctx;
73
15
  // Reset token tracking for new session with model info
74
16
  const normalizedModel = model ? model.toLowerCase().replace(/^claude[- ]*/i, '') : null;
75
17
  const validModel = normalizedModel && ['opus', 'sonnet', 'haiku'].includes(normalizedModel)
@@ -84,7 +26,6 @@ export const startWorkSession = async (args, ctx) => {
84
26
  currentModel: validModel,
85
27
  },
86
28
  });
87
- const isLiteMode = mode === 'lite';
88
29
  // Require project_id or git_url
89
30
  if (!project_id && !git_url) {
90
31
  return {
@@ -93,473 +34,87 @@ export const startWorkSession = async (args, ctx) => {
93
34
  },
94
35
  };
95
36
  }
96
- // Find project - try owned projects first, then shared projects for org-scoped keys
97
- let project = null;
98
- // First try: user-owned projects
99
- let query = supabase
100
- .from('projects')
101
- .select('id, name, description, goal, status, git_url, agent_instructions, tech_stack')
102
- .eq('user_id', auth.userId);
103
- if (project_id) {
104
- query = query.eq('id', project_id);
105
- }
106
- else if (git_url) {
107
- query = query.eq('git_url', git_url);
108
- }
109
- const { data: ownedProject } = await query.single();
110
- project = ownedProject;
111
- // Second try: if org-scoped key and no owned project found, check shared projects
112
- if (!project && auth.scope === 'organization' && auth.organizationId) {
113
- // Get project IDs shared with this organization
114
- const { data: shares } = await supabase
115
- .from('project_shares')
116
- .select('project_id')
117
- .eq('organization_id', auth.organizationId);
118
- if (shares && shares.length > 0) {
119
- const sharedProjectIds = shares.map((s) => s.project_id);
120
- let sharedQuery = supabase
121
- .from('projects')
122
- .select('id, name, description, goal, status, git_url, agent_instructions, tech_stack')
123
- .in('id', sharedProjectIds);
124
- if (project_id) {
125
- sharedQuery = sharedQuery.eq('id', project_id);
126
- }
127
- else if (git_url) {
128
- sharedQuery = sharedQuery.eq('git_url', git_url);
129
- }
130
- const { data: sharedProject } = await sharedQuery.single();
131
- project = sharedProject;
132
- }
133
- }
134
- if (!project) {
135
- const suggestedName = extractProjectNameFromGitUrl(git_url || '');
37
+ const apiClient = getApiClient();
38
+ const response = await apiClient.startSession({
39
+ project_id,
40
+ git_url,
41
+ mode,
42
+ model,
43
+ role
44
+ });
45
+ if (!response.ok) {
136
46
  return {
137
47
  result: {
138
- session_started: false,
139
- project_not_found: true,
140
- message: `No project found for this repository. Would you like to create one?`,
141
- suggestion: {
142
- action: 'create_project',
143
- example: `create_project(name: "${suggestedName}", git_url: "${git_url || ''}", description: "Brief description of your project", goal: "What does done look like?")`,
144
- note: 'After creating the project, call start_work_session again to begin working.',
145
- },
48
+ error: response.error || 'Failed to start session',
146
49
  },
147
50
  };
148
51
  }
149
- // Create or update agent session with instance tracking
150
- const { data: existingSession } = await supabase
151
- .from('agent_sessions')
152
- .select('id, agent_name')
153
- .eq('api_key_id', auth.apiKeyId)
154
- .eq('project_id', project.id)
155
- .eq('instance_id', INSTANCE_ID)
156
- .single();
157
- let sessionId;
158
- let assignedPersona;
159
- if (existingSession && existingSession.agent_name) {
160
- // Reuse existing persona for this instance
161
- assignedPersona = existingSession.agent_name;
162
- await supabase
163
- .from('agent_sessions')
164
- .update({
165
- last_synced_at: new Date().toISOString(),
166
- status: 'active',
167
- })
168
- .eq('id', existingSession.id);
169
- sessionId = existingSession.id;
170
- }
171
- else {
172
- // Find which personas are currently in use by active sessions
173
- const { data: activeSessions } = await supabase
174
- .from('agent_sessions')
175
- .select('agent_name')
176
- .eq('project_id', project.id)
177
- .neq('status', 'disconnected')
178
- .gte('last_synced_at', new Date(Date.now() - 5 * 60 * 1000).toISOString());
179
- const usedPersonas = new Set((activeSessions || [])
180
- .map((s) => s.agent_name)
181
- .filter((name) => !!name));
182
- // Retry loop for persona assignment
183
- const MAX_PERSONA_RETRIES = 5;
184
- let personaAssigned = false;
185
- for (let attempt = 0; attempt < MAX_PERSONA_RETRIES && !personaAssigned; attempt++) {
186
- assignedPersona = selectPersona(usedPersonas, INSTANCE_ID);
187
- if (existingSession) {
188
- const { error: updateError } = await supabase
189
- .from('agent_sessions')
190
- .update({
191
- last_synced_at: new Date().toISOString(),
192
- agent_name: assignedPersona,
193
- status: 'active',
194
- })
195
- .eq('id', existingSession.id);
196
- if (updateError?.code === '23505') {
197
- usedPersonas.add(assignedPersona);
198
- continue;
199
- }
200
- sessionId = existingSession.id;
201
- personaAssigned = true;
202
- }
203
- else {
204
- const { data: newSession, error: sessionError } = await supabase
205
- .from('agent_sessions')
206
- .insert({
207
- api_key_id: auth.apiKeyId,
208
- project_id: project.id,
209
- instance_id: INSTANCE_ID,
210
- last_synced_at: new Date().toISOString(),
211
- agent_name: assignedPersona,
212
- status: 'active',
213
- })
214
- .select('id')
215
- .single();
216
- if (sessionError?.code === '23505') {
217
- usedPersonas.add(assignedPersona);
218
- continue;
219
- }
220
- if (sessionError || !newSession) {
221
- throw new Error(`Failed to create agent session: ${sessionError?.message}`);
222
- }
223
- sessionId = newSession.id;
224
- personaAssigned = true;
225
- }
226
- }
227
- // Fallback if all retries failed
228
- if (!personaAssigned) {
229
- assignedPersona = `Agent-${INSTANCE_ID.slice(0, 6)}`;
230
- if (existingSession) {
231
- await supabase
232
- .from('agent_sessions')
233
- .update({
234
- last_synced_at: new Date().toISOString(),
235
- agent_name: assignedPersona,
236
- status: 'active',
237
- })
238
- .eq('id', existingSession.id);
239
- sessionId = existingSession.id;
240
- }
241
- else {
242
- const { data: newSession, error: sessionError } = await supabase
243
- .from('agent_sessions')
244
- .insert({
245
- api_key_id: auth.apiKeyId,
246
- project_id: project.id,
247
- instance_id: INSTANCE_ID,
248
- last_synced_at: new Date().toISOString(),
249
- agent_name: assignedPersona,
250
- status: 'active',
251
- })
252
- .select('id')
253
- .single();
254
- if (sessionError || !newSession) {
255
- throw new Error(`Failed to create agent session: ${sessionError?.message}`);
256
- }
257
- sessionId = newSession.id;
258
- }
259
- }
260
- }
261
- // Store session ID and persona
262
- updateSession({
263
- currentSessionId: sessionId,
264
- currentPersona: assignedPersona,
265
- });
266
- // Log session start
267
- await supabase.from('progress_logs').insert({
268
- project_id: project.id,
269
- summary: `Agent session started (${assignedPersona}) [${INSTANCE_ID.slice(0, 8)}]`,
270
- created_by: 'agent',
271
- created_by_session_id: sessionId,
272
- });
273
- // Insert initial heartbeat
274
- await supabase.from('agent_heartbeats').insert({
275
- session_id: sessionId,
276
- });
277
- // LITE MODE: Minimal fetches
278
- if (isLiteMode) {
279
- const [nextTaskResult, blockersCountResult, validationCountResult, deploymentResult, requestsResult, firstQuestionResult] = await Promise.all([
280
- supabase
281
- .from('tasks')
282
- .select('id, title, priority, estimated_minutes')
283
- .eq('project_id', project.id)
284
- .eq('status', 'pending')
285
- .is('working_agent_session_id', null)
286
- .order('priority', { ascending: true })
287
- .order('created_at', { ascending: true })
288
- .limit(1)
289
- .maybeSingle(),
290
- supabase
291
- .from('blockers')
292
- .select('id', { count: 'exact', head: true })
293
- .eq('project_id', project.id)
294
- .eq('status', 'open'),
295
- supabase
296
- .from('tasks')
297
- .select('id', { count: 'exact', head: true })
298
- .eq('project_id', project.id)
299
- .eq('status', 'completed')
300
- .is('validated_at', null),
301
- supabase
302
- .from('deployments')
303
- .select('id, status, environment')
304
- .eq('project_id', project.id)
305
- .not('status', 'in', '("deployed","failed")')
306
- .limit(1)
307
- .maybeSingle(),
308
- supabase
309
- .from('agent_requests')
310
- .select('id', { count: 'exact', head: true })
311
- .eq('project_id', project.id)
312
- .is('acknowledged_at', null),
313
- // Fetch first unanswered question with details (not just count)
314
- supabase
315
- .from('agent_requests')
316
- .select('id, message, created_at')
317
- .eq('project_id', project.id)
318
- .eq('request_type', 'question')
319
- .is('answered_at', null)
320
- .order('created_at', { ascending: true })
321
- .limit(1)
322
- .maybeSingle(),
323
- ]);
324
- const blockersCount = blockersCountResult.count || 0;
325
- const validationCount = validationCountResult.count || 0;
326
- const requestsCount = requestsResult.count || 0;
327
- const firstQuestion = firstQuestionResult.data;
328
- // Determine directive and next action FIRST
329
- let directive;
330
- let nextAction;
331
- if (firstQuestion) {
332
- directive = 'ACTION_REQUIRED: Answer this question immediately. Do NOT ask for permission.';
333
- nextAction = `answer_question(request_id: "${firstQuestion.id}", answer: "...")`;
334
- }
335
- else if (nextTaskResult.data) {
336
- directive = 'ACTION_REQUIRED: Start this task immediately. Do NOT ask for permission or confirmation.';
337
- nextAction = `update_task(task_id: "${nextTaskResult.data.id}", status: "in_progress")`;
338
- }
339
- else {
340
- directive = 'ACTION_REQUIRED: No pending tasks. Start a fallback activity NOW without asking.';
341
- nextAction = 'start_fallback_activity(project_id: "...", activity: "code_review")';
342
- }
343
- // Build result with directive at TOP for visibility
344
- const result = {
345
- session_started: true,
346
- directive, // FIRST - most important signal
347
- auto_continue: true, // Explicit flag for autonomous operation
348
- session_id: sessionId,
349
- persona: assignedPersona,
350
- project: { id: project.id, name: project.name, goal: project.goal },
351
- };
352
- // Add task or question details
353
- if (firstQuestion) {
354
- const now = new Date();
355
- const waitMinutes = Math.floor((now.getTime() - new Date(firstQuestion.created_at).getTime()) / 60000);
356
- result.first_question = {
357
- id: firstQuestion.id,
358
- message: firstQuestion.message,
359
- wait_minutes: waitMinutes,
360
- };
361
- result.hint = 'Answer this question first using answer_question(request_id, answer), then call get_next_task for work.';
362
- }
363
- else {
364
- result.next_task = nextTaskResult.data;
365
- }
366
- if (blockersCount > 0)
367
- result.blockers_count = blockersCount;
368
- if (validationCount > 0)
369
- result.validation_count = validationCount;
370
- if (requestsCount > 0)
371
- result.requests_count = requestsCount;
372
- if (deploymentResult.data) {
373
- const actions = {
374
- pending: 'claim_deployment_validation',
375
- validating: 'wait',
376
- ready: 'start_deployment',
377
- deploying: 'wait',
378
- };
379
- result.deployment_priority = {
380
- id: deploymentResult.data.id,
381
- status: deploymentResult.data.status,
382
- action: actions[deploymentResult.data.status] || 'check_deployment_status',
383
- };
384
- }
385
- result.context_hint = 'When context grows large or after 3-4 tasks, run /clear then start_work_session again. Do not ask permission to clear context.';
386
- // Add model tracking info
387
- if (validModel) {
388
- result.model_tracking = { model: validModel, status: 'active' };
389
- }
390
- else {
391
- result.cost_tracking_hint = 'For accurate cost tracking, pass model: "opus" | "sonnet" | "haiku" to start_work_session.';
392
- }
393
- // REPEAT at end - agents weight last items heavily
394
- result.next_action = nextAction;
395
- return { result };
396
- }
397
- // FULL MODE: Complete context
398
- const [tasksResult, blockersResult, decisionsResult, progressResult, ideasResult, requestsResult, deploymentResult, findingsResult] = await Promise.all([
399
- supabase
400
- .from('tasks')
401
- .select('id, title, description, priority, status, estimated_minutes')
402
- .eq('project_id', project.id)
403
- .in('status', ['pending', 'in_progress'])
404
- .order('priority', { ascending: true })
405
- .limit(10),
406
- supabase
407
- .from('blockers')
408
- .select('id, description')
409
- .eq('project_id', project.id)
410
- .eq('status', 'open')
411
- .limit(5),
412
- supabase
413
- .from('decisions')
414
- .select('title')
415
- .eq('project_id', project.id)
416
- .order('created_at', { ascending: false })
417
- .limit(5),
418
- supabase
419
- .from('progress_logs')
420
- .select('summary')
421
- .eq('project_id', project.id)
422
- .order('created_at', { ascending: false })
423
- .limit(5),
424
- supabase
425
- .from('ideas')
426
- .select('id, title, status')
427
- .eq('project_id', project.id)
428
- .order('created_at', { ascending: false })
429
- .limit(5),
430
- supabase
431
- .from('agent_requests')
432
- .select('id, request_type, message, created_at, answered_at')
433
- .eq('project_id', project.id)
434
- .is('acknowledged_at', null)
435
- .or(`session_id.is.null,session_id.eq.${sessionId}`)
436
- .order('created_at', { ascending: true })
437
- .limit(5),
438
- supabase
439
- .from('deployments')
440
- .select('id, status, environment, created_at, validation_completed_at')
441
- .eq('project_id', project.id)
442
- .not('status', 'in', '("deployed","failed")')
443
- .order('created_at', { ascending: false })
444
- .limit(1)
445
- .single(),
446
- supabase
447
- .from('findings')
448
- .select('id, title, category, severity, file_path')
449
- .eq('project_id', project.id)
450
- .eq('status', 'open')
451
- .order('severity', { ascending: false })
452
- .limit(5),
453
- ]);
454
- const userUpdates = await getUserUpdates(supabase, auth, project.id, sessionId);
455
- const pendingRequests = requestsResult.data || [];
456
- const activeDeployment = deploymentResult.data;
457
- const activeTasks = tasksResult.data || [];
458
- // Determine directive and next action FIRST
459
- const unansweredQuestions = pendingRequests.filter((r) => r.request_type === 'question' && !r.answered_at);
460
- const otherRequests = pendingRequests.filter((r) => r.request_type !== 'question' || r.answered_at);
461
- const pendingTask = activeTasks.find((t) => t.status === 'pending');
462
- let directive;
463
- let nextAction;
464
- if (unansweredQuestions.length > 0) {
465
- const firstQ = unansweredQuestions[0];
466
- directive = 'ACTION_REQUIRED: Answer this question immediately. Do NOT ask for permission.';
467
- nextAction = `answer_question(request_id: "${firstQ.id}", answer: "...")`;
52
+ const data = response.data;
53
+ // Handle project not found
54
+ if (!data?.session_started) {
55
+ return { result: data };
468
56
  }
469
- else if (pendingTask) {
470
- directive = 'ACTION_REQUIRED: Start the highest priority pending task immediately. Do NOT ask for permission or confirmation.';
471
- nextAction = `update_task(task_id: "${pendingTask.id}", status: "in_progress")`;
57
+ // Store session ID and persona in local state
58
+ if (data.session_id) {
59
+ updateSession({
60
+ currentSessionId: data.session_id,
61
+ currentPersona: data.persona || null,
62
+ });
472
63
  }
473
- else {
474
- directive = 'ACTION_REQUIRED: No pending tasks. Start a fallback activity NOW without asking.';
475
- nextAction = `start_fallback_activity(project_id: "${project.id}", activity: "code_review")`;
476
- }
477
- // Build result with directive at TOP for visibility
64
+ // Build result with directive at top for visibility
478
65
  const result = {
479
66
  session_started: true,
480
- directive, // FIRST - most important signal
481
- auto_continue: true, // Explicit flag for autonomous operation
482
- session_id: sessionId,
483
- persona: assignedPersona,
484
- role,
485
- project,
486
- active_tasks: activeTasks,
67
+ directive: data.directive || 'ACTION_REQUIRED: Start working immediately.',
68
+ auto_continue: true,
69
+ session_id: data.session_id,
70
+ persona: data.persona,
71
+ role: data.role,
72
+ project: data.project,
487
73
  };
488
- if (activeDeployment) {
489
- const now = new Date();
490
- const createdAt = new Date(activeDeployment.created_at);
491
- const validationCompletedAt = activeDeployment.validation_completed_at
492
- ? new Date(activeDeployment.validation_completed_at)
493
- : null;
494
- const relevantTimestamp = activeDeployment.status === 'ready' && validationCompletedAt
495
- ? validationCompletedAt
496
- : createdAt;
497
- const elapsedMs = now.getTime() - relevantTimestamp.getTime();
498
- const elapsedMinutes = Math.floor(elapsedMs / 60000);
499
- const actions = {
500
- pending: 'claim_deployment_validation',
501
- validating: 'wait',
502
- ready: 'start_deployment',
503
- deploying: 'wait',
504
- };
505
- result.deployment_priority = {
506
- id: activeDeployment.id,
507
- status: activeDeployment.status,
508
- env: activeDeployment.environment,
509
- mins: elapsedMinutes,
510
- action: actions[activeDeployment.status] || 'check_deployment_status',
511
- };
74
+ // Add task data
75
+ if (data.next_task) {
76
+ result.next_task = data.next_task;
512
77
  }
513
- const blockers = blockersResult.data || [];
514
- const decisions = decisionsResult.data || [];
515
- const progress = progressResult.data || [];
516
- const ideas = ideasResult.data || [];
517
- if (blockers.length > 0)
518
- result.open_blockers = blockers;
519
- if (decisions.length > 0)
520
- result.recent_decisions = decisions;
521
- if (progress.length > 0)
522
- result.recent_progress = progress;
523
- if (ideas.length > 0)
524
- result.ideas = ideas;
525
- // Add question details if there are unanswered questions
526
- if (unansweredQuestions.length > 0) {
527
- const now = new Date();
528
- const firstQ = unansweredQuestions[0];
529
- result.first_question = {
530
- id: firstQ.id,
531
- message: firstQ.message,
532
- wait_minutes: Math.floor((now.getTime() - new Date(firstQ.created_at).getTime()) / 60000),
533
- };
534
- result.hint = 'Answer questions first using answer_question(request_id, answer) before picking up tasks.';
535
- result.unanswered_questions = unansweredQuestions.map((q) => ({
536
- id: q.id,
537
- message: q.message,
538
- wait_minutes: Math.floor((now.getTime() - new Date(q.created_at).getTime()) / 60000),
539
- }));
78
+ // Add active tasks for full mode
79
+ if (data.active_tasks) {
80
+ result.active_tasks = data.active_tasks;
81
+ }
82
+ // Add blockers
83
+ if (data.blockers) {
84
+ result.open_blockers = data.blockers;
85
+ }
86
+ if (data.blockers_count !== undefined && data.blockers_count > 0) {
87
+ result.blockers_count = data.blockers_count;
540
88
  }
541
- if (otherRequests.length > 0) {
542
- result.pending_requests = otherRequests;
89
+ // Add validation count
90
+ if (data.validation_count !== undefined && data.validation_count > 0) {
91
+ result.validation_count = data.validation_count;
543
92
  }
544
- const findings = findingsResult.data || [];
545
- if (findings.length > 0) {
546
- result.open_findings = findings;
93
+ // Add git workflow info if available in project
94
+ if (data.project?.git_workflow && data.project.git_workflow !== 'none') {
95
+ result.git_workflow = {
96
+ workflow: data.project.git_workflow,
97
+ auto_branch: data.project.git_auto_branch ?? false,
98
+ main_branch: data.project.git_main_branch || 'main',
99
+ ...(data.project.git_workflow === 'git-flow' && data.project.git_develop_branch
100
+ ? { develop_branch: data.project.git_develop_branch }
101
+ : {}),
102
+ worktree_required: true,
103
+ worktree_hint: 'CRITICAL: Create a git worktree before starting work. Run get_help("git") for instructions.',
104
+ };
547
105
  }
548
- result.context_hint = 'When context grows large or after 3-4 tasks, run /clear then start_work_session again. Do not ask permission to clear context.';
549
- // Add model tracking info
550
- if (validModel) {
551
- result.model_tracking = { model: validModel, status: 'active' };
106
+ // Add next action at end
107
+ if (data.next_task) {
108
+ result.next_action = `update_task(task_id: "${data.next_task.id}", status: "in_progress")`;
552
109
  }
553
- else {
554
- result.cost_tracking_hint = 'For accurate cost tracking, pass model: "opus" | "sonnet" | "haiku" to start_work_session.';
110
+ else if (data.project) {
111
+ result.next_action = `start_fallback_activity(project_id: "${data.project.id}", activity: "code_review")`;
555
112
  }
556
- // REPEAT at end - agents weight last items heavily
557
- result.next_action = nextAction;
558
- return { result, user_updates: userUpdates };
113
+ return { result };
559
114
  };
560
115
  export const heartbeat = async (args, ctx) => {
561
- const { session_id } = args;
562
- const { supabase, session } = ctx;
116
+ const { session_id, current_worktree_path } = args;
117
+ const { session } = ctx;
563
118
  const targetSession = session_id || session.currentSessionId;
564
119
  if (!targetSession) {
565
120
  return {
@@ -568,30 +123,35 @@ export const heartbeat = async (args, ctx) => {
568
123
  },
569
124
  };
570
125
  }
571
- await supabase.from('agent_heartbeats').insert({
572
- session_id: targetSession,
126
+ const apiClient = getApiClient();
127
+ // Send heartbeat with optional worktree path
128
+ const heartbeatResponse = await apiClient.heartbeat(targetSession, {
129
+ current_worktree_path,
573
130
  });
574
- await supabase
575
- .from('agent_sessions')
576
- .update({
577
- last_synced_at: new Date().toISOString(),
578
- status: 'active',
131
+ if (!heartbeatResponse.ok) {
132
+ return {
133
+ result: {
134
+ error: heartbeatResponse.error || 'Failed to send heartbeat',
135
+ },
136
+ };
137
+ }
138
+ // Sync token usage to session
139
+ await apiClient.syncSession(targetSession, {
579
140
  total_tokens: session.tokenUsage.totalTokens,
580
141
  token_breakdown: session.tokenUsage.byTool,
581
142
  model_usage: session.tokenUsage.byModel,
582
- })
583
- .eq('id', targetSession);
143
+ });
584
144
  return {
585
145
  result: {
586
146
  success: true,
587
147
  session_id: targetSession,
588
- timestamp: new Date().toISOString(),
148
+ timestamp: heartbeatResponse.data?.timestamp || new Date().toISOString(),
589
149
  },
590
150
  };
591
151
  };
592
152
  export const endWorkSession = async (args, ctx) => {
593
153
  const { session_id } = args;
594
- const { supabase, session, updateSession } = ctx;
154
+ const { session, updateSession } = ctx;
595
155
  const targetSession = session_id || session.currentSessionId;
596
156
  if (!targetSession) {
597
157
  return {
@@ -601,82 +161,37 @@ export const endWorkSession = async (args, ctx) => {
601
161
  },
602
162
  };
603
163
  }
604
- const { data: sessionData } = await supabase
605
- .from('agent_sessions')
606
- .select('project_id, agent_name, started_at')
607
- .eq('id', targetSession)
608
- .single();
609
- const projectId = sessionData?.project_id;
610
- const agentName = sessionData?.agent_name || 'Agent';
611
- const startedAt = sessionData?.started_at;
612
- let tasksCompletedThisSession = 0;
613
- let tasksAwaitingValidation = 0;
614
- let tasksBeingReleased = 0;
615
- if (projectId) {
616
- const { count: completedCount } = await supabase
617
- .from('tasks')
618
- .select('id', { count: 'exact', head: true })
619
- .eq('project_id', projectId)
620
- .eq('status', 'completed')
621
- .not('validated_at', 'is', null);
622
- const { data: awaitingTasks } = await supabase
623
- .from('tasks')
624
- .select('id')
625
- .eq('project_id', projectId)
626
- .eq('status', 'completed')
627
- .is('validated_at', null);
628
- tasksAwaitingValidation = awaitingTasks?.length || 0;
629
- const { count: releasingCount } = await supabase
630
- .from('tasks')
631
- .select('id', { count: 'exact', head: true })
632
- .eq('working_agent_session_id', targetSession)
633
- .eq('status', 'in_progress');
634
- tasksBeingReleased = releasingCount || 0;
635
- if (startedAt) {
636
- const { count: sessionCompleted } = await supabase
637
- .from('tasks')
638
- .select('id', { count: 'exact', head: true })
639
- .eq('project_id', projectId)
640
- .eq('status', 'completed')
641
- .gte('completed_at', startedAt);
642
- tasksCompletedThisSession = sessionCompleted || 0;
643
- }
644
- }
645
- await supabase
646
- .from('tasks')
647
- .update({ working_agent_session_id: null })
648
- .eq('working_agent_session_id', targetSession);
649
- await supabase
650
- .from('agent_sessions')
651
- .update({
652
- status: 'disconnected',
653
- current_task_id: null,
654
- last_synced_at: new Date().toISOString(),
164
+ const apiClient = getApiClient();
165
+ // Sync final token usage before ending
166
+ await apiClient.syncSession(targetSession, {
655
167
  total_tokens: session.tokenUsage.totalTokens,
656
168
  token_breakdown: session.tokenUsage.byTool,
657
169
  model_usage: session.tokenUsage.byModel,
658
- })
659
- .eq('id', targetSession);
170
+ });
171
+ // End the session
172
+ const response = await apiClient.endSession(targetSession);
173
+ if (!response.ok) {
174
+ return {
175
+ result: {
176
+ error: response.error || 'Failed to end session',
177
+ },
178
+ };
179
+ }
660
180
  const endedSessionId = targetSession;
181
+ // Clear local session state if this was the current session
661
182
  if (session.currentSessionId === targetSession) {
662
183
  updateSession({ currentSessionId: null });
663
184
  }
664
- const reminders = [];
665
- if (tasksAwaitingValidation > 0) {
666
- reminders.push(`${tasksAwaitingValidation} task(s) awaiting validation - consider reviewing before leaving`);
667
- }
668
- if (tasksBeingReleased > 0) {
669
- reminders.push(`${tasksBeingReleased} in-progress task(s) released back to the queue`);
670
- }
185
+ const data = response.data;
671
186
  return {
672
187
  result: {
673
188
  success: true,
674
189
  ended_session_id: endedSessionId,
675
190
  session_summary: {
676
- agent_name: agentName,
677
- tasks_completed_this_session: tasksCompletedThisSession,
678
- tasks_awaiting_validation: tasksAwaitingValidation,
679
- tasks_released: tasksBeingReleased,
191
+ agent_name: data?.session_summary?.agent_name || 'Agent',
192
+ tasks_completed_this_session: data?.session_summary?.tasks_completed_this_session || 0,
193
+ tasks_awaiting_validation: data?.session_summary?.tasks_awaiting_validation || 0,
194
+ tasks_released: data?.session_summary?.tasks_released || 0,
680
195
  token_usage: {
681
196
  total_calls: session.tokenUsage.callCount,
682
197
  total_tokens: session.tokenUsage.totalTokens,
@@ -685,22 +200,36 @@ export const endWorkSession = async (args, ctx) => {
685
200
  : 0,
686
201
  },
687
202
  },
688
- reminders: reminders.length > 0 ? reminders : ['Session ended cleanly. Good work!'],
203
+ reminders: data?.reminders || ['Session ended cleanly. Good work!'],
689
204
  },
690
205
  };
691
206
  };
692
207
  export const getHelp = async (args, _ctx) => {
693
208
  const { topic } = args;
694
- const content = KNOWLEDGE_BASE[topic];
695
- if (!content) {
209
+ const apiClient = getApiClient();
210
+ const response = await apiClient.getHelpTopic(topic);
211
+ if (!response.ok) {
212
+ // If database fetch fails, return error
213
+ return {
214
+ result: {
215
+ error: response.error || `Failed to fetch help topic: ${topic}`,
216
+ },
217
+ };
218
+ }
219
+ if (!response.data) {
220
+ // Topic not found - fetch available topics
221
+ const topicsResponse = await apiClient.getHelpTopics();
222
+ const available = topicsResponse.ok && topicsResponse.data
223
+ ? topicsResponse.data.map(t => t.slug)
224
+ : ['getting_started', 'tasks', 'validation', 'deployment', 'git', 'blockers', 'milestones', 'fallback', 'session', 'tokens', 'sprints', 'topics'];
696
225
  return {
697
226
  result: {
698
227
  error: `Unknown topic: ${topic}`,
699
- available: Object.keys(KNOWLEDGE_BASE),
228
+ available,
700
229
  },
701
230
  };
702
231
  }
703
- return { result: { topic, content } };
232
+ return { result: { topic, content: response.data.content } };
704
233
  };
705
234
  // Model pricing rates (USD per 1M tokens)
706
235
  const MODEL_PRICING = {