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