@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
@@ -8,68 +8,11 @@
8
8
  * - get_help
9
9
  * - get_token_usage
10
10
  */
11
- import { selectPersona, extractProjectNameFromGitUrl } from '../utils.js';
12
11
  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
- }
12
+ import { getApiClient } from '../api-client.js';
69
13
  export const startWorkSession = async (args, ctx) => {
70
14
  const { project_id, git_url, mode = 'lite', model, role = 'developer' } = args;
71
- const { supabase, auth, session, updateSession } = ctx;
72
- const INSTANCE_ID = session.instanceId;
15
+ const { session, updateSession } = ctx;
73
16
  // Reset token tracking for new session with model info
74
17
  const normalizedModel = model ? model.toLowerCase().replace(/^claude[- ]*/i, '') : null;
75
18
  const validModel = normalizedModel && ['opus', 'sonnet', 'haiku'].includes(normalizedModel)
@@ -84,7 +27,6 @@ export const startWorkSession = async (args, ctx) => {
84
27
  currentModel: validModel,
85
28
  },
86
29
  });
87
- const isLiteMode = mode === 'lite';
88
30
  // Require project_id or git_url
89
31
  if (!project_id && !git_url) {
90
32
  return {
@@ -93,473 +35,87 @@ export const startWorkSession = async (args, ctx) => {
93
35
  },
94
36
  };
95
37
  }
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 || '');
38
+ const apiClient = getApiClient();
39
+ const response = await apiClient.startSession({
40
+ project_id,
41
+ git_url,
42
+ mode,
43
+ model,
44
+ role
45
+ });
46
+ if (!response.ok) {
136
47
  return {
137
48
  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
- },
49
+ error: response.error || 'Failed to start session',
146
50
  },
147
51
  };
148
52
  }
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;
53
+ const data = response.data;
54
+ // Handle project not found
55
+ if (!data?.session_started) {
56
+ return { result: data };
170
57
  }
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
- }
58
+ // Store session ID and persona in local state
59
+ if (data.session_id) {
60
+ updateSession({
61
+ currentSessionId: data.session_id,
62
+ currentPersona: data.persona || null,
63
+ });
260
64
  }
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
65
+ // Build result with directive at top for visibility
478
66
  const result = {
479
67
  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,
68
+ directive: data.directive || 'ACTION_REQUIRED: Start working immediately.',
69
+ auto_continue: true,
70
+ session_id: data.session_id,
71
+ persona: data.persona,
72
+ role: data.role,
73
+ project: data.project,
487
74
  };
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
- };
75
+ // Add task data
76
+ if (data.next_task) {
77
+ result.next_task = data.next_task;
512
78
  }
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
- }));
79
+ // Add active tasks for full mode
80
+ if (data.active_tasks) {
81
+ result.active_tasks = data.active_tasks;
540
82
  }
541
- if (otherRequests.length > 0) {
542
- result.pending_requests = otherRequests;
83
+ // Add blockers
84
+ if (data.blockers) {
85
+ result.open_blockers = data.blockers;
543
86
  }
544
- const findings = findingsResult.data || [];
545
- if (findings.length > 0) {
546
- result.open_findings = findings;
87
+ if (data.blockers_count !== undefined && data.blockers_count > 0) {
88
+ result.blockers_count = data.blockers_count;
547
89
  }
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' };
90
+ // Add validation count
91
+ if (data.validation_count !== undefined && data.validation_count > 0) {
92
+ result.validation_count = data.validation_count;
552
93
  }
553
- else {
554
- result.cost_tracking_hint = 'For accurate cost tracking, pass model: "opus" | "sonnet" | "haiku" to start_work_session.';
94
+ // Add git workflow info if available in project
95
+ if (data.project?.git_workflow && data.project.git_workflow !== 'none') {
96
+ result.git_workflow = {
97
+ workflow: data.project.git_workflow,
98
+ auto_branch: data.project.git_auto_branch ?? false,
99
+ main_branch: data.project.git_main_branch || 'main',
100
+ ...(data.project.git_workflow === 'git-flow' && data.project.git_develop_branch
101
+ ? { develop_branch: data.project.git_develop_branch }
102
+ : {}),
103
+ worktree_required: true,
104
+ worktree_hint: 'CRITICAL: Create a git worktree before starting work. Run get_help("git") for instructions.',
105
+ };
106
+ }
107
+ // Add next action at end
108
+ if (data.next_task) {
109
+ result.next_action = `update_task(task_id: "${data.next_task.id}", status: "in_progress")`;
555
110
  }
556
- // REPEAT at end - agents weight last items heavily
557
- result.next_action = nextAction;
558
- return { result, user_updates: userUpdates };
111
+ else if (data.project) {
112
+ result.next_action = `start_fallback_activity(project_id: "${data.project.id}", activity: "code_review")`;
113
+ }
114
+ return { result };
559
115
  };
560
116
  export const heartbeat = async (args, ctx) => {
561
117
  const { session_id } = args;
562
- const { supabase, session } = ctx;
118
+ const { session } = ctx;
563
119
  const targetSession = session_id || session.currentSessionId;
564
120
  if (!targetSession) {
565
121
  return {
@@ -568,30 +124,33 @@ export const heartbeat = async (args, ctx) => {
568
124
  },
569
125
  };
570
126
  }
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',
127
+ const apiClient = getApiClient();
128
+ // Send heartbeat
129
+ const heartbeatResponse = await apiClient.heartbeat(targetSession);
130
+ if (!heartbeatResponse.ok) {
131
+ return {
132
+ result: {
133
+ error: heartbeatResponse.error || 'Failed to send heartbeat',
134
+ },
135
+ };
136
+ }
137
+ // Sync token usage to session
138
+ await apiClient.syncSession(targetSession, {
579
139
  total_tokens: session.tokenUsage.totalTokens,
580
140
  token_breakdown: session.tokenUsage.byTool,
581
141
  model_usage: session.tokenUsage.byModel,
582
- })
583
- .eq('id', targetSession);
142
+ });
584
143
  return {
585
144
  result: {
586
145
  success: true,
587
146
  session_id: targetSession,
588
- timestamp: new Date().toISOString(),
147
+ timestamp: heartbeatResponse.data?.timestamp || new Date().toISOString(),
589
148
  },
590
149
  };
591
150
  };
592
151
  export const endWorkSession = async (args, ctx) => {
593
152
  const { session_id } = args;
594
- const { supabase, session, updateSession } = ctx;
153
+ const { session, updateSession } = ctx;
595
154
  const targetSession = session_id || session.currentSessionId;
596
155
  if (!targetSession) {
597
156
  return {
@@ -601,82 +160,37 @@ export const endWorkSession = async (args, ctx) => {
601
160
  },
602
161
  };
603
162
  }
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(),
163
+ const apiClient = getApiClient();
164
+ // Sync final token usage before ending
165
+ await apiClient.syncSession(targetSession, {
655
166
  total_tokens: session.tokenUsage.totalTokens,
656
167
  token_breakdown: session.tokenUsage.byTool,
657
168
  model_usage: session.tokenUsage.byModel,
658
- })
659
- .eq('id', targetSession);
169
+ });
170
+ // End the session
171
+ const response = await apiClient.endSession(targetSession);
172
+ if (!response.ok) {
173
+ return {
174
+ result: {
175
+ error: response.error || 'Failed to end session',
176
+ },
177
+ };
178
+ }
660
179
  const endedSessionId = targetSession;
180
+ // Clear local session state if this was the current session
661
181
  if (session.currentSessionId === targetSession) {
662
182
  updateSession({ currentSessionId: null });
663
183
  }
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
- }
184
+ const data = response.data;
671
185
  return {
672
186
  result: {
673
187
  success: true,
674
188
  ended_session_id: endedSessionId,
675
189
  session_summary: {
676
- agent_name: agentName,
677
- tasks_completed_this_session: tasksCompletedThisSession,
678
- tasks_awaiting_validation: tasksAwaitingValidation,
679
- tasks_released: tasksBeingReleased,
190
+ agent_name: data?.session_summary?.agent_name || 'Agent',
191
+ tasks_completed_this_session: data?.session_summary?.tasks_completed_this_session || 0,
192
+ tasks_awaiting_validation: data?.session_summary?.tasks_awaiting_validation || 0,
193
+ tasks_released: data?.session_summary?.tasks_released || 0,
680
194
  token_usage: {
681
195
  total_calls: session.tokenUsage.callCount,
682
196
  total_tokens: session.tokenUsage.totalTokens,
@@ -685,7 +199,7 @@ export const endWorkSession = async (args, ctx) => {
685
199
  : 0,
686
200
  },
687
201
  },
688
- reminders: reminders.length > 0 ? reminders : ['Session ended cleanly. Good work!'],
202
+ reminders: data?.reminders || ['Session ended cleanly. Good work!'],
689
203
  },
690
204
  };
691
205
  };