tanuki-telemetry 1.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.
- package/Dockerfile +22 -0
- package/bin/tanuki.mjs +251 -0
- package/frontend/eslint.config.js +23 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +39 -0
- package/frontend/src/App.tsx +232 -0
- package/frontend/src/assets/hero.png +0 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/assets/vite.svg +1 -0
- package/frontend/src/components/ArtifactsPanel.tsx +429 -0
- package/frontend/src/components/ChildStreams.tsx +176 -0
- package/frontend/src/components/CoordinatorPage.tsx +317 -0
- package/frontend/src/components/Header.tsx +108 -0
- package/frontend/src/components/InsightsPanel.tsx +142 -0
- package/frontend/src/components/IterationsTable.tsx +98 -0
- package/frontend/src/components/KnowledgePage.tsx +308 -0
- package/frontend/src/components/LoginPage.tsx +55 -0
- package/frontend/src/components/PlanProgress.tsx +163 -0
- package/frontend/src/components/QualityReport.tsx +276 -0
- package/frontend/src/components/ScreenshotUpload.tsx +117 -0
- package/frontend/src/components/ScreenshotsGrid.tsx +266 -0
- package/frontend/src/components/SessionDetail.tsx +265 -0
- package/frontend/src/components/SessionList.tsx +234 -0
- package/frontend/src/components/SettingsPage.tsx +213 -0
- package/frontend/src/components/StreamComms.tsx +228 -0
- package/frontend/src/components/TanukiLogo.tsx +16 -0
- package/frontend/src/components/Timeline.tsx +416 -0
- package/frontend/src/components/WalkthroughPage.tsx +458 -0
- package/frontend/src/hooks/useApi.ts +81 -0
- package/frontend/src/hooks/useAuth.ts +54 -0
- package/frontend/src/hooks/useKnowledge.ts +33 -0
- package/frontend/src/hooks/useWebSocket.ts +95 -0
- package/frontend/src/index.css +66 -0
- package/frontend/src/lib/api.ts +15 -0
- package/frontend/src/lib/utils.ts +58 -0
- package/frontend/src/main.tsx +10 -0
- package/frontend/src/types.ts +181 -0
- package/frontend/tsconfig.app.json +32 -0
- package/frontend/tsconfig.json +7 -0
- package/frontend/vite.config.ts +25 -0
- package/install.sh +87 -0
- package/package.json +63 -0
- package/src/api-keys.ts +97 -0
- package/src/auth.ts +165 -0
- package/src/coordinator.ts +136 -0
- package/src/dashboard-server.ts +5 -0
- package/src/dashboard.ts +826 -0
- package/src/db.ts +1009 -0
- package/src/index.ts +20 -0
- package/src/middleware.ts +76 -0
- package/src/tools.ts +864 -0
- package/src/types-shim.d.ts +18 -0
- package/src/types.ts +171 -0
- package/tsconfig.json +19 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
import {
|
|
5
|
+
createSession,
|
|
6
|
+
insertEvent,
|
|
7
|
+
insertIteration,
|
|
8
|
+
insertScreenshot,
|
|
9
|
+
insertArtifact,
|
|
10
|
+
insertInsight,
|
|
11
|
+
validateInsight,
|
|
12
|
+
getInsightsForContext,
|
|
13
|
+
createPlan,
|
|
14
|
+
updatePlanStep,
|
|
15
|
+
getPlanSteps,
|
|
16
|
+
endSession,
|
|
17
|
+
getSessionSummary,
|
|
18
|
+
listSessions,
|
|
19
|
+
getComparisonResults,
|
|
20
|
+
createWalkthrough,
|
|
21
|
+
insertWalkthroughAction,
|
|
22
|
+
insertWalkthroughScreenshot,
|
|
23
|
+
endWalkthrough,
|
|
24
|
+
} from "./db.js";
|
|
25
|
+
import type { FinalResult, Session, SessionSummary, Insight, PlanStep } from "./types.js";
|
|
26
|
+
import {
|
|
27
|
+
saveCoordinatorState,
|
|
28
|
+
getCoordinatorState,
|
|
29
|
+
getLatestCoordinatorSession,
|
|
30
|
+
listCoordinatorSessions,
|
|
31
|
+
compactCoordinatorContext,
|
|
32
|
+
getCoordinatorHistory,
|
|
33
|
+
} from "./coordinator.js";
|
|
34
|
+
|
|
35
|
+
function jsonResponse(data: unknown): { content: Array<{ type: "text"; text: string }> } {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatSessionSummary(summary: SessionSummary): string {
|
|
42
|
+
const { session, events, iterations, screenshots } = summary;
|
|
43
|
+
const finalResult = session.final_result
|
|
44
|
+
? JSON.parse(session.final_result)
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
let md = `# Session Summary\n\n`;
|
|
48
|
+
md += `- **Session ID:** ${session.id}\n`;
|
|
49
|
+
md += `- **Worktree:** ${session.worktree_name}\n`;
|
|
50
|
+
if (session.ticket_id)
|
|
51
|
+
md += `- **Ticket:** ${session.ticket_id} — ${session.ticket_title ?? ""}\n`;
|
|
52
|
+
if (session.branch_name) md += `- **Branch:** ${session.branch_name}\n`;
|
|
53
|
+
md += `- **Mode:** ${session.mode}\n`;
|
|
54
|
+
md += `- **Status:** ${session.status}\n`;
|
|
55
|
+
md += `- **Iterations:** ${session.total_iterations} / ${session.max_iterations}\n`;
|
|
56
|
+
md += `- **Tokens:** ${session.total_input_tokens} in / ${session.total_output_tokens} out\n`;
|
|
57
|
+
md += `- **Started:** ${session.started_at}\n`;
|
|
58
|
+
if (session.ended_at) md += `- **Ended:** ${session.ended_at}\n`;
|
|
59
|
+
if (session.duration_seconds != null)
|
|
60
|
+
md += `- **Duration:** ${session.duration_seconds}s\n`;
|
|
61
|
+
|
|
62
|
+
if (finalResult) {
|
|
63
|
+
md += `\n## Final Result\n\n`;
|
|
64
|
+
md += `- Tests: ${finalResult.tests_passed ? "PASS" : "FAIL"}\n`;
|
|
65
|
+
md += `- Lint: ${finalResult.lint_passed ? "PASS" : "FAIL"}\n`;
|
|
66
|
+
md += `- Typecheck: ${finalResult.typecheck_passed ? "PASS" : "FAIL"}\n`;
|
|
67
|
+
if (finalResult.pr_url) md += `- PR: ${finalResult.pr_url}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (events.length > 0) {
|
|
71
|
+
md += `\n## Events (${events.length})\n\n`;
|
|
72
|
+
for (const e of events) {
|
|
73
|
+
md += `- **[${e.timestamp}] [${e.phase}] ${e.event_type}:** ${e.message}\n`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (iterations.length > 0) {
|
|
78
|
+
md += `\n## Iterations (${iterations.length})\n\n`;
|
|
79
|
+
for (const it of iterations) {
|
|
80
|
+
md += `### Iteration ${it.iteration_number} — ${it.result.toUpperCase()}\n`;
|
|
81
|
+
md += `- **Trigger:** ${it.trigger}\n`;
|
|
82
|
+
md += `- **Error:** ${it.error_summary}\n`;
|
|
83
|
+
if (it.fix_description) md += `- **Fix:** ${it.fix_description}\n`;
|
|
84
|
+
if (it.files_changed) {
|
|
85
|
+
const files = JSON.parse(it.files_changed) as string[];
|
|
86
|
+
md += `- **Files:** ${files.join(", ")}\n`;
|
|
87
|
+
}
|
|
88
|
+
if (it.duration_seconds != null)
|
|
89
|
+
md += `- **Duration:** ${it.duration_seconds}s\n`;
|
|
90
|
+
md += `\n`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (screenshots.length > 0) {
|
|
95
|
+
md += `\n## Screenshots (${screenshots.length})\n\n`;
|
|
96
|
+
for (const s of screenshots) {
|
|
97
|
+
md += `- **[${s.phase}]** ${s.description} — \`${s.file_path}\`\n`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const artifacts = summary.artifacts || [];
|
|
102
|
+
if (artifacts.length > 0) {
|
|
103
|
+
md += `\n## Artifacts (${artifacts.length})\n\n`;
|
|
104
|
+
for (const a of artifacts) {
|
|
105
|
+
md += `- **[${a.artifact_type}]** ${a.description} — \`${a.file_path}\` (${a.mime_type || "unknown"})\n`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const insights = summary.insights || [];
|
|
110
|
+
if (insights.length > 0) {
|
|
111
|
+
md += `\n## Insights (${insights.length})\n\n`;
|
|
112
|
+
for (const ins of insights) {
|
|
113
|
+
md += `### [${ins.insight_type}] ${ins.title}\n`;
|
|
114
|
+
md += `- **Category:** ${ins.category}\n`;
|
|
115
|
+
md += `- **Confidence:** ${(ins.confidence * 100).toFixed(0)}%\n`;
|
|
116
|
+
md += `- ${ins.description}\n`;
|
|
117
|
+
if (ins.evidence) md += `- **Evidence:** ${ins.evidence}\n`;
|
|
118
|
+
md += `\n`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return md;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function registerTools(server: McpServer, userEmail?: string): void {
|
|
126
|
+
// userEmail is injected from the MCP connection's API key auth.
|
|
127
|
+
// It's always set when auth is enabled, and falls back to "local@localhost" otherwise.
|
|
128
|
+
|
|
129
|
+
server.tool(
|
|
130
|
+
"log_session_start",
|
|
131
|
+
"Creates a new telemetry session for tracking an autonomous workflow",
|
|
132
|
+
{
|
|
133
|
+
worktree_name: z.string().describe("Name of the worktree"),
|
|
134
|
+
ticket_id: z.string().optional().describe("Linear ticket ID"),
|
|
135
|
+
ticket_title: z.string().optional().describe("Ticket title"),
|
|
136
|
+
branch_name: z.string().optional().describe("Git branch name"),
|
|
137
|
+
mode: z
|
|
138
|
+
.enum(["local", "remote"])
|
|
139
|
+
.optional()
|
|
140
|
+
.default("local")
|
|
141
|
+
.describe("Execution mode"),
|
|
142
|
+
max_iterations: z
|
|
143
|
+
.number()
|
|
144
|
+
.optional()
|
|
145
|
+
.default(10)
|
|
146
|
+
.describe("Maximum fix iterations allowed"),
|
|
147
|
+
parent_session_id: z
|
|
148
|
+
.string()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe("Parent session ID — set this when spawning a child stream from a parent session"),
|
|
151
|
+
},
|
|
152
|
+
async (params) => {
|
|
153
|
+
const session_id = uuidv4();
|
|
154
|
+
createSession(
|
|
155
|
+
session_id,
|
|
156
|
+
params.worktree_name,
|
|
157
|
+
params.ticket_id,
|
|
158
|
+
params.ticket_title,
|
|
159
|
+
params.branch_name,
|
|
160
|
+
params.mode,
|
|
161
|
+
params.max_iterations,
|
|
162
|
+
params.parent_session_id,
|
|
163
|
+
userEmail
|
|
164
|
+
);
|
|
165
|
+
return jsonResponse({ session_id, user_email: userEmail });
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
server.tool(
|
|
170
|
+
"log_event",
|
|
171
|
+
"Logs a significant action, decision, or error during a session",
|
|
172
|
+
{
|
|
173
|
+
session_id: z.string().describe("Session ID"),
|
|
174
|
+
phase: z
|
|
175
|
+
.string()
|
|
176
|
+
.describe(
|
|
177
|
+
"Current phase: setup, scope, implementation, verification, deliverables"
|
|
178
|
+
),
|
|
179
|
+
event_type: z
|
|
180
|
+
.enum(["decision", "action", "error", "fix", "info", "phase_change", "review_pass", "review_flag", "review_dispatch", "error_resolve", "cost_checkpoint", "pattern_detect", "knowledge_extract"])
|
|
181
|
+
.describe("Type of event"),
|
|
182
|
+
message: z.string().describe("Human-readable event description"),
|
|
183
|
+
metadata: z
|
|
184
|
+
.union([z.record(z.string(), z.unknown()), z.string()])
|
|
185
|
+
.optional()
|
|
186
|
+
.transform((val) => {
|
|
187
|
+
if (typeof val === "string") {
|
|
188
|
+
try { return JSON.parse(val) as Record<string, unknown>; } catch { return { raw: val }; }
|
|
189
|
+
}
|
|
190
|
+
return val;
|
|
191
|
+
})
|
|
192
|
+
.describe("Additional context as JSON"),
|
|
193
|
+
tokens_used: z.coerce.number().optional().describe("Tokens consumed by this action (legacy — prefer input_tokens/output_tokens)"),
|
|
194
|
+
input_tokens: z.coerce.number().optional().describe("Input tokens consumed by this action"),
|
|
195
|
+
output_tokens: z.coerce.number().optional().describe("Output tokens consumed by this action"),
|
|
196
|
+
},
|
|
197
|
+
async (params) => {
|
|
198
|
+
const event_id = insertEvent(
|
|
199
|
+
params.session_id,
|
|
200
|
+
params.phase,
|
|
201
|
+
params.event_type,
|
|
202
|
+
params.message,
|
|
203
|
+
params.metadata,
|
|
204
|
+
params.tokens_used,
|
|
205
|
+
params.input_tokens,
|
|
206
|
+
params.output_tokens
|
|
207
|
+
);
|
|
208
|
+
return jsonResponse({ event_id });
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
server.tool(
|
|
213
|
+
"log_iteration",
|
|
214
|
+
"Logs a test-fix iteration cycle",
|
|
215
|
+
{
|
|
216
|
+
session_id: z.string().describe("Session ID"),
|
|
217
|
+
iteration_number: z.coerce.number().describe("Iteration number (1-based)"),
|
|
218
|
+
trigger: z
|
|
219
|
+
.string()
|
|
220
|
+
.describe("What failed: test, lint, typecheck"),
|
|
221
|
+
error_summary: z.string().describe("Summary of the error"),
|
|
222
|
+
fix_description: z
|
|
223
|
+
.string()
|
|
224
|
+
.optional()
|
|
225
|
+
.describe("Description of the fix applied"),
|
|
226
|
+
files_changed: z
|
|
227
|
+
.array(z.string())
|
|
228
|
+
.optional()
|
|
229
|
+
.describe("List of file paths changed"),
|
|
230
|
+
result: z
|
|
231
|
+
.enum(["pass", "fail", "partial"])
|
|
232
|
+
.describe("Outcome of this iteration"),
|
|
233
|
+
duration_seconds: z
|
|
234
|
+
.number()
|
|
235
|
+
.optional()
|
|
236
|
+
.describe("How long the iteration took"),
|
|
237
|
+
},
|
|
238
|
+
async (params) => {
|
|
239
|
+
const iteration_id = insertIteration(
|
|
240
|
+
params.session_id,
|
|
241
|
+
params.iteration_number,
|
|
242
|
+
params.trigger,
|
|
243
|
+
params.error_summary,
|
|
244
|
+
params.fix_description,
|
|
245
|
+
params.files_changed,
|
|
246
|
+
params.result,
|
|
247
|
+
params.duration_seconds
|
|
248
|
+
);
|
|
249
|
+
return jsonResponse({ iteration_id });
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
server.tool(
|
|
254
|
+
"log_screenshot",
|
|
255
|
+
"Associates a screenshot file with a session. Copies the file into telemetry storage and generates a thumbnail for the dashboard.",
|
|
256
|
+
{
|
|
257
|
+
session_id: z.string().describe("Session ID"),
|
|
258
|
+
iteration_number: z
|
|
259
|
+
.coerce.number()
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("Iteration number, or null for final screenshots"),
|
|
262
|
+
phase: z.string().describe("Phase when screenshot was taken"),
|
|
263
|
+
description: z.string().describe("What the screenshot shows"),
|
|
264
|
+
file_path: z.string().describe("Path to the screenshot file on host"),
|
|
265
|
+
event_id: z.coerce.number().optional().describe("ID of the event this screenshot belongs to — links screenshot to a specific timeline event"),
|
|
266
|
+
},
|
|
267
|
+
async (params) => {
|
|
268
|
+
const screenshot_id = insertScreenshot(
|
|
269
|
+
params.session_id,
|
|
270
|
+
params.iteration_number,
|
|
271
|
+
params.phase,
|
|
272
|
+
params.description,
|
|
273
|
+
params.file_path,
|
|
274
|
+
params.event_id
|
|
275
|
+
);
|
|
276
|
+
return jsonResponse({ screenshot_id });
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
server.tool(
|
|
281
|
+
"log_artifact",
|
|
282
|
+
"Logs a file artifact (template, rubric, report, PPTX, etc.) associated with a session. Copies the file into telemetry storage for serving via the dashboard.",
|
|
283
|
+
{
|
|
284
|
+
session_id: z.string().describe("Session ID"),
|
|
285
|
+
file_path: z.string().describe("Path to the artifact file on host"),
|
|
286
|
+
artifact_type: z.string().describe("Type of artifact: template, rubric, report, pptx, summary, config, output, etc."),
|
|
287
|
+
description: z.string().describe("What the artifact is / what it contains"),
|
|
288
|
+
metadata: z
|
|
289
|
+
.union([z.record(z.string(), z.unknown()), z.string()])
|
|
290
|
+
.optional()
|
|
291
|
+
.transform((val) => {
|
|
292
|
+
if (typeof val === "string") {
|
|
293
|
+
try { return JSON.parse(val) as Record<string, unknown>; } catch { return { raw: val }; }
|
|
294
|
+
}
|
|
295
|
+
return val;
|
|
296
|
+
})
|
|
297
|
+
.describe("Additional context as JSON"),
|
|
298
|
+
event_id: z.coerce.number().optional().describe("ID of the event this artifact belongs to"),
|
|
299
|
+
},
|
|
300
|
+
async (params) => {
|
|
301
|
+
const artifact_id = insertArtifact(
|
|
302
|
+
params.session_id,
|
|
303
|
+
params.file_path,
|
|
304
|
+
params.artifact_type,
|
|
305
|
+
params.description,
|
|
306
|
+
params.metadata,
|
|
307
|
+
params.event_id
|
|
308
|
+
);
|
|
309
|
+
return jsonResponse({ artifact_id });
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
server.tool(
|
|
314
|
+
"log_session_end",
|
|
315
|
+
"Closes a session with final status and stats",
|
|
316
|
+
{
|
|
317
|
+
session_id: z.string().describe("Session ID"),
|
|
318
|
+
status: z
|
|
319
|
+
.enum(["completed", "failed", "interrupted"])
|
|
320
|
+
.describe("Final session status"),
|
|
321
|
+
total_input_tokens: z
|
|
322
|
+
.coerce.number()
|
|
323
|
+
.optional()
|
|
324
|
+
.describe("Total input tokens used (overrides accumulated count if larger)"),
|
|
325
|
+
total_output_tokens: z
|
|
326
|
+
.coerce.number()
|
|
327
|
+
.optional()
|
|
328
|
+
.describe("Total output tokens used (overrides accumulated count if larger)"),
|
|
329
|
+
final_result: z
|
|
330
|
+
.object({
|
|
331
|
+
tests_passed: z.boolean().optional(),
|
|
332
|
+
lint_passed: z.boolean().optional(),
|
|
333
|
+
typecheck_passed: z.boolean().optional(),
|
|
334
|
+
pr_url: z.string().optional(),
|
|
335
|
+
pr_number: z.coerce.number().optional(),
|
|
336
|
+
})
|
|
337
|
+
.optional()
|
|
338
|
+
.describe("Final verification results"),
|
|
339
|
+
},
|
|
340
|
+
async (params) => {
|
|
341
|
+
const result = endSession(
|
|
342
|
+
params.session_id,
|
|
343
|
+
params.status,
|
|
344
|
+
params.total_input_tokens,
|
|
345
|
+
params.total_output_tokens,
|
|
346
|
+
params.final_result as FinalResult | undefined
|
|
347
|
+
);
|
|
348
|
+
return jsonResponse(result);
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
server.tool(
|
|
353
|
+
"get_session_summary",
|
|
354
|
+
"Retrieves a full session summary with all events, iterations, and screenshots",
|
|
355
|
+
{
|
|
356
|
+
session_id: z
|
|
357
|
+
.string()
|
|
358
|
+
.optional()
|
|
359
|
+
.describe("Session ID — omit to get the most recent session"),
|
|
360
|
+
},
|
|
361
|
+
async (params) => {
|
|
362
|
+
const summary = getSessionSummary(params.session_id);
|
|
363
|
+
if (!summary) {
|
|
364
|
+
return jsonResponse({ error: "No session found" });
|
|
365
|
+
}
|
|
366
|
+
const markdown = formatSessionSummary(summary);
|
|
367
|
+
return { content: [{ type: "text" as const, text: markdown }] };
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
server.tool(
|
|
372
|
+
"log_insight",
|
|
373
|
+
"Records a learning/insight from session reflection — mistakes, patterns, gotchas, or rules discovered during autonomous work",
|
|
374
|
+
{
|
|
375
|
+
session_id: z.string().describe("Session ID this insight came from"),
|
|
376
|
+
insight_type: z
|
|
377
|
+
.enum(["mistake", "success_pattern", "codebase_gotcha", "optimization", "rule_learned"])
|
|
378
|
+
.describe("Type of insight"),
|
|
379
|
+
category: z
|
|
380
|
+
.string()
|
|
381
|
+
.describe("Domain category: e.g., 'auth', 'testing', 'react-patterns', 'api-design', 'typescript', 'lint', 'state-management'"),
|
|
382
|
+
title: z.string().describe("Short title for this insight"),
|
|
383
|
+
description: z
|
|
384
|
+
.string()
|
|
385
|
+
.describe("Full description — what happened, why it matters, what to do differently"),
|
|
386
|
+
evidence: z
|
|
387
|
+
.string()
|
|
388
|
+
.optional()
|
|
389
|
+
.describe("The specific event/error/code that led to this insight"),
|
|
390
|
+
confidence: z
|
|
391
|
+
.number()
|
|
392
|
+
.min(0)
|
|
393
|
+
.max(1)
|
|
394
|
+
.optional()
|
|
395
|
+
.default(0.5)
|
|
396
|
+
.describe("How confident this insight is correct (0-1). Increases as it gets validated across sessions"),
|
|
397
|
+
file_patterns: z
|
|
398
|
+
.array(z.string())
|
|
399
|
+
.optional()
|
|
400
|
+
.describe("File glob patterns this insight applies to, e.g. ['src/components/**', '*.test.ts']"),
|
|
401
|
+
error_patterns: z
|
|
402
|
+
.array(z.string())
|
|
403
|
+
.optional()
|
|
404
|
+
.describe("Error message patterns that trigger this insight, e.g. ['Cannot read property', 'Type.*not assignable']"),
|
|
405
|
+
},
|
|
406
|
+
async (params) => {
|
|
407
|
+
const insight_id = insertInsight(
|
|
408
|
+
params.session_id,
|
|
409
|
+
params.insight_type,
|
|
410
|
+
params.category,
|
|
411
|
+
params.title,
|
|
412
|
+
params.description,
|
|
413
|
+
params.evidence,
|
|
414
|
+
params.confidence,
|
|
415
|
+
params.file_patterns,
|
|
416
|
+
params.error_patterns
|
|
417
|
+
);
|
|
418
|
+
return jsonResponse({ insight_id });
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
server.tool(
|
|
423
|
+
"validate_insight",
|
|
424
|
+
"Confirms an existing insight was relevant/correct in a new session — increases its confidence score",
|
|
425
|
+
{
|
|
426
|
+
insight_id: z.coerce.number().describe("ID of the insight to validate"),
|
|
427
|
+
},
|
|
428
|
+
async (params) => {
|
|
429
|
+
validateInsight(params.insight_id);
|
|
430
|
+
return jsonResponse({ validated: true });
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
server.tool(
|
|
435
|
+
"query_knowledge",
|
|
436
|
+
"Retrieves accumulated insights/learnings to inform the current session — call this at the start of work to learn from past mistakes",
|
|
437
|
+
{
|
|
438
|
+
category: z
|
|
439
|
+
.string()
|
|
440
|
+
.optional()
|
|
441
|
+
.describe("Filter by category (e.g., 'auth', 'testing', 'react-patterns')"),
|
|
442
|
+
file_pattern: z
|
|
443
|
+
.string()
|
|
444
|
+
.optional()
|
|
445
|
+
.describe("Filter by file pattern — finds insights related to files matching this pattern"),
|
|
446
|
+
limit: z.coerce.number().optional().default(20).describe("Max insights to return"),
|
|
447
|
+
},
|
|
448
|
+
async (params) => {
|
|
449
|
+
const insights = getInsightsForContext(
|
|
450
|
+
params.category,
|
|
451
|
+
params.file_pattern,
|
|
452
|
+
params.limit
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
if (insights.length === 0) {
|
|
456
|
+
return { content: [{ type: "text" as const, text: "No insights found for this context. This is a fresh area — be extra careful and log what you learn." }] };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let md = `# Knowledge Base — ${insights.length} insights\n\n`;
|
|
460
|
+
md += `Sorted by confidence (validated across sessions).\n\n`;
|
|
461
|
+
|
|
462
|
+
for (const ins of insights) {
|
|
463
|
+
const icon =
|
|
464
|
+
ins.insight_type === "mistake" ? "⚠" :
|
|
465
|
+
ins.insight_type === "success_pattern" ? "✓" :
|
|
466
|
+
ins.insight_type === "codebase_gotcha" ? "!" :
|
|
467
|
+
ins.insight_type === "optimization" ? "→" :
|
|
468
|
+
"§";
|
|
469
|
+
md += `### ${icon} [${ins.insight_type}] ${ins.title}\n`;
|
|
470
|
+
md += `**Category:** ${ins.category} | **Confidence:** ${(ins.confidence * 100).toFixed(0)}% | **Validated:** ${ins.times_validated}x\n`;
|
|
471
|
+
md += `${ins.description}\n`;
|
|
472
|
+
if (ins.evidence) md += `**Evidence:** ${ins.evidence}\n`;
|
|
473
|
+
if (ins.file_patterns) md += `**Applies to:** ${JSON.parse(ins.file_patterns).join(", ")}\n`;
|
|
474
|
+
if (ins.error_patterns) md += `**Error patterns:** ${JSON.parse(ins.error_patterns).join(", ")}\n`;
|
|
475
|
+
md += `\n---\n\n`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return { content: [{ type: "text" as const, text: md }] };
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
server.tool(
|
|
483
|
+
"create_plan",
|
|
484
|
+
"Creates an implementation plan with numbered steps — the agent updates each step as it progresses. Steps appear live on the dashboard.",
|
|
485
|
+
{
|
|
486
|
+
session_id: z.string().describe("Session ID"),
|
|
487
|
+
steps: z.array(z.object({
|
|
488
|
+
step_number: z.coerce.number().describe("Step number (1-based, used for ordering)"),
|
|
489
|
+
title: z.string().describe("Short title for this step"),
|
|
490
|
+
description: z.string().optional().describe("Detailed description of what this step involves"),
|
|
491
|
+
parent_step: z.coerce.number().optional().describe("Parent step number if this is a sub-step"),
|
|
492
|
+
file_targets: z.array(z.string()).optional().describe("Files this step will touch"),
|
|
493
|
+
})).describe("Array of plan steps"),
|
|
494
|
+
},
|
|
495
|
+
async (params) => {
|
|
496
|
+
const ids = createPlan(params.session_id, params.steps);
|
|
497
|
+
return jsonResponse({ step_ids: ids, total_steps: ids.length });
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
server.tool(
|
|
502
|
+
"update_plan_step",
|
|
503
|
+
"Updates the status of a plan step — call this as you start, complete, skip, or fail each step",
|
|
504
|
+
{
|
|
505
|
+
step_id: z.coerce.number().describe("Step ID (returned by create_plan)"),
|
|
506
|
+
status: z.enum(["pending", "in_progress", "completed", "skipped", "failed"]).describe("New status"),
|
|
507
|
+
outcome: z.string().optional().describe("What happened — result summary, error message, or reason for skip"),
|
|
508
|
+
},
|
|
509
|
+
async (params) => {
|
|
510
|
+
updatePlanStep(params.step_id, params.status, params.outcome);
|
|
511
|
+
return jsonResponse({ updated: true });
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
server.tool(
|
|
516
|
+
"get_plan",
|
|
517
|
+
"Retrieves the current plan for a session — use this to check what's next or review progress",
|
|
518
|
+
{
|
|
519
|
+
session_id: z.string().describe("Session ID"),
|
|
520
|
+
},
|
|
521
|
+
async (params) => {
|
|
522
|
+
const steps = getPlanSteps(params.session_id);
|
|
523
|
+
if (steps.length === 0) {
|
|
524
|
+
return { content: [{ type: "text" as const, text: "No plan found for this session." }] };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const completed = steps.filter(s => s.status === "completed").length;
|
|
528
|
+
const failed = steps.filter(s => s.status === "failed").length;
|
|
529
|
+
const inProgress = steps.filter(s => s.status === "in_progress").length;
|
|
530
|
+
const pending = steps.filter(s => s.status === "pending").length;
|
|
531
|
+
|
|
532
|
+
let md = `# Plan Progress: ${completed}/${steps.length} completed\n\n`;
|
|
533
|
+
if (failed > 0) md += `**${failed} failed** | `;
|
|
534
|
+
if (inProgress > 0) md += `**${inProgress} in progress** | `;
|
|
535
|
+
md += `**${pending} pending**\n\n`;
|
|
536
|
+
|
|
537
|
+
for (const step of steps) {
|
|
538
|
+
const icon =
|
|
539
|
+
step.status === "completed" ? "[x]" :
|
|
540
|
+
step.status === "in_progress" ? "[>]" :
|
|
541
|
+
step.status === "failed" ? "[!]" :
|
|
542
|
+
step.status === "skipped" ? "[-]" :
|
|
543
|
+
"[ ]";
|
|
544
|
+
const indent = step.parent_step ? " " : "";
|
|
545
|
+
md += `${indent}${icon} **Step ${step.step_number}:** ${step.title}\n`;
|
|
546
|
+
if (step.description) md += `${indent} ${step.description}\n`;
|
|
547
|
+
if (step.outcome) md += `${indent} → ${step.outcome}\n`;
|
|
548
|
+
if (step.file_targets) {
|
|
549
|
+
const files = JSON.parse(step.file_targets) as string[];
|
|
550
|
+
md += `${indent} files: ${files.join(", ")}\n`;
|
|
551
|
+
}
|
|
552
|
+
if (step.duration_seconds != null) md += `${indent} (${step.duration_seconds}s)\n`;
|
|
553
|
+
md += `\n`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return { content: [{ type: "text" as const, text: md }] };
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
server.tool(
|
|
561
|
+
"list_sessions",
|
|
562
|
+
"Browse past telemetry sessions. By default only shows top-level sessions (not children). Pass parent_session_id to list child streams. Pass user_email to filter by user.",
|
|
563
|
+
{
|
|
564
|
+
limit: z.coerce.number().optional().default(10).describe("Max sessions to return"),
|
|
565
|
+
status: z
|
|
566
|
+
.string()
|
|
567
|
+
.optional()
|
|
568
|
+
.describe("Filter by status: in_progress, completed, failed, interrupted"),
|
|
569
|
+
parent_session_id: z
|
|
570
|
+
.string()
|
|
571
|
+
.optional()
|
|
572
|
+
.describe("Filter to children of this parent session ID"),
|
|
573
|
+
user_email: z
|
|
574
|
+
.string()
|
|
575
|
+
.optional()
|
|
576
|
+
.describe("Filter to sessions owned by this user email"),
|
|
577
|
+
},
|
|
578
|
+
async (params) => {
|
|
579
|
+
const sessions = listSessions(params.limit, params.status, params.parent_session_id, params.user_email);
|
|
580
|
+
const rows = sessions.map((s) => ({
|
|
581
|
+
id: s.id,
|
|
582
|
+
worktree: s.worktree_name,
|
|
583
|
+
ticket: s.ticket_id ?? "-",
|
|
584
|
+
status: s.status,
|
|
585
|
+
iterations: `${s.total_iterations}/${s.max_iterations}`,
|
|
586
|
+
tokens: `${s.total_input_tokens} in / ${s.total_output_tokens} out`,
|
|
587
|
+
duration: s.duration_seconds != null ? `${s.duration_seconds}s` : "ongoing",
|
|
588
|
+
started: s.started_at,
|
|
589
|
+
user: (s as Session & { user_email?: string }).user_email ?? "-",
|
|
590
|
+
}));
|
|
591
|
+
return jsonResponse(rows);
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// --- Walkthrough tools ---
|
|
596
|
+
|
|
597
|
+
server.tool(
|
|
598
|
+
"log_walkthrough_start",
|
|
599
|
+
"Creates a new walkthrough session for tracking a UI walkthrough/scenario run",
|
|
600
|
+
{
|
|
601
|
+
url: z.string().describe("Starting URL of the walkthrough"),
|
|
602
|
+
app_name: z.string().optional().describe("Application name (e.g., 'my-app')"),
|
|
603
|
+
scenario: z.string().optional().describe("Scenario name (e.g., 'login-happy')"),
|
|
604
|
+
},
|
|
605
|
+
async (params) => {
|
|
606
|
+
const walkthrough_id = createWalkthrough(params.url, params.app_name, params.scenario);
|
|
607
|
+
return jsonResponse({ walkthrough_id });
|
|
608
|
+
}
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
server.tool(
|
|
612
|
+
"log_walkthrough_action",
|
|
613
|
+
"Logs an action taken during a walkthrough — navigate, click, type, assert, wait, or screenshot",
|
|
614
|
+
{
|
|
615
|
+
walkthrough_id: z.coerce.number().describe("Walkthrough ID"),
|
|
616
|
+
action_type: z.enum(["navigate", "click", "type", "assert", "wait", "screenshot"]).describe("Type of action"),
|
|
617
|
+
target: z.string().describe("Target selector or URL"),
|
|
618
|
+
value: z.string().optional().describe("Value typed or assertion expected"),
|
|
619
|
+
status: z.enum(["pass", "fail"]).describe("Whether the action succeeded"),
|
|
620
|
+
message: z.string().optional().describe("Additional context or error message"),
|
|
621
|
+
},
|
|
622
|
+
async (params) => {
|
|
623
|
+
const action_id = insertWalkthroughAction(
|
|
624
|
+
params.walkthrough_id,
|
|
625
|
+
params.action_type,
|
|
626
|
+
params.target,
|
|
627
|
+
params.value,
|
|
628
|
+
params.status,
|
|
629
|
+
params.message
|
|
630
|
+
);
|
|
631
|
+
return jsonResponse({ action_id });
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
server.tool(
|
|
636
|
+
"log_walkthrough_screenshot",
|
|
637
|
+
"Uploads a screenshot taken during a walkthrough. Provide either file_path (preferred — path to screenshot on disk) or image_data (base64-encoded).",
|
|
638
|
+
{
|
|
639
|
+
walkthrough_id: z.coerce.number().describe("Walkthrough ID"),
|
|
640
|
+
name: z.string().describe("Screenshot name (e.g., 'login-page')"),
|
|
641
|
+
file_path: z.string().optional().describe("Path to screenshot file on disk (preferred over base64)"),
|
|
642
|
+
image_data: z.string().optional().describe("Base64-encoded image data (fallback if file_path not available)"),
|
|
643
|
+
annotation: z.string().optional().describe("Description of what the screenshot shows"),
|
|
644
|
+
action_id: z.coerce.number().optional().describe("ID of the action this screenshot belongs to"),
|
|
645
|
+
},
|
|
646
|
+
async (params) => {
|
|
647
|
+
const screenshot_id = insertWalkthroughScreenshot(
|
|
648
|
+
params.walkthrough_id,
|
|
649
|
+
params.name,
|
|
650
|
+
params.image_data,
|
|
651
|
+
params.annotation,
|
|
652
|
+
params.action_id,
|
|
653
|
+
params.file_path
|
|
654
|
+
);
|
|
655
|
+
return jsonResponse({ screenshot_id });
|
|
656
|
+
}
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
server.tool(
|
|
660
|
+
"log_walkthrough_end",
|
|
661
|
+
"Finalizes a walkthrough with pass/fail status and summary stats",
|
|
662
|
+
{
|
|
663
|
+
walkthrough_id: z.coerce.number().describe("Walkthrough ID"),
|
|
664
|
+
status: z.enum(["pass", "fail", "partial"]).describe("Overall walkthrough result"),
|
|
665
|
+
summary: z.string().describe("Summary of walkthrough results"),
|
|
666
|
+
total_actions: z.coerce.number().optional().describe("Total number of actions taken"),
|
|
667
|
+
passed: z.coerce.number().optional().describe("Number of actions that passed"),
|
|
668
|
+
failed: z.coerce.number().optional().describe("Number of actions that failed"),
|
|
669
|
+
},
|
|
670
|
+
async (params) => {
|
|
671
|
+
endWalkthrough(
|
|
672
|
+
params.walkthrough_id,
|
|
673
|
+
params.status,
|
|
674
|
+
params.summary,
|
|
675
|
+
params.total_actions,
|
|
676
|
+
params.passed,
|
|
677
|
+
params.failed
|
|
678
|
+
);
|
|
679
|
+
return jsonResponse({ ended: true });
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
// --- Coordinator tools ---
|
|
684
|
+
|
|
685
|
+
server.tool(
|
|
686
|
+
"save_coordinator_state",
|
|
687
|
+
"Save or update the coordinator session state. Merges with existing state — partial updates are fine. Use this to track workspace statuses, pending tasks, and decisions.",
|
|
688
|
+
{
|
|
689
|
+
session_id: z.string().describe("Coordinator session ID"),
|
|
690
|
+
workspaces: z
|
|
691
|
+
.record(z.string(), z.object({
|
|
692
|
+
name: z.string(),
|
|
693
|
+
status: z.enum(["idle", "working", "done", "failed", "paused"]),
|
|
694
|
+
last_task: z.string().optional(),
|
|
695
|
+
pending: z.array(z.string()).optional(),
|
|
696
|
+
session_id: z.string().optional(),
|
|
697
|
+
last_updated: z.string().optional(),
|
|
698
|
+
}))
|
|
699
|
+
.optional()
|
|
700
|
+
.describe("Workspace states keyed by workspace ID"),
|
|
701
|
+
pending_tasks: z.array(z.string()).optional().describe("Tasks queued for dispatch"),
|
|
702
|
+
decisions: z.array(z.string()).optional().describe("Key decisions made this session"),
|
|
703
|
+
notes: z.string().optional().describe("Free-form notes"),
|
|
704
|
+
},
|
|
705
|
+
async (params) => {
|
|
706
|
+
const state = saveCoordinatorState(params.session_id, {
|
|
707
|
+
workspaces: params.workspaces,
|
|
708
|
+
pending_tasks: params.pending_tasks,
|
|
709
|
+
decisions: params.decisions,
|
|
710
|
+
notes: params.notes,
|
|
711
|
+
});
|
|
712
|
+
return jsonResponse({ saved: true, workspace_count: Object.keys(state.workspaces).length });
|
|
713
|
+
}
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
server.tool(
|
|
717
|
+
"get_coordinator_state",
|
|
718
|
+
"Load coordinator session state. Omit session_id to get the most recent coordinator session. Returns full state with workspaces, pending tasks, decisions, notes, and context history.",
|
|
719
|
+
{
|
|
720
|
+
session_id: z.string().optional().describe("Coordinator session ID — omit for most recent"),
|
|
721
|
+
},
|
|
722
|
+
async (params) => {
|
|
723
|
+
const state = params.session_id
|
|
724
|
+
? getCoordinatorState(params.session_id)
|
|
725
|
+
: getLatestCoordinatorSession();
|
|
726
|
+
|
|
727
|
+
if (!state) {
|
|
728
|
+
return { content: [{ type: "text" as const, text: "No coordinator session found. Create one with save_coordinator_state." }] };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const history = getCoordinatorHistory(state.session_id);
|
|
732
|
+
|
|
733
|
+
let md = `# Coordinator Session: ${state.session_id}\n\n`;
|
|
734
|
+
md += `**Started:** ${state.started_at}\n`;
|
|
735
|
+
md += `**Last updated:** ${state.last_updated}\n\n`;
|
|
736
|
+
|
|
737
|
+
// Workspaces
|
|
738
|
+
md += `## Workspaces (${Object.keys(state.workspaces).length})\n\n`;
|
|
739
|
+
for (const [id, ws] of Object.entries(state.workspaces)) {
|
|
740
|
+
const icon = ws.status === "done" ? "[x]" : ws.status === "working" ? "[>]" : ws.status === "failed" ? "[!]" : ws.status === "paused" ? "[-]" : "[ ]";
|
|
741
|
+
md += `${icon} **${id}** — ${ws.name} (${ws.status})`;
|
|
742
|
+
if (ws.last_task) md += ` — last: ${ws.last_task}`;
|
|
743
|
+
md += `\n`;
|
|
744
|
+
if (ws.pending && ws.pending.length > 0) {
|
|
745
|
+
for (const p of ws.pending) md += ` → ${p}\n`;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Pending tasks
|
|
750
|
+
if (state.pending_tasks.length > 0) {
|
|
751
|
+
md += `\n## Pending Tasks\n\n`;
|
|
752
|
+
for (const t of state.pending_tasks) md += `- ${t}\n`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Decisions
|
|
756
|
+
if (state.decisions.length > 0) {
|
|
757
|
+
md += `\n## Decisions\n\n`;
|
|
758
|
+
for (const d of state.decisions) md += `- ${d}\n`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Notes
|
|
762
|
+
if (state.notes) {
|
|
763
|
+
md += `\n## Notes\n\n${state.notes}\n`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Context history
|
|
767
|
+
if (history.length > 0) {
|
|
768
|
+
md += `\n## Context History (${history.length} snapshots)\n\n`;
|
|
769
|
+
for (const h of history) {
|
|
770
|
+
md += `### ${h.timestamp}\n`;
|
|
771
|
+
md += `${h.summary}\n`;
|
|
772
|
+
if (h.key_decisions.length > 0) {
|
|
773
|
+
md += `**Decisions:** ${h.key_decisions.join("; ")}\n`;
|
|
774
|
+
}
|
|
775
|
+
if (h.pending_work.length > 0) {
|
|
776
|
+
md += `**Pending:** ${h.pending_work.join("; ")}\n`;
|
|
777
|
+
}
|
|
778
|
+
md += `\n`;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return { content: [{ type: "text" as const, text: md }] };
|
|
783
|
+
}
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
server.tool(
|
|
787
|
+
"list_coordinator_sessions",
|
|
788
|
+
"List past coordinator sessions with summaries",
|
|
789
|
+
{
|
|
790
|
+
limit: z.coerce.number().optional().default(5).describe("Max sessions to return"),
|
|
791
|
+
},
|
|
792
|
+
async (params) => {
|
|
793
|
+
const sessions = listCoordinatorSessions(params.limit);
|
|
794
|
+
if (sessions.length === 0) {
|
|
795
|
+
return { content: [{ type: "text" as const, text: "No coordinator sessions found." }] };
|
|
796
|
+
}
|
|
797
|
+
const rows = sessions.map(s => ({
|
|
798
|
+
session_id: s.session_id,
|
|
799
|
+
started: s.started_at,
|
|
800
|
+
last_updated: s.last_updated,
|
|
801
|
+
workspaces: Object.keys(s.workspaces).length,
|
|
802
|
+
pending_tasks: s.pending_tasks.length,
|
|
803
|
+
decisions: s.decisions.length,
|
|
804
|
+
}));
|
|
805
|
+
return jsonResponse(rows);
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
server.tool(
|
|
810
|
+
"compact_coordinator_context",
|
|
811
|
+
"Save a context snapshot before conversation compaction. Appends to the session's history.jsonl. Load it on resume to recover the full picture.",
|
|
812
|
+
{
|
|
813
|
+
session_id: z.string().describe("Coordinator session ID"),
|
|
814
|
+
summary: z.string().describe("Summary of what happened in this conversation segment"),
|
|
815
|
+
key_decisions: z.array(z.string()).optional().describe("Important decisions made"),
|
|
816
|
+
workspace_states: z
|
|
817
|
+
.record(z.string(), z.object({
|
|
818
|
+
name: z.string(),
|
|
819
|
+
status: z.enum(["idle", "working", "done", "failed", "paused"]),
|
|
820
|
+
last_task: z.string().optional(),
|
|
821
|
+
pending: z.array(z.string()).optional(),
|
|
822
|
+
}))
|
|
823
|
+
.optional()
|
|
824
|
+
.describe("Current workspace states"),
|
|
825
|
+
pending_work: z.array(z.string()).optional().describe("Work items still pending"),
|
|
826
|
+
},
|
|
827
|
+
async (params) => {
|
|
828
|
+
compactCoordinatorContext(params.session_id, {
|
|
829
|
+
timestamp: new Date().toISOString(),
|
|
830
|
+
summary: params.summary,
|
|
831
|
+
key_decisions: params.key_decisions || [],
|
|
832
|
+
workspace_states: params.workspace_states || {},
|
|
833
|
+
pending_work: params.pending_work || [],
|
|
834
|
+
});
|
|
835
|
+
return jsonResponse({ compacted: true });
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
server.tool(
|
|
840
|
+
"get_comparison_results",
|
|
841
|
+
"Retrieves visual comparison findings from a /compare-image session. Returns per-slide severity, diff percentage, findings, and screenshot URLs. Use this to decide what needs iteration after a visual QA run.",
|
|
842
|
+
{
|
|
843
|
+
session_id: z.string().describe("Session ID of the compare-image run"),
|
|
844
|
+
min_severity: z
|
|
845
|
+
.enum(["CRITICAL", "NOTABLE", "MINOR", "GOOD"])
|
|
846
|
+
.optional()
|
|
847
|
+
.describe("Minimum severity to include — e.g. 'NOTABLE' returns NOTABLE + CRITICAL only"),
|
|
848
|
+
},
|
|
849
|
+
async (params) => {
|
|
850
|
+
const { results, summary } = getComparisonResults(
|
|
851
|
+
params.session_id,
|
|
852
|
+
params.min_severity
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
if (results.length === 0) {
|
|
856
|
+
return jsonResponse({
|
|
857
|
+
error: "No comparison results found for this session. Ensure the session used /compare-image.",
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return jsonResponse({ results, summary, total: results.length });
|
|
862
|
+
}
|
|
863
|
+
);
|
|
864
|
+
}
|