proteum 2.2.9 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/AGENTS.md +10 -4
  2. package/README.md +58 -15
  3. package/agents/project/AGENTS.md +53 -10
  4. package/agents/project/DOCUMENTATION.md +1326 -0
  5. package/agents/project/app-root/AGENTS.md +2 -2
  6. package/agents/project/diagnostics.md +12 -7
  7. package/agents/project/optimizations.md +1 -0
  8. package/agents/project/root/AGENTS.md +24 -9
  9. package/agents/project/tests/AGENTS.md +7 -0
  10. package/agents/project/tests/e2e/AGENTS.md +13 -0
  11. package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
  12. package/cli/commands/connect.ts +40 -4
  13. package/cli/commands/dev.ts +148 -25
  14. package/cli/commands/diagnose.ts +138 -5
  15. package/cli/commands/doctor.ts +24 -4
  16. package/cli/commands/explain.ts +134 -6
  17. package/cli/commands/mcp.ts +133 -0
  18. package/cli/commands/orient.ts +93 -3
  19. package/cli/commands/perf.ts +118 -13
  20. package/cli/commands/runtime.ts +234 -0
  21. package/cli/commands/trace.ts +116 -21
  22. package/cli/mcp/router.ts +1010 -0
  23. package/cli/presentation/commands.ts +93 -26
  24. package/cli/presentation/devSession.ts +2 -0
  25. package/cli/presentation/help.ts +1 -1
  26. package/cli/runtime/commands.ts +215 -24
  27. package/cli/runtime/devSessions.ts +328 -2
  28. package/cli/runtime/mcpDaemon.ts +288 -0
  29. package/cli/runtime/ports.ts +151 -0
  30. package/cli/utils/agentOutput.ts +46 -0
  31. package/cli/utils/agents.ts +194 -51
  32. package/cli/utils/appRoots.ts +232 -0
  33. package/common/dev/diagnostics.ts +1 -1
  34. package/common/dev/inspection.ts +22 -7
  35. package/common/dev/mcpPayloads.ts +1150 -0
  36. package/common/dev/mcpServer.ts +287 -0
  37. package/docs/agent-routing.md +137 -0
  38. package/docs/dev-commands.md +2 -0
  39. package/docs/dev-sessions.md +4 -1
  40. package/docs/diagnostics.md +70 -24
  41. package/docs/mcp.md +206 -0
  42. package/docs/migrate-from-2.1.3.md +14 -6
  43. package/docs/request-tracing.md +12 -6
  44. package/package.json +11 -3
  45. package/server/app/devMcp.ts +204 -0
  46. package/server/services/router/http/cache.ts +116 -0
  47. package/server/services/router/http/index.ts +94 -35
  48. package/server/services/router/index.ts +8 -11
  49. package/server/services/router/request/ip.test.cjs +0 -1
  50. package/tests/agents-utils.test.cjs +92 -14
  51. package/tests/cli-mcp-command.test.cjs +262 -0
  52. package/tests/codex-mcp-usage.test.cjs +307 -0
  53. package/tests/dev-sessions.test.cjs +113 -0
  54. package/tests/dev-transpile-watch.test.cjs +117 -9
  55. package/tests/eslint-rules.test.cjs +0 -1
  56. package/tests/inspection.test.cjs +66 -0
  57. package/tests/mcp.test.cjs +873 -0
  58. package/tests/router-cache-config.test.cjs +73 -0
  59. package/vitest.config.mjs +9 -0
@@ -28,19 +28,24 @@ import { clearInteractiveConsole } from '../presentation/welcome';
28
28
  import { renderWarning } from '../presentation/ink';
29
29
  import {
30
30
  createDevSessionRecord,
31
- inspectDevSessionFile,
32
31
  listDevSessionInspections,
32
+ prepareDevSessionStart,
33
33
  removeDevSessionRecord,
34
34
  removeDevSessionRecordSync,
35
35
  resolveDevSessionFilePath,
36
36
  stopDevSessionFile,
37
37
  updateDevSessionRecord,
38
+ writeDevSessionRecord,
39
+ writeMachineDevSessionRecord,
38
40
  type TDevSessionInspection,
39
41
  type TStopDevSessionResult,
40
42
  } from '../runtime/devSessions';
41
43
  import { resolveFrameworkInstallInfo } from '../paths';
42
44
  import { logVerbose } from '../runtime/verbose';
45
+ import { ensureMachineMcpDaemonProcess } from '../runtime/mcpDaemon';
46
+ import { inspectDevPort, type TDevPortInspection } from '../runtime/ports';
43
47
  import { configureProjectAgentInstructions, resolveProjectAgentMonorepoRoot } from '../utils/agents';
48
+ import { quoteCommandArgument } from '../utils/agentOutput';
44
49
 
45
50
  // Core
46
51
  import { app, App } from '../app';
@@ -335,6 +340,40 @@ const describeStopResult = (result: TStopDevSessionResult) => {
335
340
  .join(' | ');
336
341
  };
337
342
 
343
+ const describeBlockingDevSession = (inspection: TDevSessionInspection) => {
344
+ if (!inspection.record) {
345
+ return [
346
+ '- invalid session',
347
+ inspection.sessionFilePath,
348
+ inspection.parseError || 'Unreadable session file.',
349
+ ]
350
+ .filter(Boolean)
351
+ .join(' | ');
352
+ }
353
+
354
+ const publicUrl = inspection.record.publicUrl || `http://localhost:${inspection.record.routerPort}`;
355
+
356
+ return [
357
+ `- pid ${inspection.record.pid}`,
358
+ `port ${inspection.record.routerPort}`,
359
+ publicUrl,
360
+ `session ${inspection.sessionFilePath}`,
361
+ ].join(' | ');
362
+ };
363
+
364
+ const createBlockingDevSessionMessage = (blocking: TDevSessionInspection[]) => {
365
+ const firstSessionFilePath = blocking[0]?.sessionFilePath || '<session-file>';
366
+
367
+ return [
368
+ `A Proteum dev session is already running for ${app.paths.root}.`,
369
+ 'Stop the existing session before starting another server in the same worktree:',
370
+ ...blocking.map(describeBlockingDevSession),
371
+ '',
372
+ `Run: npx proteum dev stop --session-file ${quoteCommandArgument(firstSessionFilePath)}`,
373
+ 'Then start dev again with the intended session file and port.',
374
+ ].join('\n');
375
+ };
376
+
338
377
  const printJson = (payload: unknown) => {
339
378
  process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
340
379
  };
@@ -400,38 +439,120 @@ const runStopCommand = async () => {
400
439
 
401
440
  const ensureDevSessionSlot = async () => {
402
441
  const sessionFilePath = getResolvedDevSessionFilePath();
403
- const existingInspection = await inspectDevSessionFile(sessionFilePath);
404
-
405
- if (existingInspection?.record && existingInspection.live && existingInspection.record.pid !== process.pid) {
406
- if (cli.args.replaceExisting !== true) {
407
- throw new Error(
408
- `A Proteum dev session is already registered at ${sessionFilePath} (pid ${existingInspection.record.pid}, port ${existingInspection.record.routerPort}). ` +
409
- 'Use `proteum dev stop` or restart with `proteum dev --replace-existing`.',
410
- );
411
- }
442
+ const startPreparation = await prepareDevSessionStart({
443
+ appRoot: app.paths.root,
444
+ replaceExisting: cli.args.replaceExisting === true,
445
+ sessionFilePath,
446
+ });
412
447
 
413
- const stopResult = await stopDevSessionFile(sessionFilePath);
414
- if (!stopResult.stopped) {
415
- throw new Error(`Could not stop the existing Proteum dev session registered at ${sessionFilePath}.`);
416
- }
417
- } else if (existingInspection) {
418
- await stopDevSessionFile(sessionFilePath);
448
+ if (startPreparation.blocking.length > 0) {
449
+ throw new Error(createBlockingDevSessionMessage(startPreparation.blocking));
419
450
  }
420
451
 
421
452
  currentDevSessionFilePath = sessionFilePath;
422
453
  registerDevSessionExitCleanup();
423
- await fs.ensureDir(path.dirname(sessionFilePath));
424
- await fs.writeJson(
454
+ const sessionRecord = createDevSessionRecord({
455
+ appRoot: app.paths.root,
456
+ port: app.env.router.port,
425
457
  sessionFilePath,
426
- createDevSessionRecord({
427
- appRoot: app.paths.root,
428
- port: app.env.router.port,
429
- sessionFilePath,
430
- }),
431
- { spaces: 2 },
432
- );
458
+ });
459
+
460
+ await writeDevSessionRecord(sessionRecord);
461
+ await writeMachineDevSessionRecord(sessionRecord);
433
462
 
434
463
  logVerbose(`Registered Proteum dev session at ${sessionFilePath}.`);
464
+ if (startPreparation.cleaned.length > 0) {
465
+ logVerbose(
466
+ `Cleaned ${startPreparation.cleaned.length} stale Proteum dev session file${startPreparation.cleaned.length === 1 ? '' : 's'}.`,
467
+ );
468
+ }
469
+ if (startPreparation.replaced) {
470
+ logVerbose(`Replaced Proteum dev session at ${startPreparation.replaced.sessionFilePath}.`);
471
+ }
472
+ };
473
+
474
+ const ensureMachineMcpDaemonForDev = async () => {
475
+ try {
476
+ const result = await ensureMachineMcpDaemonProcess({ coreRoot: cli.paths.core.root });
477
+ const record = result.inspection.record;
478
+ if (!record) return;
479
+
480
+ logVerbose(
481
+ result.started
482
+ ? `Started Proteum machine MCP daemon at ${record.mcpUrl}.`
483
+ : `Proteum machine MCP daemon already running at ${record.mcpUrl}.`,
484
+ );
485
+ } catch (error) {
486
+ console.warn(
487
+ `Warning: Proteum could not ensure the machine MCP daemon. ${
488
+ error instanceof Error ? error.message : String(error)
489
+ }`,
490
+ );
491
+ }
492
+ };
493
+
494
+ const describeDevPortBlocker = (inspection: TDevPortInspection) => {
495
+ const lines = [
496
+ `Proteum cannot start this dev server on port ${inspection.router.port}.`,
497
+ `Router port ${inspection.router.port}: ${inspection.router.available ? 'free' : 'occupied'}.`,
498
+ `HMR port ${inspection.hmr.port}: ${inspection.hmr.available ? 'free' : 'occupied'}.`,
499
+ ];
500
+
501
+ if (!inspection.router.available && inspection.router.proteum && inspection.router.matchesApp) {
502
+ lines.push(
503
+ `The router port is already serving this Proteum app${inspection.router.app?.appRoot ? ` from ${inspection.router.app.appRoot}` : ''}.`,
504
+ );
505
+ lines.push('Next action: run `npx proteum runtime status`, use or stop the existing runtime, then start one tracked dev session.');
506
+ lines.push('Do not start a second dev server for the same worktree.');
507
+ } else if (!inspection.router.available && inspection.router.proteum) {
508
+ lines.push(
509
+ `The router port is already serving ${inspection.router.app?.identifier || inspection.router.app?.name || 'another Proteum app'}${inspection.router.app?.appRoot ? ` from ${inspection.router.app.appRoot}` : ''}.`,
510
+ );
511
+ lines.push(
512
+ inspection.recommendedPort
513
+ ? `Next action: npx proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${inspection.recommendedPort}`
514
+ : 'Next action: choose a free router/HMR port pair, then rerun proteum dev with --port <free-port>.',
515
+ );
516
+ } else if (!inspection.router.available) {
517
+ lines.push('The router port is occupied by a non-Proteum or unrecognized process.');
518
+ lines.push(
519
+ inspection.recommendedPort
520
+ ? `Next action: npx proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${inspection.recommendedPort}`
521
+ : 'Next action: choose a free router/HMR port pair, then rerun proteum dev with --port <free-port>.',
522
+ );
523
+ } else if (!inspection.hmr.available) {
524
+ lines.push('The HMR port is occupied.');
525
+ lines.push(
526
+ inspection.recommendedPort
527
+ ? `Next action: npx proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${inspection.recommendedPort}`
528
+ : 'Next action: choose a free router/HMR port pair, then rerun proteum dev with --port <free-port>.',
529
+ );
530
+ }
531
+
532
+ lines.push('Do not inspect page bodies to identify the owner; use `npx proteum runtime status` for compact port/runtime state.');
533
+
534
+ return lines.join('\n');
535
+ };
536
+
537
+ const ensureConfiguredDevPortsAvailable = async () => {
538
+ const inspection = await inspectDevPort({
539
+ appRoot: app.paths.root,
540
+ port: app.env.router.port,
541
+ });
542
+
543
+ if (inspection.canStartOnConfiguredPort) return;
544
+
545
+ if (cli.args.replaceExisting === true) {
546
+ const requestedSessionFilePath = getResolvedDevSessionFilePath();
547
+ const [requestedSession] = await listDevSessionInspections({
548
+ appRoot: app.paths.root,
549
+ sessionFilePath: requestedSessionFilePath,
550
+ });
551
+
552
+ if (requestedSession?.record && requestedSession.live) return;
553
+ }
554
+
555
+ throw new Error(describeDevPortBlocker(inspection));
435
556
  };
436
557
 
437
558
  async function startApp(app: App) {
@@ -757,8 +878,10 @@ const createIndexedSourceWatching = ({
757
878
 
758
879
  const runDevLoop = async () => {
759
880
  devSessionStopping = false;
881
+ await ensureConfiguredDevPortsAvailable();
760
882
  clearInteractiveConsole();
761
883
  await ensureDevSessionSlot();
884
+ await ensureMachineMcpDaemonForDev();
762
885
  const proteumInstall = resolveFrameworkInstallInfo({
763
886
  appRoot: app.paths.root,
764
887
  framework: cli.paths.framework,
@@ -12,6 +12,7 @@ import type { TRequestTraceErrorResponse, TRequestTraceArmResponse } from '../..
12
12
  import type { TDevSessionErrorResponse, TDevSessionStartResponse } from '../../common/dev/session';
13
13
  import { summarizeTraceForDiagnose } from '@common/dev/inspection';
14
14
  import { readProteumManifest } from '../compiler/common/proteumManifest';
15
+ import { compactList, printAgentResponse, printJson, quoteCommandArgument, truncateForAgent } from '../utils/agentOutput';
15
16
 
16
17
  const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
17
18
  const truncate = (value: string, max = 160) => (value.length <= max ? value : `${value.slice(0, max)}...`);
@@ -186,6 +187,7 @@ const renderOrientation = (response: TDiagnoseResponse) =>
186
187
  : [
187
188
  'Orientation',
188
189
  `- agents=${response.orientation.guidance.agents}`,
190
+ `- documentation=${response.orientation.guidance.documentation}`,
189
191
  `- diagnostics=${response.orientation.guidance.diagnostics}`,
190
192
  `- optimizations=${response.orientation.guidance.optimizations}`,
191
193
  `- codingStyle=${response.orientation.guidance.codingStyle}`,
@@ -237,6 +239,133 @@ const renderHuman = (manifest: ReturnType<typeof readProteumManifest>, response:
237
239
  renderLogs(response.serverLogs),
238
240
  ].join('\n');
239
241
 
242
+ const compactOwnerMatch = (match: TDiagnoseResponse['owner']['matches'][number]) => ({
243
+ kind: match.kind,
244
+ label: match.label,
245
+ score: match.score,
246
+ scope: match.scopeLabel,
247
+ origin: match.originHint,
248
+ source: match.source,
249
+ });
250
+
251
+ const compactDiagnostic = (diagnostic: TDiagnoseResponse['doctor']['diagnostics'][number]) => ({
252
+ level: diagnostic.level,
253
+ code: diagnostic.code,
254
+ message: truncateForAgent(diagnostic.message),
255
+ filepath: diagnostic.filepath,
256
+ sourceLocation: diagnostic.sourceLocation,
257
+ fixHint: diagnostic.fixHint ? truncateForAgent(diagnostic.fixHint) : undefined,
258
+ });
259
+
260
+ const compactRequest = (request: TDiagnoseResponse['request']) =>
261
+ request
262
+ ? {
263
+ id: request.id,
264
+ method: request.method,
265
+ path: request.path,
266
+ statusCode: request.statusCode,
267
+ durationMs: request.durationMs,
268
+ capture: request.capture,
269
+ user: request.user,
270
+ errorMessage: request.errorMessage,
271
+ counts: {
272
+ calls: request.calls.length,
273
+ events: request.events.length,
274
+ sqlQueries: request.sqlQueries.length,
275
+ droppedEvents: request.droppedEvents,
276
+ },
277
+ }
278
+ : undefined;
279
+
280
+ const buildDiagnoseFullDetailCommand = ({
281
+ hitPath,
282
+ query,
283
+ }: {
284
+ hitPath: string;
285
+ query: string;
286
+ }) =>
287
+ [
288
+ 'proteum diagnose',
289
+ quoteCommandArgument(query),
290
+ hitPath ? `--hit ${quoteCommandArgument(hitPath)}` : '',
291
+ typeof cli.args.port === 'string' && cli.args.port ? `--port ${cli.args.port}` : '',
292
+ typeof cli.args.url === 'string' && cli.args.url ? `--url ${quoteCommandArgument(cli.args.url)}` : '',
293
+ '--full',
294
+ ]
295
+ .filter(Boolean)
296
+ .join(' ');
297
+
298
+ const printCompactDiagnose = ({
299
+ hitPath,
300
+ query,
301
+ response,
302
+ }: {
303
+ hitPath: string;
304
+ query: string;
305
+ response: TDiagnoseResponse;
306
+ }) => {
307
+ const request = compactRequest(response.request);
308
+ const traceSummary = summarizeTraceForDiagnose(response.request);
309
+ const doctorSummary = `${response.doctor.summary.errors} doctor errors/${response.doctor.summary.warnings} warnings`;
310
+ const contractsSummary = `${response.contracts.summary.errors} contract errors/${response.contracts.summary.warnings} warnings`;
311
+ const summary = `${response.query || query || 'request'}: ${traceSummary}; ${doctorSummary}; ${contractsSummary}`;
312
+ const nextActions = response.orientation?.nextSteps || [];
313
+ const requestId = response.request?.id;
314
+
315
+ printAgentResponse({
316
+ summary,
317
+ data: {
318
+ query: response.query,
319
+ request,
320
+ owner: {
321
+ top: response.owner.matches[0] ? compactOwnerMatch(response.owner.matches[0]) : undefined,
322
+ matches: compactList(response.owner.matches, 4).map(compactOwnerMatch),
323
+ totalReturned: response.owner.matches.length,
324
+ },
325
+ suspects: compactList(response.suspects, 6),
326
+ chain: compactList(response.chain || [], 8),
327
+ diagnostics: {
328
+ doctor: {
329
+ summary: response.doctor.summary,
330
+ top: compactList(response.doctor.diagnostics, 6).map(compactDiagnostic),
331
+ total: response.doctor.diagnostics.length,
332
+ },
333
+ contracts: {
334
+ summary: response.contracts.summary,
335
+ top: compactList(response.contracts.diagnostics, 6).map(compactDiagnostic),
336
+ total: response.contracts.diagnostics.length,
337
+ },
338
+ },
339
+ logs: compactList(response.serverLogs.logs, 10).map((entry) => ({
340
+ level: entry.level,
341
+ time: entry.time,
342
+ text: truncateForAgent(entry.text),
343
+ })),
344
+ instructions: response.orientation
345
+ ? {
346
+ mustRead: [...new Set([response.orientation.guidance.agents, ...response.orientation.guidance.areaAgents])],
347
+ documentation: response.orientation.guidance.documentation,
348
+ diagnostics: response.orientation.guidance.diagnostics,
349
+ codingStyle: response.orientation.guidance.codingStyle,
350
+ optimizations: response.orientation.guidance.optimizations,
351
+ }
352
+ : undefined,
353
+ },
354
+ nextActions,
355
+ fullDetailCommand: buildDiagnoseFullDetailCommand({ hitPath, query }),
356
+ omitted: [
357
+ ...(requestId
358
+ ? [
359
+ {
360
+ reason: 'Full request events, API call payloads, and SQL text are omitted from the default diagnose response.',
361
+ command: `proteum trace show ${quoteCommandArgument(requestId)} --events`,
362
+ },
363
+ ]
364
+ : []),
365
+ ],
366
+ });
367
+ };
368
+
240
369
  const resolveManifest = async () => {
241
370
  try {
242
371
  return readProteumManifest(cli.paths.appRoot);
@@ -258,7 +387,6 @@ export const run = async () => {
258
387
  const method = typeof cli.args.method === 'string' && cli.args.method ? cli.args.method.trim().toUpperCase() : 'GET';
259
388
  const logsLevel = typeof cli.args.logsLevel === 'string' && cli.args.logsLevel ? cli.args.logsLevel.trim() : 'warn';
260
389
  const logsLimit = typeof cli.args.logsLimit === 'string' && cli.args.logsLimit ? cli.args.logsLimit.trim() : '40';
261
- const shouldPrintJson = cli.args.json === true;
262
390
  const hitPath = hit || (target.startsWith('/') ? target : '');
263
391
  const query = target || hitPath;
264
392
  let parsedDataJson: unknown;
@@ -308,11 +436,16 @@ export const run = async () => {
308
436
  const diagnose = await requestJson<TDiagnoseResponse>(
309
437
  `/__proteum/diagnose?${new URLSearchParams(diagnoseRequest).toString()}`,
310
438
  );
311
- if (shouldPrintJson) {
312
- console.log(JSON.stringify(diagnose.body, null, 2));
439
+ if (cli.args.full === true) {
440
+ printJson(diagnose.body);
441
+ return;
442
+ }
443
+
444
+ if (cli.args.human === true) {
445
+ const manifest = await resolveManifest();
446
+ console.log(renderHuman(manifest, diagnose.body));
313
447
  return;
314
448
  }
315
449
 
316
- const manifest = await resolveManifest();
317
- console.log(renderHuman(manifest, diagnose.body));
450
+ printCompactDiagnose({ hitPath, query, response: diagnose.body });
318
451
  };
@@ -3,8 +3,9 @@ import Compiler from '../compiler';
3
3
  import { readProteumManifest } from '../compiler/common/proteumManifest';
4
4
  import { buildContractsDoctorResponse } from '@common/dev/contractsDoctor';
5
5
  import { buildDoctorResponse, renderDoctorHuman, renderDoctorResponseHuman } from '@common/dev/diagnostics';
6
+ import { compactList, printAgentResponse, printJson, truncateForAgent } from '../utils/agentOutput';
6
7
 
7
- const allowedDoctorArgs = new Set(['contracts', 'json', 'strict']);
8
+ const allowedDoctorArgs = new Set(['contracts', 'full', 'human', 'json', 'strict']);
8
9
 
9
10
  const validateDoctorArgs = () => {
10
11
  const enabledArgs = Object.entries(cli.args)
@@ -20,6 +21,15 @@ const validateDoctorArgs = () => {
20
21
  }
21
22
  };
22
23
 
24
+ const compactDiagnostic = (diagnostic: ReturnType<typeof buildDoctorResponse>['diagnostics'][number]) => ({
25
+ level: diagnostic.level,
26
+ code: diagnostic.code,
27
+ message: truncateForAgent(diagnostic.message),
28
+ filepath: diagnostic.filepath,
29
+ sourceLocation: diagnostic.sourceLocation,
30
+ fixHint: diagnostic.fixHint ? truncateForAgent(diagnostic.fixHint) : undefined,
31
+ });
32
+
23
33
  export const run = async (): Promise<void> => {
24
34
  validateDoctorArgs();
25
35
 
@@ -32,9 +42,9 @@ export const run = async (): Promise<void> => {
32
42
  ? buildContractsDoctorResponse(manifest, cli.args.strict === true)
33
43
  : buildDoctorResponse(manifest, cli.args.strict === true);
34
44
 
35
- if (cli.args.json === true) {
36
- console.log(JSON.stringify(response, null, 2));
37
- } else {
45
+ if (cli.args.full === true) {
46
+ printJson(response);
47
+ } else if (cli.args.human === true) {
38
48
  console.log(
39
49
  cli.args.contracts === true
40
50
  ? renderDoctorResponseHuman({
@@ -45,6 +55,16 @@ export const run = async (): Promise<void> => {
45
55
  })
46
56
  : renderDoctorHuman(manifest, cli.args.strict === true),
47
57
  );
58
+ } else {
59
+ printAgentResponse({
60
+ summary: `${cli.args.contracts === true ? 'Doctor contracts' : 'Doctor'}: ${response.summary.errors} errors, ${response.summary.warnings} warnings`,
61
+ data: {
62
+ summary: response.summary,
63
+ diagnostics: compactList(response.diagnostics, 10).map(compactDiagnostic),
64
+ totalDiagnostics: response.diagnostics.length,
65
+ },
66
+ fullDetailCommand: `proteum doctor${cli.args.contracts === true ? ' --contracts' : ''} --full`,
67
+ });
48
68
  }
49
69
 
50
70
  if (cli.args.strict === true && response.diagnostics.length > 0) {
@@ -2,14 +2,16 @@ import cli from '..';
2
2
  import Compiler from '../compiler';
3
3
  import { readProteumManifest } from '../compiler/common/proteumManifest';
4
4
  import {
5
+ buildExplainSummaryItems,
5
6
  explainSectionNames,
6
7
  pickExplainManifestSections,
7
8
  renderExplainHuman,
8
9
  type TExplainSectionName,
9
10
  } from '@common/dev/diagnostics';
10
11
  import { explainOwner } from '@common/dev/inspection';
12
+ import { compactList, printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
11
13
 
12
- const allowedExplainArgs = new Set(['json', 'all', ...explainSectionNames]);
14
+ const allowedExplainArgs = new Set(['json', 'all', 'full', 'human', 'manifest', ...explainSectionNames]);
13
15
 
14
16
  const validateExplainArgs = () => {
15
17
  const enabledArgs = Object.entries(cli.args)
@@ -31,6 +33,122 @@ const getSelectedSections = (): TExplainSectionName[] => {
31
33
  return explainSectionNames.filter((sectionName) => cli.args[sectionName] === true);
32
34
  };
33
35
 
36
+ const compactOwnerMatch = (match: ReturnType<typeof explainOwner>['matches'][number]) => ({
37
+ kind: match.kind,
38
+ label: match.label,
39
+ score: match.score,
40
+ scope: match.scopeLabel,
41
+ origin: match.originHint,
42
+ source: match.source,
43
+ });
44
+
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
+ };
69
+
70
+ const printCompactOwner = (ownerQuery: string, response: ReturnType<typeof explainOwner>) => {
71
+ const topOwner = response.matches[0];
72
+
73
+ printAgentResponse({
74
+ summary: topOwner
75
+ ? `${ownerQuery} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
76
+ : `${ownerQuery} -> no manifest owner matched`,
77
+ data: {
78
+ query: ownerQuery,
79
+ normalizedQuery: response.normalizedQuery,
80
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
81
+ matches: compactList(response.matches, 6).map(compactOwnerMatch),
82
+ totalReturned: response.matches.length,
83
+ },
84
+ nextActions: [
85
+ {
86
+ label: 'Orient',
87
+ command: `proteum orient ${quoteCommandArgument(ownerQuery)}`,
88
+ reason: 'Resolve the owner together with the relevant instruction files and next command.',
89
+ },
90
+ ],
91
+ fullDetailCommand: `proteum explain owner ${quoteCommandArgument(ownerQuery)} --full`,
92
+ });
93
+ };
94
+
95
+ const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>, selectedSections: TExplainSectionName[] = []) => {
96
+ const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
97
+ const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
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';
101
+
102
+ printAgentResponse({
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`,
106
+ data: {
107
+ app: {
108
+ root: manifest.app.root,
109
+ coreRoot: manifest.app.coreRoot,
110
+ identifier: manifest.app.identity.identifier,
111
+ name: manifest.app.identity.name,
112
+ },
113
+ counts: {
114
+ commands: manifest.commands.length,
115
+ connectedProjects: manifest.connectedProjects.length,
116
+ controllers: manifest.controllers.length,
117
+ diagnostics: manifest.diagnostics.length,
118
+ layouts: manifest.layouts.length,
119
+ routesClient: manifest.routes.client.length,
120
+ routesServer: manifest.routes.server.length,
121
+ servicesApp: manifest.services.app.length,
122
+ servicesRouterPlugins: manifest.services.routerPlugins.length,
123
+ },
124
+ diagnostics: { errors, warnings },
125
+ selectedSections: hasSelectedSections ? selectedSections.map((sectionName) => summarizeSelectedSection(manifest, sectionName)) : undefined,
126
+ env: {
127
+ requiredProvided: requiredEnvProvided,
128
+ requiredTotal: manifest.env.requiredVariables.length,
129
+ routerPort: manifest.env.resolved.routerPort,
130
+ },
131
+ summaryItems: buildExplainSummaryItems(manifest),
132
+ },
133
+ nextActions: [
134
+ {
135
+ label: 'Orient Target',
136
+ command: 'proteum orient <route|file|controller|error>',
137
+ reason: 'Use orient for task-specific owner, instruction, and next-command routing.',
138
+ },
139
+ ],
140
+ fullDetailCommand,
141
+ omitted: [
142
+ {
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,
147
+ },
148
+ ],
149
+ });
150
+ };
151
+
34
152
  export const run = async (): Promise<void> => {
35
153
  validateExplainArgs();
36
154
 
@@ -42,8 +160,13 @@ export const run = async (): Promise<void> => {
42
160
 
43
161
  if (ownerQuery) {
44
162
  const response = explainOwner(manifest, ownerQuery);
45
- if (cli.args.json === true) {
46
- console.log(JSON.stringify(response, null, 2));
163
+ if (cli.args.full === true || cli.args.manifest === true) {
164
+ printJson(response);
165
+ return;
166
+ }
167
+
168
+ if (cli.args.human !== true) {
169
+ printCompactOwner(ownerQuery, response);
47
170
  return;
48
171
  }
49
172
 
@@ -64,10 +187,15 @@ export const run = async (): Promise<void> => {
64
187
 
65
188
  const selectedSections = getSelectedSections();
66
189
 
67
- if (cli.args.json === true) {
68
- console.log(JSON.stringify(pickExplainManifestSections(manifest, selectedSections), null, 2));
190
+ if (hasExplicitDetailSelection()) {
191
+ printJson(pickExplainManifestSections(manifest, cli.args.manifest === true ? [...explainSectionNames] : selectedSections));
192
+ return;
193
+ }
194
+
195
+ if (cli.args.human === true) {
196
+ console.log(renderExplainHuman(manifest, selectedSections));
69
197
  return;
70
198
  }
71
199
 
72
- console.log(renderExplainHuman(manifest, selectedSections));
200
+ printCompactExplain(manifest, selectedSections);
73
201
  };