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