proteum 2.3.0 → 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 +8 -3
- package/README.md +20 -15
- package/agents/project/AGENTS.md +15 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +9 -8
- package/agents/project/root/AGENTS.md +14 -8
- package/agents/project/tests/AGENTS.md +1 -0
- package/cli/commands/dev.ts +148 -25
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/explain.ts +38 -9
- package/cli/commands/mcp.ts +126 -9
- package/cli/commands/orient.ts +44 -17
- package/cli/commands/runtime.ts +100 -17
- package/cli/mcp/router.ts +1010 -0
- package/cli/presentation/commands.ts +34 -24
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +129 -21
- package/cli/runtime/devSessions.ts +328 -2
- package/cli/runtime/mcpDaemon.ts +288 -0
- package/cli/runtime/ports.ts +151 -0
- package/cli/utils/agents.ts +93 -17
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +8 -1
- package/common/dev/mcpPayloads.ts +431 -17
- package/common/dev/mcpServer.ts +33 -0
- package/docs/agent-routing.md +32 -21
- package/docs/dev-commands.md +1 -1
- package/docs/dev-sessions.md +3 -1
- package/docs/diagnostics.md +21 -20
- package/docs/mcp.md +109 -52
- package/docs/migrate-from-2.1.3.md +3 -5
- package/docs/request-tracing.md +3 -3
- package/package.json +10 -3
- package/server/app/devMcp.ts +45 -0
- package/server/services/router/request/ip.test.cjs +0 -1
- package/tests/agents-utils.test.cjs +58 -3
- 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 +0 -1
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +0 -1
- package/tests/mcp.test.cjs +748 -2
- package/tests/router-cache-config.test.cjs +0 -1
- package/vitest.config.mjs +9 -0
- package/cli/mcp/provider.ts +0 -365
- package/cli/mcp/stdio.ts +0 -16
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from './console';
|
|
2
2
|
import type { TDoctorResponse } from './diagnostics';
|
|
3
3
|
import { buildExplainSummaryItems } from './diagnostics';
|
|
4
|
-
import type
|
|
4
|
+
import { explainOwner, type TDiagnoseResponse, type TExplainOwnerResponse, type TOrientResponse } from './inspection';
|
|
5
5
|
import type { TPerfRequestResponse, TPerfTopResponse } from './performance';
|
|
6
6
|
import type { TProteumManifest } from './proteumManifest';
|
|
7
7
|
import type { TRequestTrace } from './requestTrace';
|
|
@@ -38,6 +38,7 @@ type TNodeFs = {
|
|
|
38
38
|
|
|
39
39
|
type TNodePath = {
|
|
40
40
|
dirname: (filepath: string) => string;
|
|
41
|
+
isAbsolute: (filepath: string) => boolean;
|
|
41
42
|
join: (...segments: string[]) => string;
|
|
42
43
|
relative: (from: string, to: string) => string;
|
|
43
44
|
resolve: (...segments: string[]) => string;
|
|
@@ -72,6 +73,95 @@ export const truncateForMcp = (value: string, max = maxTextLength) =>
|
|
|
72
73
|
|
|
73
74
|
export const compactList = <TValue>(values: TValue[], limit: number) => values.slice(0, Math.max(0, limit));
|
|
74
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
|
+
|
|
75
165
|
export const createMcpPayload = <TData extends object>({
|
|
76
166
|
data,
|
|
77
167
|
nextActions,
|
|
@@ -178,6 +268,23 @@ export const compactOrientationResponse = (response: TOrientResponse) => {
|
|
|
178
268
|
const summary = topOwner
|
|
179
269
|
? `${response.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
|
|
180
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
|
+
});
|
|
181
288
|
|
|
182
289
|
return createMcpPayload({
|
|
183
290
|
summary,
|
|
@@ -190,8 +297,19 @@ export const compactOrientationResponse = (response: TOrientResponse) => {
|
|
|
190
297
|
totalReturned: response.owner.matches.length,
|
|
191
298
|
},
|
|
192
299
|
instructions: {
|
|
193
|
-
mustRead: [
|
|
300
|
+
mustRead: [
|
|
301
|
+
...new Set([
|
|
302
|
+
response.guidance.agents,
|
|
303
|
+
...response.guidance.areaAgents,
|
|
304
|
+
...triggered.map((entry) => entry.file),
|
|
305
|
+
]),
|
|
306
|
+
],
|
|
307
|
+
triggered,
|
|
194
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
|
+
},
|
|
195
313
|
{
|
|
196
314
|
file: response.guidance.diagnostics,
|
|
197
315
|
when: 'Raw errors, failing requests, traces, perf regressions, or reproduction work.',
|
|
@@ -214,11 +332,39 @@ export const compactOrientationResponse = (response: TOrientResponse) => {
|
|
|
214
332
|
},
|
|
215
333
|
warnings: response.warnings,
|
|
216
334
|
},
|
|
217
|
-
nextActions:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
],
|
|
222
368
|
});
|
|
223
369
|
};
|
|
224
370
|
|
|
@@ -233,6 +379,12 @@ export const compactExplainSummary = ({
|
|
|
233
379
|
}) => {
|
|
234
380
|
if (owner) {
|
|
235
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;
|
|
236
388
|
return createMcpPayload({
|
|
237
389
|
summary: topOwner
|
|
238
390
|
? `${query || owner.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
|
|
@@ -247,6 +399,22 @@ export const compactExplainSummary = ({
|
|
|
247
399
|
},
|
|
248
400
|
manifest: summarizeManifest(manifest),
|
|
249
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,
|
|
250
418
|
});
|
|
251
419
|
}
|
|
252
420
|
|
|
@@ -320,6 +488,7 @@ export const compactDiagnoseResponse = (response: TDiagnoseResponse) => {
|
|
|
320
488
|
instructions: response.orientation
|
|
321
489
|
? {
|
|
322
490
|
mustRead: [...new Set([response.orientation.guidance.agents, ...response.orientation.guidance.areaAgents])],
|
|
491
|
+
documentation: response.orientation.guidance.documentation,
|
|
323
492
|
diagnostics: response.orientation.guidance.diagnostics,
|
|
324
493
|
codingStyle: response.orientation.guidance.codingStyle,
|
|
325
494
|
optimizations: response.orientation.guidance.optimizations,
|
|
@@ -618,6 +787,28 @@ const resolveDocumentFile = ({
|
|
|
618
787
|
return nearestRoot ? path.join(nearestRoot, relativeFilepath) : undefined;
|
|
619
788
|
};
|
|
620
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
|
+
|
|
621
812
|
export const resolveInstructionRouting = ({
|
|
622
813
|
appRoot,
|
|
623
814
|
query = '',
|
|
@@ -627,7 +818,7 @@ export const resolveInstructionRouting = ({
|
|
|
627
818
|
}) => {
|
|
628
819
|
const normalizedQuery = query.trim();
|
|
629
820
|
const repoRoot = findLikelyRepoRoot(appRoot);
|
|
630
|
-
const selected = new Map<string,
|
|
821
|
+
const selected = new Map<string, ReturnType<typeof createSelectedInstruction>>();
|
|
631
822
|
const readWhen: Array<{ file?: string; when: string }> = [];
|
|
632
823
|
const addInstruction = (relativeFilepath: string, reason: string, preferAppRoot = true) => {
|
|
633
824
|
if (path === undefined) return;
|
|
@@ -635,7 +826,7 @@ export const resolveInstructionRouting = ({
|
|
|
635
826
|
for (const root of [...new Set(roots)]) {
|
|
636
827
|
const filepath = path.join(root, relativeFilepath);
|
|
637
828
|
if (!fileExists(filepath)) continue;
|
|
638
|
-
selected.set(filepath,
|
|
829
|
+
selected.set(filepath, createSelectedInstruction(filepath, reason));
|
|
639
830
|
return;
|
|
640
831
|
}
|
|
641
832
|
};
|
|
@@ -644,14 +835,16 @@ export const resolveInstructionRouting = ({
|
|
|
644
835
|
readWhen.push({ file: filepath, when });
|
|
645
836
|
};
|
|
646
837
|
const lowerQuery = normalizedQuery.toLowerCase();
|
|
647
|
-
const
|
|
838
|
+
const looksLikeRoutePath = /(^|\s)\/[a-z0-9_./:-]*/i.test(lowerQuery);
|
|
839
|
+
const looksLikePage = looksLikeRoutePath || lowerQuery.includes('client/pages') || lowerQuery.includes('.tsx');
|
|
648
840
|
const looksLikeClient = looksLikePage || lowerQuery.includes('client/') || lowerQuery.includes('component') || lowerQuery.includes('island');
|
|
649
841
|
const looksLikeServerRoute =
|
|
650
842
|
lowerQuery.includes('server/routes') ||
|
|
651
843
|
lowerQuery.includes('route') ||
|
|
652
844
|
lowerQuery.includes('sitemap') ||
|
|
653
845
|
lowerQuery.includes('rss') ||
|
|
654
|
-
|
|
846
|
+
/^\/api(\/|$)/.test(lowerQuery) ||
|
|
847
|
+
/\s\/api(\/|$)/.test(lowerQuery);
|
|
655
848
|
const looksLikeService =
|
|
656
849
|
lowerQuery.includes('server/services') ||
|
|
657
850
|
lowerQuery.includes('.controller') ||
|
|
@@ -670,6 +863,23 @@ export const resolveInstructionRouting = ({
|
|
|
670
863
|
addInstruction('tests/e2e/REAL_WORLD_JOURNEY_TESTS.md', 'Real-world journey coverage may be in scope.');
|
|
671
864
|
}
|
|
672
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
|
+
);
|
|
673
883
|
addReadWhen('diagnostics.md', 'Read for raw errors, failing requests, traces, perf regressions, or reproduction work.');
|
|
674
884
|
addReadWhen('CODING_STYLE.md', 'Read before editing implementation files.');
|
|
675
885
|
addReadWhen('optimizations.md', 'Read for client-side implementation, packages, build, runtime, or performance work.');
|
|
@@ -683,6 +893,7 @@ export const resolveInstructionRouting = ({
|
|
|
683
893
|
repoRoot,
|
|
684
894
|
selected: selectedFiles,
|
|
685
895
|
readWhen,
|
|
896
|
+
fullReadPolicy: fullInstructionReadPolicy,
|
|
686
897
|
missingRuntime:
|
|
687
898
|
selectedFiles.length === 0
|
|
688
899
|
? 'No tracked instruction files were found. Run `proteum configure agents` or start `proteum dev` to refresh managed instructions.'
|
|
@@ -691,6 +902,213 @@ export const resolveInstructionRouting = ({
|
|
|
691
902
|
});
|
|
692
903
|
};
|
|
693
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
|
+
|
|
694
1112
|
export const buildRuntimeStatusPayload = ({
|
|
695
1113
|
appRoot,
|
|
696
1114
|
health,
|
|
@@ -717,7 +1135,7 @@ export const buildRuntimeStatusPayload = ({
|
|
|
717
1135
|
sessions,
|
|
718
1136
|
health,
|
|
719
1137
|
},
|
|
720
|
-
nextActions: runtime
|
|
1138
|
+
nextActions: runtime && isReachableHealth(health)
|
|
721
1139
|
? [
|
|
722
1140
|
{
|
|
723
1141
|
label: 'Diagnose Root',
|
|
@@ -727,10 +1145,6 @@ export const buildRuntimeStatusPayload = ({
|
|
|
727
1145
|
},
|
|
728
1146
|
]
|
|
729
1147
|
: [
|
|
730
|
-
|
|
731
|
-
label: 'Start Dev',
|
|
732
|
-
command: 'proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port <free-port>',
|
|
733
|
-
reason: 'Create a tracked dev session before request-time diagnostics.',
|
|
734
|
-
},
|
|
1148
|
+
createRuntimeDownNextAction(),
|
|
735
1149
|
],
|
|
736
1150
|
});
|
package/common/dev/mcpServer.ts
CHANGED
|
@@ -22,9 +22,11 @@ export type TProteumMcpProvider = {
|
|
|
22
22
|
perfRequest: (input: { query: string }) => Promise<TProteumMcpPayload>;
|
|
23
23
|
perfTop: (input: { groupBy?: 'path' | 'route' | 'controller'; limit?: number; since?: string }) => Promise<TProteumMcpPayload>;
|
|
24
24
|
readResource: (uri: string) => Promise<TProteumMcpPayload>;
|
|
25
|
+
routeCandidates: (input: { limit?: number; query: string }) => Promise<TProteumMcpPayload>;
|
|
25
26
|
runtimeStatus: (input: Record<string, never>) => Promise<TProteumMcpPayload>;
|
|
26
27
|
traceLatest: (input: { detail?: TProteumMcpDetail; limit?: number; offset?: number }) => Promise<TProteumMcpPayload>;
|
|
27
28
|
traceShow: (input: { detail?: TProteumMcpDetail; limit?: number; offset?: number; requestId: string }) => Promise<TProteumMcpPayload>;
|
|
29
|
+
workflowStart: (input: { file?: string; query?: string; route?: string; task?: string }) => Promise<TProteumMcpPayload>;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
type TCreateProteumMcpServerArgs = {
|
|
@@ -76,6 +78,23 @@ export const createProteumMcpServer = ({ provider, version }: TCreateProteumMcpS
|
|
|
76
78
|
},
|
|
77
79
|
);
|
|
78
80
|
|
|
81
|
+
server.registerTool(
|
|
82
|
+
'workflow_start',
|
|
83
|
+
{
|
|
84
|
+
annotations: readOnlyAnnotations,
|
|
85
|
+
description:
|
|
86
|
+
'Bootstrap an agent workflow with compact runtime, instruction, owner, doctor, and next-action data in one read.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
file: z.string().optional().describe('Optional source file or generated artifact path in scope.'),
|
|
89
|
+
query: z.string().optional().describe('Optional task, route, controller, file, or owner query.'),
|
|
90
|
+
route: z.string().optional().describe('Optional route path in scope.'),
|
|
91
|
+
task: z.string().optional().describe('Optional short natural-language task description.'),
|
|
92
|
+
},
|
|
93
|
+
title: 'Proteum Workflow Start',
|
|
94
|
+
},
|
|
95
|
+
async ({ file, query, route, task }) => jsonToolResult(await provider.workflowStart({ file, query, route, task })),
|
|
96
|
+
);
|
|
97
|
+
|
|
79
98
|
server.registerTool(
|
|
80
99
|
'runtime_status',
|
|
81
100
|
{
|
|
@@ -126,6 +145,20 @@ export const createProteumMcpServer = ({ provider, version }: TCreateProteumMcpS
|
|
|
126
145
|
async ({ query }) => jsonToolResult(await provider.explainSummary({ query })),
|
|
127
146
|
);
|
|
128
147
|
|
|
148
|
+
server.registerTool(
|
|
149
|
+
'route_candidates',
|
|
150
|
+
{
|
|
151
|
+
annotations: readOnlyAnnotations,
|
|
152
|
+
description: 'Return compact route candidates for a query without dumping raw route arrays.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
155
|
+
query: z.string().min(1).describe('Route path or route-like search query.'),
|
|
156
|
+
},
|
|
157
|
+
title: 'Proteum Route Candidates',
|
|
158
|
+
},
|
|
159
|
+
async ({ limit, query }) => jsonToolResult(await provider.routeCandidates({ limit, query })),
|
|
160
|
+
);
|
|
161
|
+
|
|
129
162
|
server.registerTool(
|
|
130
163
|
'doctor',
|
|
131
164
|
{
|