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.
- package/AGENTS.md +10 -4
- package/README.md +58 -15
- package/agents/project/AGENTS.md +53 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +12 -7
- package/agents/project/optimizations.md +1 -0
- package/agents/project/root/AGENTS.md +24 -9
- package/agents/project/tests/AGENTS.md +7 -0
- package/agents/project/tests/e2e/AGENTS.md +13 -0
- package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
- package/cli/commands/connect.ts +40 -4
- package/cli/commands/dev.ts +148 -25
- package/cli/commands/diagnose.ts +138 -5
- package/cli/commands/doctor.ts +24 -4
- package/cli/commands/explain.ts +134 -6
- package/cli/commands/mcp.ts +133 -0
- package/cli/commands/orient.ts +93 -3
- package/cli/commands/perf.ts +118 -13
- package/cli/commands/runtime.ts +234 -0
- package/cli/commands/trace.ts +116 -21
- package/cli/mcp/router.ts +1010 -0
- package/cli/presentation/commands.ts +93 -26
- package/cli/presentation/devSession.ts +2 -0
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +215 -24
- package/cli/runtime/devSessions.ts +328 -2
- package/cli/runtime/mcpDaemon.ts +288 -0
- package/cli/runtime/ports.ts +151 -0
- package/cli/utils/agentOutput.ts +46 -0
- package/cli/utils/agents.ts +194 -51
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +22 -7
- package/common/dev/mcpPayloads.ts +1150 -0
- package/common/dev/mcpServer.ts +287 -0
- package/docs/agent-routing.md +137 -0
- package/docs/dev-commands.md +2 -0
- package/docs/dev-sessions.md +4 -1
- package/docs/diagnostics.md +70 -24
- package/docs/mcp.md +206 -0
- package/docs/migrate-from-2.1.3.md +14 -6
- package/docs/request-tracing.md +12 -6
- package/package.json +11 -3
- package/server/app/devMcp.ts +204 -0
- package/server/services/router/http/cache.ts +116 -0
- package/server/services/router/http/index.ts +94 -35
- package/server/services/router/index.ts +8 -11
- package/server/services/router/request/ip.test.cjs +0 -1
- package/tests/agents-utils.test.cjs +92 -14
- package/tests/cli-mcp-command.test.cjs +262 -0
- package/tests/codex-mcp-usage.test.cjs +307 -0
- package/tests/dev-sessions.test.cjs +113 -0
- package/tests/dev-transpile-watch.test.cjs +117 -9
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +66 -0
- package/tests/mcp.test.cjs +873 -0
- package/tests/router-cache-config.test.cjs +73 -0
- 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
|
+
});
|