@vibescope/mcp-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +34 -0
  3. package/dist/cli.js +356 -0
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +367 -0
  6. package/dist/handlers/__test-utils__.d.ts +72 -0
  7. package/dist/handlers/__test-utils__.js +176 -0
  8. package/dist/handlers/blockers.d.ts +18 -0
  9. package/dist/handlers/blockers.js +81 -0
  10. package/dist/handlers/bodies-of-work.d.ts +34 -0
  11. package/dist/handlers/bodies-of-work.js +614 -0
  12. package/dist/handlers/checkouts.d.ts +37 -0
  13. package/dist/handlers/checkouts.js +377 -0
  14. package/dist/handlers/cost.d.ts +39 -0
  15. package/dist/handlers/cost.js +247 -0
  16. package/dist/handlers/decisions.d.ts +16 -0
  17. package/dist/handlers/decisions.js +64 -0
  18. package/dist/handlers/deployment.d.ts +36 -0
  19. package/dist/handlers/deployment.js +1062 -0
  20. package/dist/handlers/discovery.d.ts +14 -0
  21. package/dist/handlers/discovery.js +870 -0
  22. package/dist/handlers/fallback.d.ts +18 -0
  23. package/dist/handlers/fallback.js +216 -0
  24. package/dist/handlers/findings.d.ts +18 -0
  25. package/dist/handlers/findings.js +110 -0
  26. package/dist/handlers/git-issues.d.ts +22 -0
  27. package/dist/handlers/git-issues.js +247 -0
  28. package/dist/handlers/ideas.d.ts +19 -0
  29. package/dist/handlers/ideas.js +188 -0
  30. package/dist/handlers/index.d.ts +29 -0
  31. package/dist/handlers/index.js +65 -0
  32. package/dist/handlers/knowledge-query.d.ts +22 -0
  33. package/dist/handlers/knowledge-query.js +253 -0
  34. package/dist/handlers/knowledge.d.ts +12 -0
  35. package/dist/handlers/knowledge.js +108 -0
  36. package/dist/handlers/milestones.d.ts +20 -0
  37. package/dist/handlers/milestones.js +179 -0
  38. package/dist/handlers/organizations.d.ts +36 -0
  39. package/dist/handlers/organizations.js +428 -0
  40. package/dist/handlers/progress.d.ts +14 -0
  41. package/dist/handlers/progress.js +149 -0
  42. package/dist/handlers/project.d.ts +20 -0
  43. package/dist/handlers/project.js +278 -0
  44. package/dist/handlers/requests.d.ts +16 -0
  45. package/dist/handlers/requests.js +131 -0
  46. package/dist/handlers/roles.d.ts +30 -0
  47. package/dist/handlers/roles.js +281 -0
  48. package/dist/handlers/session.d.ts +20 -0
  49. package/dist/handlers/session.js +791 -0
  50. package/dist/handlers/tasks.d.ts +52 -0
  51. package/dist/handlers/tasks.js +1111 -0
  52. package/dist/handlers/tasks.test.d.ts +1 -0
  53. package/dist/handlers/tasks.test.js +431 -0
  54. package/dist/handlers/types.d.ts +94 -0
  55. package/dist/handlers/types.js +1 -0
  56. package/dist/handlers/validation.d.ts +16 -0
  57. package/dist/handlers/validation.js +188 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2707 -0
  60. package/dist/knowledge.d.ts +6 -0
  61. package/dist/knowledge.js +121 -0
  62. package/dist/tools.d.ts +2 -0
  63. package/dist/tools.js +2498 -0
  64. package/dist/utils.d.ts +149 -0
  65. package/dist/utils.js +317 -0
  66. package/dist/utils.test.d.ts +1 -0
  67. package/dist/utils.test.js +532 -0
  68. package/dist/validators.d.ts +35 -0
  69. package/dist/validators.js +111 -0
  70. package/dist/validators.test.d.ts +1 -0
  71. package/dist/validators.test.js +176 -0
  72. package/package.json +44 -0
  73. package/src/cli.test.ts +442 -0
  74. package/src/cli.ts +439 -0
  75. package/src/handlers/__test-utils__.ts +217 -0
  76. package/src/handlers/blockers.test.ts +390 -0
  77. package/src/handlers/blockers.ts +110 -0
  78. package/src/handlers/bodies-of-work.test.ts +1276 -0
  79. package/src/handlers/bodies-of-work.ts +783 -0
  80. package/src/handlers/cost.test.ts +436 -0
  81. package/src/handlers/cost.ts +322 -0
  82. package/src/handlers/decisions.test.ts +401 -0
  83. package/src/handlers/decisions.ts +86 -0
  84. package/src/handlers/deployment.test.ts +516 -0
  85. package/src/handlers/deployment.ts +1289 -0
  86. package/src/handlers/discovery.test.ts +254 -0
  87. package/src/handlers/discovery.ts +969 -0
  88. package/src/handlers/fallback.test.ts +687 -0
  89. package/src/handlers/fallback.ts +260 -0
  90. package/src/handlers/findings.test.ts +565 -0
  91. package/src/handlers/findings.ts +153 -0
  92. package/src/handlers/ideas.test.ts +753 -0
  93. package/src/handlers/ideas.ts +247 -0
  94. package/src/handlers/index.ts +69 -0
  95. package/src/handlers/milestones.test.ts +584 -0
  96. package/src/handlers/milestones.ts +217 -0
  97. package/src/handlers/organizations.test.ts +997 -0
  98. package/src/handlers/organizations.ts +550 -0
  99. package/src/handlers/progress.test.ts +369 -0
  100. package/src/handlers/progress.ts +188 -0
  101. package/src/handlers/project.test.ts +562 -0
  102. package/src/handlers/project.ts +352 -0
  103. package/src/handlers/requests.test.ts +531 -0
  104. package/src/handlers/requests.ts +150 -0
  105. package/src/handlers/session.test.ts +459 -0
  106. package/src/handlers/session.ts +912 -0
  107. package/src/handlers/tasks.test.ts +602 -0
  108. package/src/handlers/tasks.ts +1393 -0
  109. package/src/handlers/types.ts +88 -0
  110. package/src/handlers/validation.test.ts +880 -0
  111. package/src/handlers/validation.ts +223 -0
  112. package/src/index.ts +3205 -0
  113. package/src/knowledge.ts +132 -0
  114. package/src/tmpclaude-0078-cwd +1 -0
  115. package/src/tmpclaude-0ee1-cwd +1 -0
  116. package/src/tmpclaude-2dd5-cwd +1 -0
  117. package/src/tmpclaude-344c-cwd +1 -0
  118. package/src/tmpclaude-3860-cwd +1 -0
  119. package/src/tmpclaude-4b63-cwd +1 -0
  120. package/src/tmpclaude-5c73-cwd +1 -0
  121. package/src/tmpclaude-5ee3-cwd +1 -0
  122. package/src/tmpclaude-6795-cwd +1 -0
  123. package/src/tmpclaude-709e-cwd +1 -0
  124. package/src/tmpclaude-9839-cwd +1 -0
  125. package/src/tmpclaude-d829-cwd +1 -0
  126. package/src/tmpclaude-e072-cwd +1 -0
  127. package/src/tmpclaude-f6ee-cwd +1 -0
  128. package/src/utils.test.ts +681 -0
  129. package/src/utils.ts +375 -0
  130. package/src/validators.test.ts +223 -0
  131. package/src/validators.ts +122 -0
  132. package/tmpclaude-0439-cwd +1 -0
  133. package/tmpclaude-132f-cwd +1 -0
  134. package/tmpclaude-15bb-cwd +1 -0
  135. package/tmpclaude-165a-cwd +1 -0
  136. package/tmpclaude-1ba9-cwd +1 -0
  137. package/tmpclaude-21a3-cwd +1 -0
  138. package/tmpclaude-2a38-cwd +1 -0
  139. package/tmpclaude-2adf-cwd +1 -0
  140. package/tmpclaude-2f56-cwd +1 -0
  141. package/tmpclaude-3626-cwd +1 -0
  142. package/tmpclaude-3727-cwd +1 -0
  143. package/tmpclaude-40bc-cwd +1 -0
  144. package/tmpclaude-436f-cwd +1 -0
  145. package/tmpclaude-4783-cwd +1 -0
  146. package/tmpclaude-4b6d-cwd +1 -0
  147. package/tmpclaude-4ba4-cwd +1 -0
  148. package/tmpclaude-51e6-cwd +1 -0
  149. package/tmpclaude-5ecf-cwd +1 -0
  150. package/tmpclaude-6f97-cwd +1 -0
  151. package/tmpclaude-7fb2-cwd +1 -0
  152. package/tmpclaude-825c-cwd +1 -0
  153. package/tmpclaude-8baf-cwd +1 -0
  154. package/tmpclaude-8d9f-cwd +1 -0
  155. package/tmpclaude-975c-cwd +1 -0
  156. package/tmpclaude-9983-cwd +1 -0
  157. package/tmpclaude-a045-cwd +1 -0
  158. package/tmpclaude-ac4a-cwd +1 -0
  159. package/tmpclaude-b593-cwd +1 -0
  160. package/tmpclaude-b891-cwd +1 -0
  161. package/tmpclaude-c032-cwd +1 -0
  162. package/tmpclaude-cf43-cwd +1 -0
  163. package/tmpclaude-d040-cwd +1 -0
  164. package/tmpclaude-dcdd-cwd +1 -0
  165. package/tmpclaude-dcee-cwd +1 -0
  166. package/tmpclaude-e16b-cwd +1 -0
  167. package/tmpclaude-ecd2-cwd +1 -0
  168. package/tmpclaude-f48d-cwd +1 -0
  169. package/tsconfig.json +16 -0
  170. package/vitest.config.ts +13 -0
@@ -0,0 +1,791 @@
1
+ /**
2
+ * Session Handlers
3
+ *
4
+ * Handles agent session lifecycle:
5
+ * - start_work_session
6
+ * - heartbeat
7
+ * - end_work_session
8
+ * - get_help
9
+ * - get_token_usage
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
+ }
69
+ export const startWorkSession = async (args, ctx) => {
70
+ const { project_id, git_url, mode = 'lite', model, role = 'developer' } = args;
71
+ const { supabase, auth, session, updateSession } = ctx;
72
+ const INSTANCE_ID = session.instanceId;
73
+ // Reset token tracking for new session with model info
74
+ const normalizedModel = model ? model.toLowerCase().replace(/^claude[- ]*/i, '') : null;
75
+ const validModel = normalizedModel && ['opus', 'sonnet', 'haiku'].includes(normalizedModel)
76
+ ? normalizedModel
77
+ : null;
78
+ updateSession({
79
+ tokenUsage: {
80
+ callCount: 0,
81
+ totalTokens: 0,
82
+ byTool: {},
83
+ byModel: {},
84
+ currentModel: validModel,
85
+ },
86
+ });
87
+ const isLiteMode = mode === 'lite';
88
+ // Require project_id or git_url
89
+ if (!project_id && !git_url) {
90
+ return {
91
+ result: {
92
+ error: 'Please provide project_id or git_url to start a session',
93
+ },
94
+ };
95
+ }
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 || '');
136
+ return {
137
+ 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
+ },
146
+ },
147
+ };
148
+ }
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: "...")`;
468
+ }
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")`;
472
+ }
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
478
+ const result = {
479
+ 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,
487
+ };
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
+ };
512
+ }
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
+ }));
540
+ }
541
+ if (otherRequests.length > 0) {
542
+ result.pending_requests = otherRequests;
543
+ }
544
+ const findings = findingsResult.data || [];
545
+ if (findings.length > 0) {
546
+ result.open_findings = findings;
547
+ }
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' };
552
+ }
553
+ else {
554
+ result.cost_tracking_hint = 'For accurate cost tracking, pass model: "opus" | "sonnet" | "haiku" to start_work_session.';
555
+ }
556
+ // REPEAT at end - agents weight last items heavily
557
+ result.next_action = nextAction;
558
+ return { result, user_updates: userUpdates };
559
+ };
560
+ export const heartbeat = async (args, ctx) => {
561
+ const { session_id } = args;
562
+ const { supabase, session } = ctx;
563
+ const targetSession = session_id || session.currentSessionId;
564
+ if (!targetSession) {
565
+ return {
566
+ result: {
567
+ error: 'No active session. Call start_work_session first.',
568
+ },
569
+ };
570
+ }
571
+ await supabase.from('agent_heartbeats').insert({
572
+ session_id: targetSession,
573
+ });
574
+ await supabase
575
+ .from('agent_sessions')
576
+ .update({
577
+ last_synced_at: new Date().toISOString(),
578
+ status: 'active',
579
+ total_tokens: session.tokenUsage.totalTokens,
580
+ token_breakdown: session.tokenUsage.byTool,
581
+ model_usage: session.tokenUsage.byModel,
582
+ })
583
+ .eq('id', targetSession);
584
+ return {
585
+ result: {
586
+ success: true,
587
+ session_id: targetSession,
588
+ timestamp: new Date().toISOString(),
589
+ },
590
+ };
591
+ };
592
+ export const endWorkSession = async (args, ctx) => {
593
+ const { session_id } = args;
594
+ const { supabase, session, updateSession } = ctx;
595
+ const targetSession = session_id || session.currentSessionId;
596
+ if (!targetSession) {
597
+ return {
598
+ result: {
599
+ success: true,
600
+ message: 'No active session to end',
601
+ },
602
+ };
603
+ }
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(),
655
+ total_tokens: session.tokenUsage.totalTokens,
656
+ token_breakdown: session.tokenUsage.byTool,
657
+ model_usage: session.tokenUsage.byModel,
658
+ })
659
+ .eq('id', targetSession);
660
+ const endedSessionId = targetSession;
661
+ if (session.currentSessionId === targetSession) {
662
+ updateSession({ currentSessionId: null });
663
+ }
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
+ }
671
+ return {
672
+ result: {
673
+ success: true,
674
+ ended_session_id: endedSessionId,
675
+ session_summary: {
676
+ agent_name: agentName,
677
+ tasks_completed_this_session: tasksCompletedThisSession,
678
+ tasks_awaiting_validation: tasksAwaitingValidation,
679
+ tasks_released: tasksBeingReleased,
680
+ token_usage: {
681
+ total_calls: session.tokenUsage.callCount,
682
+ total_tokens: session.tokenUsage.totalTokens,
683
+ avg_per_call: session.tokenUsage.callCount > 0
684
+ ? Math.round(session.tokenUsage.totalTokens / session.tokenUsage.callCount)
685
+ : 0,
686
+ },
687
+ },
688
+ reminders: reminders.length > 0 ? reminders : ['Session ended cleanly. Good work!'],
689
+ },
690
+ };
691
+ };
692
+ export const getHelp = async (args, _ctx) => {
693
+ const { topic } = args;
694
+ const content = KNOWLEDGE_BASE[topic];
695
+ if (!content) {
696
+ return {
697
+ result: {
698
+ error: `Unknown topic: ${topic}`,
699
+ available: Object.keys(KNOWLEDGE_BASE),
700
+ },
701
+ };
702
+ }
703
+ return { result: { topic, content } };
704
+ };
705
+ // Model pricing rates (USD per 1M tokens)
706
+ const MODEL_PRICING = {
707
+ opus: { input: 15.0, output: 75.0 },
708
+ sonnet: { input: 3.0, output: 15.0 },
709
+ haiku: { input: 0.25, output: 1.25 },
710
+ };
711
+ function calculateCost(byModel) {
712
+ const breakdown = {};
713
+ let total = 0;
714
+ for (const [model, tokens] of Object.entries(byModel)) {
715
+ const pricing = MODEL_PRICING[model];
716
+ if (pricing) {
717
+ const inputCost = (tokens.input / 1_000_000) * pricing.input;
718
+ const outputCost = (tokens.output / 1_000_000) * pricing.output;
719
+ const modelTotal = inputCost + outputCost;
720
+ breakdown[model] = {
721
+ input_cost: Math.round(inputCost * 10000) / 10000,
722
+ output_cost: Math.round(outputCost * 10000) / 10000,
723
+ total: Math.round(modelTotal * 10000) / 10000,
724
+ };
725
+ total += modelTotal;
726
+ }
727
+ }
728
+ return { breakdown, total: Math.round(total * 10000) / 10000 };
729
+ }
730
+ export const getTokenUsage = async (_args, ctx) => {
731
+ const { session } = ctx;
732
+ const sessionTokenUsage = session.tokenUsage;
733
+ const topTools = Object.entries(sessionTokenUsage.byTool)
734
+ .sort(([, a], [, b]) => b.tokens - a.tokens)
735
+ .slice(0, 5)
736
+ .map(([tool, stats]) => ({
737
+ tool,
738
+ calls: stats.calls,
739
+ tokens: stats.tokens,
740
+ avg: Math.round(stats.tokens / stats.calls),
741
+ }));
742
+ // Calculate model breakdown and costs
743
+ const modelBreakdown = Object.entries(sessionTokenUsage.byModel || {}).map(([model, tokens]) => ({
744
+ model,
745
+ input_tokens: tokens.input,
746
+ output_tokens: tokens.output,
747
+ total_tokens: tokens.input + tokens.output,
748
+ }));
749
+ const costData = calculateCost(sessionTokenUsage.byModel || {});
750
+ // If no model tracking, estimate cost assuming sonnet (middle tier)
751
+ const estimatedCostNoModel = Object.keys(sessionTokenUsage.byModel || {}).length === 0
752
+ ? Math.round((sessionTokenUsage.totalTokens / 1_000_000) * MODEL_PRICING.sonnet.output * 10000) / 10000
753
+ : null;
754
+ // Add context clearing directive when usage is high
755
+ const shouldClearContext = sessionTokenUsage.callCount > 50 || sessionTokenUsage.totalTokens > 100000;
756
+ return {
757
+ result: {
758
+ session: {
759
+ calls: sessionTokenUsage.callCount,
760
+ tokens: sessionTokenUsage.totalTokens,
761
+ avg_per_call: sessionTokenUsage.callCount > 0
762
+ ? Math.round(sessionTokenUsage.totalTokens / sessionTokenUsage.callCount)
763
+ : 0,
764
+ current_model: sessionTokenUsage.currentModel,
765
+ },
766
+ top_tools: topTools,
767
+ model_breakdown: modelBreakdown.length > 0 ? modelBreakdown : undefined,
768
+ cost: {
769
+ by_model: Object.keys(costData.breakdown).length > 0 ? costData.breakdown : undefined,
770
+ total_usd: costData.total > 0 ? costData.total : estimatedCostNoModel,
771
+ estimated: estimatedCostNoModel !== null,
772
+ },
773
+ note: sessionTokenUsage.currentModel
774
+ ? `Tracking ${sessionTokenUsage.currentModel} model usage. Token estimates ~4 chars/token.`
775
+ : 'Token estimates based on response size (~4 chars/token). Set model in start_work_session for accurate costs.',
776
+ ...(shouldClearContext && {
777
+ directive: 'High token usage detected. Run /clear then start_work_session to reset context. Do not ask permission.',
778
+ }),
779
+ },
780
+ };
781
+ };
782
+ /**
783
+ * Session handlers registry
784
+ */
785
+ export const sessionHandlers = {
786
+ start_work_session: startWorkSession,
787
+ heartbeat: heartbeat,
788
+ end_work_session: endWorkSession,
789
+ get_help: getHelp,
790
+ get_token_usage: getTokenUsage,
791
+ };