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
@@ -42,8 +42,30 @@ const compactOwnerMatch = (match: ReturnType<typeof explainOwner>['matches'][num
42
42
  source: match.source,
43
43
  });
44
44
 
45
- const hasExplicitDetailSelection = (selectedSections: TExplainSectionName[]) =>
46
- cli.args.full === true || cli.args.manifest === true || cli.args.all === true || selectedSections.length > 0;
45
+ const hasExplicitDetailSelection = () => cli.args.full === true || cli.args.manifest === true;
46
+
47
+ const buildSectionFlagCommand = (selectedSections: TExplainSectionName[]) =>
48
+ selectedSections.length === explainSectionNames.length
49
+ ? 'proteum explain --all --full'
50
+ : `proteum explain ${selectedSections.map((sectionName) => `--${sectionName}`).join(' ')} --full`;
51
+
52
+ const summarizeSelectedSection = (manifest: ReturnType<typeof readProteumManifest>, sectionName: TExplainSectionName) => {
53
+ if (sectionName === 'app') return { section: sectionName, count: 1 };
54
+ if (sectionName === 'conventions')
55
+ return {
56
+ section: sectionName,
57
+ count: manifest.conventions.routeOptionKeys.length + manifest.conventions.reservedRouteOptionKeys.length,
58
+ };
59
+ if (sectionName === 'env') return { section: sectionName, count: manifest.env.requiredVariables.length };
60
+ if (sectionName === 'connected') return { section: sectionName, count: manifest.connectedProjects.length };
61
+ if (sectionName === 'services')
62
+ return { section: sectionName, count: manifest.services.app.length + manifest.services.routerPlugins.length };
63
+ if (sectionName === 'controllers') return { section: sectionName, count: manifest.controllers.length };
64
+ if (sectionName === 'commands') return { section: sectionName, count: manifest.commands.length };
65
+ if (sectionName === 'routes') return { section: sectionName, count: manifest.routes.client.length + manifest.routes.server.length };
66
+ if (sectionName === 'layouts') return { section: sectionName, count: manifest.layouts.length };
67
+ return { section: sectionName, count: manifest.diagnostics.length };
68
+ };
47
69
 
48
70
  const printCompactOwner = (ownerQuery: string, response: ReturnType<typeof explainOwner>) => {
49
71
  const topOwner = response.matches[0];
@@ -70,13 +92,17 @@ const printCompactOwner = (ownerQuery: string, response: ReturnType<typeof expla
70
92
  });
71
93
  };
72
94
 
73
- const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>) => {
95
+ const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>, selectedSections: TExplainSectionName[] = []) => {
74
96
  const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
75
97
  const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
76
98
  const requiredEnvProvided = manifest.env.requiredVariables.filter((variable) => variable.provided).length;
99
+ const hasSelectedSections = selectedSections.length > 0;
100
+ const fullDetailCommand = hasSelectedSections ? buildSectionFlagCommand(selectedSections) : 'proteum explain --manifest';
77
101
 
78
102
  printAgentResponse({
79
- summary: `${manifest.app.identity.identifier}: ${manifest.controllers.length} controllers, ${manifest.routes.client.length + manifest.routes.server.length} routes, ${errors} errors, ${warnings} warnings`,
103
+ summary: hasSelectedSections
104
+ ? `${manifest.app.identity.identifier}: summarized ${selectedSections.join(', ')} sections; use --full for raw section arrays`
105
+ : `${manifest.app.identity.identifier}: ${manifest.controllers.length} controllers, ${manifest.routes.client.length + manifest.routes.server.length} routes, ${errors} errors, ${warnings} warnings`,
80
106
  data: {
81
107
  app: {
82
108
  root: manifest.app.root,
@@ -96,6 +122,7 @@ const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>) =
96
122
  servicesRouterPlugins: manifest.services.routerPlugins.length,
97
123
  },
98
124
  diagnostics: { errors, warnings },
125
+ selectedSections: hasSelectedSections ? selectedSections.map((sectionName) => summarizeSelectedSection(manifest, sectionName)) : undefined,
99
126
  env: {
100
127
  requiredProvided: requiredEnvProvided,
101
128
  requiredTotal: manifest.env.requiredVariables.length,
@@ -110,11 +137,13 @@ const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>) =
110
137
  reason: 'Use orient for task-specific owner, instruction, and next-command routing.',
111
138
  },
112
139
  ],
113
- fullDetailCommand: 'proteum explain --manifest',
140
+ fullDetailCommand,
114
141
  omitted: [
115
142
  {
116
- reason: 'Full manifest sections are omitted from the default agent summary.',
117
- command: 'proteum explain --manifest',
143
+ reason: hasSelectedSections
144
+ ? 'Selected manifest sections are summarized by default to avoid large route/controller dumps.'
145
+ : 'Full manifest sections are omitted from the default agent summary.',
146
+ command: fullDetailCommand,
118
147
  },
119
148
  ],
120
149
  });
@@ -158,7 +187,7 @@ export const run = async (): Promise<void> => {
158
187
 
159
188
  const selectedSections = getSelectedSections();
160
189
 
161
- if (hasExplicitDetailSelection(selectedSections)) {
190
+ if (hasExplicitDetailSelection()) {
162
191
  printJson(pickExplainManifestSections(manifest, cli.args.manifest === true ? [...explainSectionNames] : selectedSections));
163
192
  return;
164
193
  }
@@ -168,5 +197,5 @@ export const run = async (): Promise<void> => {
168
197
  return;
169
198
  }
170
199
 
171
- printCompactExplain(manifest);
200
+ printCompactExplain(manifest, selectedSections);
172
201
  };
@@ -1,16 +1,133 @@
1
1
  import cli from '..';
2
- import { CliProteumMcpProvider } from '../mcp/provider';
3
- import { startProteumMcpStdioServer } from '../mcp/stdio';
2
+ import { startProteumMachineMcpRouter, startProteumMachineMcpRouterHttp } from '../mcp/router';
3
+ import {
4
+ ensureMachineMcpDaemonProcess,
5
+ inspectMachineMcpDaemonRecord,
6
+ resolveMachineMcpDaemonPort,
7
+ stopMachineMcpDaemonProcess,
8
+ } from '../runtime/mcpDaemon';
4
9
 
5
- export const run = async () => {
6
- const provider = new CliProteumMcpProvider({
7
- appRoot: cli.paths.appRoot,
8
- sessionFilePath: typeof cli.args.sessionFile === 'string' && cli.args.sessionFile ? cli.args.sessionFile : undefined,
9
- url: typeof cli.args.url === 'string' && cli.args.url ? cli.args.url : undefined,
10
+ const printJson = (payload: unknown) => {
11
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
12
+ };
13
+
14
+ const printStatus = async () => {
15
+ const inspection = await inspectMachineMcpDaemonRecord({ cleanStale: true });
16
+
17
+ if (cli.args.json === true) {
18
+ printJson({
19
+ daemon: inspection
20
+ ? {
21
+ live: inspection.live,
22
+ stale: inspection.stale,
23
+ invalid: inspection.invalid,
24
+ parseError: inspection.parseError,
25
+ record: inspection.record,
26
+ }
27
+ : null,
28
+ });
29
+ return;
30
+ }
31
+
32
+ if (!inspection?.record || !inspection.live) {
33
+ console.info('No live Proteum machine MCP daemon found.');
34
+ return;
35
+ }
36
+
37
+ console.info(
38
+ [
39
+ `Proteum machine MCP daemon is running.`,
40
+ `pid ${inspection.record.pid}`,
41
+ `mcp ${inspection.record.mcpUrl}`,
42
+ `health ${inspection.record.healthUrl}`,
43
+ ].join('\n'),
44
+ );
45
+ };
46
+
47
+ const runDaemon = async () => {
48
+ const existing = await inspectMachineMcpDaemonRecord({ cleanStale: true });
49
+
50
+ if (existing?.record && existing.live && existing.record.pid !== process.pid) {
51
+ if (cli.args.json === true) {
52
+ printJson({ started: false, daemon: existing.record });
53
+ return;
54
+ }
55
+
56
+ console.info(`Proteum machine MCP daemon is already running at ${existing.record.mcpUrl} (pid ${existing.record.pid}).`);
57
+ return;
58
+ }
59
+
60
+ const port = resolveMachineMcpDaemonPort(typeof cli.args.port === 'string' ? cli.args.port : undefined);
61
+
62
+ await startProteumMachineMcpRouterHttp({
63
+ port,
64
+ version: String(cli.packageJson.version || ''),
10
65
  });
11
66
 
12
- await startProteumMcpStdioServer({
13
- provider,
67
+ if (cli.args.json === true) {
68
+ printJson({
69
+ started: true,
70
+ daemon: {
71
+ pid: process.pid,
72
+ mcpUrl: `http://127.0.0.1:${port}/mcp`,
73
+ healthUrl: `http://127.0.0.1:${port}/health`,
74
+ },
75
+ });
76
+ } else {
77
+ console.info(`Proteum machine MCP daemon started at http://127.0.0.1:${port}/mcp.`);
78
+ }
79
+ };
80
+
81
+ const ensureDaemon = async () => {
82
+ const result = await ensureMachineMcpDaemonProcess({
83
+ coreRoot: cli.paths.core.root,
84
+ port: typeof cli.args.port === 'string' ? cli.args.port : undefined,
85
+ });
86
+
87
+ if (cli.args.json === true) {
88
+ printJson({ started: result.started, daemon: result.inspection.record });
89
+ return;
90
+ }
91
+
92
+ if (result.inspection.record) {
93
+ console.info(
94
+ result.started
95
+ ? `Proteum machine MCP daemon started at ${result.inspection.record.mcpUrl}.`
96
+ : `Proteum machine MCP daemon is already running at ${result.inspection.record.mcpUrl}.`,
97
+ );
98
+ }
99
+ };
100
+
101
+ export const run = async () => {
102
+ if (cli.args.action === 'status') {
103
+ await printStatus();
104
+ return;
105
+ }
106
+
107
+ if (cli.args.action === 'stop') {
108
+ const result = await stopMachineMcpDaemonProcess();
109
+ if (cli.args.json === true) {
110
+ printJson({ stopped: result.stopped, daemon: result.inspection?.record || null });
111
+ } else if (result.stopped) {
112
+ console.info('Proteum machine MCP daemon stopped.');
113
+ } else if (result.inspection?.record) {
114
+ console.info(`Could not stop Proteum machine MCP daemon pid ${result.inspection.record.pid}.`);
115
+ process.exitCode = 1;
116
+ }
117
+ return;
118
+ }
119
+
120
+ if (cli.args.daemon === true) {
121
+ await runDaemon();
122
+ return;
123
+ }
124
+
125
+ if (cli.args.stdio !== true && (process.stdout.isTTY || cli.args.json === true)) {
126
+ await ensureDaemon();
127
+ return;
128
+ }
129
+
130
+ await startProteumMachineMcpRouter({
14
131
  version: String(cli.packageJson.version || ''),
15
132
  });
16
133
  };
@@ -7,6 +7,7 @@ import cli from '..';
7
7
  import Compiler from '../compiler';
8
8
  import { readProteumManifest } from '../compiler/common/proteumManifest';
9
9
  import { buildOrientationResponse, type TOrientResponse } from '@common/dev/inspection';
10
+ import { resolveTriggeredInstructionReads } from '@common/dev/mcpPayloads';
10
11
  import type { TProteumManifest } from '@common/dev/proteumManifest';
11
12
  import { compactList, printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
12
13
 
@@ -125,6 +126,7 @@ const renderHuman = (response: TOrientResponse) =>
125
126
  ...(response.app.routerPort ? [`- routerPort=${response.app.routerPort}`] : []),
126
127
  'Guidance',
127
128
  `- agents=${response.guidance.agents}`,
129
+ `- documentation=${response.guidance.documentation}`,
128
130
  `- diagnostics=${response.guidance.diagnostics}`,
129
131
  `- optimizations=${response.guidance.optimizations}`,
130
132
  `- codingStyle=${response.guidance.codingStyle}`,
@@ -163,23 +165,48 @@ const compactOwnerMatch = (match: TOrientResponse['owner']['matches'][number]) =
163
165
  source: match.source,
164
166
  });
165
167
 
166
- const buildInstructionPlan = (response: TOrientResponse) => ({
167
- mustRead: [...new Set([response.guidance.agents, ...response.guidance.areaAgents])],
168
- readWhen: [
169
- {
170
- file: response.guidance.diagnostics,
171
- when: 'Read only for raw errors, failing requests, traces, perf regressions, or reproduction work.',
172
- },
173
- {
174
- file: response.guidance.codingStyle,
175
- when: 'Read before editing implementation files.',
176
- },
177
- {
178
- file: response.guidance.optimizations,
179
- when: 'Read after client-side implementation or when the task explicitly concerns packages, build, runtime, or performance.',
180
- },
181
- ],
182
- });
168
+ const buildInstructionPlan = (response: TOrientResponse) => {
169
+ const triggered = resolveTriggeredInstructionReads({
170
+ codingStyle: response.guidance.codingStyle,
171
+ diagnostics: response.guidance.diagnostics,
172
+ documentation: response.guidance.documentation,
173
+ optimizations: response.guidance.optimizations,
174
+ query: response.normalizedQuery || response.query,
175
+ rootAgentsFile:
176
+ response.app.repoRoot !== response.app.appRoot
177
+ ? path.join(response.app.repoRoot, 'AGENTS.md')
178
+ : response.guidance.agents,
179
+ });
180
+
181
+ return {
182
+ mustRead: [
183
+ ...new Set([
184
+ response.guidance.agents,
185
+ ...response.guidance.areaAgents,
186
+ ...triggered.map((entry) => entry.file),
187
+ ]),
188
+ ],
189
+ triggered,
190
+ readWhen: [
191
+ {
192
+ file: response.guidance.documentation,
193
+ when: 'Read before non-trivial coding tasks to choose the smallest `/docs` pack and update docs after changes.',
194
+ },
195
+ {
196
+ file: response.guidance.diagnostics,
197
+ when: 'Read only for raw errors, failing requests, traces, perf regressions, or reproduction work.',
198
+ },
199
+ {
200
+ file: response.guidance.codingStyle,
201
+ when: 'Read before editing implementation files.',
202
+ },
203
+ {
204
+ file: response.guidance.optimizations,
205
+ when: 'Read after client-side implementation or when the task explicitly concerns packages, build, runtime, or performance.',
206
+ },
207
+ ],
208
+ };
209
+ };
183
210
 
184
211
  const printCompactOrient = (response: TOrientResponse) => {
185
212
  const topOwner = response.owner.matches[0];
@@ -5,7 +5,8 @@ import { UsageError } from 'clipanion';
5
5
 
6
6
  import cli from '..';
7
7
  import { readProteumManifest } from '../compiler/common/proteumManifest';
8
- import { listDevSessionInspections, type TDevSessionInspection } from '../runtime/devSessions';
8
+ import { listDevSessionInspections, writeMachineDevSessionRecord, type TDevSessionInspection } from '../runtime/devSessions';
9
+ import { inspectDevPort, type TDevPortInspection } from '../runtime/ports';
9
10
  import { printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
10
11
  import type { TDoctorResponse } from '@common/dev/diagnostics';
11
12
  import type { TProteumManifest } from '@common/dev/proteumManifest';
@@ -40,6 +41,11 @@ const getSessionUrl = (inspection: TDevSessionInspection) => {
40
41
  return `http://localhost:${inspection.record.routerPort}`;
41
42
  };
42
43
 
44
+ const getSessionMcpUrl = (inspection: TDevSessionInspection) => {
45
+ const sessionUrl = getSessionUrl(inspection);
46
+ return sessionUrl ? `${sessionUrl}/__proteum/mcp` : '';
47
+ };
48
+
43
49
  const probeDoctor = async (baseUrl: string) => {
44
50
  if (!baseUrl) return { reachable: false, error: 'No dev URL is registered.' };
45
51
 
@@ -78,11 +84,90 @@ const compactSession = (inspection: TDevSessionInspection) => ({
78
84
  pid: inspection.record?.pid,
79
85
  routerPort: inspection.record?.routerPort,
80
86
  publicUrl: inspection.record?.publicUrl,
87
+ mcpUrl: inspection.record ? getSessionMcpUrl(inspection) : undefined,
81
88
  state: inspection.record?.state,
82
89
  startedAt: inspection.record?.startedAt,
83
90
  updatedAt: inspection.record?.updatedAt,
84
91
  });
85
92
 
93
+ const createStartDevCommand = (port?: number) =>
94
+ `proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${port || '<free-port>'}`;
95
+
96
+ const describePortOwner = (portInspection?: TDevPortInspection) => {
97
+ if (!portInspection || portInspection.router.available) return '';
98
+ if (portInspection.router.proteum) {
99
+ const appLabel =
100
+ portInspection.router.app?.identifier ||
101
+ portInspection.router.app?.name ||
102
+ portInspection.router.app?.appRoot ||
103
+ 'another Proteum app';
104
+ return `Configured router port ${portInspection.router.port} is already occupied by ${appLabel}.`;
105
+ }
106
+
107
+ return `Configured router port ${portInspection.router.port} is already occupied by a non-Proteum or unrecognized process.`;
108
+ };
109
+
110
+ const getNextActions = ({
111
+ health,
112
+ portInspection,
113
+ selectedSession,
114
+ }: {
115
+ health: { reachable: boolean };
116
+ portInspection?: TDevPortInspection;
117
+ selectedSession: TDevSessionInspection | undefined;
118
+ }) => {
119
+ if (!selectedSession?.record || !selectedSession.live) {
120
+ const portOwner = describePortOwner(portInspection);
121
+
122
+ if (portInspection?.router.proteum && portInspection.router.matchesApp) {
123
+ return [
124
+ {
125
+ label: 'Use Existing Runtime',
126
+ command: `proteum diagnose ${quoteCommandArgument('/')} --port ${portInspection.router.port}`,
127
+ reason:
128
+ 'A Proteum runtime for this app already responds on the configured router port, but no tracked session file is live. Do not start a second dev server; use this port for CLI evidence or stop the owning process before starting a tracked session.',
129
+ },
130
+ ];
131
+ }
132
+
133
+ const startPort =
134
+ portInspection && !portInspection.canStartOnConfiguredPort ? portInspection.recommendedPort : portInspection?.router.port;
135
+
136
+ return [
137
+ {
138
+ label: 'Start Dev',
139
+ command: createStartDevCommand(startPort),
140
+ reason: portOwner
141
+ ? `${portOwner} Use an alternate free router/HMR port pair; do not probe page bodies to identify port owners.`
142
+ : 'Create a tracked dev session before request-time diagnostics.',
143
+ },
144
+ ];
145
+ }
146
+
147
+ if (!health.reachable) {
148
+ return [
149
+ {
150
+ label: 'Stop Unreachable Dev',
151
+ command: `proteum dev stop --session-file ${quoteCommandArgument(selectedSession.sessionFilePath)}`,
152
+ reason: 'A tracked session exists but the runtime and MCP endpoint are unreachable.',
153
+ },
154
+ {
155
+ label: 'Start Dev',
156
+ command: createStartDevCommand(portInspection?.recommendedPort),
157
+ reason: 'Start a fresh tracked session after stopping the unreachable one.',
158
+ },
159
+ ];
160
+ }
161
+
162
+ return [
163
+ {
164
+ label: 'Diagnose Root',
165
+ command: `proteum diagnose ${quoteCommandArgument('/')} --port ${selectedSession.record.routerPort}`,
166
+ reason: 'Use the selected runtime for the smallest request-level diagnostic pass.',
167
+ },
168
+ ];
169
+ };
170
+
86
171
  export const run = async () => {
87
172
  const action = getAction();
88
173
  if (action !== 'status') return;
@@ -93,10 +178,21 @@ export const run = async () => {
93
178
  sessionFilePath: typeof cli.args.sessionFile === 'string' && cli.args.sessionFile ? cli.args.sessionFile : undefined,
94
179
  });
95
180
  const liveSessions = sessions.filter((inspection) => inspection.live && inspection.record);
181
+ await Promise.allSettled(
182
+ liveSessions.map((inspection) =>
183
+ inspection.record ? writeMachineDevSessionRecord(inspection.record) : Promise.resolve(undefined),
184
+ ),
185
+ );
96
186
  const selectedSession =
97
187
  liveSessions.find((inspection) => inspection.record?.state === 'ready') || liveSessions[0] || sessions.find((inspection) => inspection.record);
98
188
  const selectedBaseUrl = selectedSession ? getSessionUrl(selectedSession) : '';
99
189
  const health = selectedSession && selectedSession.live ? await probeDoctor(selectedBaseUrl) : { reachable: false, error: 'No live tracked dev session.' };
190
+ const configuredDevPort = manifest
191
+ ? await inspectDevPort({
192
+ appRoot: cli.paths.appRoot,
193
+ port: manifest.env.resolved.routerPort,
194
+ })
195
+ : undefined;
100
196
 
101
197
  const payload = {
102
198
  appRoot: cli.paths.appRoot,
@@ -119,6 +215,7 @@ export const run = async () => {
119
215
  selected: selectedSession ? compactSession(selectedSession) : undefined,
120
216
  sessions: sessions.map(compactSession),
121
217
  health,
218
+ configuredDevPort,
122
219
  };
123
220
 
124
221
  if (cli.args.full === true) {
@@ -129,23 +226,9 @@ export const run = async () => {
129
226
  printAgentResponse({
130
227
  summary: selectedSession
131
228
  ? `${selectedSession.live ? 'live' : 'stale'} dev session on ${selectedSession.record?.routerPort || 'unknown port'}; health=${health.reachable ? 'reachable' : 'unreachable'}`
132
- : 'No tracked Proteum dev session found.',
229
+ : describePortOwner(configuredDevPort) || 'No tracked Proteum dev session found.',
133
230
  data: payload,
134
- nextActions: selectedSession?.record
135
- ? [
136
- {
137
- label: 'Diagnose Root',
138
- command: `proteum diagnose ${quoteCommandArgument('/')} --port ${selectedSession.record.routerPort}`,
139
- reason: 'Use the selected runtime for the smallest request-level diagnostic pass.',
140
- },
141
- ]
142
- : [
143
- {
144
- label: 'Start Dev',
145
- command: 'proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port <free-port>',
146
- reason: 'Create a tracked dev session before request-time diagnostics.',
147
- },
148
- ],
231
+ nextActions: getNextActions({ health, portInspection: configuredDevPort, selectedSession }),
149
232
  fullDetailCommand: 'proteum runtime status --full',
150
233
  });
151
234
  };