proteum 2.3.0 → 2.4.2

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 (56) hide show
  1. package/AGENTS.md +8 -3
  2. package/README.md +20 -15
  3. package/agents/project/AGENTS.md +16 -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 +10 -9
  7. package/agents/project/optimizations.md +1 -1
  8. package/agents/project/root/AGENTS.md +15 -8
  9. package/agents/project/server/services/AGENTS.md +1 -0
  10. package/agents/project/tests/AGENTS.md +1 -0
  11. package/cli/commands/db.ts +160 -0
  12. package/cli/commands/dev.ts +148 -25
  13. package/cli/commands/diagnose.ts +2 -0
  14. package/cli/commands/explain.ts +38 -9
  15. package/cli/commands/mcp.ts +126 -9
  16. package/cli/commands/orient.ts +44 -17
  17. package/cli/commands/runtime.ts +100 -17
  18. package/cli/mcp/router.ts +1028 -0
  19. package/cli/presentation/commands.ts +56 -25
  20. package/cli/presentation/help.ts +1 -1
  21. package/cli/runtime/commands.ts +163 -21
  22. package/cli/runtime/devSessions.ts +328 -2
  23. package/cli/runtime/mcpDaemon.ts +288 -0
  24. package/cli/runtime/ports.ts +151 -0
  25. package/cli/utils/agents.ts +94 -17
  26. package/cli/utils/appRoots.ts +232 -0
  27. package/common/dev/database.ts +226 -0
  28. package/common/dev/diagnostics.ts +1 -1
  29. package/common/dev/inspection.ts +8 -1
  30. package/common/dev/mcpPayloads.ts +456 -17
  31. package/common/dev/mcpServer.ts +51 -0
  32. package/docs/agent-routing.md +32 -21
  33. package/docs/dev-commands.md +1 -1
  34. package/docs/dev-sessions.md +3 -1
  35. package/docs/diagnostics.md +21 -20
  36. package/docs/mcp.md +114 -50
  37. package/docs/migrate-from-2.1.3.md +3 -5
  38. package/docs/request-tracing.md +3 -3
  39. package/package.json +10 -3
  40. package/server/app/devDiagnostics.ts +92 -0
  41. package/server/app/devMcp.ts +55 -0
  42. package/server/services/prisma/mariadb.ts +7 -3
  43. package/server/services/router/http/index.ts +25 -0
  44. package/server/services/router/request/ip.test.cjs +0 -1
  45. package/tests/agents-utils.test.cjs +58 -3
  46. package/tests/cli-mcp-command.test.cjs +327 -0
  47. package/tests/codex-mcp-usage.test.cjs +307 -0
  48. package/tests/dev-sessions.test.cjs +113 -0
  49. package/tests/dev-transpile-watch.test.cjs +0 -1
  50. package/tests/eslint-rules.test.cjs +0 -1
  51. package/tests/inspection.test.cjs +0 -1
  52. package/tests/mcp.test.cjs +769 -2
  53. package/tests/router-cache-config.test.cjs +0 -1
  54. package/vitest.config.mjs +9 -0
  55. package/cli/mcp/provider.ts +0 -365
  56. package/cli/mcp/stdio.ts +0 -16
@@ -1,7 +1,8 @@
1
1
  import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from './console';
2
+ import type { TDatabaseReadQueryResponse } from './database';
2
3
  import type { TDoctorResponse } from './diagnostics';
3
4
  import { buildExplainSummaryItems } from './diagnostics';
4
- import type { TDiagnoseResponse, TExplainOwnerResponse, TOrientResponse } from './inspection';
5
+ import { explainOwner, type TDiagnoseResponse, type TExplainOwnerResponse, type TOrientResponse } from './inspection';
5
6
  import type { TPerfRequestResponse, TPerfTopResponse } from './performance';
6
7
  import type { TProteumManifest } from './proteumManifest';
7
8
  import type { TRequestTrace } from './requestTrace';
@@ -38,6 +39,7 @@ type TNodeFs = {
38
39
 
39
40
  type TNodePath = {
40
41
  dirname: (filepath: string) => string;
42
+ isAbsolute: (filepath: string) => boolean;
41
43
  join: (...segments: string[]) => string;
42
44
  relative: (from: string, to: string) => string;
43
45
  resolve: (...segments: string[]) => string;
@@ -72,6 +74,95 @@ export const truncateForMcp = (value: string, max = maxTextLength) =>
72
74
 
73
75
  export const compactList = <TValue>(values: TValue[], limit: number) => values.slice(0, Math.max(0, limit));
74
76
 
77
+ export type TTriggeredInstructionRead = {
78
+ file: string;
79
+ reason: string;
80
+ };
81
+
82
+ const matchesInstructionTrigger = (query: string, pattern: RegExp) => pattern.test(query);
83
+
84
+ const resolveRootContractFallbackFile = (rootAgentsFile?: string) => {
85
+ if (fs === undefined || path === undefined || !rootAgentsFile || !fileExists(rootAgentsFile)) return undefined;
86
+
87
+ const content = fs.readFileSync(rootAgentsFile, 'utf8');
88
+ const match = content.match(/Root contract fallback:\s+(.+?)\s*$/m);
89
+ const candidate = match?.[1]?.trim();
90
+ if (!candidate) return undefined;
91
+
92
+ const filepath = path.isAbsolute(candidate) ? candidate : path.resolve(path.dirname(rootAgentsFile), candidate);
93
+ return fileExists(filepath) ? filepath : undefined;
94
+ };
95
+
96
+ export const resolveTriggeredInstructionReads = ({
97
+ codingStyle,
98
+ diagnostics,
99
+ documentation,
100
+ optimizations,
101
+ query,
102
+ rootAgentsFile,
103
+ }: {
104
+ codingStyle?: string;
105
+ diagnostics?: string;
106
+ documentation?: string;
107
+ optimizations?: string;
108
+ query: string;
109
+ rootAgentsFile?: string;
110
+ }) => {
111
+ const normalizedQuery = query.toLowerCase();
112
+ const reads = new Map<string, TTriggeredInstructionRead>();
113
+ const addRead = (file: string | undefined, reason: string) => {
114
+ if (!file || !fileExists(file) || reads.has(file)) return;
115
+ reads.set(file, { file, reason });
116
+ };
117
+ const rootContract = resolveRootContractFallbackFile(rootAgentsFile);
118
+ const looksLikeGitLifecycle = matchesInstructionTrigger(
119
+ normalizedQuery,
120
+ /\b(commit|stage|push)\b|\band commit\b|\bpr\b|pull[- ]requests?|git add|git commit/,
121
+ );
122
+ const looksLikeFinishLifecycle = matchesInstructionTrigger(
123
+ normalizedQuery,
124
+ /\b(finish|finishing|done|complete|completion|final|validate|validation|verify|verification)\b/,
125
+ );
126
+ const looksLikeRuntimeVisible = matchesInstructionTrigger(
127
+ normalizedQuery,
128
+ /\b(runtime|request-time|request time|router|ssr|browser-visible|browser visible|controller|diagnose|trace|perf|repro|reproduction|failing|error|bug)\b/,
129
+ );
130
+ const looksLikeImplementationEdit = matchesInstructionTrigger(
131
+ normalizedQuery,
132
+ /\b(implement|change|edit|update|modify|fix|add|remove|refactor|increase|decrease|code)\b/,
133
+ );
134
+ const looksLikeProductOrDocs = matchesInstructionTrigger(
135
+ normalizedQuery,
136
+ /\b(feature|product|business|acceptance|docs|documentation|ux|copy|onboarding|pricing|commercial|semantics)\b/,
137
+ );
138
+ const looksLikeOptimization = matchesInstructionTrigger(
139
+ normalizedQuery,
140
+ /\b(optimize|optimization|performance|package|dependency|build|bundle)\b/,
141
+ );
142
+
143
+ if (looksLikeGitLifecycle) {
144
+ addRead(rootContract, 'Git lifecycle trigger; read the canonical root contract before any git write.');
145
+ }
146
+ if (looksLikeFinishLifecycle) {
147
+ addRead(rootContract, 'Finish or verification trigger; read the canonical root lifecycle contract.');
148
+ }
149
+ if (looksLikeRuntimeVisible) {
150
+ addRead(rootContract, 'Runtime-visible behavior trigger; read the canonical root verification contract.');
151
+ addRead(diagnostics, 'Runtime, request, trace, perf, reproduction, or error trigger.');
152
+ }
153
+ if (looksLikeImplementationEdit) {
154
+ addRead(codingStyle, 'Implementation edit trigger; read coding style before editing.');
155
+ }
156
+ if (looksLikeProductOrDocs) {
157
+ addRead(documentation, 'Feature, product, business-rule, UX, copy, or docs trigger.');
158
+ }
159
+ if (looksLikeOptimization) {
160
+ addRead(optimizations, 'Package, build, runtime, performance, or optimization trigger.');
161
+ }
162
+
163
+ return [...reads.values()];
164
+ };
165
+
75
166
  export const createMcpPayload = <TData extends object>({
76
167
  data,
77
168
  nextActions,
@@ -178,6 +269,23 @@ export const compactOrientationResponse = (response: TOrientResponse) => {
178
269
  const summary = topOwner
179
270
  ? `${response.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
180
271
  : `${response.query} -> no manifest owner matched`;
272
+ const topPath =
273
+ topOwner && (topOwner.kind === 'route' || topOwner.kind === 'controller') && topOwner.label.startsWith('/')
274
+ ? topOwner.label
275
+ : response.query.startsWith('/')
276
+ ? response.query
277
+ : undefined;
278
+ const triggered = resolveTriggeredInstructionReads({
279
+ codingStyle: response.guidance.codingStyle,
280
+ diagnostics: response.guidance.diagnostics,
281
+ documentation: response.guidance.documentation,
282
+ optimizations: response.guidance.optimizations,
283
+ query: response.normalizedQuery || response.query,
284
+ rootAgentsFile:
285
+ path !== undefined && response.app.repoRoot !== response.app.appRoot
286
+ ? path.join(response.app.repoRoot, 'AGENTS.md')
287
+ : response.guidance.agents,
288
+ });
181
289
 
182
290
  return createMcpPayload({
183
291
  summary,
@@ -190,8 +298,19 @@ export const compactOrientationResponse = (response: TOrientResponse) => {
190
298
  totalReturned: response.owner.matches.length,
191
299
  },
192
300
  instructions: {
193
- mustRead: [...new Set([response.guidance.agents, ...response.guidance.areaAgents])],
301
+ mustRead: [
302
+ ...new Set([
303
+ response.guidance.agents,
304
+ ...response.guidance.areaAgents,
305
+ ...triggered.map((entry) => entry.file),
306
+ ]),
307
+ ],
308
+ triggered,
194
309
  readWhen: [
310
+ {
311
+ file: response.guidance.documentation,
312
+ when: 'Non-trivial coding tasks that need the smallest `/docs` pack and post-change docs updates.',
313
+ },
195
314
  {
196
315
  file: response.guidance.diagnostics,
197
316
  when: 'Raw errors, failing requests, traces, perf regressions, or reproduction work.',
@@ -214,11 +333,39 @@ export const compactOrientationResponse = (response: TOrientResponse) => {
214
333
  },
215
334
  warnings: response.warnings,
216
335
  },
217
- nextActions: response.nextSteps.map((step) => ({
218
- command: step.command,
219
- label: step.label,
220
- reason: step.reason,
221
- })),
336
+ nextActions: [
337
+ ...(topOwner
338
+ ? [
339
+ {
340
+ label: 'Explain Summary',
341
+ tool: 'explain_summary',
342
+ toolArgs: { query: response.query },
343
+ reason: 'Use MCP owner summary before broad manifest or source searches.',
344
+ },
345
+ ]
346
+ : []),
347
+ ...(topPath
348
+ ? [
349
+ {
350
+ label: 'Diagnose Route',
351
+ tool: 'diagnose',
352
+ toolArgs: { path: topPath, query: response.query },
353
+ reason: 'Use the compact runtime diagnosis before CLI diagnose, raw traces, or browser work.',
354
+ },
355
+ {
356
+ label: 'Perf Request',
357
+ tool: 'perf_request',
358
+ toolArgs: { query: topPath },
359
+ reason: 'Use the compact request waterfall before raw perf detail.',
360
+ },
361
+ ]
362
+ : []),
363
+ ...response.nextSteps.map((step) => ({
364
+ command: step.command,
365
+ label: step.label,
366
+ reason: step.reason,
367
+ })),
368
+ ],
222
369
  });
223
370
  };
224
371
 
@@ -233,6 +380,12 @@ export const compactExplainSummary = ({
233
380
  }) => {
234
381
  if (owner) {
235
382
  const topOwner = owner.matches[0];
383
+ const topPath =
384
+ topOwner && (topOwner.kind === 'route' || topOwner.kind === 'controller') && topOwner.label.startsWith('/')
385
+ ? topOwner.label
386
+ : query && query.startsWith('/')
387
+ ? query
388
+ : undefined;
236
389
  return createMcpPayload({
237
390
  summary: topOwner
238
391
  ? `${query || owner.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
@@ -247,6 +400,22 @@ export const compactExplainSummary = ({
247
400
  },
248
401
  manifest: summarizeManifest(manifest),
249
402
  },
403
+ nextActions: topPath
404
+ ? [
405
+ {
406
+ label: 'Diagnose Route',
407
+ tool: 'diagnose',
408
+ toolArgs: { path: topPath, query: query || owner.query },
409
+ reason: 'Use compact runtime diagnosis before CLI diagnose or raw trace detail.',
410
+ },
411
+ {
412
+ label: 'Perf Request',
413
+ tool: 'perf_request',
414
+ toolArgs: { query: topPath },
415
+ reason: 'Use compact request waterfall before raw perf detail.',
416
+ },
417
+ ]
418
+ : undefined,
250
419
  });
251
420
  }
252
421
 
@@ -320,6 +489,7 @@ export const compactDiagnoseResponse = (response: TDiagnoseResponse) => {
320
489
  instructions: response.orientation
321
490
  ? {
322
491
  mustRead: [...new Set([response.orientation.guidance.agents, ...response.orientation.guidance.areaAgents])],
492
+ documentation: response.orientation.guidance.documentation,
323
493
  diagnostics: response.orientation.guidance.diagnostics,
324
494
  codingStyle: response.orientation.guidance.codingStyle,
325
495
  optimizations: response.orientation.guidance.optimizations,
@@ -564,6 +734,30 @@ export const compactLogsResponse = ({
564
734
  : undefined,
565
735
  });
566
736
 
737
+ export const compactDatabaseReadQueryResponse = (response: TDatabaseReadQueryResponse) =>
738
+ createMcpPayload({
739
+ summary: `${response.kind.toUpperCase()} returned ${response.rows.length}/${response.rowCount} rows in ${response.elapsedMs} ms${response.limited ? ` (limited to ${response.limit})` : ''}.`,
740
+ data: {
741
+ kind: response.kind,
742
+ elapsedMs: response.elapsedMs,
743
+ rowCount: response.rowCount,
744
+ returnedRowCount: response.rows.length,
745
+ limit: response.limit,
746
+ limited: response.limited,
747
+ columns: response.columns,
748
+ rows: response.rows,
749
+ },
750
+ omitted: response.limited
751
+ ? [
752
+ {
753
+ reason: `Rows are capped at ${response.limit}. Raise the limit up to 500 or make the read query narrower if more detail is needed.`,
754
+ tool: 'db_query',
755
+ toolArgs: { sql: response.sql, limit: Math.min(response.limit * 2, 500) },
756
+ },
757
+ ]
758
+ : undefined,
759
+ });
760
+
567
761
  const readPreview = (filepath: string) => {
568
762
  if (fs === undefined) return undefined;
569
763
  try {
@@ -618,6 +812,28 @@ const resolveDocumentFile = ({
618
812
  return nearestRoot ? path.join(nearestRoot, relativeFilepath) : undefined;
619
813
  };
620
814
 
815
+ const fullInstructionReadPolicy = {
816
+ default: 'Use selected previews as the instruction source for read-only discovery and diagnostics.',
817
+ requiredWhen: [
818
+ 'editing files governed by the selected scope',
819
+ 'performing git writes such as stage, commit, push, or PR work',
820
+ 'changing schema, auth, runtime, generated contracts, or framework integration behavior',
821
+ 'the compact preview is insufficient for the current decision',
822
+ ],
823
+ };
824
+
825
+ const inferInstructionReadMode = (reason: string) =>
826
+ /git lifecycle|implementation edit|finish or verification|schema|migration/i.test(reason)
827
+ ? 'full-before-action'
828
+ : 'preview-first';
829
+
830
+ const createSelectedInstruction = (file: string, reason: string) => ({
831
+ file,
832
+ fullRead: inferInstructionReadMode(reason),
833
+ preview: readPreview(file),
834
+ reason,
835
+ });
836
+
621
837
  export const resolveInstructionRouting = ({
622
838
  appRoot,
623
839
  query = '',
@@ -627,7 +843,7 @@ export const resolveInstructionRouting = ({
627
843
  }) => {
628
844
  const normalizedQuery = query.trim();
629
845
  const repoRoot = findLikelyRepoRoot(appRoot);
630
- const selected = new Map<string, { file: string; reason: string; preview?: string }>();
846
+ const selected = new Map<string, ReturnType<typeof createSelectedInstruction>>();
631
847
  const readWhen: Array<{ file?: string; when: string }> = [];
632
848
  const addInstruction = (relativeFilepath: string, reason: string, preferAppRoot = true) => {
633
849
  if (path === undefined) return;
@@ -635,7 +851,7 @@ export const resolveInstructionRouting = ({
635
851
  for (const root of [...new Set(roots)]) {
636
852
  const filepath = path.join(root, relativeFilepath);
637
853
  if (!fileExists(filepath)) continue;
638
- selected.set(filepath, { file: filepath, reason, preview: readPreview(filepath) });
854
+ selected.set(filepath, createSelectedInstruction(filepath, reason));
639
855
  return;
640
856
  }
641
857
  };
@@ -644,14 +860,16 @@ export const resolveInstructionRouting = ({
644
860
  readWhen.push({ file: filepath, when });
645
861
  };
646
862
  const lowerQuery = normalizedQuery.toLowerCase();
647
- const looksLikePage = lowerQuery.startsWith('/') || lowerQuery.includes('client/pages') || lowerQuery.includes('.tsx');
863
+ const looksLikeRoutePath = /(^|\s)\/[a-z0-9_./:-]*/i.test(lowerQuery);
864
+ const looksLikePage = looksLikeRoutePath || lowerQuery.includes('client/pages') || lowerQuery.includes('.tsx');
648
865
  const looksLikeClient = looksLikePage || lowerQuery.includes('client/') || lowerQuery.includes('component') || lowerQuery.includes('island');
649
866
  const looksLikeServerRoute =
650
867
  lowerQuery.includes('server/routes') ||
651
868
  lowerQuery.includes('route') ||
652
869
  lowerQuery.includes('sitemap') ||
653
870
  lowerQuery.includes('rss') ||
654
- lowerQuery.startsWith('/api');
871
+ /^\/api(\/|$)/.test(lowerQuery) ||
872
+ /\s\/api(\/|$)/.test(lowerQuery);
655
873
  const looksLikeService =
656
874
  lowerQuery.includes('server/services') ||
657
875
  lowerQuery.includes('.controller') ||
@@ -670,6 +888,23 @@ export const resolveInstructionRouting = ({
670
888
  addInstruction('tests/e2e/REAL_WORLD_JOURNEY_TESTS.md', 'Real-world journey coverage may be in scope.');
671
889
  }
672
890
 
891
+ const appAgentsFile = resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'AGENTS.md' });
892
+ const repoAgentsFile = path !== undefined && repoRoot !== appRoot ? path.join(repoRoot, 'AGENTS.md') : undefined;
893
+ for (const triggered of resolveTriggeredInstructionReads({
894
+ codingStyle: resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'CODING_STYLE.md' }),
895
+ diagnostics: resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'diagnostics.md' }),
896
+ documentation: resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'DOCUMENTATION.md' }),
897
+ optimizations: resolveDocumentFile({ appRoot, repoRoot, relativeFilepath: 'optimizations.md' }),
898
+ query: normalizedQuery,
899
+ rootAgentsFile: repoAgentsFile && fileExists(repoAgentsFile) ? repoAgentsFile : appAgentsFile,
900
+ })) {
901
+ selected.set(triggered.file, createSelectedInstruction(triggered.file, triggered.reason));
902
+ }
903
+
904
+ addReadWhen(
905
+ 'DOCUMENTATION.md',
906
+ 'Read before non-trivial coding tasks to choose the smallest `/docs` pack and update docs after changes.',
907
+ );
673
908
  addReadWhen('diagnostics.md', 'Read for raw errors, failing requests, traces, perf regressions, or reproduction work.');
674
909
  addReadWhen('CODING_STYLE.md', 'Read before editing implementation files.');
675
910
  addReadWhen('optimizations.md', 'Read for client-side implementation, packages, build, runtime, or performance work.');
@@ -683,6 +918,7 @@ export const resolveInstructionRouting = ({
683
918
  repoRoot,
684
919
  selected: selectedFiles,
685
920
  readWhen,
921
+ fullReadPolicy: fullInstructionReadPolicy,
686
922
  missingRuntime:
687
923
  selectedFiles.length === 0
688
924
  ? 'No tracked instruction files were found. Run `proteum configure agents` or start `proteum dev` to refresh managed instructions.'
@@ -691,6 +927,213 @@ export const resolveInstructionRouting = ({
691
927
  });
692
928
  };
693
929
 
930
+ const chooseWorkflowOwnerQuery = ({
931
+ file,
932
+ query,
933
+ route,
934
+ }: {
935
+ file?: string;
936
+ query?: string;
937
+ route?: string;
938
+ }) => [route, file, query].map((value) => value?.trim()).find((value): value is string => Boolean(value));
939
+
940
+ const chooseWorkflowInstructionQuery = ({
941
+ file,
942
+ query,
943
+ route,
944
+ task,
945
+ }: {
946
+ file?: string;
947
+ query?: string;
948
+ route?: string;
949
+ task?: string;
950
+ }) =>
951
+ [task, query, route, file]
952
+ .map((value) => value?.trim())
953
+ .filter((value): value is string => Boolean(value))
954
+ .join(' ');
955
+
956
+ const isReachableHealth = (health: object | undefined) => {
957
+ if (!health || !('reachable' in health)) return true;
958
+
959
+ return (health as { reachable?: unknown }).reachable === true;
960
+ };
961
+
962
+ const createRuntimeDownNextAction = () => ({
963
+ label: 'Start Dev',
964
+ command: 'proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port <free-port>',
965
+ reason: 'Runtime is not reachable; start or repair one tracked dev session before diagnose, trace, or perf reads.',
966
+ });
967
+
968
+ export const compactWorkflowStartResponse = ({
969
+ contracts,
970
+ doctor,
971
+ file,
972
+ health,
973
+ manifest,
974
+ owner,
975
+ query,
976
+ route,
977
+ runtime,
978
+ task,
979
+ }: {
980
+ contracts: TDoctorResponse;
981
+ doctor: TDoctorResponse;
982
+ file?: string;
983
+ health?: object;
984
+ manifest: TProteumManifest;
985
+ owner?: TExplainOwnerResponse;
986
+ query?: string;
987
+ route?: string;
988
+ runtime?: object;
989
+ task?: string;
990
+ }) => {
991
+ const ownerQuery = chooseWorkflowOwnerQuery({ file, query, route });
992
+ const instructionQuery = chooseWorkflowInstructionQuery({ file, query, route, task });
993
+ const instructions = resolveInstructionRouting({
994
+ appRoot: manifest.app.root,
995
+ query: instructionQuery,
996
+ });
997
+ const topOwner = owner?.matches[0];
998
+ const topPath =
999
+ topOwner && (topOwner.kind === 'route' || topOwner.kind === 'controller') && topOwner.label.startsWith('/')
1000
+ ? topOwner.label
1001
+ : route && route.startsWith('/')
1002
+ ? route
1003
+ : ownerQuery && ownerQuery.startsWith('/')
1004
+ ? ownerQuery
1005
+ : undefined;
1006
+ const runtimeReachable = isReachableHealth(health);
1007
+
1008
+ return createMcpPayload({
1009
+ summary: `${manifest.app.identity.identifier}: workflow start${ownerQuery ? ` for ${ownerQuery}` : ''}; ${instructions.data.selected.length} instruction file${instructions.data.selected.length === 1 ? '' : 's'}`,
1010
+ data: {
1011
+ workflow: {
1012
+ task: task?.trim() || undefined,
1013
+ query: ownerQuery,
1014
+ route: route?.trim() || undefined,
1015
+ file: file?.trim() || undefined,
1016
+ },
1017
+ runtime: {
1018
+ appRoot: manifest.app.root,
1019
+ manifest: summarizeManifest(manifest),
1020
+ runtime,
1021
+ health,
1022
+ },
1023
+ instructions: {
1024
+ selected: compactList(instructions.data.selected, 8),
1025
+ readWhen: compactList(instructions.data.readWhen, 6),
1026
+ fullReadPolicy: fullInstructionReadPolicy,
1027
+ totalSelected: instructions.data.selected.length,
1028
+ },
1029
+ owner: owner
1030
+ ? {
1031
+ query: owner.query,
1032
+ normalizedQuery: owner.normalizedQuery,
1033
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
1034
+ matches: compactList(owner.matches, 5).map(compactOwnerMatch),
1035
+ totalReturned: owner.matches.length,
1036
+ }
1037
+ : undefined,
1038
+ diagnostics: {
1039
+ doctor: doctor.summary,
1040
+ contracts: contracts.summary,
1041
+ },
1042
+ duplicateAvoidance: [
1043
+ 'If owner.top resolves a route or file, do not run broad source searches for the same owner.',
1044
+ 'If this runtime block is present, do not run CLI runtime status for the same app.',
1045
+ 'If diagnose succeeds for this path or request, do not rerun CLI diagnose for the same read.',
1046
+ 'Open full traces, logs, or instruction files only when compact output says the omitted detail is needed.',
1047
+ ],
1048
+ },
1049
+ nextActions: [
1050
+ ...(!runtimeReachable ? [createRuntimeDownNextAction()] : []),
1051
+ ...(topPath && runtimeReachable
1052
+ ? [
1053
+ {
1054
+ label: 'Diagnose Route',
1055
+ tool: 'diagnose',
1056
+ toolArgs: { path: topPath, query: ownerQuery || topPath },
1057
+ reason: 'Use compact runtime diagnosis before CLI diagnose, raw traces, browser work, or broad source search.',
1058
+ },
1059
+ {
1060
+ label: 'Perf Request',
1061
+ tool: 'perf_request',
1062
+ toolArgs: { query: topPath },
1063
+ reason: 'Use the compact request waterfall before raw perf detail.',
1064
+ },
1065
+ ]
1066
+ : []),
1067
+ ...(!ownerQuery && instructionQuery
1068
+ ? [
1069
+ {
1070
+ label: 'Orient Query',
1071
+ tool: 'orient',
1072
+ toolArgs: { query: instructionQuery },
1073
+ reason: 'Use MCP orientation only if the workflow bootstrap did not include a concrete owner query.',
1074
+ },
1075
+ ]
1076
+ : []),
1077
+ ],
1078
+ omitted: [
1079
+ {
1080
+ reason: 'Full instruction files are omitted. Use selected previews for read-only work; read full files only when the fullReadPolicy requires it.',
1081
+ tool: 'instructions_resolve',
1082
+ toolArgs: { query: instructionQuery },
1083
+ },
1084
+ ],
1085
+ });
1086
+ };
1087
+
1088
+ export const compactRouteCandidatesResponse = ({
1089
+ limit = 8,
1090
+ manifest,
1091
+ query,
1092
+ }: {
1093
+ limit?: number;
1094
+ manifest: TProteumManifest;
1095
+ query: string;
1096
+ }) => {
1097
+ const owner = explainOwner(manifest, query);
1098
+ const routeMatches = owner.matches.filter((match) => match.kind === 'route');
1099
+
1100
+ return createMcpPayload({
1101
+ summary:
1102
+ routeMatches.length === 0
1103
+ ? `${query} -> no route candidates`
1104
+ : `${query} -> ${routeMatches.length} route candidate${routeMatches.length === 1 ? '' : 's'}`,
1105
+ data: {
1106
+ query,
1107
+ normalizedQuery: owner.normalizedQuery,
1108
+ candidates: compactList(routeMatches, limit).map(compactOwnerMatch),
1109
+ returned: Math.min(routeMatches.length, limit),
1110
+ totalMatches: routeMatches.length,
1111
+ manifest: summarizeManifest(manifest),
1112
+ },
1113
+ nextActions:
1114
+ routeMatches.length > 0
1115
+ ? [
1116
+ {
1117
+ label: 'Explain Top Route',
1118
+ tool: 'explain_summary',
1119
+ toolArgs: { query: routeMatches[0].label },
1120
+ reason: 'Inspect the top route owner without dumping raw route arrays.',
1121
+ },
1122
+ ]
1123
+ : undefined,
1124
+ omitted:
1125
+ routeMatches.length > limit
1126
+ ? [
1127
+ {
1128
+ reason: `Route candidates are capped at ${limit}. Refine the query before requesting raw route arrays.`,
1129
+ tool: 'route_candidates',
1130
+ toolArgs: { query, limit: Math.min(50, limit * 2) },
1131
+ },
1132
+ ]
1133
+ : undefined,
1134
+ });
1135
+ };
1136
+
694
1137
  export const buildRuntimeStatusPayload = ({
695
1138
  appRoot,
696
1139
  health,
@@ -717,7 +1160,7 @@ export const buildRuntimeStatusPayload = ({
717
1160
  sessions,
718
1161
  health,
719
1162
  },
720
- nextActions: runtime
1163
+ nextActions: runtime && isReachableHealth(health)
721
1164
  ? [
722
1165
  {
723
1166
  label: 'Diagnose Root',
@@ -727,10 +1170,6 @@ export const buildRuntimeStatusPayload = ({
727
1170
  },
728
1171
  ]
729
1172
  : [
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
- },
1173
+ createRuntimeDownNextAction(),
735
1174
  ],
736
1175
  });