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
@@ -8,7 +8,10 @@ import express from 'express';
8
8
  import http from 'http';
9
9
  import https from 'https';
10
10
  import path from 'path';
11
+ import { randomUUID } from 'crypto';
11
12
  import cors, { CorsOptions } from 'cors';
13
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
14
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
12
15
 
13
16
  // Middlewares (npm)
14
17
  import morgan from 'morgan';
@@ -37,9 +40,16 @@ import {
37
40
  parseConnectedProjectProxyPath,
38
41
  } from '@common/connectedProjects';
39
42
  import { profilerTraceRequestIdHeader } from '@common/dev/profiler';
43
+ import { createProteumMcpServer } from '@common/dev/mcpServer';
44
+ import { createRuntimeProteumMcpProvider } from '@server/app/devMcp';
40
45
 
41
46
  // Middlewaees (core)
42
47
  import { isMutipart, MiddlewareFormData } from './multipart';
48
+ import {
49
+ resolveHttpCacheConfig,
50
+ resolvePublicAssetCacheControl,
51
+ type THttpCacheConfig,
52
+ } from './cache';
43
53
 
44
54
  /*----------------------------------
45
55
  - CONFIG
@@ -58,12 +68,15 @@ export type Config = {
58
68
  maxSize: string; // Expression package bytes
59
69
  };
60
70
  csp: { default?: string[]; styles?: string[]; images?: string[]; scripts: string[] };
71
+ cache?: THttpCacheConfig;
61
72
  cors?: CorsOptions;
62
73
  helmet?: Parameters<typeof helmet>[0];
63
74
  };
64
75
 
65
76
  export type Hooks = {};
66
77
 
78
+ export { resolveHttpCacheConfig, type THttpCacheConfig } from './cache';
79
+
67
80
  type TContentSecurityPolicyOptions = NonNullable<Parameters<typeof helmet.contentSecurityPolicy>[0]>;
68
81
  type TContentSecurityPolicyDirectives = NonNullable<TContentSecurityPolicyOptions['directives']>;
69
82
  type TDevSessionAuthService = {
@@ -94,37 +107,9 @@ const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPol
94
107
  };
95
108
  };
96
109
 
97
- const immutablePublicAssetCacheControl = 'public, max-age=31536000, immutable';
98
- const devPublicAssetCacheControl = 'no-store';
99
- const revalidatedPublicAssetCacheControl = 'public, max-age=0, must-revalidate';
100
- const hashedPublicAssetPattern = /(^|[-_.])[a-f0-9]{6,}(?=(\.[^.]+)+$)/i;
101
110
  const connectedProjectBootRetryCount = 10;
102
111
  const connectedProjectBootRetryDelayMs = 5_000;
103
112
 
104
- const isVersionedPublicAssetRequest = (res: undefined | express.Response | http.ServerResponse, filePath: string) => {
105
- const request =
106
- res && typeof res === 'object' && 'req' in res
107
- ? ((res as express.Response | (http.ServerResponse & { req?: express.Request })).req ?? undefined)
108
- : undefined;
109
- const requestUrl = request?.originalUrl || request?.url || '';
110
- const searchParams = new URL(requestUrl, 'http://proteum.local').searchParams;
111
- if (searchParams.has('v')) return true;
112
-
113
- return hashedPublicAssetPattern.test(path.basename(filePath));
114
- };
115
-
116
- const resolvePublicAssetCacheControl = ({
117
- res,
118
- filePath,
119
- profile,
120
- }: {
121
- res: undefined | express.Response | http.ServerResponse;
122
- filePath: string;
123
- profile: string;
124
- }) => {
125
- if (profile === 'dev') return devPublicAssetCacheControl;
126
- return isVersionedPublicAssetRequest(res, filePath) ? immutablePublicAssetCacheControl : revalidatedPublicAssetCacheControl;
127
- };
128
113
  const wait = async (durationMs: number) =>
129
114
  await new Promise<void>((resolve) => {
130
115
  setTimeout(resolve, durationMs);
@@ -406,20 +391,22 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
406
391
  // Normalement, seulement utile pour le mode production,
407
392
  // Quand mode debug, les ressources client semblent servies par le dev middlewae
408
393
  // Sauf que les ressources serveur ne semblent pas trouvées par le dev-middleware
409
- const disablePublicAssetCaching = this.app.env.profile === 'dev';
394
+ const publicAssetCache = resolveHttpCacheConfig(this.config.cache).publicAssets;
395
+ const defaultPublicAssetValidators = this.app.env.profile !== 'dev';
396
+ const publicAssetEtag = publicAssetCache.etag ?? defaultPublicAssetValidators;
397
+ const publicAssetLastModified = publicAssetCache.lastModified ?? defaultPublicAssetValidators;
398
+
410
399
  routes.use(compression());
411
400
  routes.use('/public', cors());
412
401
  routes.use(
413
402
  '/public',
414
403
  express.static(path.join(Container.path.root, APP_OUTPUT_DIR, 'public'), {
415
404
  dotfiles: 'deny',
416
- etag: !disablePublicAssetCaching,
417
- lastModified: !disablePublicAssetCaching,
405
+ etag: publicAssetEtag,
406
+ lastModified: publicAssetLastModified,
418
407
  setHeaders: (res, filePath) => {
419
- if (disablePublicAssetCaching) {
420
- res.removeHeader('ETag');
421
- res.removeHeader('Last-Modified');
422
- }
408
+ if (!publicAssetEtag) res.removeHeader('ETag');
409
+ if (!publicAssetLastModified) res.removeHeader('Last-Modified');
423
410
 
424
411
  res.setHeader(
425
412
  'Cache-Control',
@@ -427,6 +414,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
427
414
  res,
428
415
  filePath,
429
416
  profile: this.app.env.profile,
417
+ cache: publicAssetCache,
430
418
  }),
431
419
  );
432
420
  },
@@ -510,6 +498,8 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
510
498
  private registerDevTraceRoutes(routes: express.Express) {
511
499
  if (!__DEV__ || this.app.env.profile !== 'dev') return;
512
500
 
501
+ this.registerDevMcpRoute(routes);
502
+
513
503
  if (this.app.container.Trace.isDevTraceEnabled()) {
514
504
  routes.get('/__proteum/trace/requests', (req, res) => {
515
505
  const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
@@ -836,4 +826,73 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
836
826
  private getCronManager() {
837
827
  return (this.app as typeof this.app & { Cron?: CronManager }).Cron;
838
828
  }
829
+
830
+ private registerDevMcpRoute(routes: express.Express) {
831
+ type TMcpTransportEntry = {
832
+ server: ReturnType<typeof createProteumMcpServer>;
833
+ transport: StreamableHTTPServerTransport;
834
+ };
835
+ const transports = new Map<string, TMcpTransportEntry>();
836
+ const readSessionId = (req: express.Request) => {
837
+ const value = req.headers['mcp-session-id'];
838
+ if (Array.isArray(value)) return value[0];
839
+ return typeof value === 'string' && value.trim() ? value : undefined;
840
+ };
841
+ const writeJsonRpcError = (res: express.Response, statusCode: number, message: string) => {
842
+ res.status(statusCode).json({
843
+ jsonrpc: '2.0',
844
+ error: {
845
+ code: -32000,
846
+ message,
847
+ },
848
+ id: null,
849
+ });
850
+ };
851
+
852
+ routes.all('/__proteum/mcp', async (req, res) => {
853
+ const sessionId = readSessionId(req);
854
+ let entry = sessionId ? transports.get(sessionId) : undefined;
855
+
856
+ try {
857
+ if (!entry && !sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
858
+ const provider = createRuntimeProteumMcpProvider({
859
+ app: this.app,
860
+ publicUrl: this.publicUrl,
861
+ routerPort: this.config.port,
862
+ });
863
+ const server = createProteumMcpServer({
864
+ provider,
865
+ version: String(process.env.npm_package_version || 'runtime'),
866
+ });
867
+ const transport = new StreamableHTTPServerTransport({
868
+ sessionIdGenerator: () => randomUUID(),
869
+ onsessioninitialized: (initializedSessionId) => {
870
+ transports.set(initializedSessionId, { server, transport });
871
+ },
872
+ });
873
+
874
+ transport.onclose = () => {
875
+ const transportSessionId = transport.sessionId;
876
+ if (transportSessionId) transports.delete(transportSessionId);
877
+ void server.close().catch(() => undefined);
878
+ };
879
+
880
+ await server.connect(transport);
881
+ entry = { server, transport };
882
+ }
883
+
884
+ if (!entry) {
885
+ writeJsonRpcError(res, 400, 'Bad Request: initialize the Proteum MCP session before sending tool or resource requests.');
886
+ return;
887
+ }
888
+
889
+ await entry.transport.handleRequest(req, res, req.body);
890
+ } catch (error) {
891
+ console.error('Error handling Proteum MCP request:', error);
892
+ if (!res.headersSent) {
893
+ writeJsonRpcError(res, 500, 'Internal Proteum MCP server error.');
894
+ }
895
+ }
896
+ });
897
+ }
839
898
  }
@@ -52,7 +52,7 @@ import { AnyRouterService } from './service';
52
52
  import ServerRequest from './request';
53
53
  import ServerResponse, { TRouterContext, TRouterContextServices } from './response';
54
54
  import Page from './response/page';
55
- import HTTP, { Config as HttpServiceConfig } from './http';
55
+ import HTTP, { Config as HttpServiceConfig, resolveHttpCacheConfig } from './http';
56
56
  import DocumentRenderer from './response/page/document';
57
57
  import { loadGeneratedRuntimeBundle } from './generatedRuntime';
58
58
 
@@ -97,9 +97,6 @@ export type TApiResponseData = { data: any; triggers?: { [cle: string]: any } };
97
97
 
98
98
  export type HttpHeaders = { [cle: string]: string };
99
99
 
100
- const dynamicHtmlCacheControl = 'no-store, no-cache, must-revalidate, proxy-revalidate';
101
- const staticHtmlCacheControl = 'public, max-age=0, must-revalidate';
102
-
103
100
  /*----------------------------------
104
101
  - SERVICE CONFIG
105
102
  ----------------------------------*/
@@ -1136,15 +1133,15 @@ export default class ServerRouter<
1136
1133
  }
1137
1134
 
1138
1135
  private applyHtmlCacheHeaders(res: express.Response, isStaticHtml: boolean) {
1139
- if (isStaticHtml) {
1136
+ const cache = resolveHttpCacheConfig(this.config.http.cache);
1137
+ const htmlCache = isStaticHtml ? cache.html.static : cache.html.dynamic;
1138
+
1139
+ if (htmlCache.surrogateControl === false) {
1140
1140
  res.removeHeader('Surrogate-Control');
1141
- res.setHeader('Cache-Control', staticHtmlCacheControl);
1142
- return;
1141
+ } else {
1142
+ res.setHeader('Surrogate-Control', htmlCache.surrogateControl);
1143
1143
  }
1144
1144
 
1145
- // Don't cache dynamic HTML, because updated releases can change asset hashes.
1146
- // https://github.com/helmetjs/nocache/blob/main/index.ts
1147
- res.setHeader('Surrogate-Control', 'no-store');
1148
- res.setHeader('Cache-Control', dynamicHtmlCacheControl);
1145
+ res.setHeader('Cache-Control', htmlCache.cacheControl);
1149
1146
  }
1150
1147
  }
@@ -1,5 +1,4 @@
1
1
  const assert = require('node:assert/strict');
2
- const test = require('node:test');
3
2
 
4
3
  const { resolveRequestIp } = require('./ip.ts');
5
4
 
@@ -2,7 +2,6 @@ const assert = require('node:assert/strict');
2
2
  const fs = require('node:fs');
3
3
  const os = require('node:os');
4
4
  const path = require('node:path');
5
- const test = require('node:test');
6
5
 
7
6
  const coreRoot = path.resolve(__dirname, '..');
8
7
  process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
@@ -24,7 +23,19 @@ const createCoreFixture = () => {
24
23
 
25
24
  writeFile(path.join(agentsRoot, 'AGENTS.md'), '# Root Contract\n\n- Root rule\n');
26
25
  writeFile(path.join(agentsRoot, 'CODING_STYLE.md'), '# Coding Style\n\n- Style rule\n');
26
+ writeFile(path.join(agentsRoot, 'DOCUMENTATION.md'), '# Documentation\n\n- Documentation rule\n');
27
+ writeFile(path.join(agentsRoot, 'diagnostics.md'), '# Diagnostics\n\n- Diagnostics rule\n');
28
+ writeFile(path.join(agentsRoot, 'optimizations.md'), '# Optimizations\n\n- Optimization rule\n');
27
29
  writeFile(path.join(agentsRoot, 'client', 'AGENTS.md'), '# Client Rules\n\n- Client rule\n');
30
+ writeFile(path.join(agentsRoot, 'client', 'pages', 'AGENTS.md'), '# Page Rules\n\n- Page rule\n');
31
+ writeFile(path.join(agentsRoot, 'server', 'routes', 'AGENTS.md'), '# Route Rules\n\n- Route rule\n');
32
+ writeFile(path.join(agentsRoot, 'server', 'services', 'AGENTS.md'), '# Service Rules\n\n- Service rule\n');
33
+ writeFile(path.join(agentsRoot, 'tests', 'AGENTS.md'), '# Test Rules\n\n- Test rule\n');
34
+ writeFile(path.join(agentsRoot, 'tests', 'e2e', 'AGENTS.md'), '# E2E Rules\n\n- E2E rule\n');
35
+ writeFile(
36
+ path.join(agentsRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'),
37
+ '# Real World Journey Tests\n\n- Journey rule\n',
38
+ );
28
39
 
29
40
  return root;
30
41
  };
@@ -43,6 +54,7 @@ const createAppFixture = () => {
43
54
  '# Proteum-managed instruction files',
44
55
  '/AGENTS.md',
45
56
  '/CODING_STYLE.md',
57
+ '/DOCUMENTATION.md',
46
58
  '# End Proteum-managed instruction files',
47
59
  '/.proteum',
48
60
  '',
@@ -52,24 +64,69 @@ const createAppFixture = () => {
52
64
  return appRoot;
53
65
  };
54
66
 
55
- test('standalone configure creates tracked instruction files with embedded corpus', () => {
67
+ test('project instruction sources require unit tests for applicable production changes', () => {
68
+ const projectAgentsRoot = path.join(coreRoot, 'agents', 'project');
69
+
70
+ assert.match(
71
+ fs.readFileSync(path.join(projectAgentsRoot, 'AGENTS.md'), 'utf8'),
72
+ /production changes must always add or update focused unit tests/,
73
+ );
74
+ assert.match(
75
+ fs.readFileSync(path.join(projectAgentsRoot, 'root', 'AGENTS.md'), 'utf8'),
76
+ /always add or update focused unit tests/,
77
+ );
78
+ assert.match(
79
+ fs.readFileSync(path.join(projectAgentsRoot, 'tests', 'AGENTS.md'), 'utf8'),
80
+ /For every production change, add or update focused unit tests/,
81
+ );
82
+ });
83
+
84
+ test('standalone configure creates tracked instruction files with routing contract and split docs', () => {
56
85
  const coreRoot = createCoreFixture();
57
86
  const appRoot = createAppFixture();
58
87
  const result = configureProjectAgentInstructions({ appRoot, coreRoot });
59
88
  const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
60
89
  const codingStyleContent = fs.readFileSync(path.join(appRoot, 'CODING_STYLE.md'), 'utf8');
90
+ const documentationContent = fs.readFileSync(path.join(appRoot, 'DOCUMENTATION.md'), 'utf8');
61
91
  const gitignoreContent = fs.readFileSync(path.join(appRoot, '.gitignore'), 'utf8');
62
92
 
63
93
  assert.equal(result.blocked.length, 0);
64
94
  assert.match(agentsContent, /^# Proteum Instructions/m);
65
95
  assert.match(agentsContent, /<!-- proteum-instructions:start -->/);
66
- assert.match(agentsContent, /## Source: AGENTS\.md/);
67
- assert.match(agentsContent, /## Root Contract/);
68
- assert.match(agentsContent, /## Source: CODING_STYLE\.md/);
69
- assert.match(codingStyleContent, /## Source: client\/AGENTS\.md/);
96
+ assert.match(agentsContent, /## Agent Routing Contract/);
97
+ assert.match(agentsContent, /npx proteum runtime status/);
98
+ assert.match(agentsContent, /MCP `workflow_start`/);
99
+ assert.match(agentsContent, /project_resolve \{ cwd \}/);
100
+ assert.match(agentsContent, /instructions_resolve \{ projectId \}/);
101
+ assert.match(agentsContent, /Do not run CLI equivalents after a successful MCP result/);
102
+ assert.match(agentsContent, /Read full files only before edits or git writes/);
103
+ assert.match(agentsContent, /explain_summary/);
104
+ assert.match(agentsContent, /\/__proteum\/mcp/);
105
+ assert.match(agentsContent, /proteum-mcp-v1/);
106
+ assert.match(agentsContent, /## Triggered Instruction Reads/);
107
+ assert.match(agentsContent, /Git lifecycle/);
108
+ assert.match(agentsContent, /read Root contract fallback before any git write/);
109
+ assert.match(agentsContent, /add or update focused unit tests/);
110
+ assert.match(agentsContent, /read Root contract fallback, `CODING_STYLE\.md`, `tests\/AGENTS\.md`/);
111
+ assert.match(agentsContent, /MCP-selected previews are enough/);
112
+ assert.doesNotMatch(agentsContent, /Conventional Commits/);
113
+ assert.match(agentsContent, /They are not deleted/);
114
+ assert.doesNotMatch(agentsContent, /## Source: CODING_STYLE\.md/);
115
+ assert.match(codingStyleContent, /## Source: CODING_STYLE\.md/);
116
+ assert.match(codingStyleContent, /## Coding Style/);
117
+ assert.doesNotMatch(codingStyleContent, /## Source: client\/AGENTS\.md/);
118
+ assert.match(documentationContent, /## Source: DOCUMENTATION\.md/);
119
+ assert.match(documentationContent, /## Documentation/);
120
+ assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'AGENTS.md')), true);
121
+ assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'AGENTS.md'), 'utf8'), /Test rule/);
122
+ assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), true);
123
+ assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md')), true);
124
+ assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /Journey rule/);
125
+ assert.doesNotMatch(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
70
126
  assert.doesNotMatch(agentsContent, /Before reading or applying instructions from this file/);
71
127
  assert.doesNotMatch(gitignoreContent, /Proteum-managed instruction files/);
72
128
  assert.doesNotMatch(gitignoreContent, /^\/AGENTS\.md$/m);
129
+ assert.doesNotMatch(gitignoreContent, /^\/DOCUMENTATION\.md$/m);
73
130
  });
74
131
 
75
132
  test('configure preserves project content outside the managed section', () => {
@@ -102,7 +159,8 @@ test('configure preserves project content outside the managed section', () => {
102
159
  const content = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
103
160
  assert.match(content, /# Product Notes/);
104
161
  assert.match(content, /Keep this product note\./);
105
- assert.match(content, /## Source: CODING_STYLE\.md/);
162
+ assert.match(content, /## Agent Routing Contract/);
163
+ assert.doesNotMatch(content, /## Source: CODING_STYLE\.md/);
106
164
  assert.doesNotMatch(content, /Old managed content/);
107
165
  assert.match(content, /# Local Footer/);
108
166
  assert.match(content, /Keep this footer\./);
@@ -144,7 +202,8 @@ test('configure preserves project content around legacy managed stubs', () => {
144
202
  assert.match(content, /## Product Bootstrap/);
145
203
  assert.match(content, /Keep these local bootstrap notes\./);
146
204
  assert.match(content, /# Proteum Instructions/);
147
- assert.match(content, /## Source: CODING_STYLE\.md/);
205
+ assert.match(content, /## Agent Routing Contract/);
206
+ assert.doesNotMatch(content, /## Source: CODING_STYLE\.md/);
148
207
  assert.doesNotMatch(content, /# Proteum Managed Instructions/);
149
208
  assert.doesNotMatch(content, /Before reading or applying instructions from this file/);
150
209
  assert.match(content, /## Local Footer/);
@@ -158,6 +217,10 @@ test('monorepo configure writes root and app instruction files', () => {
158
217
 
159
218
  fs.mkdirSync(path.join(monorepoRoot, '.git'));
160
219
  fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
220
+ fs.mkdirSync(path.join(appRoot, 'server'), { recursive: true });
221
+ writeFile(path.join(appRoot, 'package.json'), '{"name":"product"}\n');
222
+ writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
223
+ writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
161
224
 
162
225
  configureProjectAgentInstructions({ appRoot, coreRoot });
163
226
 
@@ -165,12 +228,27 @@ test('monorepo configure writes root and app instruction files', () => {
165
228
 
166
229
  assert.equal(result.mode, 'monorepo');
167
230
  assert.equal(resolveProjectAgentMonorepoRoot(appRoot), fs.realpathSync(monorepoRoot));
168
- assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Source: AGENTS\.md/);
231
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
232
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Known Proteum Apps/);
233
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /apps\/product/);
234
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Do not start `npx proteum dev` from this root/);
169
235
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
170
- assert.match(fs.readFileSync(path.join(monorepoRoot, 'diagnostics.md'), 'utf8'), /## Source: AGENTS\.md/);
171
- assert.match(fs.readFileSync(path.join(monorepoRoot, 'optimizations.md'), 'utf8'), /## Source: AGENTS\.md/);
172
- assert.match(fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
236
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'DOCUMENTATION.md'), 'utf8'), /## Source: DOCUMENTATION\.md/);
237
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'diagnostics.md'), 'utf8'), /## Source: diagnostics\.md/);
238
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'optimizations.md'), 'utf8'), /## Source: optimizations\.md/);
239
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'AGENTS.md'), 'utf8'), /## Source: tests\/AGENTS\.md/);
240
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'AGENTS.md'), 'utf8'), /## Source: tests\/e2e\/AGENTS\.md/);
241
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: tests\/e2e\/REAL_WORLD_JOURNEY_TESTS\.md/);
242
+ assert.doesNotMatch(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
243
+ assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'AGENTS.md')), false);
244
+ assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), false);
245
+ const appAgentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
246
+ assert.match(appAgentsContent, /## Agent Routing Contract/);
247
+ assert.doesNotMatch(appAgentsContent, /## Known Proteum Apps/);
248
+ assert.doesNotMatch(appAgentsContent, /Do not start `npx proteum dev` from this root/);
249
+ assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
173
250
  assert.equal(fs.existsSync(path.join(appRoot, 'CODING_STYLE.md')), false);
251
+ assert.equal(fs.existsSync(path.join(appRoot, 'DOCUMENTATION.md')), false);
174
252
  assert.equal(fs.existsSync(path.join(appRoot, 'diagnostics.md')), false);
175
253
  assert.equal(fs.existsSync(path.join(appRoot, 'optimizations.md')), false);
176
254
  assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
@@ -222,7 +300,7 @@ test('monorepo configure strips retired managed sections from local app-root doc
222
300
  assert.equal(result.updated.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
223
301
  });
224
302
 
225
- test('configure migrates legacy managed symlinks to embedded files', () => {
303
+ test('configure migrates legacy managed symlinks to tracked files', () => {
226
304
  const coreRoot = createCoreFixture();
227
305
  const appRoot = createAppFixture();
228
306
  const installedCoreRoot = createCoreFixture();
@@ -258,5 +336,5 @@ test('configure reports blocked paths unless overwrite is allowed', () => {
258
336
 
259
337
  assert.equal(result.overwritten.some((entry) => entry.endsWith('/CODING_STYLE.md')), true);
260
338
  assert.equal(fs.lstatSync(blockedPath).isFile(), true);
261
- assert.match(fs.readFileSync(blockedPath, 'utf8'), /## Source: AGENTS\.md/);
339
+ assert.match(fs.readFileSync(blockedPath, 'utf8'), /## Source: CODING_STYLE\.md/);
262
340
  });