proteum 2.2.8 → 2.3.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 (51) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +50 -12
  3. package/agents/project/AGENTS.md +47 -10
  4. package/agents/project/CODING_STYLE.md +5 -1
  5. package/agents/project/client/AGENTS.md +2 -0
  6. package/agents/project/diagnostics.md +8 -5
  7. package/agents/project/optimizations.md +1 -0
  8. package/agents/project/root/AGENTS.md +18 -10
  9. package/agents/project/tests/AGENTS.md +6 -1
  10. package/agents/project/tests/e2e/AGENTS.md +13 -0
  11. package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
  12. package/cli/commands/check.ts +21 -3
  13. package/cli/commands/configure.ts +1 -0
  14. package/cli/commands/connect.ts +40 -4
  15. package/cli/commands/diagnose.ts +136 -5
  16. package/cli/commands/doctor.ts +24 -4
  17. package/cli/commands/explain.ts +105 -6
  18. package/cli/commands/mcp.ts +16 -0
  19. package/cli/commands/orient.ts +66 -3
  20. package/cli/commands/perf.ts +118 -13
  21. package/cli/commands/runtime.ts +151 -0
  22. package/cli/commands/trace.ts +116 -21
  23. package/cli/mcp/provider.ts +365 -0
  24. package/cli/mcp/stdio.ts +16 -0
  25. package/cli/presentation/commands.ts +79 -22
  26. package/cli/presentation/devSession.ts +2 -0
  27. package/cli/runtime/commands.ts +95 -12
  28. package/cli/utils/agentOutput.ts +46 -0
  29. package/cli/utils/agents.ts +225 -48
  30. package/common/dev/inspection.ts +30 -9
  31. package/common/dev/mcpPayloads.ts +736 -0
  32. package/common/dev/mcpServer.ts +254 -0
  33. package/docs/agent-routing.md +126 -0
  34. package/docs/dev-commands.md +2 -0
  35. package/docs/dev-sessions.md +2 -1
  36. package/docs/diagnostics.md +68 -23
  37. package/docs/mcp.md +149 -0
  38. package/docs/migrate-from-2.1.3.md +15 -5
  39. package/docs/request-tracing.md +12 -6
  40. package/eslint.js +220 -0
  41. package/package.json +2 -1
  42. package/server/app/devMcp.ts +159 -0
  43. package/server/services/router/http/cache.ts +116 -0
  44. package/server/services/router/http/index.ts +94 -35
  45. package/server/services/router/index.ts +8 -11
  46. package/tests/agents-utils.test.cjs +89 -11
  47. package/tests/dev-transpile-watch.test.cjs +117 -8
  48. package/tests/eslint-rules.test.cjs +110 -0
  49. package/tests/inspection.test.cjs +67 -0
  50. package/tests/mcp.test.cjs +127 -0
  51. package/tests/router-cache-config.test.cjs +74 -0
@@ -0,0 +1,736 @@
1
+ import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from './console';
2
+ import type { TDoctorResponse } from './diagnostics';
3
+ import { buildExplainSummaryItems } from './diagnostics';
4
+ import type { TDiagnoseResponse, TExplainOwnerResponse, TOrientResponse } from './inspection';
5
+ import type { TPerfRequestResponse, TPerfTopResponse } from './performance';
6
+ import type { TProteumManifest } from './proteumManifest';
7
+ import type { TRequestTrace } from './requestTrace';
8
+
9
+ export type TProteumMcpNextAction = {
10
+ command?: string;
11
+ label: string;
12
+ reason?: string;
13
+ tool?: string;
14
+ toolArgs?: Record<string, unknown>;
15
+ };
16
+
17
+ export type TProteumMcpOmittedDetail = {
18
+ reason: string;
19
+ command?: string;
20
+ tool?: string;
21
+ toolArgs?: Record<string, unknown>;
22
+ };
23
+
24
+ export type TProteumMcpPayload<TData extends object = Record<string, unknown>> = {
25
+ ok: true;
26
+ format: 'proteum-mcp-v1';
27
+ summary: string;
28
+ data: TData;
29
+ nextActions?: TProteumMcpNextAction[];
30
+ omitted?: TProteumMcpOmittedDetail[];
31
+ };
32
+
33
+ type TNodeFs = {
34
+ existsSync: (filepath: string) => boolean;
35
+ readFileSync: (filepath: string, encoding: 'utf8') => string;
36
+ statSync: (filepath: string) => { isDirectory: () => boolean };
37
+ };
38
+
39
+ type TNodePath = {
40
+ dirname: (filepath: string) => string;
41
+ join: (...segments: string[]) => string;
42
+ relative: (from: string, to: string) => string;
43
+ resolve: (...segments: string[]) => string;
44
+ };
45
+
46
+ const maxInstructionPreviewLength = 360;
47
+ const maxTextLength = 220;
48
+ const nodeRequire = (() => {
49
+ try {
50
+ return eval('require') as NodeRequire;
51
+ } catch (_error) {
52
+ return undefined;
53
+ }
54
+ })();
55
+ const fs = nodeRequire ? (nodeRequire('fs') as TNodeFs) : undefined;
56
+ const path = nodeRequire ? (nodeRequire('path') as TNodePath) : undefined;
57
+
58
+ const hasNodeFs = () => fs !== undefined;
59
+ const hasNodePath = () => path !== undefined;
60
+ const fileExists = (filepath: string) => fs !== undefined && fs.existsSync(filepath);
61
+ const directoryExists = (filepath: string) => {
62
+ if (fs === undefined || !fs.existsSync(filepath)) return false;
63
+ try {
64
+ return fs.statSync(filepath).isDirectory();
65
+ } catch (_error) {
66
+ return false;
67
+ }
68
+ };
69
+
70
+ export const truncateForMcp = (value: string, max = maxTextLength) =>
71
+ value.length <= max ? value : `${value.slice(0, max)}...`;
72
+
73
+ export const compactList = <TValue>(values: TValue[], limit: number) => values.slice(0, Math.max(0, limit));
74
+
75
+ export const createMcpPayload = <TData extends object>({
76
+ data,
77
+ nextActions,
78
+ omitted,
79
+ summary,
80
+ }: Omit<TProteumMcpPayload<TData>, 'format' | 'ok'>): TProteumMcpPayload<TData> => ({
81
+ ok: true,
82
+ format: 'proteum-mcp-v1',
83
+ summary,
84
+ data,
85
+ ...(nextActions && nextActions.length > 0 ? { nextActions } : {}),
86
+ ...(omitted && omitted.length > 0 ? { omitted } : {}),
87
+ });
88
+
89
+ export const stringifyMcpPayload = (value: object) => JSON.stringify(value);
90
+
91
+ export const summarizeManifest = (manifest: TProteumManifest | undefined) => {
92
+ if (!manifest) return undefined;
93
+
94
+ const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
95
+ const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
96
+
97
+ return {
98
+ appRoot: manifest.app.root,
99
+ coreRoot: manifest.app.coreRoot,
100
+ identifier: manifest.app.identity.identifier,
101
+ name: manifest.app.identity.name,
102
+ routerPort: manifest.env.resolved.routerPort,
103
+ env: {
104
+ name: manifest.env.resolved.name,
105
+ profile: manifest.env.resolved.profile,
106
+ internalUrl: manifest.env.resolved.routerInternalUrl,
107
+ },
108
+ diagnostics: { errors, warnings },
109
+ counts: {
110
+ commands: manifest.commands.length,
111
+ connectedProjects: manifest.connectedProjects.length,
112
+ controllers: manifest.controllers.length,
113
+ layouts: manifest.layouts.length,
114
+ routes: manifest.routes.client.length + manifest.routes.server.length,
115
+ clientRoutes: manifest.routes.client.length,
116
+ serverRoutes: manifest.routes.server.length,
117
+ services: manifest.services.app.length + manifest.services.routerPlugins.length,
118
+ },
119
+ };
120
+ };
121
+
122
+ const compactOwnerMatch = (match: TExplainOwnerResponse['matches'][number]) => ({
123
+ kind: match.kind,
124
+ label: match.label,
125
+ score: match.score,
126
+ scope: match.scopeLabel,
127
+ origin: match.originHint,
128
+ source: match.source,
129
+ });
130
+
131
+ const compactDiagnostic = (diagnostic: TDoctorResponse['diagnostics'][number]) => ({
132
+ level: diagnostic.level,
133
+ code: diagnostic.code,
134
+ message: truncateForMcp(diagnostic.message),
135
+ filepath: diagnostic.filepath,
136
+ sourceLocation: diagnostic.sourceLocation,
137
+ fixHint: diagnostic.fixHint ? truncateForMcp(diagnostic.fixHint) : undefined,
138
+ });
139
+
140
+ export const compactDoctorResponse = ({
141
+ contracts,
142
+ doctor,
143
+ }: {
144
+ contracts?: TDoctorResponse;
145
+ doctor: TDoctorResponse;
146
+ }) =>
147
+ createMcpPayload({
148
+ summary: contracts
149
+ ? `Doctor: ${doctor.summary.errors} errors/${doctor.summary.warnings} warnings; contracts: ${contracts.summary.errors} errors/${contracts.summary.warnings} warnings`
150
+ : `Doctor: ${doctor.summary.errors} errors/${doctor.summary.warnings} warnings`,
151
+ data: {
152
+ doctor: {
153
+ summary: doctor.summary,
154
+ top: compactList(doctor.diagnostics, 8).map(compactDiagnostic),
155
+ total: doctor.diagnostics.length,
156
+ },
157
+ contracts: contracts
158
+ ? {
159
+ summary: contracts.summary,
160
+ top: compactList(contracts.diagnostics, 8).map(compactDiagnostic),
161
+ total: contracts.diagnostics.length,
162
+ }
163
+ : undefined,
164
+ },
165
+ omitted:
166
+ doctor.diagnostics.length > 8 || (contracts && contracts.diagnostics.length > 8)
167
+ ? [
168
+ {
169
+ reason: 'Diagnostics are capped in MCP output. Use the CLI full-detail command when every diagnostic is required.',
170
+ command: 'proteum doctor --full',
171
+ },
172
+ ]
173
+ : undefined,
174
+ });
175
+
176
+ export const compactOrientationResponse = (response: TOrientResponse) => {
177
+ const topOwner = response.owner.matches[0];
178
+ const summary = topOwner
179
+ ? `${response.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
180
+ : `${response.query} -> no manifest owner matched`;
181
+
182
+ return createMcpPayload({
183
+ summary,
184
+ data: {
185
+ query: response.query,
186
+ app: response.app,
187
+ owner: {
188
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
189
+ matches: compactList(response.owner.matches, 5).map(compactOwnerMatch),
190
+ totalReturned: response.owner.matches.length,
191
+ },
192
+ instructions: {
193
+ mustRead: [...new Set([response.guidance.agents, ...response.guidance.areaAgents])],
194
+ readWhen: [
195
+ {
196
+ file: response.guidance.diagnostics,
197
+ when: 'Raw errors, failing requests, traces, perf regressions, or reproduction work.',
198
+ },
199
+ {
200
+ file: response.guidance.codingStyle,
201
+ when: 'Before editing implementation files.',
202
+ },
203
+ {
204
+ file: response.guidance.optimizations,
205
+ when: 'Client-side implementation, packages, build, runtime, or performance work.',
206
+ },
207
+ ],
208
+ },
209
+ connected: {
210
+ imports: compactList(response.connected.imports, 5),
211
+ producers: compactList(response.connected.producers, 4),
212
+ totalImports: response.connected.imports.length,
213
+ totalProducers: response.connected.producers.length,
214
+ },
215
+ warnings: response.warnings,
216
+ },
217
+ nextActions: response.nextSteps.map((step) => ({
218
+ command: step.command,
219
+ label: step.label,
220
+ reason: step.reason,
221
+ })),
222
+ });
223
+ };
224
+
225
+ export const compactExplainSummary = ({
226
+ manifest,
227
+ owner,
228
+ query,
229
+ }: {
230
+ manifest: TProteumManifest;
231
+ owner?: TExplainOwnerResponse;
232
+ query?: string;
233
+ }) => {
234
+ if (owner) {
235
+ const topOwner = owner.matches[0];
236
+ return createMcpPayload({
237
+ summary: topOwner
238
+ ? `${query || owner.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
239
+ : `${query || owner.query} -> no owner matched`,
240
+ data: {
241
+ query: query || owner.query,
242
+ normalizedQuery: owner.normalizedQuery,
243
+ owner: {
244
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
245
+ matches: compactList(owner.matches, 8).map(compactOwnerMatch),
246
+ totalReturned: owner.matches.length,
247
+ },
248
+ manifest: summarizeManifest(manifest),
249
+ },
250
+ });
251
+ }
252
+
253
+ const items = buildExplainSummaryItems(manifest).map((item) => truncateForMcp(item, 300));
254
+ return createMcpPayload({
255
+ summary: `Manifest ${manifest.app.identity.identifier}: ${manifest.controllers.length} controllers, ${manifest.routes.client.length + manifest.routes.server.length} routes`,
256
+ data: {
257
+ manifest: summarizeManifest(manifest),
258
+ summaryItems: items,
259
+ },
260
+ });
261
+ };
262
+
263
+ const compactRequest = (request: TDiagnoseResponse['request']) =>
264
+ request
265
+ ? {
266
+ id: request.id,
267
+ method: request.method,
268
+ path: request.path,
269
+ statusCode: request.statusCode,
270
+ durationMs: request.durationMs,
271
+ capture: request.capture,
272
+ user: request.user,
273
+ errorMessage: request.errorMessage ? truncateForMcp(request.errorMessage) : undefined,
274
+ counts: {
275
+ calls: request.calls.length,
276
+ events: request.events.length,
277
+ sqlQueries: request.sqlQueries.length,
278
+ droppedEvents: request.droppedEvents,
279
+ },
280
+ }
281
+ : undefined;
282
+
283
+ export const compactDiagnoseResponse = (response: TDiagnoseResponse) => {
284
+ const request = compactRequest(response.request);
285
+ const doctorSummary = `${response.doctor.summary.errors} doctor errors/${response.doctor.summary.warnings} warnings`;
286
+ const contractsSummary = `${response.contracts.summary.errors} contract errors/${response.contracts.summary.warnings} warnings`;
287
+ const traceSummary = request
288
+ ? `${request.method} ${request.path} status=${request.statusCode ?? 'pending'} durationMs=${request.durationMs ?? 'pending'}`
289
+ : 'no matching request trace';
290
+
291
+ return createMcpPayload({
292
+ summary: `${response.query || 'request'}: ${traceSummary}; ${doctorSummary}; ${contractsSummary}`,
293
+ data: {
294
+ query: response.query,
295
+ request,
296
+ owner: {
297
+ top: response.owner.matches[0] ? compactOwnerMatch(response.owner.matches[0]) : undefined,
298
+ matches: compactList(response.owner.matches, 5).map(compactOwnerMatch),
299
+ totalReturned: response.owner.matches.length,
300
+ },
301
+ suspects: compactList(response.suspects, 8),
302
+ chain: compactList(response.chain || [], 10),
303
+ diagnostics: {
304
+ doctor: {
305
+ summary: response.doctor.summary,
306
+ top: compactList(response.doctor.diagnostics, 8).map(compactDiagnostic),
307
+ total: response.doctor.diagnostics.length,
308
+ },
309
+ contracts: {
310
+ summary: response.contracts.summary,
311
+ top: compactList(response.contracts.diagnostics, 8).map(compactDiagnostic),
312
+ total: response.contracts.diagnostics.length,
313
+ },
314
+ },
315
+ logs: compactList(response.serverLogs.logs, 12).map((entry) => ({
316
+ level: entry.level,
317
+ time: entry.time,
318
+ text: truncateForMcp(entry.text),
319
+ })),
320
+ instructions: response.orientation
321
+ ? {
322
+ mustRead: [...new Set([response.orientation.guidance.agents, ...response.orientation.guidance.areaAgents])],
323
+ diagnostics: response.orientation.guidance.diagnostics,
324
+ codingStyle: response.orientation.guidance.codingStyle,
325
+ optimizations: response.orientation.guidance.optimizations,
326
+ }
327
+ : undefined,
328
+ },
329
+ nextActions: response.orientation?.nextSteps.map((step) => ({
330
+ command: step.command,
331
+ label: step.label,
332
+ reason: step.reason,
333
+ })),
334
+ omitted: response.request
335
+ ? [
336
+ {
337
+ reason: 'Full request events, payload summaries, and SQL text are omitted from MCP diagnose output.',
338
+ tool: 'trace_show',
339
+ toolArgs: { requestId: response.request.id, detail: 'full', limit: 50 },
340
+ },
341
+ ]
342
+ : undefined,
343
+ });
344
+ };
345
+
346
+ const compactTraceCall = (call: TRequestTrace['calls'][number]) => ({
347
+ id: call.id,
348
+ origin: call.origin,
349
+ label: call.label,
350
+ method: call.method,
351
+ path: call.path,
352
+ statusCode: call.statusCode,
353
+ durationMs: call.durationMs,
354
+ errorMessage: call.errorMessage ? truncateForMcp(call.errorMessage) : undefined,
355
+ });
356
+
357
+ const compactTraceSql = (query: TRequestTrace['sqlQueries'][number], includeQuery = false) => ({
358
+ id: query.id,
359
+ caller: query.callerLabel || `${query.callerMethod} ${query.callerPath}`,
360
+ kind: query.kind,
361
+ operation: query.operation,
362
+ model: query.model,
363
+ durationMs: query.durationMs,
364
+ fingerprint: query.fingerprint,
365
+ query: includeQuery ? truncateForMcp(query.query, 180) : undefined,
366
+ });
367
+
368
+ const compactTraceEvent = (event: TRequestTrace['events'][number], includeDetails = false) => ({
369
+ index: event.index,
370
+ elapsedMs: event.elapsedMs,
371
+ type: event.type,
372
+ detailKeys: Object.keys(event.details),
373
+ details: includeDetails ? event.details : undefined,
374
+ });
375
+
376
+ export const compactTraceResponse = ({
377
+ detail = 'compact',
378
+ limit = 50,
379
+ offset = 0,
380
+ request,
381
+ }: {
382
+ detail?: 'compact' | 'full';
383
+ limit?: number;
384
+ offset?: number;
385
+ request: TRequestTrace;
386
+ }) => {
387
+ const failedCalls = request.calls.filter((call) => call.errorMessage || (call.statusCode !== undefined && call.statusCode >= 400));
388
+ const errorEvents = request.events.filter((event) => event.type === 'error');
389
+ const hotCalls = [...request.calls].sort((left, right) => (right.durationMs || 0) - (left.durationMs || 0));
390
+ const hotSql = [...request.sqlQueries].sort((left, right) => right.durationMs - left.durationMs);
391
+ const pageOffset = Math.max(0, offset);
392
+ const pageLimit = Math.max(1, Math.min(100, limit));
393
+ const full = detail === 'full';
394
+
395
+ return createMcpPayload({
396
+ summary: `${request.id}: ${request.method} ${request.path} status=${request.statusCode ?? 'pending'} durationMs=${request.durationMs ?? 'pending'} events=${request.events.length} calls=${request.calls.length} sql=${request.sqlQueries.length}`,
397
+ data: {
398
+ request: {
399
+ id: request.id,
400
+ method: request.method,
401
+ path: request.path,
402
+ statusCode: request.statusCode,
403
+ durationMs: request.durationMs,
404
+ capture: request.capture,
405
+ user: request.user,
406
+ errorMessage: request.errorMessage ? truncateForMcp(request.errorMessage) : undefined,
407
+ droppedEvents: request.droppedEvents,
408
+ persistedFilepath: request.persistedFilepath,
409
+ },
410
+ counts: {
411
+ calls: request.calls.length,
412
+ events: request.events.length,
413
+ sqlQueries: request.sqlQueries.length,
414
+ },
415
+ failedCalls: compactList(failedCalls, 6).map(compactTraceCall),
416
+ errorEvents: compactList(errorEvents, 6).map((event) => compactTraceEvent(event, full)),
417
+ hotCalls: compactList(hotCalls, 6).map(compactTraceCall),
418
+ hotSql: compactList(hotSql, 6).map((query) => compactTraceSql(query, full)),
419
+ page: full
420
+ ? {
421
+ offset: pageOffset,
422
+ limit: pageLimit,
423
+ events: request.events.slice(pageOffset, pageOffset + pageLimit).map((event) => compactTraceEvent(event, true)),
424
+ calls: request.calls.slice(pageOffset, pageOffset + pageLimit).map(compactTraceCall),
425
+ sqlQueries: request.sqlQueries
426
+ .slice(pageOffset, pageOffset + pageLimit)
427
+ .map((query) => compactTraceSql(query, true)),
428
+ hasMore:
429
+ request.events.length > pageOffset + pageLimit ||
430
+ request.calls.length > pageOffset + pageLimit ||
431
+ request.sqlQueries.length > pageOffset + pageLimit,
432
+ }
433
+ : undefined,
434
+ },
435
+ nextActions: [
436
+ {
437
+ label: 'Diagnose Request',
438
+ tool: 'diagnose',
439
+ toolArgs: { requestId: request.id, query: request.path },
440
+ reason: 'Combine this trace with owner lookup, diagnostics, suspects, and server logs.',
441
+ },
442
+ {
443
+ label: 'Perf Request',
444
+ tool: 'perf_request',
445
+ toolArgs: { query: request.id },
446
+ reason: 'Inspect request timing, SQL, render, and memory rollups without full events.',
447
+ },
448
+ ],
449
+ omitted: full
450
+ ? undefined
451
+ : [
452
+ {
453
+ reason: 'Full events, payload summaries, raw SQL, and call bodies are omitted by default.',
454
+ tool: 'trace_show',
455
+ toolArgs: { requestId: request.id, detail: 'full', limit: 50 },
456
+ },
457
+ ],
458
+ });
459
+ };
460
+
461
+ const formatDuration = (value?: number) => (value === undefined ? 'n/a' : `${Math.round(value)} ms`);
462
+
463
+ const compactTopLikeRow = (row: TPerfTopResponse['rows'][number]) => ({
464
+ label: row.label,
465
+ requestCount: row.requestCount,
466
+ avgDurationMs: row.avgDurationMs,
467
+ p95DurationMs: row.p95DurationMs,
468
+ maxDurationMs: row.maxDurationMs,
469
+ avgCpuMs: row.avgCpuMs,
470
+ avgSqlDurationMs: row.avgSqlDurationMs,
471
+ avgRenderDurationMs: row.avgRenderDurationMs,
472
+ avgHeapDeltaBytes: row.avgHeapDeltaBytes,
473
+ slowestRequestId: row.slowestRequestId,
474
+ });
475
+
476
+ export const compactPerfTopResponse = (response: TPerfTopResponse) =>
477
+ createMcpPayload({
478
+ summary: `Perf top ${response.groupBy}: ${response.summary.requestCount} requests, ${response.summary.errorCount} errors, p95=${formatDuration(response.summary.p95DurationMs)}`,
479
+ data: {
480
+ groupBy: response.groupBy,
481
+ window: response.window,
482
+ summary: response.summary,
483
+ rows: compactList(response.rows, 10).map(compactTopLikeRow),
484
+ totalRows: response.rows.length,
485
+ },
486
+ omitted:
487
+ response.rows.length > 10
488
+ ? [{ reason: 'Perf rows are capped. Increase the tool limit or use `proteum perf top --full` for raw detail.' }]
489
+ : undefined,
490
+ });
491
+
492
+ const compactPerfSql = (query: TPerfRequestResponse['request']['hottestSqlQueries'][number]) => ({
493
+ callerLabel: query.callerLabel,
494
+ operation: query.operation,
495
+ model: query.model,
496
+ fingerprint: query.fingerprint,
497
+ durationMs: query.durationMs,
498
+ query: truncateForMcp(query.query, 160),
499
+ });
500
+
501
+ export const compactPerfRequestResponse = (response: TPerfRequestResponse) =>
502
+ createMcpPayload({
503
+ summary: `${response.request.requestId}: ${response.request.method} ${response.request.path} total=${formatDuration(response.request.totalDurationMs)} cpu=${formatDuration(response.request.cpuTotalMs)} sql=${formatDuration(response.request.sqlDurationMs)}`,
504
+ data: {
505
+ request: {
506
+ requestId: response.request.requestId,
507
+ method: response.request.method,
508
+ path: response.request.path,
509
+ statusCode: response.request.statusCode,
510
+ routeLabel: response.request.routeLabel,
511
+ controllerLabel: response.request.controllerLabel,
512
+ totalDurationMs: response.request.totalDurationMs,
513
+ cpuTotalMs: response.request.cpuTotalMs,
514
+ sqlDurationMs: response.request.sqlDurationMs,
515
+ callDurationMs: response.request.callDurationMs,
516
+ renderDurationMs: response.request.renderDurationMs,
517
+ selfDurationMs: response.request.selfDurationMs,
518
+ heapDeltaBytes: response.request.heapDeltaBytes,
519
+ },
520
+ stages: compactList(response.request.stages, 10),
521
+ hotCalls: compactList(response.request.hottestCalls, 8),
522
+ chain: compactList(response.request.chain || [], 10),
523
+ hotSql: compactList(response.request.hottestSqlQueries, 8).map(compactPerfSql),
524
+ },
525
+ nextActions: [
526
+ {
527
+ label: 'Diagnose Request',
528
+ tool: 'diagnose',
529
+ toolArgs: { query: response.request.path, requestId: response.request.requestId },
530
+ reason: 'Combine this request with owner, diagnostics, suspects, and logs.',
531
+ },
532
+ {
533
+ label: 'Trace Events',
534
+ tool: 'trace_show',
535
+ toolArgs: { requestId: response.request.requestId, detail: 'full', limit: 50 },
536
+ reason: 'Open raw event detail only if the compact waterfall is insufficient.',
537
+ },
538
+ ],
539
+ });
540
+
541
+ export const compactLogsResponse = ({
542
+ level,
543
+ limit,
544
+ response,
545
+ }: {
546
+ level: TDevConsoleLogLevel;
547
+ limit: number;
548
+ response: TDevConsoleLogsResponse;
549
+ }) =>
550
+ createMcpPayload({
551
+ summary: `${response.logs.length} dev log lines at level >= ${level}`,
552
+ data: {
553
+ level,
554
+ limit,
555
+ logs: response.logs.map((entry) => ({
556
+ level: entry.level,
557
+ time: entry.time,
558
+ text: truncateForMcp(entry.text),
559
+ })),
560
+ },
561
+ omitted:
562
+ response.logs.length >= limit
563
+ ? [{ reason: 'Log output reached the requested cap. Increase limit only when the latest compact lines are insufficient.' }]
564
+ : undefined,
565
+ });
566
+
567
+ const readPreview = (filepath: string) => {
568
+ if (fs === undefined) return undefined;
569
+ try {
570
+ return truncateForMcp(fs.readFileSync(filepath, 'utf8').replace(/\s+/g, ' ').trim(), maxInstructionPreviewLength);
571
+ } catch (_error) {
572
+ return undefined;
573
+ }
574
+ };
575
+
576
+ const findNearestRootWith = (startDir: string, relativeFilepath: string) => {
577
+ if (path === undefined) return undefined;
578
+ let current = path.resolve(startDir);
579
+
580
+ while (true) {
581
+ if (fileExists(path.join(current, relativeFilepath))) return current;
582
+ const parent = path.dirname(current);
583
+ if (parent === current) return undefined;
584
+ current = parent;
585
+ }
586
+ };
587
+
588
+ const findLikelyRepoRoot = (appRoot: string) => {
589
+ if (path === undefined) return appRoot;
590
+ let current = path.resolve(appRoot);
591
+
592
+ while (true) {
593
+ if (directoryExists(path.join(current, '.git'))) return current;
594
+ const parent = path.dirname(current);
595
+ if (parent === current) return appRoot;
596
+ current = parent;
597
+ }
598
+ };
599
+
600
+ const resolveDocumentFile = ({
601
+ appRoot,
602
+ repoRoot,
603
+ relativeFilepath,
604
+ }: {
605
+ appRoot: string;
606
+ repoRoot: string;
607
+ relativeFilepath: string;
608
+ }) => {
609
+ if (path === undefined) return undefined;
610
+
611
+ const appFilepath = path.join(appRoot, relativeFilepath);
612
+ if (fileExists(appFilepath)) return appFilepath;
613
+
614
+ const repoFilepath = path.join(repoRoot, relativeFilepath);
615
+ if (fileExists(repoFilepath)) return repoFilepath;
616
+
617
+ const nearestRoot = findNearestRootWith(appRoot, relativeFilepath);
618
+ return nearestRoot ? path.join(nearestRoot, relativeFilepath) : undefined;
619
+ };
620
+
621
+ export const resolveInstructionRouting = ({
622
+ appRoot,
623
+ query = '',
624
+ }: {
625
+ appRoot: string;
626
+ query?: string;
627
+ }) => {
628
+ const normalizedQuery = query.trim();
629
+ const repoRoot = findLikelyRepoRoot(appRoot);
630
+ const selected = new Map<string, { file: string; reason: string; preview?: string }>();
631
+ const readWhen: Array<{ file?: string; when: string }> = [];
632
+ const addInstruction = (relativeFilepath: string, reason: string, preferAppRoot = true) => {
633
+ if (path === undefined) return;
634
+ const roots = preferAppRoot ? [appRoot, repoRoot] : [repoRoot, appRoot];
635
+ for (const root of [...new Set(roots)]) {
636
+ const filepath = path.join(root, relativeFilepath);
637
+ if (!fileExists(filepath)) continue;
638
+ selected.set(filepath, { file: filepath, reason, preview: readPreview(filepath) });
639
+ return;
640
+ }
641
+ };
642
+ const addReadWhen = (relativeFilepath: string, when: string) => {
643
+ const filepath = resolveDocumentFile({ appRoot, repoRoot, relativeFilepath });
644
+ readWhen.push({ file: filepath, when });
645
+ };
646
+ const lowerQuery = normalizedQuery.toLowerCase();
647
+ const looksLikePage = lowerQuery.startsWith('/') || lowerQuery.includes('client/pages') || lowerQuery.includes('.tsx');
648
+ const looksLikeClient = looksLikePage || lowerQuery.includes('client/') || lowerQuery.includes('component') || lowerQuery.includes('island');
649
+ const looksLikeServerRoute =
650
+ lowerQuery.includes('server/routes') ||
651
+ lowerQuery.includes('route') ||
652
+ lowerQuery.includes('sitemap') ||
653
+ lowerQuery.includes('rss') ||
654
+ lowerQuery.startsWith('/api');
655
+ const looksLikeService =
656
+ lowerQuery.includes('server/services') ||
657
+ lowerQuery.includes('.controller') ||
658
+ lowerQuery.includes('controller') ||
659
+ lowerQuery.includes('service');
660
+ const looksLikeE2e = lowerQuery.includes('tests/e2e') || lowerQuery.includes('playwright') || lowerQuery.includes('journey');
661
+
662
+ addInstruction('AGENTS.md', 'Start with the root/app routing contract.');
663
+ if (repoRoot !== appRoot) addInstruction('AGENTS.md', 'Also apply the monorepo root routing contract.', false);
664
+ if (looksLikeClient) addInstruction('client/AGENTS.md', 'Client code or browser-visible behavior is in scope.');
665
+ if (looksLikePage) addInstruction('client/pages/AGENTS.md', 'Page routing, SSR data, or page render behavior may be in scope.');
666
+ if (looksLikeServerRoute) addInstruction('server/routes/AGENTS.md', 'Server route or crawlable endpoint behavior may be in scope.');
667
+ if (looksLikeService) addInstruction('server/services/AGENTS.md', 'Service/controller contracts or backend runtime behavior may be in scope.');
668
+ if (looksLikeE2e) {
669
+ addInstruction('tests/e2e/AGENTS.md', 'End-to-end behavior or Playwright workflow is in scope.');
670
+ addInstruction('tests/e2e/REAL_WORLD_JOURNEY_TESTS.md', 'Real-world journey coverage may be in scope.');
671
+ }
672
+
673
+ addReadWhen('diagnostics.md', 'Read for raw errors, failing requests, traces, perf regressions, or reproduction work.');
674
+ addReadWhen('CODING_STYLE.md', 'Read before editing implementation files.');
675
+ addReadWhen('optimizations.md', 'Read for client-side implementation, packages, build, runtime, or performance work.');
676
+
677
+ const selectedFiles = [...selected.values()];
678
+ return createMcpPayload({
679
+ summary: `${selectedFiles.length} instruction files selected for ${normalizedQuery || 'current app'}`,
680
+ data: {
681
+ query: normalizedQuery,
682
+ appRoot,
683
+ repoRoot,
684
+ selected: selectedFiles,
685
+ readWhen,
686
+ missingRuntime:
687
+ selectedFiles.length === 0
688
+ ? 'No tracked instruction files were found. Run `proteum configure agents` or start `proteum dev` to refresh managed instructions.'
689
+ : undefined,
690
+ },
691
+ });
692
+ };
693
+
694
+ export const buildRuntimeStatusPayload = ({
695
+ appRoot,
696
+ health,
697
+ manifest,
698
+ runtime,
699
+ sessions,
700
+ }: {
701
+ appRoot: string;
702
+ health?: object;
703
+ manifest?: TProteumManifest;
704
+ runtime?: object;
705
+ sessions?: object[];
706
+ }) =>
707
+ createMcpPayload({
708
+ summary: runtime
709
+ ? `Runtime available for ${manifest?.app.identity.identifier || appRoot}`
710
+ : manifest
711
+ ? `Manifest available for ${manifest.app.identity.identifier}; no live runtime selected`
712
+ : `No Proteum manifest found for ${appRoot}`,
713
+ data: {
714
+ appRoot,
715
+ manifest: summarizeManifest(manifest),
716
+ runtime,
717
+ sessions,
718
+ health,
719
+ },
720
+ nextActions: runtime
721
+ ? [
722
+ {
723
+ label: 'Diagnose Root',
724
+ tool: 'diagnose',
725
+ toolArgs: { query: '/', path: '/' },
726
+ reason: 'Use the selected runtime for the smallest request-level diagnostic pass.',
727
+ },
728
+ ]
729
+ : [
730
+ {
731
+ label: 'Start Dev',
732
+ command: 'proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port <free-port>',
733
+ reason: 'Create a tracked dev session before request-time diagnostics.',
734
+ },
735
+ ],
736
+ });