@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.
Files changed (170) hide show
  1. package/README.md +113 -98
  2. package/dist/api-client.d.ts +1114 -0
  3. package/dist/api-client.js +698 -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 +106 -476
  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 +112 -828
  16. package/dist/handlers/discovery.js +31 -0
  17. package/dist/handlers/fallback.d.ts +2 -0
  18. package/dist/handlers/fallback.js +39 -134
  19. package/dist/handlers/findings.js +43 -67
  20. package/dist/handlers/git-issues.d.ts +9 -13
  21. package/dist/handlers/git-issues.js +80 -225
  22. package/dist/handlers/ideas.d.ts +3 -0
  23. package/dist/handlers/ideas.js +53 -134
  24. package/dist/handlers/index.d.ts +2 -0
  25. package/dist/handlers/index.js +6 -0
  26. package/dist/handlers/milestones.d.ts +2 -0
  27. package/dist/handlers/milestones.js +51 -98
  28. package/dist/handlers/organizations.js +79 -275
  29. package/dist/handlers/progress.d.ts +2 -0
  30. package/dist/handlers/progress.js +25 -123
  31. package/dist/handlers/project.js +42 -221
  32. package/dist/handlers/requests.d.ts +2 -0
  33. package/dist/handlers/requests.js +23 -83
  34. package/dist/handlers/session.js +99 -585
  35. package/dist/handlers/sprints.d.ts +32 -0
  36. package/dist/handlers/sprints.js +274 -0
  37. package/dist/handlers/tasks.d.ts +7 -10
  38. package/dist/handlers/tasks.js +230 -900
  39. package/dist/handlers/tool-docs.d.ts +8 -0
  40. package/dist/handlers/tool-docs.js +657 -0
  41. package/dist/handlers/types.d.ts +11 -3
  42. package/dist/handlers/validation.d.ts +1 -1
  43. package/dist/handlers/validation.js +26 -153
  44. package/dist/index.js +473 -160
  45. package/dist/knowledge.js +106 -9
  46. package/dist/tools.js +4 -0
  47. package/dist/validators.d.ts +21 -0
  48. package/dist/validators.js +91 -0
  49. package/package.json +2 -3
  50. package/src/api-client.ts +1752 -0
  51. package/src/cli.test.ts +128 -302
  52. package/src/cli.ts +41 -285
  53. package/src/handlers/__test-setup__.ts +210 -0
  54. package/src/handlers/__test-utils__.ts +4 -134
  55. package/src/handlers/blockers.test.ts +114 -124
  56. package/src/handlers/blockers.ts +68 -70
  57. package/src/handlers/bodies-of-work.test.ts +236 -831
  58. package/src/handlers/bodies-of-work.ts +194 -525
  59. package/src/handlers/cost.test.ts +149 -113
  60. package/src/handlers/cost.ts +44 -132
  61. package/src/handlers/decisions.test.ts +111 -209
  62. package/src/handlers/decisions.ts +35 -27
  63. package/src/handlers/deployment.test.ts +193 -239
  64. package/src/handlers/deployment.ts +140 -895
  65. package/src/handlers/discovery.test.ts +20 -67
  66. package/src/handlers/discovery.ts +32 -0
  67. package/src/handlers/fallback.test.ts +128 -361
  68. package/src/handlers/fallback.ts +62 -148
  69. package/src/handlers/findings.test.ts +127 -345
  70. package/src/handlers/findings.ts +49 -66
  71. package/src/handlers/git-issues.test.ts +623 -0
  72. package/src/handlers/git-issues.ts +174 -0
  73. package/src/handlers/ideas.test.ts +229 -343
  74. package/src/handlers/ideas.ts +69 -143
  75. package/src/handlers/index.ts +6 -0
  76. package/src/handlers/milestones.test.ts +167 -281
  77. package/src/handlers/milestones.ts +54 -93
  78. package/src/handlers/organizations.test.ts +275 -467
  79. package/src/handlers/organizations.ts +84 -294
  80. package/src/handlers/progress.test.ts +112 -218
  81. package/src/handlers/progress.ts +29 -142
  82. package/src/handlers/project.test.ts +203 -226
  83. package/src/handlers/project.ts +48 -238
  84. package/src/handlers/requests.test.ts +74 -342
  85. package/src/handlers/requests.ts +25 -83
  86. package/src/handlers/session.test.ts +241 -206
  87. package/src/handlers/session.ts +110 -657
  88. package/src/handlers/sprints.test.ts +711 -0
  89. package/src/handlers/sprints.ts +497 -0
  90. package/src/handlers/tasks.test.ts +608 -353
  91. package/src/handlers/tasks.ts +248 -1025
  92. package/src/handlers/types.ts +12 -4
  93. package/src/handlers/validation.test.ts +189 -572
  94. package/src/handlers/validation.ts +29 -166
  95. package/src/index.ts +473 -184
  96. package/src/knowledge.ts +107 -9
  97. package/src/tools.ts +2506 -0
  98. package/src/validators.test.ts +223 -223
  99. package/src/validators.ts +127 -0
  100. package/tsconfig.json +1 -1
  101. package/vitest.config.ts +14 -13
  102. package/dist/cli.test.d.ts +0 -1
  103. package/dist/cli.test.js +0 -367
  104. package/dist/handlers/__test-utils__.d.ts +0 -72
  105. package/dist/handlers/__test-utils__.js +0 -176
  106. package/dist/handlers/checkouts.d.ts +0 -37
  107. package/dist/handlers/checkouts.js +0 -377
  108. package/dist/handlers/knowledge-query.d.ts +0 -22
  109. package/dist/handlers/knowledge-query.js +0 -253
  110. package/dist/handlers/knowledge.d.ts +0 -12
  111. package/dist/handlers/knowledge.js +0 -108
  112. package/dist/handlers/roles.d.ts +0 -30
  113. package/dist/handlers/roles.js +0 -281
  114. package/dist/handlers/tasks.test.d.ts +0 -1
  115. package/dist/handlers/tasks.test.js +0 -431
  116. package/dist/utils.test.d.ts +0 -1
  117. package/dist/utils.test.js +0 -532
  118. package/dist/validators.test.d.ts +0 -1
  119. package/dist/validators.test.js +0 -176
  120. package/src/tmpclaude-0078-cwd +0 -1
  121. package/src/tmpclaude-0ee1-cwd +0 -1
  122. package/src/tmpclaude-2dd5-cwd +0 -1
  123. package/src/tmpclaude-344c-cwd +0 -1
  124. package/src/tmpclaude-3860-cwd +0 -1
  125. package/src/tmpclaude-4b63-cwd +0 -1
  126. package/src/tmpclaude-5c73-cwd +0 -1
  127. package/src/tmpclaude-5ee3-cwd +0 -1
  128. package/src/tmpclaude-6795-cwd +0 -1
  129. package/src/tmpclaude-709e-cwd +0 -1
  130. package/src/tmpclaude-9839-cwd +0 -1
  131. package/src/tmpclaude-d829-cwd +0 -1
  132. package/src/tmpclaude-e072-cwd +0 -1
  133. package/src/tmpclaude-f6ee-cwd +0 -1
  134. package/tmpclaude-0439-cwd +0 -1
  135. package/tmpclaude-132f-cwd +0 -1
  136. package/tmpclaude-15bb-cwd +0 -1
  137. package/tmpclaude-165a-cwd +0 -1
  138. package/tmpclaude-1ba9-cwd +0 -1
  139. package/tmpclaude-21a3-cwd +0 -1
  140. package/tmpclaude-2a38-cwd +0 -1
  141. package/tmpclaude-2adf-cwd +0 -1
  142. package/tmpclaude-2f56-cwd +0 -1
  143. package/tmpclaude-3626-cwd +0 -1
  144. package/tmpclaude-3727-cwd +0 -1
  145. package/tmpclaude-40bc-cwd +0 -1
  146. package/tmpclaude-436f-cwd +0 -1
  147. package/tmpclaude-4783-cwd +0 -1
  148. package/tmpclaude-4b6d-cwd +0 -1
  149. package/tmpclaude-4ba4-cwd +0 -1
  150. package/tmpclaude-51e6-cwd +0 -1
  151. package/tmpclaude-5ecf-cwd +0 -1
  152. package/tmpclaude-6f97-cwd +0 -1
  153. package/tmpclaude-7fb2-cwd +0 -1
  154. package/tmpclaude-825c-cwd +0 -1
  155. package/tmpclaude-8baf-cwd +0 -1
  156. package/tmpclaude-8d9f-cwd +0 -1
  157. package/tmpclaude-975c-cwd +0 -1
  158. package/tmpclaude-9983-cwd +0 -1
  159. package/tmpclaude-a045-cwd +0 -1
  160. package/tmpclaude-ac4a-cwd +0 -1
  161. package/tmpclaude-b593-cwd +0 -1
  162. package/tmpclaude-b891-cwd +0 -1
  163. package/tmpclaude-c032-cwd +0 -1
  164. package/tmpclaude-cf43-cwd +0 -1
  165. package/tmpclaude-d040-cwd +0 -1
  166. package/tmpclaude-dcdd-cwd +0 -1
  167. package/tmpclaude-dcee-cwd +0 -1
  168. package/tmpclaude-e16b-cwd +0 -1
  169. package/tmpclaude-ecd2-cwd +0 -1
  170. package/tmpclaude-f48d-cwd +0 -1
@@ -9,76 +9,9 @@
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';
12
+ import type { Handler, HandlerRegistry, TokenUsage } from './types.js';
15
13
  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
- }
14
+ import { getApiClient } from '../api-client.js';
82
15
 
83
16
  export const startWorkSession: Handler = async (args, ctx) => {
84
17
  const { project_id, git_url, mode = 'lite', model, role = 'developer' } = args as {
@@ -89,8 +22,7 @@ export const startWorkSession: Handler = async (args, ctx) => {
89
22
  role?: 'developer' | 'validator' | 'deployer' | 'reviewer' | 'maintainer';
90
23
  };
91
24
 
92
- const { supabase, auth, session, updateSession } = ctx;
93
- const INSTANCE_ID = session.instanceId;
25
+ const { session, updateSession } = ctx;
94
26
 
95
27
  // Reset token tracking for new session with model info
96
28
  const normalizedModel = model ? model.toLowerCase().replace(/^claude[- ]*/i, '') : null;
@@ -108,8 +40,6 @@ export const startWorkSession: Handler = async (args, ctx) => {
108
40
  },
109
41
  });
110
42
 
111
- const isLiteMode = mode === 'lite';
112
-
113
43
  // Require project_id or git_url
114
44
  if (!project_id && !git_url) {
115
45
  return {
@@ -119,528 +49,99 @@ export const startWorkSession: Handler = async (args, ctx) => {
119
49
  };
120
50
  }
121
51
 
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 || '');
52
+ const apiClient = getApiClient();
53
+ const response = await apiClient.startSession({
54
+ project_id,
55
+ git_url,
56
+ mode,
57
+ model,
58
+ role
59
+ });
178
60
 
61
+ if (!response.ok) {
179
62
  return {
180
63
  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
- },
64
+ error: response.error || 'Failed to start session',
189
65
  },
190
66
  };
191
67
  }
192
68
 
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
- }
280
-
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
- }
314
- }
315
-
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;
69
+ const data = response.data;
457
70
 
458
- return { result };
71
+ // Handle project not found
72
+ if (!data?.session_started) {
73
+ return { result: data };
459
74
  }
460
75
 
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")`;
76
+ // Store session ID and persona in local state
77
+ if (data.session_id) {
78
+ updateSession({
79
+ currentSessionId: data.session_id,
80
+ currentPersona: data.persona || null,
81
+ });
548
82
  }
549
83
 
550
- // Build result with directive at TOP for visibility
84
+ // Build result with directive at top for visibility
551
85
  const result: Record<string, unknown> = {
552
86
  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,
87
+ directive: data.directive || 'ACTION_REQUIRED: Start working immediately.',
88
+ auto_continue: true,
89
+ session_id: data.session_id,
90
+ persona: data.persona,
91
+ role: data.role,
92
+ project: data.project,
560
93
  };
561
94
 
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
- };
95
+ // Add task data
96
+ if (data.next_task) {
97
+ result.next_task = data.next_task;
98
+ }
581
99
 
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
- };
100
+ // Add active tasks for full mode
101
+ if (data.active_tasks) {
102
+ result.active_tasks = data.active_tasks;
589
103
  }
590
104
 
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
- }));
105
+ // Add blockers
106
+ if (data.blockers) {
107
+ result.open_blockers = data.blockers;
616
108
  }
617
- if (otherRequests.length > 0) {
618
- result.pending_requests = otherRequests;
109
+ if (data.blockers_count !== undefined && data.blockers_count > 0) {
110
+ result.blockers_count = data.blockers_count;
619
111
  }
620
112
 
621
- const findings = findingsResult.data || [];
622
- if (findings.length > 0) {
623
- result.open_findings = findings;
113
+ // Add validation count
114
+ if (data.validation_count !== undefined && data.validation_count > 0) {
115
+ result.validation_count = data.validation_count;
624
116
  }
625
117
 
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.';
118
+ // Add git workflow info if available in project
119
+ if (data.project?.git_workflow && data.project.git_workflow !== 'none') {
120
+ result.git_workflow = {
121
+ workflow: data.project.git_workflow,
122
+ auto_branch: data.project.git_auto_branch ?? false,
123
+ main_branch: data.project.git_main_branch || 'main',
124
+ ...(data.project.git_workflow === 'git-flow' && data.project.git_develop_branch
125
+ ? { develop_branch: data.project.git_develop_branch }
126
+ : {}),
127
+ worktree_required: true,
128
+ worktree_hint: 'CRITICAL: Create a git worktree before starting work. Run get_help("git") for instructions.',
129
+ };
633
130
  }
634
131
 
635
- // REPEAT at end - agents weight last items heavily
636
- result.next_action = nextAction;
132
+ // Add next action at end
133
+ if (data.next_task) {
134
+ result.next_action = `update_task(task_id: "${data.next_task.id}", status: "in_progress")`;
135
+ } else if (data.project) {
136
+ result.next_action = `start_fallback_activity(project_id: "${data.project.id}", activity: "code_review")`;
137
+ }
637
138
 
638
- return { result, user_updates: userUpdates } as { result: Record<string, unknown>; user_updates?: UserUpdates };
139
+ return { result };
639
140
  };
640
141
 
641
142
  export const heartbeat: Handler = async (args, ctx) => {
642
143
  const { session_id } = args as { session_id?: string };
643
- const { supabase, session } = ctx;
144
+ const { session } = ctx;
644
145
  const targetSession = session_id || session.currentSessionId;
645
146
 
646
147
  if (!targetSession) {
@@ -651,33 +152,38 @@ export const heartbeat: Handler = async (args, ctx) => {
651
152
  };
652
153
  }
653
154
 
654
- await supabase.from('agent_heartbeats').insert({
655
- session_id: targetSession,
656
- });
155
+ const apiClient = getApiClient();
657
156
 
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);
157
+ // Send heartbeat
158
+ const heartbeatResponse = await apiClient.heartbeat(targetSession);
159
+
160
+ if (!heartbeatResponse.ok) {
161
+ return {
162
+ result: {
163
+ error: heartbeatResponse.error || 'Failed to send heartbeat',
164
+ },
165
+ };
166
+ }
167
+
168
+ // Sync token usage to session
169
+ await apiClient.syncSession(targetSession, {
170
+ total_tokens: session.tokenUsage.totalTokens,
171
+ token_breakdown: session.tokenUsage.byTool,
172
+ model_usage: session.tokenUsage.byModel,
173
+ });
668
174
 
669
175
  return {
670
176
  result: {
671
177
  success: true,
672
178
  session_id: targetSession,
673
- timestamp: new Date().toISOString(),
179
+ timestamp: heartbeatResponse.data?.timestamp || new Date().toISOString(),
674
180
  },
675
181
  };
676
182
  };
677
183
 
678
184
  export const endWorkSession: Handler = async (args, ctx) => {
679
185
  const { session_id } = args as { session_id?: string };
680
- const { supabase, session, updateSession } = ctx;
186
+ const { session, updateSession } = ctx;
681
187
  const targetSession = session_id || session.currentSessionId;
682
188
 
683
189
  if (!targetSession) {
@@ -689,97 +195,44 @@ export const endWorkSession: Handler = async (args, ctx) => {
689
195
  };
690
196
  }
691
197
 
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
- }
198
+ const apiClient = getApiClient();
199
+
200
+ // Sync final token usage before ending
201
+ await apiClient.syncSession(targetSession, {
202
+ total_tokens: session.tokenUsage.totalTokens,
203
+ token_breakdown: session.tokenUsage.byTool,
204
+ model_usage: session.tokenUsage.byModel,
205
+ });
742
206
 
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);
207
+ // End the session
208
+ const response = await apiClient.endSession(targetSession);
209
+
210
+ if (!response.ok) {
211
+ return {
212
+ result: {
213
+ error: response.error || 'Failed to end session',
214
+ },
215
+ };
216
+ }
759
217
 
760
218
  const endedSessionId = targetSession;
761
219
 
220
+ // Clear local session state if this was the current session
762
221
  if (session.currentSessionId === targetSession) {
763
222
  updateSession({ currentSessionId: null });
764
223
  }
765
224
 
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
- }
225
+ const data = response.data;
773
226
 
774
227
  return {
775
228
  result: {
776
229
  success: true,
777
230
  ended_session_id: endedSessionId,
778
231
  session_summary: {
779
- agent_name: agentName,
780
- tasks_completed_this_session: tasksCompletedThisSession,
781
- tasks_awaiting_validation: tasksAwaitingValidation,
782
- tasks_released: tasksBeingReleased,
232
+ agent_name: data?.session_summary?.agent_name || 'Agent',
233
+ tasks_completed_this_session: data?.session_summary?.tasks_completed_this_session || 0,
234
+ tasks_awaiting_validation: data?.session_summary?.tasks_awaiting_validation || 0,
235
+ tasks_released: data?.session_summary?.tasks_released || 0,
783
236
  token_usage: {
784
237
  total_calls: session.tokenUsage.callCount,
785
238
  total_tokens: session.tokenUsage.totalTokens,
@@ -788,7 +241,7 @@ export const endWorkSession: Handler = async (args, ctx) => {
788
241
  : 0,
789
242
  },
790
243
  },
791
- reminders: reminders.length > 0 ? reminders : ['Session ended cleanly. Good work!'],
244
+ reminders: data?.reminders || ['Session ended cleanly. Good work!'],
792
245
  },
793
246
  };
794
247
  };