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.
- package/AGENTS.md +8 -3
- package/README.md +20 -15
- package/agents/project/AGENTS.md +16 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +10 -9
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +15 -8
- package/agents/project/server/services/AGENTS.md +1 -0
- package/agents/project/tests/AGENTS.md +1 -0
- package/cli/commands/db.ts +160 -0
- package/cli/commands/dev.ts +148 -25
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/explain.ts +38 -9
- package/cli/commands/mcp.ts +126 -9
- package/cli/commands/orient.ts +44 -17
- package/cli/commands/runtime.ts +100 -17
- package/cli/mcp/router.ts +1028 -0
- package/cli/presentation/commands.ts +56 -25
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +163 -21
- package/cli/runtime/devSessions.ts +328 -2
- package/cli/runtime/mcpDaemon.ts +288 -0
- package/cli/runtime/ports.ts +151 -0
- package/cli/utils/agents.ts +94 -17
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/database.ts +226 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +8 -1
- package/common/dev/mcpPayloads.ts +456 -17
- package/common/dev/mcpServer.ts +51 -0
- package/docs/agent-routing.md +32 -21
- package/docs/dev-commands.md +1 -1
- package/docs/dev-sessions.md +3 -1
- package/docs/diagnostics.md +21 -20
- package/docs/mcp.md +114 -50
- package/docs/migrate-from-2.1.3.md +3 -5
- package/docs/request-tracing.md +3 -3
- package/package.json +10 -3
- package/server/app/devDiagnostics.ts +92 -0
- package/server/app/devMcp.ts +55 -0
- package/server/services/prisma/mariadb.ts +7 -3
- package/server/services/router/http/index.ts +25 -0
- package/server/services/router/request/ip.test.cjs +0 -1
- package/tests/agents-utils.test.cjs +58 -3
- package/tests/cli-mcp-command.test.cjs +327 -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 +0 -1
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +0 -1
- package/tests/mcp.test.cjs +769 -2
- package/tests/router-cache-config.test.cjs +0 -1
- package/vitest.config.mjs +9 -0
- package/cli/mcp/provider.ts +0 -365
- package/cli/mcp/stdio.ts +0 -16
|
@@ -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
|
+
};
|
package/cli/utils/agents.ts
CHANGED
|
@@ -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;
|
|
@@ -61,6 +62,7 @@ const managedInstructionSectionEnd = '<!-- proteum-instructions:end -->';
|
|
|
61
62
|
const managedInstructionSectionIntro = 'This section is managed by `proteum configure agents`.';
|
|
62
63
|
|
|
63
64
|
const sharedRootDocumentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
65
|
+
{ projectPath: 'DOCUMENTATION.md', content: 'source' },
|
|
64
66
|
{ projectPath: 'CODING_STYLE.md', content: 'source' },
|
|
65
67
|
{ projectPath: 'diagnostics.md', content: 'source' },
|
|
66
68
|
{ projectPath: 'optimizations.md', content: 'source' },
|
|
@@ -73,7 +75,8 @@ const sharedAppAreaAgentInstructionDefinitions: TAgentInstructionDefinition[] =
|
|
|
73
75
|
{ projectPath: path.join('server', 'routes', 'AGENTS.md'), content: 'source' },
|
|
74
76
|
];
|
|
75
77
|
|
|
76
|
-
const
|
|
78
|
+
const sharedTestAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
79
|
+
{ projectPath: path.join('tests', 'AGENTS.md'), ensureParentDir: true, content: 'source' },
|
|
77
80
|
{ projectPath: path.join('tests', 'e2e', 'AGENTS.md'), ensureParentDir: true, content: 'source' },
|
|
78
81
|
{ projectPath: path.join('tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), ensureParentDir: true, content: 'source' },
|
|
79
82
|
];
|
|
@@ -82,7 +85,7 @@ const standaloneAppAgentInstructionDefinitions: TAgentInstructionDefinition[] =
|
|
|
82
85
|
{ projectPath: 'AGENTS.md', content: 'router' },
|
|
83
86
|
...sharedRootDocumentInstructionDefinitions,
|
|
84
87
|
...sharedAppAreaAgentInstructionDefinitions,
|
|
85
|
-
...
|
|
88
|
+
...sharedTestAgentInstructionDefinitions,
|
|
86
89
|
];
|
|
87
90
|
|
|
88
91
|
const monorepoAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
@@ -93,7 +96,7 @@ const monorepoAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
|
93
96
|
const monorepoRootAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
94
97
|
{ projectPath: 'AGENTS.md', content: 'router' },
|
|
95
98
|
...sharedRootDocumentInstructionDefinitions,
|
|
96
|
-
...
|
|
99
|
+
...sharedTestAgentInstructionDefinitions,
|
|
97
100
|
];
|
|
98
101
|
|
|
99
102
|
const legacyProjectInstructionGitignoreBlockStart = '# Proteum-managed instruction symlinks';
|
|
@@ -130,7 +133,20 @@ export function configureProjectAgentInstructions({
|
|
|
130
133
|
updated: [],
|
|
131
134
|
updatedGitignores: [],
|
|
132
135
|
};
|
|
133
|
-
const
|
|
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;
|
|
134
150
|
|
|
135
151
|
if (mode === 'monorepo' && normalizedMonorepoRoot) {
|
|
136
152
|
result.monorepoRoot = normalizedMonorepoRoot;
|
|
@@ -141,7 +157,7 @@ export function configureProjectAgentInstructions({
|
|
|
141
157
|
rootInstructions,
|
|
142
158
|
'[agents]',
|
|
143
159
|
path.join(coreRoot, 'agents', 'project'),
|
|
144
|
-
|
|
160
|
+
rootEmbeddedInstructions,
|
|
145
161
|
{
|
|
146
162
|
dryRun,
|
|
147
163
|
overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
|
|
@@ -159,7 +175,7 @@ export function configureProjectAgentInstructions({
|
|
|
159
175
|
appInstructions,
|
|
160
176
|
'[agents]',
|
|
161
177
|
path.join(coreRoot, 'agents', 'project'),
|
|
162
|
-
|
|
178
|
+
appEmbeddedInstructions,
|
|
163
179
|
{
|
|
164
180
|
dryRun,
|
|
165
181
|
overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
|
|
@@ -170,7 +186,7 @@ export function configureProjectAgentInstructions({
|
|
|
170
186
|
if (mode === 'monorepo') {
|
|
171
187
|
const retiredAppRootFiles = removeManagedInstructionFiles(
|
|
172
188
|
normalizedAppRoot,
|
|
173
|
-
[...sharedRootDocumentInstructionDefinitions, ...
|
|
189
|
+
[...sharedRootDocumentInstructionDefinitions, ...sharedTestAgentInstructionDefinitions],
|
|
174
190
|
'[agents]',
|
|
175
191
|
path.join(coreRoot, 'agents', 'project'),
|
|
176
192
|
{
|
|
@@ -182,7 +198,7 @@ export function configureProjectAgentInstructions({
|
|
|
182
198
|
|
|
183
199
|
const appGitignoreCleanupInstructions =
|
|
184
200
|
mode === 'monorepo'
|
|
185
|
-
? [...appInstructions, ...sharedRootDocumentInstructionDefinitions, ...
|
|
201
|
+
? [...appInstructions, ...sharedRootDocumentInstructionDefinitions, ...sharedTestAgentInstructionDefinitions]
|
|
186
202
|
: appInstructions;
|
|
187
203
|
|
|
188
204
|
if (
|
|
@@ -544,7 +560,41 @@ function renderSingleProjectInstruction({
|
|
|
544
560
|
return lines.join('\n');
|
|
545
561
|
}
|
|
546
562
|
|
|
547
|
-
function
|
|
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 [];
|
|
574
|
+
|
|
575
|
+
const summaries = appRoots.map((candidate) => readProteumAppRootSummary(candidate, monorepoRoot));
|
|
576
|
+
|
|
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
|
+
}
|
|
596
|
+
|
|
597
|
+
function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoRegistry = false, monorepoRoot }: TProjectInstructionArgs) {
|
|
548
598
|
const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
|
|
549
599
|
if (!fs.existsSync(agentSourceRoot)) throw new Error(`Missing project instruction source root: ${agentSourceRoot}`);
|
|
550
600
|
|
|
@@ -556,36 +606,63 @@ function renderEmbeddedProjectInstructions({ coreRoot }: TProjectInstructionArgs
|
|
|
556
606
|
'',
|
|
557
607
|
'## Agent Routing Contract',
|
|
558
608
|
'',
|
|
559
|
-
'Proteum CLI
|
|
609
|
+
'Proteum CLI and MCP outputs are optimized for agents. Do not load the whole instruction corpus up front.',
|
|
560
610
|
'',
|
|
561
|
-
'
|
|
562
|
-
'2. Read only the files returned in `mustRead` plus the conditional docs that match the current task.',
|
|
563
|
-
'3. Use `npx proteum runtime status` before starting a dev server, so an existing tracked session can be reused.',
|
|
564
|
-
'4. Use `npx proteum diagnose <target>` for request-time issues before raw trace, perf, browser, or broad source search.',
|
|
565
|
-
'5. Use `--full`, `--manifest`, or `--events` only when a compact CLI response says the omitted detail is needed.',
|
|
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.',
|
|
566
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`, `logs_tail`, and `db_query` for read-only runtime, owner, instruction, route, trace, perf, log, and database 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 }) : []),
|
|
567
629
|
'## Always-On Safety',
|
|
568
630
|
'',
|
|
569
631
|
'- Never edit generated files under `.proteum`.',
|
|
570
632
|
'- Never create or edit Prisma migration files manually.',
|
|
571
633
|
'- Never run schema-mutating SQL such as `ALTER TABLE`, `CREATE TABLE`, `DROP TABLE`, or `CREATE INDEX`.',
|
|
634
|
+
'- For read-only SQL diagnosis, use MCP `db_query` or `npx proteum db query "<sql>"`; only one capped `SELECT`, `SHOW`, or `EXPLAIN` statement is allowed.',
|
|
572
635
|
'- 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.',
|
|
636
|
+
'- For production changes, add or update focused unit tests for touched behavior when applicable, targeting 100% meaningful coverage for changed production paths.',
|
|
573
637
|
'- Do not run `git restore` or `git reset`.',
|
|
574
638
|
'- Keep `proteum dev` sessions tracked with explicit session files and do not replace another live session.',
|
|
575
639
|
'',
|
|
640
|
+
'## Triggered Instruction Reads',
|
|
641
|
+
'',
|
|
642
|
+
'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.',
|
|
643
|
+
'',
|
|
644
|
+
'- Git lifecycle (`commit`, `and commit`, `stage`, `push`, `PR`, pull request): read Root contract fallback before any git write.',
|
|
645
|
+
'- Before finishing production code changes: read Root contract fallback, `CODING_STYLE.md`, `tests/AGENTS.md`, and any touched area `AGENTS.md`.',
|
|
646
|
+
'- Runtime-visible, request-time, router, SSR, browser, or controller behavior: read Root contract fallback and `diagnostics.md` for verification routing.',
|
|
647
|
+
'- Non-trivial feature, product, business-rule, UX, copy, or docs changes: read `DOCUMENTATION.md` before editing.',
|
|
648
|
+
'- Implementation edits: read `CODING_STYLE.md` before editing, plus the matching area file from the routing table.',
|
|
649
|
+
'',
|
|
576
650
|
'## Routing Table',
|
|
577
651
|
'',
|
|
652
|
+
'- Non-trivial coding tasks, feature docs, product intent, acceptance criteria, or docs updates: read `DOCUMENTATION.md`.',
|
|
578
653
|
'- Raw errors, failing requests, traces, perf, or reproduction: read `diagnostics.md`.',
|
|
579
654
|
'- Implementation edits: read `CODING_STYLE.md` before editing.',
|
|
580
655
|
'- Client files or pages: read `client/AGENTS.md`; for page route/data/render work also read `client/pages/AGENTS.md`.',
|
|
581
656
|
'- Server services: read `server/services/AGENTS.md`.',
|
|
582
657
|
'- Manual server routes: read `server/routes/AGENTS.md`.',
|
|
583
|
-
'-
|
|
658
|
+
'- Unit tests, integration tests, or test-area work: read `tests/AGENTS.md`.',
|
|
659
|
+
'- E2E work: read `tests/AGENTS.md`, `tests/e2e/AGENTS.md`, and `tests/e2e/REAL_WORLD_JOURNEY_TESTS.md`.',
|
|
584
660
|
'- Package, runtime, build, or client-performance decisions: read `optimizations.md` after implementation or when explicitly optimizing.',
|
|
585
661
|
'',
|
|
586
662
|
'## Canonical Source Map',
|
|
587
663
|
'',
|
|
588
664
|
`- Root contract fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'AGENTS.md'))}`,
|
|
665
|
+
`- Documentation fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'DOCUMENTATION.md'))}`,
|
|
589
666
|
`- Diagnostics fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'diagnostics.md'))}`,
|
|
590
667
|
`- Optimization fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'optimizations.md'))}`,
|
|
591
668
|
`- Coding style fallback: ${normalizeProjectPathForGitignore(path.join(coreRoot, 'agents', 'project', 'CODING_STYLE.md'))}`,
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { Dirent } from 'fs';
|
|
4
|
+
|
|
5
|
+
import { readProteumManifest, type TProteumManifest } from '../compiler/common/proteumManifest';
|
|
6
|
+
|
|
7
|
+
export type TProteumAppRootSummary = {
|
|
8
|
+
appRoot: string;
|
|
9
|
+
hasManifest: boolean;
|
|
10
|
+
manifest?: {
|
|
11
|
+
counts: {
|
|
12
|
+
connectedProjects: number;
|
|
13
|
+
controllers: number;
|
|
14
|
+
routes: number;
|
|
15
|
+
};
|
|
16
|
+
diagnostics: {
|
|
17
|
+
errors: number;
|
|
18
|
+
warnings: number;
|
|
19
|
+
};
|
|
20
|
+
identifier: string;
|
|
21
|
+
name: string;
|
|
22
|
+
routerPort: number;
|
|
23
|
+
};
|
|
24
|
+
manifestError?: string;
|
|
25
|
+
packageManager: 'npm' | 'pnpm' | 'yarn' | 'unknown';
|
|
26
|
+
relativeAppRoot?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const proteumAppRootRequiredEntries = ['package.json', 'identity.config.ts', 'proteum.config.ts', 'client', 'server'];
|
|
30
|
+
const ignoredSearchDirectories = new Set([
|
|
31
|
+
'.cache',
|
|
32
|
+
'.git',
|
|
33
|
+
'.proteum',
|
|
34
|
+
'bin',
|
|
35
|
+
'coverage',
|
|
36
|
+
'dev',
|
|
37
|
+
'node_modules',
|
|
38
|
+
'playwright-report',
|
|
39
|
+
'test-results',
|
|
40
|
+
'var',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const resolveExistingPath = (value: string) => {
|
|
44
|
+
const resolved = path.resolve(value);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return fs.realpathSync(resolved);
|
|
48
|
+
} catch {
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const pathEntryExists = (filepath: string) => {
|
|
54
|
+
try {
|
|
55
|
+
fs.lstatSync(filepath);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isDirectory = (filepath: string) => {
|
|
63
|
+
try {
|
|
64
|
+
return fs.statSync(filepath).isDirectory();
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const resolveSearchRoot = (value: string) => {
|
|
71
|
+
const resolved = resolveExistingPath(value);
|
|
72
|
+
if (isDirectory(resolved)) return resolved;
|
|
73
|
+
return path.dirname(resolved);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const isProteumAppRoot = (workdir: string) =>
|
|
77
|
+
proteumAppRootRequiredEntries.every((entry) => pathEntryExists(path.join(workdir, entry)));
|
|
78
|
+
|
|
79
|
+
export const findNearestProteumAppRoot = (startPath: string) => {
|
|
80
|
+
let currentPath = resolveSearchRoot(startPath);
|
|
81
|
+
|
|
82
|
+
while (true) {
|
|
83
|
+
if (isProteumAppRoot(currentPath)) return currentPath;
|
|
84
|
+
|
|
85
|
+
const parentPath = path.dirname(currentPath);
|
|
86
|
+
if (parentPath === currentPath) return undefined;
|
|
87
|
+
currentPath = parentPath;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const findProteumAppRootsUnder = (root: string, { maxDepth = 5 }: { maxDepth?: number } = {}) => {
|
|
92
|
+
const searchRoot = resolveSearchRoot(root);
|
|
93
|
+
const appRoots: string[] = [];
|
|
94
|
+
const seen = new Set<string>();
|
|
95
|
+
|
|
96
|
+
const visit = (directory: string, depth: number) => {
|
|
97
|
+
const canonicalDirectory = resolveExistingPath(directory);
|
|
98
|
+
if (seen.has(canonicalDirectory)) return;
|
|
99
|
+
seen.add(canonicalDirectory);
|
|
100
|
+
|
|
101
|
+
if (isProteumAppRoot(canonicalDirectory)) {
|
|
102
|
+
appRoots.push(canonicalDirectory);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (depth >= maxDepth) return;
|
|
107
|
+
|
|
108
|
+
let entries: Dirent[];
|
|
109
|
+
try {
|
|
110
|
+
entries = fs.readdirSync(canonicalDirectory, { withFileTypes: true });
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (!entry.isDirectory()) continue;
|
|
117
|
+
if (ignoredSearchDirectories.has(entry.name)) continue;
|
|
118
|
+
visit(path.join(canonicalDirectory, entry.name), depth + 1);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
visit(searchRoot, 0);
|
|
123
|
+
|
|
124
|
+
return appRoots.sort((left, right) => left.localeCompare(right));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const findPackageManager = (appRoot: string): TProteumAppRootSummary['packageManager'] => {
|
|
128
|
+
let currentPath = path.resolve(appRoot);
|
|
129
|
+
|
|
130
|
+
while (true) {
|
|
131
|
+
if (pathEntryExists(path.join(currentPath, 'package-lock.json'))) return 'npm';
|
|
132
|
+
if (pathEntryExists(path.join(currentPath, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
133
|
+
if (pathEntryExists(path.join(currentPath, 'yarn.lock'))) return 'yarn';
|
|
134
|
+
|
|
135
|
+
if (pathEntryExists(path.join(currentPath, '.git'))) return 'unknown';
|
|
136
|
+
|
|
137
|
+
const parentPath = path.dirname(currentPath);
|
|
138
|
+
if (parentPath === currentPath) return 'unknown';
|
|
139
|
+
currentPath = parentPath;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const summarizeManifest = (manifest: TProteumManifest): NonNullable<TProteumAppRootSummary['manifest']> => {
|
|
144
|
+
const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
|
|
145
|
+
const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
counts: {
|
|
149
|
+
connectedProjects: manifest.connectedProjects.length,
|
|
150
|
+
controllers: manifest.controllers.length,
|
|
151
|
+
routes: manifest.routes.client.length + manifest.routes.server.length,
|
|
152
|
+
},
|
|
153
|
+
diagnostics: { errors, warnings },
|
|
154
|
+
identifier: manifest.app.identity.identifier,
|
|
155
|
+
name: manifest.app.identity.name,
|
|
156
|
+
routerPort: manifest.env.resolved.routerPort,
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const readProteumAppRootSummary = (appRoot: string, baseRoot?: string): TProteumAppRootSummary => {
|
|
161
|
+
const normalizedAppRoot = resolveExistingPath(appRoot);
|
|
162
|
+
const relativeAppRoot = baseRoot ? path.relative(resolveExistingPath(baseRoot), normalizedAppRoot) || '.' : undefined;
|
|
163
|
+
const summary: TProteumAppRootSummary = {
|
|
164
|
+
appRoot: normalizedAppRoot,
|
|
165
|
+
hasManifest: false,
|
|
166
|
+
packageManager: findPackageManager(normalizedAppRoot),
|
|
167
|
+
relativeAppRoot,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const manifest = readProteumManifest(normalizedAppRoot);
|
|
172
|
+
summary.hasManifest = true;
|
|
173
|
+
summary.manifest = summarizeManifest(manifest);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
summary.manifestError = error instanceof Error ? error.message : String(error);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return summary;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const resolveProteumAppRootContext = (cwd: string) => {
|
|
182
|
+
const normalizedCwd = resolveSearchRoot(cwd);
|
|
183
|
+
const nearestAppRoot = findNearestProteumAppRoot(normalizedCwd);
|
|
184
|
+
const appRoots = nearestAppRoot ? [nearestAppRoot] : findProteumAppRootsUnder(normalizedCwd);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
cwd: normalizedCwd,
|
|
188
|
+
isAppRoot: nearestAppRoot === normalizedCwd,
|
|
189
|
+
isWrapper: !nearestAppRoot && appRoots.length > 0,
|
|
190
|
+
nearestAppRoot,
|
|
191
|
+
appRoots,
|
|
192
|
+
appCandidates: appRoots.map((appRoot) => readProteumAppRootSummary(appRoot, normalizedCwd)),
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const quoteShellPath = (value: string) => JSON.stringify(value);
|
|
197
|
+
|
|
198
|
+
const createAppScopedCommand = ({
|
|
199
|
+
appRoot,
|
|
200
|
+
baseRoot,
|
|
201
|
+
command,
|
|
202
|
+
}: {
|
|
203
|
+
appRoot: string;
|
|
204
|
+
baseRoot?: string;
|
|
205
|
+
command: string;
|
|
206
|
+
}) => {
|
|
207
|
+
const relativeAppRoot = baseRoot ? path.relative(resolveExistingPath(baseRoot), resolveExistingPath(appRoot)) || '.' : '';
|
|
208
|
+
|
|
209
|
+
if (!relativeAppRoot || relativeAppRoot === '.') return command;
|
|
210
|
+
return `cd ${quoteShellPath(relativeAppRoot)} && ${command}`;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const createStartDevCommand = ({
|
|
214
|
+
appRoot,
|
|
215
|
+
baseRoot,
|
|
216
|
+
port,
|
|
217
|
+
}: {
|
|
218
|
+
appRoot: string;
|
|
219
|
+
baseRoot?: string;
|
|
220
|
+
port?: number;
|
|
221
|
+
}) => {
|
|
222
|
+
const command = `npx proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${port || '<free-port>'}`;
|
|
223
|
+
|
|
224
|
+
return createAppScopedCommand({ appRoot, baseRoot, command });
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const createRuntimeStatusCommand = ({ appRoot, baseRoot }: { appRoot: string; baseRoot?: string }) =>
|
|
228
|
+
createAppScopedCommand({
|
|
229
|
+
appRoot,
|
|
230
|
+
baseRoot,
|
|
231
|
+
command: 'npx proteum runtime status',
|
|
232
|
+
});
|