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.
- package/AGENTS.md +10 -4
- package/README.md +58 -15
- package/agents/project/AGENTS.md +53 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +12 -7
- package/agents/project/optimizations.md +1 -0
- package/agents/project/root/AGENTS.md +24 -9
- package/agents/project/tests/AGENTS.md +7 -0
- package/agents/project/tests/e2e/AGENTS.md +13 -0
- package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
- package/cli/commands/connect.ts +40 -4
- package/cli/commands/dev.ts +148 -25
- package/cli/commands/diagnose.ts +138 -5
- package/cli/commands/doctor.ts +24 -4
- package/cli/commands/explain.ts +134 -6
- package/cli/commands/mcp.ts +133 -0
- package/cli/commands/orient.ts +93 -3
- package/cli/commands/perf.ts +118 -13
- package/cli/commands/runtime.ts +234 -0
- package/cli/commands/trace.ts +116 -21
- package/cli/mcp/router.ts +1010 -0
- package/cli/presentation/commands.ts +93 -26
- package/cli/presentation/devSession.ts +2 -0
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +215 -24
- package/cli/runtime/devSessions.ts +328 -2
- package/cli/runtime/mcpDaemon.ts +288 -0
- package/cli/runtime/ports.ts +151 -0
- package/cli/utils/agentOutput.ts +46 -0
- package/cli/utils/agents.ts +194 -51
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +22 -7
- package/common/dev/mcpPayloads.ts +1150 -0
- package/common/dev/mcpServer.ts +287 -0
- package/docs/agent-routing.md +137 -0
- package/docs/dev-commands.md +2 -0
- package/docs/dev-sessions.md +4 -1
- package/docs/diagnostics.md +70 -24
- package/docs/mcp.md +206 -0
- package/docs/migrate-from-2.1.3.md +14 -6
- package/docs/request-tracing.md +12 -6
- package/package.json +11 -3
- package/server/app/devMcp.ts +204 -0
- package/server/services/router/http/cache.ts +116 -0
- package/server/services/router/http/index.ts +94 -35
- package/server/services/router/index.ts +8 -11
- package/server/services/router/request/ip.test.cjs +0 -1
- package/tests/agents-utils.test.cjs +92 -14
- package/tests/cli-mcp-command.test.cjs +262 -0
- package/tests/codex-mcp-usage.test.cjs +307 -0
- package/tests/dev-sessions.test.cjs +113 -0
- package/tests/dev-transpile-watch.test.cjs +117 -9
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +66 -0
- package/tests/mcp.test.cjs +873 -0
- package/tests/router-cache-config.test.cjs +73 -0
- 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
|
|
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:
|
|
417
|
-
lastModified:
|
|
405
|
+
etag: publicAssetEtag,
|
|
406
|
+
lastModified: publicAssetLastModified,
|
|
418
407
|
setHeaders: (res, filePath) => {
|
|
419
|
-
if (
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1141
|
+
} else {
|
|
1142
|
+
res.setHeader('Surrogate-Control', htmlCache.surrogateControl);
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
|
-
|
|
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
|
}
|
|
@@ -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('
|
|
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, /##
|
|
67
|
-
assert.match(agentsContent,
|
|
68
|
-
assert.match(agentsContent,
|
|
69
|
-
assert.match(
|
|
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, /##
|
|
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, /##
|
|
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'), /##
|
|
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, '
|
|
171
|
-
assert.match(fs.readFileSync(path.join(monorepoRoot, '
|
|
172
|
-
assert.match(fs.readFileSync(path.join(
|
|
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
|
|
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:
|
|
339
|
+
assert.match(fs.readFileSync(blockedPath, 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
262
340
|
});
|