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
@@ -0,0 +1,151 @@
1
+ import net from 'net';
2
+ import path from 'path';
3
+
4
+ import fs from 'fs-extra';
5
+ import got from 'got';
6
+
7
+ export type TProteumRuntimePortProbe = {
8
+ app?: {
9
+ appRoot?: string;
10
+ identifier?: string;
11
+ name?: string;
12
+ };
13
+ available: boolean;
14
+ error?: string;
15
+ listening: boolean;
16
+ matchesApp: boolean;
17
+ port: number;
18
+ proteum: boolean;
19
+ publicUrl: string;
20
+ statusCode?: number;
21
+ };
22
+
23
+ export type TDevPortInspection = {
24
+ canStartOnConfiguredPort: boolean;
25
+ hmr: {
26
+ available: boolean;
27
+ port: number;
28
+ };
29
+ recommendedPort?: number;
30
+ router: TProteumRuntimePortProbe;
31
+ };
32
+
33
+ const normalizePath = (value: string) => {
34
+ const resolved = path.resolve(value);
35
+
36
+ try {
37
+ return path.normalize(fs.realpathSync(resolved));
38
+ } catch {
39
+ return path.normalize(resolved);
40
+ }
41
+ };
42
+
43
+ export const isTcpPortAvailable = async (port: number) =>
44
+ await new Promise<boolean>((resolve) => {
45
+ const server = net.createServer();
46
+
47
+ server.once('error', () => {
48
+ resolve(false);
49
+ });
50
+
51
+ server.once('listening', () => {
52
+ server.close(() => resolve(true));
53
+ });
54
+
55
+ server.listen(port, '127.0.0.1');
56
+ });
57
+
58
+ export const areTcpPortsAvailable = async (ports: number[]) => {
59
+ const availability = await Promise.all(ports.map((port) => isTcpPortAvailable(port)));
60
+ return availability.every(Boolean);
61
+ };
62
+
63
+ export const findAvailableDevPort = async (startPort: number, { maxOffset = 30 }: { maxOffset?: number } = {}) => {
64
+ const normalizedStartPort = Math.max(1, Math.floor(startPort));
65
+
66
+ for (let port = normalizedStartPort; port <= normalizedStartPort + maxOffset; port += 1) {
67
+ if (await areTcpPortsAvailable([port, port + 1])) return port;
68
+ }
69
+
70
+ return undefined;
71
+ };
72
+
73
+ export const probeProteumRuntimePort = async ({
74
+ appRoot,
75
+ port,
76
+ }: {
77
+ appRoot: string;
78
+ port: number;
79
+ }): Promise<TProteumRuntimePortProbe> => {
80
+ const publicUrl = `http://localhost:${port}`;
81
+ const available = await isTcpPortAvailable(port);
82
+
83
+ if (available) {
84
+ return {
85
+ available: true,
86
+ listening: false,
87
+ matchesApp: false,
88
+ port,
89
+ proteum: false,
90
+ publicUrl,
91
+ };
92
+ }
93
+
94
+ try {
95
+ const response = await got(`http://127.0.0.1:${port}/__proteum/explain?section=app`, {
96
+ responseType: 'json',
97
+ retry: { limit: 0 },
98
+ throwHttpErrors: false,
99
+ timeout: { request: 700 },
100
+ });
101
+ const body = response.body as { app?: { root?: unknown; identity?: { identifier?: unknown; name?: unknown } } };
102
+ const root = typeof body.app?.root === 'string' ? body.app.root : undefined;
103
+ const identifier = typeof body.app?.identity?.identifier === 'string' ? body.app.identity.identifier : undefined;
104
+ const name = typeof body.app?.identity?.name === 'string' ? body.app.identity.name : undefined;
105
+ const proteum = response.statusCode < 400 && Boolean(root || identifier || name);
106
+
107
+ return {
108
+ app: proteum ? { appRoot: root, identifier, name } : undefined,
109
+ available: false,
110
+ error: proteum ? undefined : `Proteum explain endpoint returned HTTP ${response.statusCode}.`,
111
+ listening: true,
112
+ matchesApp: Boolean(root && normalizePath(root) === normalizePath(appRoot)),
113
+ port,
114
+ proteum,
115
+ publicUrl,
116
+ statusCode: response.statusCode,
117
+ };
118
+ } catch (error) {
119
+ return {
120
+ available: false,
121
+ error: error instanceof Error ? error.message : String(error),
122
+ listening: true,
123
+ matchesApp: false,
124
+ port,
125
+ proteum: false,
126
+ publicUrl,
127
+ };
128
+ }
129
+ };
130
+
131
+ export const inspectDevPort = async ({
132
+ appRoot,
133
+ port,
134
+ }: {
135
+ appRoot: string;
136
+ port: number;
137
+ }): Promise<TDevPortInspection> => {
138
+ const router = await probeProteumRuntimePort({ appRoot, port });
139
+ const hmr = {
140
+ port: port + 1,
141
+ available: await isTcpPortAvailable(port + 1),
142
+ };
143
+ const canStartOnConfiguredPort = router.available && hmr.available;
144
+
145
+ return {
146
+ canStartOnConfiguredPort,
147
+ hmr,
148
+ recommendedPort: canStartOnConfiguredPort ? port : await findAvailableDevPort(port + 1),
149
+ router,
150
+ };
151
+ };
@@ -0,0 +1,46 @@
1
+ /*----------------------------------
2
+ - TYPES
3
+ ----------------------------------*/
4
+
5
+ export type TAgentNextAction = {
6
+ command: string;
7
+ label: string;
8
+ reason?: string;
9
+ };
10
+
11
+ export type TAgentOmittedDetail = {
12
+ command: string;
13
+ reason: string;
14
+ };
15
+
16
+ export type TAgentResponse<TData extends object> = {
17
+ ok: true;
18
+ format: 'proteum-agent-v1';
19
+ summary: string;
20
+ data: TData;
21
+ nextActions?: TAgentNextAction[];
22
+ omitted?: TAgentOmittedDetail[];
23
+ fullDetailCommand?: string;
24
+ };
25
+
26
+ /*----------------------------------
27
+ - HELPERS
28
+ ----------------------------------*/
29
+
30
+ export const truncateForAgent = (value: string, max = 220) => (value.length <= max ? value : `${value.slice(0, max)}...`);
31
+
32
+ export const compactList = <TValue>(values: TValue[], limit: number) => values.slice(0, Math.max(0, limit));
33
+
34
+ export const printJson = (value: object) => {
35
+ console.log(JSON.stringify(value, null, 2));
36
+ };
37
+
38
+ export const printAgentResponse = <TData extends object>(response: Omit<TAgentResponse<TData>, 'format' | 'ok'>) => {
39
+ printJson({
40
+ ok: true,
41
+ format: 'proteum-agent-v1',
42
+ ...response,
43
+ });
44
+ };
45
+
46
+ export const quoteCommandArgument = (value: string) => JSON.stringify(value);
@@ -6,12 +6,13 @@
6
6
  import fs from 'fs-extra';
7
7
  import path from 'path';
8
8
  import { logVerbose } from '../runtime/verbose';
9
+ import { createStartDevCommand, findProteumAppRootsUnder, readProteumAppRootSummary } from './appRoots';
9
10
 
10
11
  /*----------------------------------
11
12
  - TYPES
12
13
  ----------------------------------*/
13
14
 
14
- type TProjectInstructionArgs = { coreRoot: string };
15
+ type TProjectInstructionArgs = { appRoot?: string; coreRoot: string; includeMonorepoRegistry?: boolean; monorepoRoot?: string };
15
16
  type TConfigureProjectAgentInstructionsArgs = {
16
17
  appRoot: string;
17
18
  coreRoot: string;
@@ -23,6 +24,7 @@ type TConfigureProjectAgentInstructionsArgs = {
23
24
  type TAgentInstructionDefinition = {
24
25
  projectPath: string;
25
26
  ensureParentDir?: boolean;
27
+ content?: 'router' | 'source';
26
28
  };
27
29
 
28
30
  type TEnsureInstructionFilesResult = {
@@ -60,33 +62,41 @@ const managedInstructionSectionEnd = '<!-- proteum-instructions:end -->';
60
62
  const managedInstructionSectionIntro = 'This section is managed by `proteum configure agents`.';
61
63
 
62
64
  const sharedRootDocumentInstructionDefinitions: TAgentInstructionDefinition[] = [
63
- { projectPath: 'CODING_STYLE.md' },
64
- { projectPath: 'diagnostics.md' },
65
- { projectPath: 'optimizations.md' },
65
+ { projectPath: 'DOCUMENTATION.md', content: 'source' },
66
+ { projectPath: 'CODING_STYLE.md', content: 'source' },
67
+ { projectPath: 'diagnostics.md', content: 'source' },
68
+ { projectPath: 'optimizations.md', content: 'source' },
66
69
  ];
67
70
 
68
- const sharedAreaAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
69
- { projectPath: path.join('client', 'AGENTS.md') },
70
- { projectPath: path.join('client', 'pages', 'AGENTS.md') },
71
- { projectPath: path.join('server', 'services', 'AGENTS.md') },
72
- { projectPath: path.join('server', 'routes', 'AGENTS.md') },
73
- { projectPath: path.join('tests', 'e2e', 'AGENTS.md') },
71
+ const sharedAppAreaAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
72
+ { projectPath: path.join('client', 'AGENTS.md'), content: 'source' },
73
+ { projectPath: path.join('client', 'pages', 'AGENTS.md'), content: 'source' },
74
+ { projectPath: path.join('server', 'services', 'AGENTS.md'), content: 'source' },
75
+ { projectPath: path.join('server', 'routes', 'AGENTS.md'), content: 'source' },
76
+ ];
77
+
78
+ const sharedTestAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
79
+ { projectPath: path.join('tests', 'AGENTS.md'), ensureParentDir: true, content: 'source' },
80
+ { projectPath: path.join('tests', 'e2e', 'AGENTS.md'), ensureParentDir: true, content: 'source' },
81
+ { projectPath: path.join('tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), ensureParentDir: true, content: 'source' },
74
82
  ];
75
83
 
76
84
  const standaloneAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
77
- { projectPath: 'AGENTS.md' },
85
+ { projectPath: 'AGENTS.md', content: 'router' },
78
86
  ...sharedRootDocumentInstructionDefinitions,
79
- ...sharedAreaAgentInstructionDefinitions,
87
+ ...sharedAppAreaAgentInstructionDefinitions,
88
+ ...sharedTestAgentInstructionDefinitions,
80
89
  ];
81
90
 
82
91
  const monorepoAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
83
- { projectPath: 'AGENTS.md' },
84
- ...sharedAreaAgentInstructionDefinitions,
92
+ { projectPath: 'AGENTS.md', content: 'router' },
93
+ ...sharedAppAreaAgentInstructionDefinitions,
85
94
  ];
86
95
 
87
96
  const monorepoRootAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
88
- { projectPath: 'AGENTS.md' },
97
+ { projectPath: 'AGENTS.md', content: 'router' },
89
98
  ...sharedRootDocumentInstructionDefinitions,
99
+ ...sharedTestAgentInstructionDefinitions,
90
100
  ];
91
101
 
92
102
  const legacyProjectInstructionGitignoreBlockStart = '# Proteum-managed instruction symlinks';
@@ -123,7 +133,20 @@ export function configureProjectAgentInstructions({
123
133
  updated: [],
124
134
  updatedGitignores: [],
125
135
  };
126
- const embeddedInstructions = renderEmbeddedProjectInstructions({ coreRoot });
136
+ const appEmbeddedInstructions = renderEmbeddedProjectInstructions({
137
+ appRoot: normalizedAppRoot,
138
+ coreRoot,
139
+ monorepoRoot: normalizedMonorepoRoot,
140
+ });
141
+ const rootEmbeddedInstructions =
142
+ mode === 'monorepo'
143
+ ? renderEmbeddedProjectInstructions({
144
+ appRoot: normalizedAppRoot,
145
+ coreRoot,
146
+ includeMonorepoRegistry: true,
147
+ monorepoRoot: normalizedMonorepoRoot,
148
+ })
149
+ : appEmbeddedInstructions;
127
150
 
128
151
  if (mode === 'monorepo' && normalizedMonorepoRoot) {
129
152
  result.monorepoRoot = normalizedMonorepoRoot;
@@ -134,7 +157,7 @@ export function configureProjectAgentInstructions({
134
157
  rootInstructions,
135
158
  '[agents]',
136
159
  path.join(coreRoot, 'agents', 'project'),
137
- embeddedInstructions,
160
+ rootEmbeddedInstructions,
138
161
  {
139
162
  dryRun,
140
163
  overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
@@ -152,7 +175,7 @@ export function configureProjectAgentInstructions({
152
175
  appInstructions,
153
176
  '[agents]',
154
177
  path.join(coreRoot, 'agents', 'project'),
155
- embeddedInstructions,
178
+ appEmbeddedInstructions,
156
179
  {
157
180
  dryRun,
158
181
  overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
@@ -163,7 +186,7 @@ export function configureProjectAgentInstructions({
163
186
  if (mode === 'monorepo') {
164
187
  const retiredAppRootFiles = removeManagedInstructionFiles(
165
188
  normalizedAppRoot,
166
- sharedRootDocumentInstructionDefinitions,
189
+ [...sharedRootDocumentInstructionDefinitions, ...sharedTestAgentInstructionDefinitions],
167
190
  '[agents]',
168
191
  path.join(coreRoot, 'agents', 'project'),
169
192
  {
@@ -174,7 +197,9 @@ export function configureProjectAgentInstructions({
174
197
  }
175
198
 
176
199
  const appGitignoreCleanupInstructions =
177
- mode === 'monorepo' ? [...appInstructions, ...sharedRootDocumentInstructionDefinitions] : appInstructions;
200
+ mode === 'monorepo'
201
+ ? [...appInstructions, ...sharedRootDocumentInstructionDefinitions, ...sharedTestAgentInstructionDefinitions]
202
+ : appInstructions;
178
203
 
179
204
  if (
180
205
  !dryRun &&
@@ -299,13 +324,21 @@ function ensureInstructionFiles(
299
324
  continue;
300
325
  }
301
326
 
327
+ const instructionContent = renderProjectInstructionContent({
328
+ instructionDefinition,
329
+ managedSourceRoot,
330
+ managedSectionContent,
331
+ });
302
332
  const existingState = inspectExistingPath({
303
333
  managedSourceRoot,
304
334
  projectFilepath,
305
335
  });
306
336
 
307
337
  if (existingState.kind === 'file') {
308
- const nextContent = upsertManagedInstructionSection(existingState.content, managedSectionContent);
338
+ const nextContent =
339
+ instructionDefinition.content === 'source'
340
+ ? instructionContent
341
+ : upsertManagedInstructionSection(existingState.content, instructionContent);
309
342
  if (nextContent === existingState.content) {
310
343
  result.skipped.push(relativeProjectPath);
311
344
  continue;
@@ -320,7 +353,7 @@ function ensureInstructionFiles(
320
353
  if (existingState.kind === 'managed-different') {
321
354
  if (!dryRun) {
322
355
  fs.removeSync(projectFilepath);
323
- fs.writeFileSync(projectFilepath, managedSectionContent);
356
+ fs.writeFileSync(projectFilepath, instructionContent);
324
357
  }
325
358
  result.updated.push(relativeProjectPath);
326
359
  logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
@@ -336,14 +369,14 @@ function ensureInstructionFiles(
336
369
  if (existingState.kind === 'blocked') {
337
370
  if (!dryRun) {
338
371
  fs.removeSync(projectFilepath);
339
- fs.writeFileSync(projectFilepath, managedSectionContent);
372
+ fs.writeFileSync(projectFilepath, instructionContent);
340
373
  }
341
374
  result.overwritten.push(relativeProjectPath);
342
375
  logVerbose(`${logPrefix} Replaced ${relativeProjectPath}`);
343
376
  continue;
344
377
  }
345
378
 
346
- if (!dryRun) fs.writeFileSync(projectFilepath, managedSectionContent);
379
+ if (!dryRun) fs.writeFileSync(projectFilepath, instructionContent);
347
380
  result.created.push(relativeProjectPath);
348
381
  logVerbose(`${logPrefix} Created ${relativeProjectPath}`);
349
382
  }
@@ -482,52 +515,162 @@ function mergeInstructionResults(
482
515
  result.blocked.push(...next.blocked.map((entry) => formatResultPath(rootDir, entry)));
483
516
  }
484
517
 
485
- function renderEmbeddedProjectInstructions({ coreRoot }: TProjectInstructionArgs) {
486
- const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
487
- if (!fs.existsSync(agentSourceRoot)) throw new Error(`Missing project instruction source root: ${agentSourceRoot}`);
518
+ function renderProjectInstructionContent({
519
+ instructionDefinition,
520
+ managedSourceRoot,
521
+ managedSectionContent,
522
+ }: {
523
+ instructionDefinition: TAgentInstructionDefinition;
524
+ managedSourceRoot: string;
525
+ managedSectionContent: string;
526
+ }) {
527
+ if (instructionDefinition.content !== 'source') return managedSectionContent;
528
+
529
+ return renderSingleProjectInstruction({
530
+ managedSourceRoot,
531
+ projectPath: instructionDefinition.projectPath,
532
+ });
533
+ }
534
+
535
+ function renderSingleProjectInstruction({
536
+ managedSourceRoot,
537
+ projectPath,
538
+ }: {
539
+ managedSourceRoot: string;
540
+ projectPath: string;
541
+ }) {
542
+ const sourceFilepath = path.join(managedSourceRoot, projectPath);
543
+ if (!fs.existsSync(sourceFilepath)) throw new Error(`Missing project instruction source file: ${sourceFilepath}`);
488
544
 
489
- const sourceFiles = collectMarkdownFiles(agentSourceRoot).sort((a, b) => a.relativePath.localeCompare(b.relativePath));
545
+ const content = fs.readFileSync(sourceFilepath, 'utf8');
546
+ const demotedContent = demoteMarkdownHeadings(content).trim();
490
547
  const lines = [
491
548
  managedInstructionSectionHeader,
492
549
  managedInstructionSectionStart,
493
550
  '',
494
551
  managedInstructionSectionIntro,
495
552
  '',
553
+ `## Source: ${normalizeProjectPathForGitignore(projectPath)}`,
554
+ '',
496
555
  ];
497
556
 
498
- for (const sourceFile of sourceFiles) {
499
- const content = fs.readFileSync(sourceFile.filepath, 'utf8');
500
- const demotedContent = demoteMarkdownHeadings(content).trim();
501
-
502
- lines.push(`## Source: ${sourceFile.relativePath}`, '');
503
- if (demotedContent) lines.push(demotedContent, '');
504
- }
505
-
557
+ if (demotedContent) lines.push(demotedContent, '');
506
558
  lines.push(managedInstructionSectionEnd, '');
507
559
 
508
560
  return lines.join('\n');
509
561
  }
510
562
 
511
- function collectMarkdownFiles(rootDir: string, currentDir = rootDir): { filepath: string; relativePath: string }[] {
512
- const files: { filepath: string; relativePath: string }[] = [];
563
+ function renderMonorepoAppRegistry({
564
+ appRoot,
565
+ monorepoRoot,
566
+ }: {
567
+ appRoot?: string;
568
+ monorepoRoot?: string;
569
+ }) {
570
+ if (!monorepoRoot || !appRoot || path.resolve(monorepoRoot) === path.resolve(appRoot)) return [];
571
+
572
+ const appRoots = findProteumAppRootsUnder(monorepoRoot);
573
+ if (appRoots.length === 0) return [];
513
574
 
514
- for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
515
- const filepath = path.join(currentDir, entry.name);
575
+ const summaries = appRoots.map((candidate) => readProteumAppRootSummary(candidate, monorepoRoot));
516
576
 
517
- if (entry.isDirectory()) {
518
- files.push(...collectMarkdownFiles(rootDir, filepath));
519
- continue;
520
- }
577
+ return [
578
+ '## Known Proteum Apps',
579
+ '',
580
+ 'This is a monorepo root wrapper. Do not start `npx proteum dev` from this root; start it from one app root below.',
581
+ '',
582
+ ...summaries.map((summary) => {
583
+ const marker = path.resolve(summary.appRoot) === path.resolve(appRoot) ? ' (current configured app)' : '';
584
+ const port = summary.manifest?.routerPort ? `, default port ${summary.manifest.routerPort}` : '';
585
+ const command = createStartDevCommand({
586
+ appRoot: summary.appRoot,
587
+ baseRoot: monorepoRoot,
588
+ port: summary.manifest?.routerPort,
589
+ });
590
+
591
+ return `- ${summary.relativeAppRoot || summary.appRoot}${marker}${port}: ${command}`;
592
+ }),
593
+ '',
594
+ ];
595
+ }
521
596
 
522
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
597
+ function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoRegistry = false, monorepoRoot }: TProjectInstructionArgs) {
598
+ const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
599
+ if (!fs.existsSync(agentSourceRoot)) throw new Error(`Missing project instruction source root: ${agentSourceRoot}`);
523
600
 
524
- files.push({
525
- filepath,
526
- relativePath: normalizeProjectPathForGitignore(path.relative(rootDir, filepath)),
527
- });
528
- }
601
+ const lines = [
602
+ managedInstructionSectionHeader,
603
+ managedInstructionSectionStart,
604
+ '',
605
+ managedInstructionSectionIntro,
606
+ '',
607
+ '## Agent Routing Contract',
608
+ '',
609
+ 'Proteum CLI and MCP outputs are optimized for agents. Do not load the whole instruction corpus up front.',
610
+ '',
611
+ 'Detailed Proteum contracts are intentionally split into the files listed in the routing table below. They are not deleted; load only the file that matches the current task, or use MCP `workflow_start` / `instructions_resolve { projectId }` to get the routed set.',
612
+ '',
613
+ '1. When a Proteum MCP client is available, call MCP `workflow_start` first. Pass `cwd` when `projectId` is not known, or pass the stable `projectId` from `projects_list` when it is known.',
614
+ '2. Use the `projectId` returned by live `workflow_start` for every follow-up app-bound MCP tool. If `workflow_start` is ambiguous or returns offline candidates, call MCP `project_resolve { cwd }`, select the intended app root, follow its port-inspected next action when needed, then retry `workflow_start`.',
615
+ '3. After `projectId` is selected, use MCP `runtime_status`, `orient`, `instructions_resolve`, `explain_summary`, `route_candidates`, `doctor`, `diagnose`, `trace_show`, `perf_request`, and `logs_tail` for read-only runtime, owner, instruction, route, trace, perf, and log reads.',
616
+ '4. Do not run CLI equivalents after a successful MCP result for the same read. Do not run broad source searches for route/page/controller ownership after `workflow_start`, `orient`, or `explain_summary` already returned the owner.',
617
+ '5. Treat selected instruction previews returned by MCP as the instruction source for read-only discovery and diagnostics. Read full files only before edits or git writes, when the returned `fullRead`/`fullReadPolicy` requires it, or when the preview is insufficient.',
618
+ '6. Use `npx proteum runtime status` before starting a dev server only when MCP runtime status is unavailable, so an existing tracked session can be reused and the configured router/HMR ports can be checked without probing page bodies. If it says health is unreachable, do not run `diagnose`, `trace`, or `perf`; stop/repair/start the dev session first.',
619
+ '7. During `npx proteum dev`, Proteum ensures one managed machine MCP daemon is running and routes app-bound reads to the read-only runtime endpoint at `/__proteum/mcp` instead of spawning equivalent CLI diagnostics.',
620
+ '8. If machine MCP routing fails, run `npx proteum mcp status` and `npx proteum runtime status`; if no live session exists, use the exact next action from MCP offline routing or runtime status instead of assuming the manifest default port. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server.',
621
+ '9. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again. Do not start a second dev server in the same worktree or a second managed MCP daemon. Then retry MCP `workflow_start`.',
622
+ '10. Use MCP `diagnose { projectId, path }` for request-time issues before raw trace, perf, browser, or broad source search; use `npx proteum diagnose <target>` only as fallback or final terminal evidence.',
623
+ '11. Use `route_candidates`, `explain_summary`, or `npx proteum explain owner <query>` to pick routes. Do not run `npx proteum explain --routes --full` unless compact route/owner tools explicitly cannot answer the raw route-array question.',
624
+ '12. Use `--full`, `--manifest`, `--events`, or MCP `detail: "full"` only when compact output says the omitted detail is needed.',
625
+ '',
626
+ 'CLI remains the reproducible surface for `dev`, `build`, `check`, `verify`, migrations, and final command evidence. MCP remains read-only and returns compact `proteum-mcp-v1` JSON.',
627
+ '',
628
+ ...(includeMonorepoRegistry ? renderMonorepoAppRegistry({ appRoot, monorepoRoot }) : []),
629
+ '## Always-On Safety',
630
+ '',
631
+ '- Never edit generated files under `.proteum`.',
632
+ '- Never create or edit Prisma migration files manually.',
633
+ '- Never run schema-mutating SQL such as `ALTER TABLE`, `CREATE TABLE`, `DROP TABLE`, or `CREATE INDEX`.',
634
+ '- If `schema.prisma` changes, ask the user to run `npx prisma migrate dev --config ./prisma.config.ts --name <migration name>` and wait for `continue` before validation.',
635
+ '- For production changes, add or update focused unit tests for touched behavior when applicable, targeting 100% meaningful coverage for changed production paths.',
636
+ '- Do not run `git restore` or `git reset`.',
637
+ '- Keep `proteum dev` sessions tracked with explicit session files and do not replace another live session.',
638
+ '',
639
+ '## Triggered Instruction Reads',
640
+ '',
641
+ 'Keep this root file as a router. MCP-selected previews are enough for read-only discovery and diagnostics. Read the referenced full instruction file only before edits or git writes, when `fullRead`/`fullReadPolicy` requires it, or when the preview is insufficient.',
642
+ '',
643
+ '- Git lifecycle (`commit`, `and commit`, `stage`, `push`, `PR`, pull request): read Root contract fallback before any git write.',
644
+ '- Before finishing production code changes: read Root contract fallback, `CODING_STYLE.md`, `tests/AGENTS.md`, and any touched area `AGENTS.md`.',
645
+ '- Runtime-visible, request-time, router, SSR, browser, or controller behavior: read Root contract fallback and `diagnostics.md` for verification routing.',
646
+ '- Non-trivial feature, product, business-rule, UX, copy, or docs changes: read `DOCUMENTATION.md` before editing.',
647
+ '- Implementation edits: read `CODING_STYLE.md` before editing, plus the matching area file from the routing table.',
648
+ '',
649
+ '## Routing Table',
650
+ '',
651
+ '- Non-trivial coding tasks, feature docs, product intent, acceptance criteria, or docs updates: read `DOCUMENTATION.md`.',
652
+ '- Raw errors, failing requests, traces, perf, or reproduction: read `diagnostics.md`.',
653
+ '- Implementation edits: read `CODING_STYLE.md` before editing.',
654
+ '- Client files or pages: read `client/AGENTS.md`; for page route/data/render work also read `client/pages/AGENTS.md`.',
655
+ '- Server services: read `server/services/AGENTS.md`.',
656
+ '- Manual server routes: read `server/routes/AGENTS.md`.',
657
+ '- Unit tests, integration tests, or test-area work: read `tests/AGENTS.md`.',
658
+ '- E2E work: read `tests/AGENTS.md`, `tests/e2e/AGENTS.md`, and `tests/e2e/REAL_WORLD_JOURNEY_TESTS.md`.',
659
+ '- Package, runtime, build, or client-performance decisions: read `optimizations.md` after implementation or when explicitly optimizing.',
660
+ '',
661
+ '## Canonical Source Map',
662
+ '',
663
+ `- Root contract fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'AGENTS.md'))}`,
664
+ `- Documentation fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'DOCUMENTATION.md'))}`,
665
+ `- Diagnostics fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'diagnostics.md'))}`,
666
+ `- Optimization fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'optimizations.md'))}`,
667
+ `- Coding style fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'CODING_STYLE.md'))}`,
668
+ '',
669
+ ];
670
+
671
+ lines.push(managedInstructionSectionEnd, '');
529
672
 
530
- return files;
673
+ return lines.join('\n');
531
674
  }
532
675
 
533
676
  function demoteMarkdownHeadings(content: string) {