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
@@ -316,7 +316,7 @@ Only bare `proteum build` and bare `proteum dev` runs print a banner. Other CLI
316
316
  - `npx proteum explain`
317
317
  - `npx proteum doctor`
318
318
  - `npx proteum connect`
319
- - `npx proteum mcp --url http://localhost:<port>` for repeated agent reads against a running dev app
319
+ - `npx proteum mcp` as the managed machine-scope MCP router; `proteum dev` ensures one daemon is running, agents call `workflow_start` first, use offline candidates to choose the correct app root when needed, then pass the returned live `projectId` to repeated reads against a live dev app
320
320
 
321
321
  CLI output uses compact `proteum-agent-v1` JSON for reproducible command evidence. MCP output uses compact `proteum-mcp-v1` JSON for repeated runtime reads. Use `npx proteum explain --manifest`, `npx proteum diagnose <target> --full`, or `npx proteum trace show <requestId> --events` only when the compact output is insufficient.
322
322
 
@@ -328,7 +328,7 @@ Current `proteum dev` supports tracked session management:
328
328
  - `npx proteum dev list --json`
329
329
  - `npx proteum dev stop --session-file var/run/proteum/dev/app.json`
330
330
 
331
- If your local workflow starts multiple dev servers, this is the current supported model.
331
+ `proteum dev` fails fast when another live tracked session already exists for the same app root. If runtime/MCP is unreachable, stop the listed session file first, then start dev again instead of launching a second server in the same worktree.
332
332
 
333
333
  ### New diagnostics are available
334
334
 
@@ -337,8 +337,7 @@ These are new capabilities, not migration requirements, but they are the fastest
337
337
  - `npx proteum connect --strict`
338
338
  - `npx proteum explain --connected --controllers`
339
339
  - `npx proteum runtime status`
340
- - `npx proteum mcp`
341
- - `npx proteum mcp --url http://localhost:<port>`
340
+ - `npx proteum mcp status` plus MCP `workflow_start`, `project_resolve`, or `projects_list`
342
341
  - `npx proteum diagnose / --port <port>`
343
342
  - `npx proteum perf top --port <port>`
344
343
  - `npx proteum trace latest --port <port>`
@@ -379,7 +378,6 @@ Then boot the app and verify the live runtime:
379
378
  ```bash
380
379
  npx proteum dev --port 3010
381
380
  npx proteum runtime status
382
- npx proteum mcp --url http://localhost:3010
383
381
  npx proteum diagnose / --port 3010
384
382
  npx proteum trace latest --port 3010
385
383
  ```
@@ -33,12 +33,12 @@ proteum perf memory --since 1h --group-by controller
33
33
 
34
34
  Default trace output is compact `proteum-agent-v1` JSON with counts, failed calls, error events, hot calls, and hot SQL. Use `--events` or `--full` only when raw event details, payload summaries, or SQL text are needed.
35
35
 
36
- When an MCP client is available, use MCP `trace_latest`, `trace_show`, `perf_top`, and `perf_request` for repeated reads against the same running app. Keep the CLI commands for reproducible terminal evidence and final verification logs.
36
+ When an MCP client is available, call `workflow_start` with `cwd` or a known `projectId`, then use the returned live `projectId` with MCP `trace_latest`, `trace_show`, `perf_top`, and `perf_request` for repeated reads against the same running app. If `workflow_start` returns offline app candidates or unreachable runtime health, start or repair exactly one `proteum dev` server from the intended app root before trace or perf reads. Keep the CLI commands for reproducible terminal evidence and final verification logs.
37
37
 
38
38
  Before reproducing a bug or starting a new test pass:
39
39
 
40
- - run `proteum runtime status` to reuse a tracked dev session when possible
41
- - use MCP `runtime_status` for repeated status checks against the same running server
40
+ - run `proteum runtime status` to reuse a tracked dev session when possible and to get an exact Start Dev action when the configured router/HMR ports are occupied
41
+ - use MCP `runtime_status` with the selected `projectId` for repeated status checks against the same running server
42
42
  - if a server is already running, inspect `proteum trace requests`, `proteum trace latest`, and compact `proteum diagnose <path>` first so you can capture past errors without dumping raw events
43
43
 
44
44
  Typical debugging flow:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.3.0",
4
+ "version": "2.4.2",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -12,6 +12,12 @@
12
12
  "keywords": [
13
13
  "framework"
14
14
  ],
15
+ "scripts": {
16
+ "test": "vitest run",
17
+ "test:codex-mcp": "PROTEUM_RUN_CODEX_MCP_USAGE_TEST=1 vitest run tests/codex-mcp-usage.test.cjs",
18
+ "test:integration": "vitest run tests/dev-transpile-watch.test.cjs",
19
+ "test:unit": "vitest run --exclude tests/dev-transpile-watch.test.cjs"
20
+ },
15
21
  "bin": {
16
22
  "proteum": "cli/bin.js"
17
23
  },
@@ -104,13 +110,14 @@
104
110
  "@types/fs-extra": "^9.0.12",
105
111
  "@types/markdown-it": "^12.2.3",
106
112
  "@types/mime-types": "^2.1.1",
107
- "@types/node": "^16.9.1",
113
+ "@types/node": "^20.19.40",
108
114
  "@types/nodemailer": "^6.4.4",
109
115
  "@types/pg": "^8.6.1",
110
116
  "@types/pg-escape": "^0.2.1",
111
117
  "@types/prompts": "^2.0.14",
112
118
  "@types/sharp": "^0.31.1",
113
119
  "@types/universal-analytics": "^0.4.5",
114
- "schema-dts": "^1.1.2"
120
+ "schema-dts": "^1.1.2",
121
+ "vitest": "^4.1.5"
115
122
  }
116
123
  }
@@ -1,8 +1,19 @@
1
1
  import fs from 'fs-extra';
2
+ import mysql from 'mysql2/promise';
2
3
  import path from 'path';
4
+ import { performance } from 'perf_hooks';
3
5
 
4
6
  import type { Application } from './index';
5
7
  import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from '@common/dev/console';
8
+ import {
9
+ normalizeDatabaseReadLimit,
10
+ normalizeDatabaseReadTimeoutMs,
11
+ validateDatabaseReadQuery,
12
+ type TDatabaseReadQueryInput,
13
+ type TDatabaseReadQueryResponse,
14
+ type TDatabaseReadQueryRow,
15
+ type TDatabaseReadQueryValue,
16
+ } from '@common/dev/database';
6
17
  import {
7
18
  buildDoctorResponse,
8
19
  explainSectionNames,
@@ -30,12 +41,31 @@ import {
30
41
  } from '@common/dev/inspection';
31
42
  import type { TProteumManifest } from '@common/dev/proteumManifest';
32
43
  import type { TRequestTrace } from '@common/dev/requestTrace';
44
+ import { parseMariaDbDatabaseUrl } from '@server/services/prisma/mariadb';
33
45
 
34
46
  const isExplainSectionName = (value: string): value is TExplainSectionName =>
35
47
  explainSectionNames.includes(value as TExplainSectionName);
36
48
  const isConsoleLogLevel = (value: string): value is TDevConsoleLogLevel =>
37
49
  ['silly', 'log', 'info', 'warn', 'error'].includes(value);
38
50
 
51
+ const normalizeDatabaseValue = (value: unknown): TDatabaseReadQueryValue => {
52
+ if (value === null || value === undefined) return null;
53
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
54
+ if (typeof value === 'bigint') return value.toString();
55
+ if (value instanceof Date) return value.toISOString();
56
+ if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
57
+
58
+ return JSON.stringify(value);
59
+ };
60
+
61
+ const normalizeDatabaseRow = (row: unknown): TDatabaseReadQueryRow => {
62
+ if (!row || typeof row !== 'object' || Array.isArray(row)) return {};
63
+
64
+ return Object.fromEntries(
65
+ Object.entries(row).map(([key, value]) => [key, normalizeDatabaseValue(value)]),
66
+ ) as TDatabaseReadQueryRow;
67
+ };
68
+
39
69
  export default class DevDiagnosticsRegistry {
40
70
  public constructor(private app: Application) {}
41
71
 
@@ -88,6 +118,68 @@ export default class DevDiagnosticsRegistry {
88
118
  return { logs: this.app.container.Console.listLogs(limit, isConsoleLogLevel(minimumLevel) ? minimumLevel : 'log') };
89
119
  }
90
120
 
121
+ public async databaseReadQuery({
122
+ limit: rawLimit,
123
+ sql: rawSql,
124
+ timeoutMs: rawTimeoutMs,
125
+ }: TDatabaseReadQueryInput): Promise<TDatabaseReadQueryResponse> {
126
+ const databaseUrl = process.env.DATABASE_URL;
127
+ if (!databaseUrl) throw new Error('DATABASE_URL is required before running database diagnostics.');
128
+
129
+ const { kind, sql } = validateDatabaseReadQuery(rawSql);
130
+ const limit = normalizeDatabaseReadLimit(rawLimit);
131
+ const timeoutMs = normalizeDatabaseReadTimeoutMs(rawTimeoutMs);
132
+ const connectionConfig = parseMariaDbDatabaseUrl(databaseUrl);
133
+ const connection = await mysql.createConnection({
134
+ host: connectionConfig.host,
135
+ port: connectionConfig.port,
136
+ user: connectionConfig.user,
137
+ password: connectionConfig.password,
138
+ database: connectionConfig.database,
139
+ connectTimeout: connectionConfig.connectTimeout,
140
+ multipleStatements: false,
141
+ supportBigNumbers: true,
142
+ bigNumberStrings: true,
143
+ dateStrings: true,
144
+ });
145
+ const startedAt = performance.now();
146
+
147
+ try {
148
+ await connection.query('START TRANSACTION READ ONLY');
149
+ const [rows, fields] = await connection.query({ sql, timeout: timeoutMs });
150
+ await connection.rollback();
151
+
152
+ const rowList = Array.isArray(rows) ? rows : [];
153
+ const normalizedRows = rowList.map(normalizeDatabaseRow);
154
+ const elapsedMs = Math.max(0, Math.round(performance.now() - startedAt));
155
+
156
+ return {
157
+ columns: Array.isArray(fields)
158
+ ? fields.map((field) => ({
159
+ name: field.name,
160
+ table: field.table || undefined,
161
+ type: field.type,
162
+ }))
163
+ : [],
164
+ elapsedMs,
165
+ kind,
166
+ limit,
167
+ limited: normalizedRows.length > limit,
168
+ rowCount: normalizedRows.length,
169
+ rows: normalizedRows.slice(0, limit),
170
+ sql,
171
+ };
172
+ } catch (error) {
173
+ try {
174
+ await connection.rollback();
175
+ } catch (_rollbackError) {}
176
+
177
+ throw error;
178
+ } finally {
179
+ await connection.end();
180
+ }
181
+ }
182
+
91
183
  private resolveRequestTrace({ path, requestId }: { path?: string; requestId?: string }): TRequestTrace | undefined {
92
184
  if (requestId) return this.app.container.Trace.getRequest(requestId);
93
185
  if (!path) return this.app.container.Trace.getLatestRequest();
@@ -2,6 +2,7 @@ import { buildContractsDoctorResponse } from '@common/dev/contractsDoctor';
2
2
  import { buildDoctorResponse } from '@common/dev/diagnostics';
3
3
  import { buildOrientationResponse, explainOwner } from '@common/dev/inspection';
4
4
  import {
5
+ compactDatabaseReadQueryResponse,
5
6
  buildRuntimeStatusPayload,
6
7
  compactDiagnoseResponse,
7
8
  compactDoctorResponse,
@@ -10,7 +11,9 @@ import {
10
11
  compactOrientationResponse,
11
12
  compactPerfRequestResponse,
12
13
  compactPerfTopResponse,
14
+ compactRouteCandidatesResponse,
13
15
  compactTraceResponse,
16
+ compactWorkflowStartResponse,
14
17
  resolveInstructionRouting,
15
18
  } from '@common/dev/mcpPayloads';
16
19
  import type { TProteumMcpProvider } from '@common/dev/mcpServer';
@@ -64,6 +67,42 @@ export const createRuntimeProteumMcpProvider = ({
64
67
  },
65
68
  });
66
69
  },
70
+ async workflowStart({ file, query, route, task }) {
71
+ const manifest = readManifest();
72
+ const doctor = buildDoctorResponse(manifest);
73
+ const contracts = buildContractsDoctorResponse(manifest);
74
+ const ownerQuery = [route, file, query]
75
+ .map((value) => value?.trim())
76
+ .find((value): value is string => Boolean(value));
77
+
78
+ return compactWorkflowStartResponse({
79
+ contracts,
80
+ doctor,
81
+ file,
82
+ health: {
83
+ reachable: true,
84
+ doctor: doctor.summary,
85
+ contracts: contracts.summary,
86
+ },
87
+ manifest,
88
+ owner: ownerQuery ? explainOwner(manifest, ownerQuery) : undefined,
89
+ query,
90
+ route,
91
+ runtime: {
92
+ publicUrl,
93
+ routerPort,
94
+ source: 'proteum-dev-runtime',
95
+ mcpUrl: `${publicUrl}/__proteum/mcp`,
96
+ traceEnabled: app.container.Trace.isDevTraceEnabled(),
97
+ profilerEnabled: app.container.Trace.isProfilingEnabled(),
98
+ connectedProjects: Object.entries(app.connectedProjects || {}).map(([namespace, project]) => ({
99
+ namespace,
100
+ urlInternal: (project as { urlInternal?: string }).urlInternal,
101
+ })),
102
+ },
103
+ task,
104
+ });
105
+ },
67
106
  async orient({ query }) {
68
107
  return compactOrientationResponse(buildOrientationResponse(readManifest(), query));
69
108
  },
@@ -80,6 +119,13 @@ export const createRuntimeProteumMcpProvider = ({
80
119
  query: normalizedQuery,
81
120
  });
82
121
  },
122
+ async routeCandidates({ limit, query }) {
123
+ return compactRouteCandidatesResponse({
124
+ limit,
125
+ manifest: readManifest(),
126
+ query,
127
+ });
128
+ },
83
129
  async doctor({ contracts = true }) {
84
130
  const manifest = readManifest();
85
131
 
@@ -144,6 +190,15 @@ export const createRuntimeProteumMcpProvider = ({
144
190
  response: diagnostics().readLogs(limit, level),
145
191
  });
146
192
  },
193
+ async dbQuery({ limit, sql, timeoutMs }) {
194
+ return compactDatabaseReadQueryResponse(
195
+ await diagnostics().databaseReadQuery({
196
+ limit,
197
+ sql,
198
+ timeoutMs,
199
+ }),
200
+ );
201
+ },
147
202
  async readResource(uri) {
148
203
  if (uri === 'proteum://runtime/status') return await provider.runtimeStatus({});
149
204
  if (uri === 'proteum://instructions/router') return await provider.instructionsResolve({});
@@ -19,7 +19,7 @@ const decodeUrlSegment = (value: string) => {
19
19
  return decodeURIComponent(value);
20
20
  };
21
21
 
22
- export const createMariaDbAdapter = (databaseUrl: string) => {
22
+ export const parseMariaDbDatabaseUrl = (databaseUrl: string) => {
23
23
  const url = new URL(databaseUrl);
24
24
 
25
25
  if (url.protocol !== 'mysql:' && url.protocol !== 'mariadb:')
@@ -34,7 +34,7 @@ export const createMariaDbAdapter = (databaseUrl: string) => {
34
34
  const connectTimeoutSeconds = parseInteger(url.searchParams.get('connect_timeout'));
35
35
  const idleTimeoutSeconds = parseInteger(url.searchParams.get('max_idle_connection_lifetime'));
36
36
 
37
- return new PrismaMariaDb({
37
+ return {
38
38
  host: url.hostname,
39
39
  port: parseInteger(url.port) ?? defaultPort,
40
40
  user: decodeUrlSegment(url.username),
@@ -43,5 +43,9 @@ export const createMariaDbAdapter = (databaseUrl: string) => {
43
43
  connectTimeout: connectTimeoutSeconds ? connectTimeoutSeconds * 1_000 : defaultConnectTimeout,
44
44
  idleTimeout: idleTimeoutSeconds ?? defaultIdleTimeout,
45
45
  ...(connectionLimit !== undefined ? { connectionLimit } : {}),
46
- });
46
+ };
47
+ };
48
+
49
+ export const createMariaDbAdapter = (databaseUrl: string) => {
50
+ return new PrismaMariaDb(parseMariaDbDatabaseUrl(databaseUrl));
47
51
  };
@@ -606,6 +606,31 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
606
606
  }
607
607
  });
608
608
 
609
+ routes.post('/__proteum/db/query', async (req, res) => {
610
+ const sql = typeof req.body?.sql === 'string' ? req.body.sql : '';
611
+ const rawLimit = Number(req.body?.limit);
612
+ const rawTimeoutMs = Number(req.body?.timeoutMs);
613
+
614
+ try {
615
+ res.json(
616
+ await this.app.getDevDiagnostics().databaseReadQuery({
617
+ sql,
618
+ limit: Number.isFinite(rawLimit) ? rawLimit : undefined,
619
+ timeoutMs: Number.isFinite(rawTimeoutMs) ? rawTimeoutMs : undefined,
620
+ }),
621
+ );
622
+ } catch (error) {
623
+ const message = error instanceof Error ? error.message : String(error);
624
+ const isUsageError =
625
+ message.includes('SQL') ||
626
+ message.includes('allowed') ||
627
+ message.includes('not allowed') ||
628
+ message.includes('DATABASE_URL');
629
+
630
+ res.status(isUsageError ? 400 : 500).json({ error: message });
631
+ }
632
+ });
633
+
609
634
  routes.get('/__proteum/diagnose', (req, res) => {
610
635
  const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
611
636
  const readNumber = (value: unknown, fallback: number) => {
@@ -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,12 +23,14 @@ 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
27
  writeFile(path.join(agentsRoot, 'diagnostics.md'), '# Diagnostics\n\n- Diagnostics rule\n');
28
28
  writeFile(path.join(agentsRoot, 'optimizations.md'), '# Optimizations\n\n- Optimization rule\n');
29
29
  writeFile(path.join(agentsRoot, 'client', 'AGENTS.md'), '# Client Rules\n\n- Client rule\n');
30
30
  writeFile(path.join(agentsRoot, 'client', 'pages', 'AGENTS.md'), '# Page Rules\n\n- Page rule\n');
31
31
  writeFile(path.join(agentsRoot, 'server', 'routes', 'AGENTS.md'), '# Route Rules\n\n- Route rule\n');
32
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');
33
34
  writeFile(path.join(agentsRoot, 'tests', 'e2e', 'AGENTS.md'), '# E2E Rules\n\n- E2E rule\n');
34
35
  writeFile(
35
36
  path.join(agentsRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'),
@@ -53,6 +54,7 @@ const createAppFixture = () => {
53
54
  '# Proteum-managed instruction files',
54
55
  '/AGENTS.md',
55
56
  '/CODING_STYLE.md',
57
+ '/DOCUMENTATION.md',
56
58
  '# End Proteum-managed instruction files',
57
59
  '/.proteum',
58
60
  '',
@@ -62,23 +64,61 @@ const createAppFixture = () => {
62
64
  return appRoot;
63
65
  };
64
66
 
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
+
65
84
  test('standalone configure creates tracked instruction files with routing contract and split docs', () => {
66
85
  const coreRoot = createCoreFixture();
67
86
  const appRoot = createAppFixture();
68
87
  const result = configureProjectAgentInstructions({ appRoot, coreRoot });
69
88
  const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
70
89
  const codingStyleContent = fs.readFileSync(path.join(appRoot, 'CODING_STYLE.md'), 'utf8');
90
+ const documentationContent = fs.readFileSync(path.join(appRoot, 'DOCUMENTATION.md'), 'utf8');
71
91
  const gitignoreContent = fs.readFileSync(path.join(appRoot, '.gitignore'), 'utf8');
72
92
 
73
93
  assert.equal(result.blocked.length, 0);
74
94
  assert.match(agentsContent, /^# Proteum Instructions/m);
75
95
  assert.match(agentsContent, /<!-- proteum-instructions:start -->/);
76
96
  assert.match(agentsContent, /## Agent Routing Contract/);
77
- assert.match(agentsContent, /npx proteum orient <query>/);
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/);
78
114
  assert.doesNotMatch(agentsContent, /## Source: CODING_STYLE\.md/);
79
115
  assert.match(codingStyleContent, /## Source: CODING_STYLE\.md/);
80
116
  assert.match(codingStyleContent, /## Coding Style/);
81
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/);
82
122
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), true);
83
123
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md')), true);
84
124
  assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /Journey rule/);
@@ -86,6 +126,7 @@ test('standalone configure creates tracked instruction files with routing contra
86
126
  assert.doesNotMatch(agentsContent, /Before reading or applying instructions from this file/);
87
127
  assert.doesNotMatch(gitignoreContent, /Proteum-managed instruction files/);
88
128
  assert.doesNotMatch(gitignoreContent, /^\/AGENTS\.md$/m);
129
+ assert.doesNotMatch(gitignoreContent, /^\/DOCUMENTATION\.md$/m);
89
130
  });
90
131
 
91
132
  test('configure preserves project content outside the managed section', () => {
@@ -176,6 +217,10 @@ test('monorepo configure writes root and app instruction files', () => {
176
217
 
177
218
  fs.mkdirSync(path.join(monorepoRoot, '.git'));
178
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');
179
224
 
180
225
  configureProjectAgentInstructions({ appRoot, coreRoot });
181
226
 
@@ -184,16 +229,26 @@ test('monorepo configure writes root and app instruction files', () => {
184
229
  assert.equal(result.mode, 'monorepo');
185
230
  assert.equal(resolveProjectAgentMonorepoRoot(appRoot), fs.realpathSync(monorepoRoot));
186
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/);
187
235
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
236
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'DOCUMENTATION.md'), 'utf8'), /## Source: DOCUMENTATION\.md/);
188
237
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'diagnostics.md'), 'utf8'), /## Source: diagnostics\.md/);
189
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/);
190
240
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'AGENTS.md'), 'utf8'), /## Source: tests\/e2e\/AGENTS\.md/);
191
241
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: tests\/e2e\/REAL_WORLD_JOURNEY_TESTS\.md/);
192
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);
193
244
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), false);
194
- assert.match(fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
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/);
195
249
  assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
196
250
  assert.equal(fs.existsSync(path.join(appRoot, 'CODING_STYLE.md')), false);
251
+ assert.equal(fs.existsSync(path.join(appRoot, 'DOCUMENTATION.md')), false);
197
252
  assert.equal(fs.existsSync(path.join(appRoot, 'diagnostics.md')), false);
198
253
  assert.equal(fs.existsSync(path.join(appRoot, 'optimizations.md')), false);
199
254
  assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);