proteum 2.2.9 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/AGENTS.md +10 -4
  2. package/README.md +58 -15
  3. package/agents/project/AGENTS.md +53 -10
  4. package/agents/project/DOCUMENTATION.md +1326 -0
  5. package/agents/project/app-root/AGENTS.md +2 -2
  6. package/agents/project/diagnostics.md +12 -7
  7. package/agents/project/optimizations.md +1 -0
  8. package/agents/project/root/AGENTS.md +24 -9
  9. package/agents/project/tests/AGENTS.md +7 -0
  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/connect.ts +40 -4
  13. package/cli/commands/dev.ts +148 -25
  14. package/cli/commands/diagnose.ts +138 -5
  15. package/cli/commands/doctor.ts +24 -4
  16. package/cli/commands/explain.ts +134 -6
  17. package/cli/commands/mcp.ts +133 -0
  18. package/cli/commands/orient.ts +93 -3
  19. package/cli/commands/perf.ts +118 -13
  20. package/cli/commands/runtime.ts +234 -0
  21. package/cli/commands/trace.ts +116 -21
  22. package/cli/mcp/router.ts +1010 -0
  23. package/cli/presentation/commands.ts +93 -26
  24. package/cli/presentation/devSession.ts +2 -0
  25. package/cli/presentation/help.ts +1 -1
  26. package/cli/runtime/commands.ts +215 -24
  27. package/cli/runtime/devSessions.ts +328 -2
  28. package/cli/runtime/mcpDaemon.ts +288 -0
  29. package/cli/runtime/ports.ts +151 -0
  30. package/cli/utils/agentOutput.ts +46 -0
  31. package/cli/utils/agents.ts +194 -51
  32. package/cli/utils/appRoots.ts +232 -0
  33. package/common/dev/diagnostics.ts +1 -1
  34. package/common/dev/inspection.ts +22 -7
  35. package/common/dev/mcpPayloads.ts +1150 -0
  36. package/common/dev/mcpServer.ts +287 -0
  37. package/docs/agent-routing.md +137 -0
  38. package/docs/dev-commands.md +2 -0
  39. package/docs/dev-sessions.md +4 -1
  40. package/docs/diagnostics.md +70 -24
  41. package/docs/mcp.md +206 -0
  42. package/docs/migrate-from-2.1.3.md +14 -6
  43. package/docs/request-tracing.md +12 -6
  44. package/package.json +11 -3
  45. package/server/app/devMcp.ts +204 -0
  46. package/server/services/router/http/cache.ts +116 -0
  47. package/server/services/router/http/index.ts +94 -35
  48. package/server/services/router/index.ts +8 -11
  49. package/server/services/router/request/ip.test.cjs +0 -1
  50. package/tests/agents-utils.test.cjs +92 -14
  51. package/tests/cli-mcp-command.test.cjs +262 -0
  52. package/tests/codex-mcp-usage.test.cjs +307 -0
  53. package/tests/dev-sessions.test.cjs +113 -0
  54. package/tests/dev-transpile-watch.test.cjs +117 -9
  55. package/tests/eslint-rules.test.cjs +0 -1
  56. package/tests/inspection.test.cjs +66 -0
  57. package/tests/mcp.test.cjs +873 -0
  58. package/tests/router-cache-config.test.cjs +73 -0
  59. package/vitest.config.mjs +9 -0
@@ -0,0 +1,1150 @@
1
+ import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from './console';
2
+ import type { TDoctorResponse } from './diagnostics';
3
+ import { buildExplainSummaryItems } from './diagnostics';
4
+ import { explainOwner, type TDiagnoseResponse, type TExplainOwnerResponse, type 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
+ isAbsolute: (filepath: string) => boolean;
42
+ join: (...segments: string[]) => string;
43
+ relative: (from: string, to: string) => string;
44
+ resolve: (...segments: string[]) => string;
45
+ };
46
+
47
+ const maxInstructionPreviewLength = 360;
48
+ const maxTextLength = 220;
49
+ const nodeRequire = (() => {
50
+ try {
51
+ return eval('require') as NodeRequire;
52
+ } catch (_error) {
53
+ return undefined;
54
+ }
55
+ })();
56
+ const fs = nodeRequire ? (nodeRequire('fs') as TNodeFs) : undefined;
57
+ const path = nodeRequire ? (nodeRequire('path') as TNodePath) : undefined;
58
+
59
+ const hasNodeFs = () => fs !== undefined;
60
+ const hasNodePath = () => path !== undefined;
61
+ const fileExists = (filepath: string) => fs !== undefined && fs.existsSync(filepath);
62
+ const directoryExists = (filepath: string) => {
63
+ if (fs === undefined || !fs.existsSync(filepath)) return false;
64
+ try {
65
+ return fs.statSync(filepath).isDirectory();
66
+ } catch (_error) {
67
+ return false;
68
+ }
69
+ };
70
+
71
+ export const truncateForMcp = (value: string, max = maxTextLength) =>
72
+ value.length <= max ? value : `${value.slice(0, max)}...`;
73
+
74
+ export const compactList = <TValue>(values: TValue[], limit: number) => values.slice(0, Math.max(0, limit));
75
+
76
+ export type TTriggeredInstructionRead = {
77
+ file: string;
78
+ reason: string;
79
+ };
80
+
81
+ const matchesInstructionTrigger = (query: string, pattern: RegExp) => pattern.test(query);
82
+
83
+ const resolveRootContractFallbackFile = (rootAgentsFile?: string) => {
84
+ if (fs === undefined || path === undefined || !rootAgentsFile || !fileExists(rootAgentsFile)) return undefined;
85
+
86
+ const content = fs.readFileSync(rootAgentsFile, 'utf8');
87
+ const match = content.match(/Root contract fallback:\s+(.+?)\s*$/m);
88
+ const candidate = match?.[1]?.trim();
89
+ if (!candidate) return undefined;
90
+
91
+ const filepath = path.isAbsolute(candidate) ? candidate : path.resolve(path.dirname(rootAgentsFile), candidate);
92
+ return fileExists(filepath) ? filepath : undefined;
93
+ };
94
+
95
+ export const resolveTriggeredInstructionReads = ({
96
+ codingStyle,
97
+ diagnostics,
98
+ documentation,
99
+ optimizations,
100
+ query,
101
+ rootAgentsFile,
102
+ }: {
103
+ codingStyle?: string;
104
+ diagnostics?: string;
105
+ documentation?: string;
106
+ optimizations?: string;
107
+ query: string;
108
+ rootAgentsFile?: string;
109
+ }) => {
110
+ const normalizedQuery = query.toLowerCase();
111
+ const reads = new Map<string, TTriggeredInstructionRead>();
112
+ const addRead = (file: string | undefined, reason: string) => {
113
+ if (!file || !fileExists(file) || reads.has(file)) return;
114
+ reads.set(file, { file, reason });
115
+ };
116
+ const rootContract = resolveRootContractFallbackFile(rootAgentsFile);
117
+ const looksLikeGitLifecycle = matchesInstructionTrigger(
118
+ normalizedQuery,
119
+ /\b(commit|stage|push)\b|\band commit\b|\bpr\b|pull[- ]requests?|git add|git commit/,
120
+ );
121
+ const looksLikeFinishLifecycle = matchesInstructionTrigger(
122
+ normalizedQuery,
123
+ /\b(finish|finishing|done|complete|completion|final|validate|validation|verify|verification)\b/,
124
+ );
125
+ const looksLikeRuntimeVisible = matchesInstructionTrigger(
126
+ normalizedQuery,
127
+ /\b(runtime|request-time|request time|router|ssr|browser-visible|browser visible|controller|diagnose|trace|perf|repro|reproduction|failing|error|bug)\b/,
128
+ );
129
+ const looksLikeImplementationEdit = matchesInstructionTrigger(
130
+ normalizedQuery,
131
+ /\b(implement|change|edit|update|modify|fix|add|remove|refactor|increase|decrease|code)\b/,
132
+ );
133
+ const looksLikeProductOrDocs = matchesInstructionTrigger(
134
+ normalizedQuery,
135
+ /\b(feature|product|business|acceptance|docs|documentation|ux|copy|onboarding|pricing|commercial|semantics)\b/,
136
+ );
137
+ const looksLikeOptimization = matchesInstructionTrigger(
138
+ normalizedQuery,
139
+ /\b(optimize|optimization|performance|package|dependency|build|bundle)\b/,
140
+ );
141
+
142
+ if (looksLikeGitLifecycle) {
143
+ addRead(rootContract, 'Git lifecycle trigger; read the canonical root contract before any git write.');
144
+ }
145
+ if (looksLikeFinishLifecycle) {
146
+ addRead(rootContract, 'Finish or verification trigger; read the canonical root lifecycle contract.');
147
+ }
148
+ if (looksLikeRuntimeVisible) {
149
+ addRead(rootContract, 'Runtime-visible behavior trigger; read the canonical root verification contract.');
150
+ addRead(diagnostics, 'Runtime, request, trace, perf, reproduction, or error trigger.');
151
+ }
152
+ if (looksLikeImplementationEdit) {
153
+ addRead(codingStyle, 'Implementation edit trigger; read coding style before editing.');
154
+ }
155
+ if (looksLikeProductOrDocs) {
156
+ addRead(documentation, 'Feature, product, business-rule, UX, copy, or docs trigger.');
157
+ }
158
+ if (looksLikeOptimization) {
159
+ addRead(optimizations, 'Package, build, runtime, performance, or optimization trigger.');
160
+ }
161
+
162
+ return [...reads.values()];
163
+ };
164
+
165
+ export const createMcpPayload = <TData extends object>({
166
+ data,
167
+ nextActions,
168
+ omitted,
169
+ summary,
170
+ }: Omit<TProteumMcpPayload<TData>, 'format' | 'ok'>): TProteumMcpPayload<TData> => ({
171
+ ok: true,
172
+ format: 'proteum-mcp-v1',
173
+ summary,
174
+ data,
175
+ ...(nextActions && nextActions.length > 0 ? { nextActions } : {}),
176
+ ...(omitted && omitted.length > 0 ? { omitted } : {}),
177
+ });
178
+
179
+ export const stringifyMcpPayload = (value: object) => JSON.stringify(value);
180
+
181
+ export const summarizeManifest = (manifest: TProteumManifest | undefined) => {
182
+ if (!manifest) return undefined;
183
+
184
+ const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
185
+ const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
186
+
187
+ return {
188
+ appRoot: manifest.app.root,
189
+ coreRoot: manifest.app.coreRoot,
190
+ identifier: manifest.app.identity.identifier,
191
+ name: manifest.app.identity.name,
192
+ routerPort: manifest.env.resolved.routerPort,
193
+ env: {
194
+ name: manifest.env.resolved.name,
195
+ profile: manifest.env.resolved.profile,
196
+ internalUrl: manifest.env.resolved.routerInternalUrl,
197
+ },
198
+ diagnostics: { errors, warnings },
199
+ counts: {
200
+ commands: manifest.commands.length,
201
+ connectedProjects: manifest.connectedProjects.length,
202
+ controllers: manifest.controllers.length,
203
+ layouts: manifest.layouts.length,
204
+ routes: manifest.routes.client.length + manifest.routes.server.length,
205
+ clientRoutes: manifest.routes.client.length,
206
+ serverRoutes: manifest.routes.server.length,
207
+ services: manifest.services.app.length + manifest.services.routerPlugins.length,
208
+ },
209
+ };
210
+ };
211
+
212
+ const compactOwnerMatch = (match: TExplainOwnerResponse['matches'][number]) => ({
213
+ kind: match.kind,
214
+ label: match.label,
215
+ score: match.score,
216
+ scope: match.scopeLabel,
217
+ origin: match.originHint,
218
+ source: match.source,
219
+ });
220
+
221
+ const compactDiagnostic = (diagnostic: TDoctorResponse['diagnostics'][number]) => ({
222
+ level: diagnostic.level,
223
+ code: diagnostic.code,
224
+ message: truncateForMcp(diagnostic.message),
225
+ filepath: diagnostic.filepath,
226
+ sourceLocation: diagnostic.sourceLocation,
227
+ fixHint: diagnostic.fixHint ? truncateForMcp(diagnostic.fixHint) : undefined,
228
+ });
229
+
230
+ export const compactDoctorResponse = ({
231
+ contracts,
232
+ doctor,
233
+ }: {
234
+ contracts?: TDoctorResponse;
235
+ doctor: TDoctorResponse;
236
+ }) =>
237
+ createMcpPayload({
238
+ summary: contracts
239
+ ? `Doctor: ${doctor.summary.errors} errors/${doctor.summary.warnings} warnings; contracts: ${contracts.summary.errors} errors/${contracts.summary.warnings} warnings`
240
+ : `Doctor: ${doctor.summary.errors} errors/${doctor.summary.warnings} warnings`,
241
+ data: {
242
+ doctor: {
243
+ summary: doctor.summary,
244
+ top: compactList(doctor.diagnostics, 8).map(compactDiagnostic),
245
+ total: doctor.diagnostics.length,
246
+ },
247
+ contracts: contracts
248
+ ? {
249
+ summary: contracts.summary,
250
+ top: compactList(contracts.diagnostics, 8).map(compactDiagnostic),
251
+ total: contracts.diagnostics.length,
252
+ }
253
+ : undefined,
254
+ },
255
+ omitted:
256
+ doctor.diagnostics.length > 8 || (contracts && contracts.diagnostics.length > 8)
257
+ ? [
258
+ {
259
+ reason: 'Diagnostics are capped in MCP output. Use the CLI full-detail command when every diagnostic is required.',
260
+ command: 'proteum doctor --full',
261
+ },
262
+ ]
263
+ : undefined,
264
+ });
265
+
266
+ export const compactOrientationResponse = (response: TOrientResponse) => {
267
+ const topOwner = response.owner.matches[0];
268
+ const summary = topOwner
269
+ ? `${response.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
270
+ : `${response.query} -> no manifest owner matched`;
271
+ const topPath =
272
+ topOwner && (topOwner.kind === 'route' || topOwner.kind === 'controller') && topOwner.label.startsWith('/')
273
+ ? topOwner.label
274
+ : response.query.startsWith('/')
275
+ ? response.query
276
+ : undefined;
277
+ const triggered = resolveTriggeredInstructionReads({
278
+ codingStyle: response.guidance.codingStyle,
279
+ diagnostics: response.guidance.diagnostics,
280
+ documentation: response.guidance.documentation,
281
+ optimizations: response.guidance.optimizations,
282
+ query: response.normalizedQuery || response.query,
283
+ rootAgentsFile:
284
+ path !== undefined && response.app.repoRoot !== response.app.appRoot
285
+ ? path.join(response.app.repoRoot, 'AGENTS.md')
286
+ : response.guidance.agents,
287
+ });
288
+
289
+ return createMcpPayload({
290
+ summary,
291
+ data: {
292
+ query: response.query,
293
+ app: response.app,
294
+ owner: {
295
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
296
+ matches: compactList(response.owner.matches, 5).map(compactOwnerMatch),
297
+ totalReturned: response.owner.matches.length,
298
+ },
299
+ instructions: {
300
+ mustRead: [
301
+ ...new Set([
302
+ response.guidance.agents,
303
+ ...response.guidance.areaAgents,
304
+ ...triggered.map((entry) => entry.file),
305
+ ]),
306
+ ],
307
+ triggered,
308
+ readWhen: [
309
+ {
310
+ file: response.guidance.documentation,
311
+ when: 'Non-trivial coding tasks that need the smallest `/docs` pack and post-change docs updates.',
312
+ },
313
+ {
314
+ file: response.guidance.diagnostics,
315
+ when: 'Raw errors, failing requests, traces, perf regressions, or reproduction work.',
316
+ },
317
+ {
318
+ file: response.guidance.codingStyle,
319
+ when: 'Before editing implementation files.',
320
+ },
321
+ {
322
+ file: response.guidance.optimizations,
323
+ when: 'Client-side implementation, packages, build, runtime, or performance work.',
324
+ },
325
+ ],
326
+ },
327
+ connected: {
328
+ imports: compactList(response.connected.imports, 5),
329
+ producers: compactList(response.connected.producers, 4),
330
+ totalImports: response.connected.imports.length,
331
+ totalProducers: response.connected.producers.length,
332
+ },
333
+ warnings: response.warnings,
334
+ },
335
+ nextActions: [
336
+ ...(topOwner
337
+ ? [
338
+ {
339
+ label: 'Explain Summary',
340
+ tool: 'explain_summary',
341
+ toolArgs: { query: response.query },
342
+ reason: 'Use MCP owner summary before broad manifest or source searches.',
343
+ },
344
+ ]
345
+ : []),
346
+ ...(topPath
347
+ ? [
348
+ {
349
+ label: 'Diagnose Route',
350
+ tool: 'diagnose',
351
+ toolArgs: { path: topPath, query: response.query },
352
+ reason: 'Use the compact runtime diagnosis before CLI diagnose, raw traces, or browser work.',
353
+ },
354
+ {
355
+ label: 'Perf Request',
356
+ tool: 'perf_request',
357
+ toolArgs: { query: topPath },
358
+ reason: 'Use the compact request waterfall before raw perf detail.',
359
+ },
360
+ ]
361
+ : []),
362
+ ...response.nextSteps.map((step) => ({
363
+ command: step.command,
364
+ label: step.label,
365
+ reason: step.reason,
366
+ })),
367
+ ],
368
+ });
369
+ };
370
+
371
+ export const compactExplainSummary = ({
372
+ manifest,
373
+ owner,
374
+ query,
375
+ }: {
376
+ manifest: TProteumManifest;
377
+ owner?: TExplainOwnerResponse;
378
+ query?: string;
379
+ }) => {
380
+ if (owner) {
381
+ const topOwner = owner.matches[0];
382
+ const topPath =
383
+ topOwner && (topOwner.kind === 'route' || topOwner.kind === 'controller') && topOwner.label.startsWith('/')
384
+ ? topOwner.label
385
+ : query && query.startsWith('/')
386
+ ? query
387
+ : undefined;
388
+ return createMcpPayload({
389
+ summary: topOwner
390
+ ? `${query || owner.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
391
+ : `${query || owner.query} -> no owner matched`,
392
+ data: {
393
+ query: query || owner.query,
394
+ normalizedQuery: owner.normalizedQuery,
395
+ owner: {
396
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
397
+ matches: compactList(owner.matches, 8).map(compactOwnerMatch),
398
+ totalReturned: owner.matches.length,
399
+ },
400
+ manifest: summarizeManifest(manifest),
401
+ },
402
+ nextActions: topPath
403
+ ? [
404
+ {
405
+ label: 'Diagnose Route',
406
+ tool: 'diagnose',
407
+ toolArgs: { path: topPath, query: query || owner.query },
408
+ reason: 'Use compact runtime diagnosis before CLI diagnose or raw trace detail.',
409
+ },
410
+ {
411
+ label: 'Perf Request',
412
+ tool: 'perf_request',
413
+ toolArgs: { query: topPath },
414
+ reason: 'Use compact request waterfall before raw perf detail.',
415
+ },
416
+ ]
417
+ : undefined,
418
+ });
419
+ }
420
+
421
+ const items = buildExplainSummaryItems(manifest).map((item) => truncateForMcp(item, 300));
422
+ return createMcpPayload({
423
+ summary: `Manifest ${manifest.app.identity.identifier}: ${manifest.controllers.length} controllers, ${manifest.routes.client.length + manifest.routes.server.length} routes`,
424
+ data: {
425
+ manifest: summarizeManifest(manifest),
426
+ summaryItems: items,
427
+ },
428
+ });
429
+ };
430
+
431
+ const compactRequest = (request: TDiagnoseResponse['request']) =>
432
+ request
433
+ ? {
434
+ id: request.id,
435
+ method: request.method,
436
+ path: request.path,
437
+ statusCode: request.statusCode,
438
+ durationMs: request.durationMs,
439
+ capture: request.capture,
440
+ user: request.user,
441
+ errorMessage: request.errorMessage ? truncateForMcp(request.errorMessage) : undefined,
442
+ counts: {
443
+ calls: request.calls.length,
444
+ events: request.events.length,
445
+ sqlQueries: request.sqlQueries.length,
446
+ droppedEvents: request.droppedEvents,
447
+ },
448
+ }
449
+ : undefined;
450
+
451
+ export const compactDiagnoseResponse = (response: TDiagnoseResponse) => {
452
+ const request = compactRequest(response.request);
453
+ const doctorSummary = `${response.doctor.summary.errors} doctor errors/${response.doctor.summary.warnings} warnings`;
454
+ const contractsSummary = `${response.contracts.summary.errors} contract errors/${response.contracts.summary.warnings} warnings`;
455
+ const traceSummary = request
456
+ ? `${request.method} ${request.path} status=${request.statusCode ?? 'pending'} durationMs=${request.durationMs ?? 'pending'}`
457
+ : 'no matching request trace';
458
+
459
+ return createMcpPayload({
460
+ summary: `${response.query || 'request'}: ${traceSummary}; ${doctorSummary}; ${contractsSummary}`,
461
+ data: {
462
+ query: response.query,
463
+ request,
464
+ owner: {
465
+ top: response.owner.matches[0] ? compactOwnerMatch(response.owner.matches[0]) : undefined,
466
+ matches: compactList(response.owner.matches, 5).map(compactOwnerMatch),
467
+ totalReturned: response.owner.matches.length,
468
+ },
469
+ suspects: compactList(response.suspects, 8),
470
+ chain: compactList(response.chain || [], 10),
471
+ diagnostics: {
472
+ doctor: {
473
+ summary: response.doctor.summary,
474
+ top: compactList(response.doctor.diagnostics, 8).map(compactDiagnostic),
475
+ total: response.doctor.diagnostics.length,
476
+ },
477
+ contracts: {
478
+ summary: response.contracts.summary,
479
+ top: compactList(response.contracts.diagnostics, 8).map(compactDiagnostic),
480
+ total: response.contracts.diagnostics.length,
481
+ },
482
+ },
483
+ logs: compactList(response.serverLogs.logs, 12).map((entry) => ({
484
+ level: entry.level,
485
+ time: entry.time,
486
+ text: truncateForMcp(entry.text),
487
+ })),
488
+ instructions: response.orientation
489
+ ? {
490
+ mustRead: [...new Set([response.orientation.guidance.agents, ...response.orientation.guidance.areaAgents])],
491
+ documentation: response.orientation.guidance.documentation,
492
+ diagnostics: response.orientation.guidance.diagnostics,
493
+ codingStyle: response.orientation.guidance.codingStyle,
494
+ optimizations: response.orientation.guidance.optimizations,
495
+ }
496
+ : undefined,
497
+ },
498
+ nextActions: response.orientation?.nextSteps.map((step) => ({
499
+ command: step.command,
500
+ label: step.label,
501
+ reason: step.reason,
502
+ })),
503
+ omitted: response.request
504
+ ? [
505
+ {
506
+ reason: 'Full request events, payload summaries, and SQL text are omitted from MCP diagnose output.',
507
+ tool: 'trace_show',
508
+ toolArgs: { requestId: response.request.id, detail: 'full', limit: 50 },
509
+ },
510
+ ]
511
+ : undefined,
512
+ });
513
+ };
514
+
515
+ const compactTraceCall = (call: TRequestTrace['calls'][number]) => ({
516
+ id: call.id,
517
+ origin: call.origin,
518
+ label: call.label,
519
+ method: call.method,
520
+ path: call.path,
521
+ statusCode: call.statusCode,
522
+ durationMs: call.durationMs,
523
+ errorMessage: call.errorMessage ? truncateForMcp(call.errorMessage) : undefined,
524
+ });
525
+
526
+ const compactTraceSql = (query: TRequestTrace['sqlQueries'][number], includeQuery = false) => ({
527
+ id: query.id,
528
+ caller: query.callerLabel || `${query.callerMethod} ${query.callerPath}`,
529
+ kind: query.kind,
530
+ operation: query.operation,
531
+ model: query.model,
532
+ durationMs: query.durationMs,
533
+ fingerprint: query.fingerprint,
534
+ query: includeQuery ? truncateForMcp(query.query, 180) : undefined,
535
+ });
536
+
537
+ const compactTraceEvent = (event: TRequestTrace['events'][number], includeDetails = false) => ({
538
+ index: event.index,
539
+ elapsedMs: event.elapsedMs,
540
+ type: event.type,
541
+ detailKeys: Object.keys(event.details),
542
+ details: includeDetails ? event.details : undefined,
543
+ });
544
+
545
+ export const compactTraceResponse = ({
546
+ detail = 'compact',
547
+ limit = 50,
548
+ offset = 0,
549
+ request,
550
+ }: {
551
+ detail?: 'compact' | 'full';
552
+ limit?: number;
553
+ offset?: number;
554
+ request: TRequestTrace;
555
+ }) => {
556
+ const failedCalls = request.calls.filter((call) => call.errorMessage || (call.statusCode !== undefined && call.statusCode >= 400));
557
+ const errorEvents = request.events.filter((event) => event.type === 'error');
558
+ const hotCalls = [...request.calls].sort((left, right) => (right.durationMs || 0) - (left.durationMs || 0));
559
+ const hotSql = [...request.sqlQueries].sort((left, right) => right.durationMs - left.durationMs);
560
+ const pageOffset = Math.max(0, offset);
561
+ const pageLimit = Math.max(1, Math.min(100, limit));
562
+ const full = detail === 'full';
563
+
564
+ return createMcpPayload({
565
+ 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}`,
566
+ data: {
567
+ request: {
568
+ id: request.id,
569
+ method: request.method,
570
+ path: request.path,
571
+ statusCode: request.statusCode,
572
+ durationMs: request.durationMs,
573
+ capture: request.capture,
574
+ user: request.user,
575
+ errorMessage: request.errorMessage ? truncateForMcp(request.errorMessage) : undefined,
576
+ droppedEvents: request.droppedEvents,
577
+ persistedFilepath: request.persistedFilepath,
578
+ },
579
+ counts: {
580
+ calls: request.calls.length,
581
+ events: request.events.length,
582
+ sqlQueries: request.sqlQueries.length,
583
+ },
584
+ failedCalls: compactList(failedCalls, 6).map(compactTraceCall),
585
+ errorEvents: compactList(errorEvents, 6).map((event) => compactTraceEvent(event, full)),
586
+ hotCalls: compactList(hotCalls, 6).map(compactTraceCall),
587
+ hotSql: compactList(hotSql, 6).map((query) => compactTraceSql(query, full)),
588
+ page: full
589
+ ? {
590
+ offset: pageOffset,
591
+ limit: pageLimit,
592
+ events: request.events.slice(pageOffset, pageOffset + pageLimit).map((event) => compactTraceEvent(event, true)),
593
+ calls: request.calls.slice(pageOffset, pageOffset + pageLimit).map(compactTraceCall),
594
+ sqlQueries: request.sqlQueries
595
+ .slice(pageOffset, pageOffset + pageLimit)
596
+ .map((query) => compactTraceSql(query, true)),
597
+ hasMore:
598
+ request.events.length > pageOffset + pageLimit ||
599
+ request.calls.length > pageOffset + pageLimit ||
600
+ request.sqlQueries.length > pageOffset + pageLimit,
601
+ }
602
+ : undefined,
603
+ },
604
+ nextActions: [
605
+ {
606
+ label: 'Diagnose Request',
607
+ tool: 'diagnose',
608
+ toolArgs: { requestId: request.id, query: request.path },
609
+ reason: 'Combine this trace with owner lookup, diagnostics, suspects, and server logs.',
610
+ },
611
+ {
612
+ label: 'Perf Request',
613
+ tool: 'perf_request',
614
+ toolArgs: { query: request.id },
615
+ reason: 'Inspect request timing, SQL, render, and memory rollups without full events.',
616
+ },
617
+ ],
618
+ omitted: full
619
+ ? undefined
620
+ : [
621
+ {
622
+ reason: 'Full events, payload summaries, raw SQL, and call bodies are omitted by default.',
623
+ tool: 'trace_show',
624
+ toolArgs: { requestId: request.id, detail: 'full', limit: 50 },
625
+ },
626
+ ],
627
+ });
628
+ };
629
+
630
+ const formatDuration = (value?: number) => (value === undefined ? 'n/a' : `${Math.round(value)} ms`);
631
+
632
+ const compactTopLikeRow = (row: TPerfTopResponse['rows'][number]) => ({
633
+ label: row.label,
634
+ requestCount: row.requestCount,
635
+ avgDurationMs: row.avgDurationMs,
636
+ p95DurationMs: row.p95DurationMs,
637
+ maxDurationMs: row.maxDurationMs,
638
+ avgCpuMs: row.avgCpuMs,
639
+ avgSqlDurationMs: row.avgSqlDurationMs,
640
+ avgRenderDurationMs: row.avgRenderDurationMs,
641
+ avgHeapDeltaBytes: row.avgHeapDeltaBytes,
642
+ slowestRequestId: row.slowestRequestId,
643
+ });
644
+
645
+ export const compactPerfTopResponse = (response: TPerfTopResponse) =>
646
+ createMcpPayload({
647
+ summary: `Perf top ${response.groupBy}: ${response.summary.requestCount} requests, ${response.summary.errorCount} errors, p95=${formatDuration(response.summary.p95DurationMs)}`,
648
+ data: {
649
+ groupBy: response.groupBy,
650
+ window: response.window,
651
+ summary: response.summary,
652
+ rows: compactList(response.rows, 10).map(compactTopLikeRow),
653
+ totalRows: response.rows.length,
654
+ },
655
+ omitted:
656
+ response.rows.length > 10
657
+ ? [{ reason: 'Perf rows are capped. Increase the tool limit or use `proteum perf top --full` for raw detail.' }]
658
+ : undefined,
659
+ });
660
+
661
+ const compactPerfSql = (query: TPerfRequestResponse['request']['hottestSqlQueries'][number]) => ({
662
+ callerLabel: query.callerLabel,
663
+ operation: query.operation,
664
+ model: query.model,
665
+ fingerprint: query.fingerprint,
666
+ durationMs: query.durationMs,
667
+ query: truncateForMcp(query.query, 160),
668
+ });
669
+
670
+ export const compactPerfRequestResponse = (response: TPerfRequestResponse) =>
671
+ createMcpPayload({
672
+ 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)}`,
673
+ data: {
674
+ request: {
675
+ requestId: response.request.requestId,
676
+ method: response.request.method,
677
+ path: response.request.path,
678
+ statusCode: response.request.statusCode,
679
+ routeLabel: response.request.routeLabel,
680
+ controllerLabel: response.request.controllerLabel,
681
+ totalDurationMs: response.request.totalDurationMs,
682
+ cpuTotalMs: response.request.cpuTotalMs,
683
+ sqlDurationMs: response.request.sqlDurationMs,
684
+ callDurationMs: response.request.callDurationMs,
685
+ renderDurationMs: response.request.renderDurationMs,
686
+ selfDurationMs: response.request.selfDurationMs,
687
+ heapDeltaBytes: response.request.heapDeltaBytes,
688
+ },
689
+ stages: compactList(response.request.stages, 10),
690
+ hotCalls: compactList(response.request.hottestCalls, 8),
691
+ chain: compactList(response.request.chain || [], 10),
692
+ hotSql: compactList(response.request.hottestSqlQueries, 8).map(compactPerfSql),
693
+ },
694
+ nextActions: [
695
+ {
696
+ label: 'Diagnose Request',
697
+ tool: 'diagnose',
698
+ toolArgs: { query: response.request.path, requestId: response.request.requestId },
699
+ reason: 'Combine this request with owner, diagnostics, suspects, and logs.',
700
+ },
701
+ {
702
+ label: 'Trace Events',
703
+ tool: 'trace_show',
704
+ toolArgs: { requestId: response.request.requestId, detail: 'full', limit: 50 },
705
+ reason: 'Open raw event detail only if the compact waterfall is insufficient.',
706
+ },
707
+ ],
708
+ });
709
+
710
+ export const compactLogsResponse = ({
711
+ level,
712
+ limit,
713
+ response,
714
+ }: {
715
+ level: TDevConsoleLogLevel;
716
+ limit: number;
717
+ response: TDevConsoleLogsResponse;
718
+ }) =>
719
+ createMcpPayload({
720
+ summary: `${response.logs.length} dev log lines at level >= ${level}`,
721
+ data: {
722
+ level,
723
+ limit,
724
+ logs: response.logs.map((entry) => ({
725
+ level: entry.level,
726
+ time: entry.time,
727
+ text: truncateForMcp(entry.text),
728
+ })),
729
+ },
730
+ omitted:
731
+ response.logs.length >= limit
732
+ ? [{ reason: 'Log output reached the requested cap. Increase limit only when the latest compact lines are insufficient.' }]
733
+ : undefined,
734
+ });
735
+
736
+ const readPreview = (filepath: string) => {
737
+ if (fs === undefined) return undefined;
738
+ try {
739
+ return truncateForMcp(fs.readFileSync(filepath, 'utf8').replace(/\s+/g, ' ').trim(), maxInstructionPreviewLength);
740
+ } catch (_error) {
741
+ return undefined;
742
+ }
743
+ };
744
+
745
+ const findNearestRootWith = (startDir: string, relativeFilepath: string) => {
746
+ if (path === undefined) return undefined;
747
+ let current = path.resolve(startDir);
748
+
749
+ while (true) {
750
+ if (fileExists(path.join(current, relativeFilepath))) return current;
751
+ const parent = path.dirname(current);
752
+ if (parent === current) return undefined;
753
+ current = parent;
754
+ }
755
+ };
756
+
757
+ const findLikelyRepoRoot = (appRoot: string) => {
758
+ if (path === undefined) return appRoot;
759
+ let current = path.resolve(appRoot);
760
+
761
+ while (true) {
762
+ if (directoryExists(path.join(current, '.git'))) return current;
763
+ const parent = path.dirname(current);
764
+ if (parent === current) return appRoot;
765
+ current = parent;
766
+ }
767
+ };
768
+
769
+ const resolveDocumentFile = ({
770
+ appRoot,
771
+ repoRoot,
772
+ relativeFilepath,
773
+ }: {
774
+ appRoot: string;
775
+ repoRoot: string;
776
+ relativeFilepath: string;
777
+ }) => {
778
+ if (path === undefined) return undefined;
779
+
780
+ const appFilepath = path.join(appRoot, relativeFilepath);
781
+ if (fileExists(appFilepath)) return appFilepath;
782
+
783
+ const repoFilepath = path.join(repoRoot, relativeFilepath);
784
+ if (fileExists(repoFilepath)) return repoFilepath;
785
+
786
+ const nearestRoot = findNearestRootWith(appRoot, relativeFilepath);
787
+ return nearestRoot ? path.join(nearestRoot, relativeFilepath) : undefined;
788
+ };
789
+
790
+ const fullInstructionReadPolicy = {
791
+ default: 'Use selected previews as the instruction source for read-only discovery and diagnostics.',
792
+ requiredWhen: [
793
+ 'editing files governed by the selected scope',
794
+ 'performing git writes such as stage, commit, push, or PR work',
795
+ 'changing schema, auth, runtime, generated contracts, or framework integration behavior',
796
+ 'the compact preview is insufficient for the current decision',
797
+ ],
798
+ };
799
+
800
+ const inferInstructionReadMode = (reason: string) =>
801
+ /git lifecycle|implementation edit|finish or verification|schema|migration/i.test(reason)
802
+ ? 'full-before-action'
803
+ : 'preview-first';
804
+
805
+ const createSelectedInstruction = (file: string, reason: string) => ({
806
+ file,
807
+ fullRead: inferInstructionReadMode(reason),
808
+ preview: readPreview(file),
809
+ reason,
810
+ });
811
+
812
+ export const resolveInstructionRouting = ({
813
+ appRoot,
814
+ query = '',
815
+ }: {
816
+ appRoot: string;
817
+ query?: string;
818
+ }) => {
819
+ const normalizedQuery = query.trim();
820
+ const repoRoot = findLikelyRepoRoot(appRoot);
821
+ const selected = new Map<string, ReturnType<typeof createSelectedInstruction>>();
822
+ const readWhen: Array<{ file?: string; when: string }> = [];
823
+ const addInstruction = (relativeFilepath: string, reason: string, preferAppRoot = true) => {
824
+ if (path === undefined) return;
825
+ const roots = preferAppRoot ? [appRoot, repoRoot] : [repoRoot, appRoot];
826
+ for (const root of [...new Set(roots)]) {
827
+ const filepath = path.join(root, relativeFilepath);
828
+ if (!fileExists(filepath)) continue;
829
+ selected.set(filepath, createSelectedInstruction(filepath, reason));
830
+ return;
831
+ }
832
+ };
833
+ const addReadWhen = (relativeFilepath: string, when: string) => {
834
+ const filepath = resolveDocumentFile({ appRoot, repoRoot, relativeFilepath });
835
+ readWhen.push({ file: filepath, when });
836
+ };
837
+ const lowerQuery = normalizedQuery.toLowerCase();
838
+ const looksLikeRoutePath = /(^|\s)\/[a-z0-9_./:-]*/i.test(lowerQuery);
839
+ const looksLikePage = looksLikeRoutePath || lowerQuery.includes('client/pages') || lowerQuery.includes('.tsx');
840
+ const looksLikeClient = looksLikePage || lowerQuery.includes('client/') || lowerQuery.includes('component') || lowerQuery.includes('island');
841
+ const looksLikeServerRoute =
842
+ lowerQuery.includes('server/routes') ||
843
+ lowerQuery.includes('route') ||
844
+ lowerQuery.includes('sitemap') ||
845
+ lowerQuery.includes('rss') ||
846
+ /^\/api(\/|$)/.test(lowerQuery) ||
847
+ /\s\/api(\/|$)/.test(lowerQuery);
848
+ const looksLikeService =
849
+ lowerQuery.includes('server/services') ||
850
+ lowerQuery.includes('.controller') ||
851
+ lowerQuery.includes('controller') ||
852
+ lowerQuery.includes('service');
853
+ const looksLikeE2e = lowerQuery.includes('tests/e2e') || lowerQuery.includes('playwright') || lowerQuery.includes('journey');
854
+
855
+ addInstruction('AGENTS.md', 'Start with the root/app routing contract.');
856
+ if (repoRoot !== appRoot) addInstruction('AGENTS.md', 'Also apply the monorepo root routing contract.', false);
857
+ if (looksLikeClient) addInstruction('client/AGENTS.md', 'Client code or browser-visible behavior is in scope.');
858
+ if (looksLikePage) addInstruction('client/pages/AGENTS.md', 'Page routing, SSR data, or page render behavior may be in scope.');
859
+ if (looksLikeServerRoute) addInstruction('server/routes/AGENTS.md', 'Server route or crawlable endpoint behavior may be in scope.');
860
+ if (looksLikeService) addInstruction('server/services/AGENTS.md', 'Service/controller contracts or backend runtime behavior may be in scope.');
861
+ if (looksLikeE2e) {
862
+ addInstruction('tests/e2e/AGENTS.md', 'End-to-end behavior or Playwright workflow is in scope.');
863
+ addInstruction('tests/e2e/REAL_WORLD_JOURNEY_TESTS.md', 'Real-world journey coverage may be in scope.');
864
+ }
865
+
866
+ const appAgentsFile = resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'AGENTS.md' });
867
+ const repoAgentsFile = path !== undefined && repoRoot !== appRoot ? path.join(repoRoot, 'AGENTS.md') : undefined;
868
+ for (const triggered of resolveTriggeredInstructionReads({
869
+ codingStyle: resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'CODING_STYLE.md' }),
870
+ diagnostics: resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'diagnostics.md' }),
871
+ documentation: resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'DOCUMENTATION.md' }),
872
+ optimizations: resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'optimizations.md' }),
873
+ query: normalizedQuery,
874
+ rootAgentsFile: repoAgentsFile && fileExists(repoAgentsFile) ? repoAgentsFile : appAgentsFile,
875
+ })) {
876
+ selected.set(triggered.file, createSelectedInstruction(triggered.file, triggered.reason));
877
+ }
878
+
879
+ addReadWhen(
880
+ 'DOCUMENTATION.md',
881
+ 'Read before non-trivial coding tasks to choose the smallest `/docs` pack and update docs after changes.',
882
+ );
883
+ addReadWhen('diagnostics.md', 'Read for raw errors, failing requests, traces, perf regressions, or reproduction work.');
884
+ addReadWhen('CODING_STYLE.md', 'Read before editing implementation files.');
885
+ addReadWhen('optimizations.md', 'Read for client-side implementation, packages, build, runtime, or performance work.');
886
+
887
+ const selectedFiles = [...selected.values()];
888
+ return createMcpPayload({
889
+ summary: `${selectedFiles.length} instruction files selected for ${normalizedQuery || 'current app'}`,
890
+ data: {
891
+ query: normalizedQuery,
892
+ appRoot,
893
+ repoRoot,
894
+ selected: selectedFiles,
895
+ readWhen,
896
+ fullReadPolicy: fullInstructionReadPolicy,
897
+ missingRuntime:
898
+ selectedFiles.length === 0
899
+ ? 'No tracked instruction files were found. Run `proteum configure agents` or start `proteum dev` to refresh managed instructions.'
900
+ : undefined,
901
+ },
902
+ });
903
+ };
904
+
905
+ const chooseWorkflowOwnerQuery = ({
906
+ file,
907
+ query,
908
+ route,
909
+ }: {
910
+ file?: string;
911
+ query?: string;
912
+ route?: string;
913
+ }) => [route, file, query].map((value) => value?.trim()).find((value): value is string => Boolean(value));
914
+
915
+ const chooseWorkflowInstructionQuery = ({
916
+ file,
917
+ query,
918
+ route,
919
+ task,
920
+ }: {
921
+ file?: string;
922
+ query?: string;
923
+ route?: string;
924
+ task?: string;
925
+ }) =>
926
+ [task, query, route, file]
927
+ .map((value) => value?.trim())
928
+ .filter((value): value is string => Boolean(value))
929
+ .join(' ');
930
+
931
+ const isReachableHealth = (health: object | undefined) => {
932
+ if (!health || !('reachable' in health)) return true;
933
+
934
+ return (health as { reachable?: unknown }).reachable === true;
935
+ };
936
+
937
+ const createRuntimeDownNextAction = () => ({
938
+ label: 'Start Dev',
939
+ command: 'proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port <free-port>',
940
+ reason: 'Runtime is not reachable; start or repair one tracked dev session before diagnose, trace, or perf reads.',
941
+ });
942
+
943
+ export const compactWorkflowStartResponse = ({
944
+ contracts,
945
+ doctor,
946
+ file,
947
+ health,
948
+ manifest,
949
+ owner,
950
+ query,
951
+ route,
952
+ runtime,
953
+ task,
954
+ }: {
955
+ contracts: TDoctorResponse;
956
+ doctor: TDoctorResponse;
957
+ file?: string;
958
+ health?: object;
959
+ manifest: TProteumManifest;
960
+ owner?: TExplainOwnerResponse;
961
+ query?: string;
962
+ route?: string;
963
+ runtime?: object;
964
+ task?: string;
965
+ }) => {
966
+ const ownerQuery = chooseWorkflowOwnerQuery({ file, query, route });
967
+ const instructionQuery = chooseWorkflowInstructionQuery({ file, query, route, task });
968
+ const instructions = resolveInstructionRouting({
969
+ appRoot: manifest.app.root,
970
+ query: instructionQuery,
971
+ });
972
+ const topOwner = owner?.matches[0];
973
+ const topPath =
974
+ topOwner && (topOwner.kind === 'route' || topOwner.kind === 'controller') && topOwner.label.startsWith('/')
975
+ ? topOwner.label
976
+ : route && route.startsWith('/')
977
+ ? route
978
+ : ownerQuery && ownerQuery.startsWith('/')
979
+ ? ownerQuery
980
+ : undefined;
981
+ const runtimeReachable = isReachableHealth(health);
982
+
983
+ return createMcpPayload({
984
+ summary: `${manifest.app.identity.identifier}: workflow start${ownerQuery ? ` for ${ownerQuery}` : ''}; ${instructions.data.selected.length} instruction file${instructions.data.selected.length === 1 ? '' : 's'}`,
985
+ data: {
986
+ workflow: {
987
+ task: task?.trim() || undefined,
988
+ query: ownerQuery,
989
+ route: route?.trim() || undefined,
990
+ file: file?.trim() || undefined,
991
+ },
992
+ runtime: {
993
+ appRoot: manifest.app.root,
994
+ manifest: summarizeManifest(manifest),
995
+ runtime,
996
+ health,
997
+ },
998
+ instructions: {
999
+ selected: compactList(instructions.data.selected, 8),
1000
+ readWhen: compactList(instructions.data.readWhen, 6),
1001
+ fullReadPolicy: fullInstructionReadPolicy,
1002
+ totalSelected: instructions.data.selected.length,
1003
+ },
1004
+ owner: owner
1005
+ ? {
1006
+ query: owner.query,
1007
+ normalizedQuery: owner.normalizedQuery,
1008
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
1009
+ matches: compactList(owner.matches, 5).map(compactOwnerMatch),
1010
+ totalReturned: owner.matches.length,
1011
+ }
1012
+ : undefined,
1013
+ diagnostics: {
1014
+ doctor: doctor.summary,
1015
+ contracts: contracts.summary,
1016
+ },
1017
+ duplicateAvoidance: [
1018
+ 'If owner.top resolves a route or file, do not run broad source searches for the same owner.',
1019
+ 'If this runtime block is present, do not run CLI runtime status for the same app.',
1020
+ 'If diagnose succeeds for this path or request, do not rerun CLI diagnose for the same read.',
1021
+ 'Open full traces, logs, or instruction files only when compact output says the omitted detail is needed.',
1022
+ ],
1023
+ },
1024
+ nextActions: [
1025
+ ...(!runtimeReachable ? [createRuntimeDownNextAction()] : []),
1026
+ ...(topPath && runtimeReachable
1027
+ ? [
1028
+ {
1029
+ label: 'Diagnose Route',
1030
+ tool: 'diagnose',
1031
+ toolArgs: { path: topPath, query: ownerQuery || topPath },
1032
+ reason: 'Use compact runtime diagnosis before CLI diagnose, raw traces, browser work, or broad source search.',
1033
+ },
1034
+ {
1035
+ label: 'Perf Request',
1036
+ tool: 'perf_request',
1037
+ toolArgs: { query: topPath },
1038
+ reason: 'Use the compact request waterfall before raw perf detail.',
1039
+ },
1040
+ ]
1041
+ : []),
1042
+ ...(!ownerQuery && instructionQuery
1043
+ ? [
1044
+ {
1045
+ label: 'Orient Query',
1046
+ tool: 'orient',
1047
+ toolArgs: { query: instructionQuery },
1048
+ reason: 'Use MCP orientation only if the workflow bootstrap did not include a concrete owner query.',
1049
+ },
1050
+ ]
1051
+ : []),
1052
+ ],
1053
+ omitted: [
1054
+ {
1055
+ reason: 'Full instruction files are omitted. Use selected previews for read-only work; read full files only when the fullReadPolicy requires it.',
1056
+ tool: 'instructions_resolve',
1057
+ toolArgs: { query: instructionQuery },
1058
+ },
1059
+ ],
1060
+ });
1061
+ };
1062
+
1063
+ export const compactRouteCandidatesResponse = ({
1064
+ limit = 8,
1065
+ manifest,
1066
+ query,
1067
+ }: {
1068
+ limit?: number;
1069
+ manifest: TProteumManifest;
1070
+ query: string;
1071
+ }) => {
1072
+ const owner = explainOwner(manifest, query);
1073
+ const routeMatches = owner.matches.filter((match) => match.kind === 'route');
1074
+
1075
+ return createMcpPayload({
1076
+ summary:
1077
+ routeMatches.length === 0
1078
+ ? `${query} -> no route candidates`
1079
+ : `${query} -> ${routeMatches.length} route candidate${routeMatches.length === 1 ? '' : 's'}`,
1080
+ data: {
1081
+ query,
1082
+ normalizedQuery: owner.normalizedQuery,
1083
+ candidates: compactList(routeMatches, limit).map(compactOwnerMatch),
1084
+ returned: Math.min(routeMatches.length, limit),
1085
+ totalMatches: routeMatches.length,
1086
+ manifest: summarizeManifest(manifest),
1087
+ },
1088
+ nextActions:
1089
+ routeMatches.length > 0
1090
+ ? [
1091
+ {
1092
+ label: 'Explain Top Route',
1093
+ tool: 'explain_summary',
1094
+ toolArgs: { query: routeMatches[0].label },
1095
+ reason: 'Inspect the top route owner without dumping raw route arrays.',
1096
+ },
1097
+ ]
1098
+ : undefined,
1099
+ omitted:
1100
+ routeMatches.length > limit
1101
+ ? [
1102
+ {
1103
+ reason: `Route candidates are capped at ${limit}. Refine the query before requesting raw route arrays.`,
1104
+ tool: 'route_candidates',
1105
+ toolArgs: { query, limit: Math.min(50, limit * 2) },
1106
+ },
1107
+ ]
1108
+ : undefined,
1109
+ });
1110
+ };
1111
+
1112
+ export const buildRuntimeStatusPayload = ({
1113
+ appRoot,
1114
+ health,
1115
+ manifest,
1116
+ runtime,
1117
+ sessions,
1118
+ }: {
1119
+ appRoot: string;
1120
+ health?: object;
1121
+ manifest?: TProteumManifest;
1122
+ runtime?: object;
1123
+ sessions?: object[];
1124
+ }) =>
1125
+ createMcpPayload({
1126
+ summary: runtime
1127
+ ? `Runtime available for ${manifest?.app.identity.identifier || appRoot}`
1128
+ : manifest
1129
+ ? `Manifest available for ${manifest.app.identity.identifier}; no live runtime selected`
1130
+ : `No Proteum manifest found for ${appRoot}`,
1131
+ data: {
1132
+ appRoot,
1133
+ manifest: summarizeManifest(manifest),
1134
+ runtime,
1135
+ sessions,
1136
+ health,
1137
+ },
1138
+ nextActions: runtime && isReachableHealth(health)
1139
+ ? [
1140
+ {
1141
+ label: 'Diagnose Root',
1142
+ tool: 'diagnose',
1143
+ toolArgs: { query: '/', path: '/' },
1144
+ reason: 'Use the selected runtime for the smallest request-level diagnostic pass.',
1145
+ },
1146
+ ]
1147
+ : [
1148
+ createRuntimeDownNextAction(),
1149
+ ],
1150
+ });