proteum 2.2.2 → 2.2.6

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.
@@ -12,7 +12,7 @@ import { logVerbose } from '../runtime/verbose';
12
12
  ----------------------------------*/
13
13
 
14
14
  type TProjectInstructionArgs = { coreRoot: string };
15
- type TConfigureProjectAgentSymlinksArgs = {
15
+ type TConfigureProjectAgentInstructionsArgs = {
16
16
  appRoot: string;
17
17
  coreRoot: string;
18
18
  dryRun?: boolean;
@@ -20,9 +20,14 @@ type TConfigureProjectAgentSymlinksArgs = {
20
20
  overwriteBlockedPaths?: string[];
21
21
  };
22
22
 
23
- type TAgentLinkDefinition = { projectPath: string; sourcePath: string; ensureParentDir?: boolean };
23
+ type TAgentInstructionDefinition = {
24
+ includeWorktreeBootstrap?: boolean;
25
+ projectPath: string;
26
+ sourcePath: string;
27
+ ensureParentDir?: boolean;
28
+ };
24
29
 
25
- type TEnsureSymlinksResult = {
30
+ type TEnsureInstructionStubsResult = {
26
31
  blocked: string[];
27
32
  created: string[];
28
33
  overwritten: string[];
@@ -30,7 +35,7 @@ type TEnsureSymlinksResult = {
30
35
  updated: string[];
31
36
  };
32
37
 
33
- export type TConfigureProjectAgentSymlinksResult = {
38
+ export type TConfigureProjectAgentInstructionsResult = {
34
39
  appRoot: string;
35
40
  blocked: string[];
36
41
  created: string[];
@@ -51,7 +56,24 @@ export type TProjectAgentFileInspection = {
51
56
  - CONSTANTS
52
57
  ----------------------------------*/
53
58
 
54
- const sharedAppAgentLinkDefinitions: TAgentLinkDefinition[] = [
59
+ const managedInstructionStubHeader = '# Proteum Managed Instructions';
60
+ const managedInstructionStubIntro = 'This file is managed by `proteum configure agents`.';
61
+ const newProteumWorktreeBootstrapBlock = [
62
+ '## New Proteum Worktree Bootstrap',
63
+ '',
64
+ 'When starting work in a newly created Proteum worktree, immediately copy the `.env` file from the main worktree, change it to use a different free port by updating `PORT`, `URL`, and `URL_INTERNAL`, and run:',
65
+ '',
66
+ '```',
67
+ 'npx proteum configure agents',
68
+ 'npm i',
69
+ 'npx prisma generate',
70
+ 'npx proteum refresh',
71
+ '```',
72
+ '',
73
+ 'Run it before reading project-specific AGENTS files or making code changes. If the command is interactive, answer according to the current repository layout, then continue by reading the generated AGENTS.md files.',
74
+ ];
75
+
76
+ const sharedAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
55
77
  { projectPath: 'CODING_STYLE.md', sourcePath: 'CODING_STYLE.md' },
56
78
  { projectPath: 'diagnostics.md', sourcePath: 'diagnostics.md' },
57
79
  { projectPath: 'optimizations.md', sourcePath: 'optimizations.md' },
@@ -65,34 +87,36 @@ const sharedAppAgentLinkDefinitions: TAgentLinkDefinition[] = [
65
87
  { projectPath: path.join('tests', 'e2e', 'AGENTS.md'), sourcePath: path.join('tests', 'AGENTS.md') },
66
88
  ];
67
89
 
68
- const standaloneAppAgentLinkDefinitions: TAgentLinkDefinition[] = [
69
- { projectPath: 'AGENTS.md', sourcePath: 'AGENTS.md' },
70
- ...sharedAppAgentLinkDefinitions,
90
+ const standaloneAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
91
+ { projectPath: 'AGENTS.md', sourcePath: 'AGENTS.md', includeWorktreeBootstrap: true },
92
+ ...sharedAppAgentInstructionDefinitions,
71
93
  ];
72
94
 
73
- const monorepoAppAgentLinkDefinitions: TAgentLinkDefinition[] = [
95
+ const monorepoAppAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
74
96
  { projectPath: 'AGENTS.md', sourcePath: path.join('app-root', 'AGENTS.md') },
75
- ...sharedAppAgentLinkDefinitions,
97
+ ...sharedAppAgentInstructionDefinitions,
76
98
  ];
77
99
 
78
- const monorepoRootAgentLinkDefinitions: TAgentLinkDefinition[] = [
79
- { projectPath: 'AGENTS.md', sourcePath: path.join('root', 'AGENTS.md') },
100
+ const monorepoRootAgentInstructionDefinitions: TAgentInstructionDefinition[] = [
101
+ { projectPath: 'AGENTS.md', sourcePath: path.join('root', 'AGENTS.md'), includeWorktreeBootstrap: true },
80
102
  ];
81
103
 
82
- const projectInstructionGitignoreBlockStart = '# Proteum-managed instruction symlinks';
83
- const projectInstructionGitignoreBlockEnd = '# End Proteum-managed instruction symlinks';
104
+ const legacyProjectInstructionGitignoreBlockStart = '# Proteum-managed instruction symlinks';
105
+ const legacyProjectInstructionGitignoreBlockEnd = '# End Proteum-managed instruction symlinks';
106
+ const projectInstructionGitignoreBlockStart = '# Proteum-managed instruction files';
107
+ const projectInstructionGitignoreBlockEnd = '# End Proteum-managed instruction files';
84
108
 
85
109
  /*----------------------------------
86
110
  - PUBLIC API
87
111
  ----------------------------------*/
88
112
 
89
- export function configureProjectAgentSymlinks({
113
+ export function configureProjectAgentInstructions({
90
114
  appRoot,
91
115
  coreRoot,
92
116
  dryRun = false,
93
117
  monorepoRoot,
94
118
  overwriteBlockedPaths = [],
95
- }: TConfigureProjectAgentSymlinksArgs): TConfigureProjectAgentSymlinksResult {
119
+ }: TConfigureProjectAgentInstructionsArgs): TConfigureProjectAgentInstructionsResult {
96
120
  const normalizedAppRoot = path.resolve(appRoot);
97
121
  const normalizedMonorepoRoot = monorepoRoot ? path.resolve(monorepoRoot) : undefined;
98
122
  const normalizedOverwriteBlockedPaths = new Set(
@@ -100,7 +124,7 @@ export function configureProjectAgentSymlinks({
100
124
  );
101
125
  const mode =
102
126
  normalizedMonorepoRoot && normalizedMonorepoRoot !== normalizedAppRoot ? ('monorepo' as const) : ('standalone' as const);
103
- const result: TConfigureProjectAgentSymlinksResult = {
127
+ const result: TConfigureProjectAgentInstructionsResult = {
104
128
  appRoot: normalizedAppRoot,
105
129
  blocked: [],
106
130
  created: [],
@@ -114,50 +138,66 @@ export function configureProjectAgentSymlinks({
114
138
  if (mode === 'monorepo' && normalizedMonorepoRoot) {
115
139
  result.monorepoRoot = normalizedMonorepoRoot;
116
140
 
117
- const rootLinks = getRootAgentLinkDefinitions({ coreRoot });
118
- const rootSymlinks = ensureSymlinks(normalizedMonorepoRoot, rootLinks, '[agents]', path.join(coreRoot, 'agents', 'project'), {
119
- dryRun,
120
- overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
121
- });
122
- mergeSymlinkResults(result, rootSymlinks, normalizedMonorepoRoot);
123
-
124
- if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedMonorepoRoot, linkDefinitions: rootLinks }))
141
+ const rootInstructions = getRootAgentInstructionDefinitions({ coreRoot });
142
+ const rootStubs = ensureInstructionStubs(
143
+ normalizedMonorepoRoot,
144
+ rootInstructions,
145
+ '[agents]',
146
+ path.join(coreRoot, 'agents', 'project'),
147
+ {
148
+ dryRun,
149
+ overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
150
+ },
151
+ );
152
+ mergeInstructionResults(result, rootStubs, normalizedMonorepoRoot);
153
+
154
+ if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedMonorepoRoot, instructionDefinitions: rootInstructions }))
125
155
  result.updatedGitignores.push(path.join(normalizedMonorepoRoot, '.gitignore'));
126
156
  }
127
157
 
128
- const appLinks = getAppAgentLinkDefinitions({ coreRoot, mode });
129
- const appSymlinks = ensureSymlinks(normalizedAppRoot, appLinks, '[agents]', path.join(coreRoot, 'agents', 'project'), {
130
- dryRun,
131
- overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
132
- });
133
- mergeSymlinkResults(result, appSymlinks, normalizedAppRoot);
158
+ const appInstructions = getAppAgentInstructionDefinitions({ coreRoot, mode });
159
+ const appStubs = ensureInstructionStubs(
160
+ normalizedAppRoot,
161
+ appInstructions,
162
+ '[agents]',
163
+ path.join(coreRoot, 'agents', 'project'),
164
+ {
165
+ dryRun,
166
+ overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
167
+ },
168
+ );
169
+ mergeInstructionResults(result, appStubs, normalizedAppRoot);
134
170
 
135
- if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedAppRoot, linkDefinitions: appLinks }))
171
+ if (!dryRun && ensureInstructionGitignoreEntries({ rootDir: normalizedAppRoot, instructionDefinitions: appInstructions }))
136
172
  result.updatedGitignores.push(path.join(normalizedAppRoot, '.gitignore'));
137
173
 
138
174
  return result;
139
175
  }
140
176
 
177
+ export const configureProjectAgentSymlinks = configureProjectAgentInstructions;
178
+
141
179
  export function getProjectInstructionGitignoreEntries({ coreRoot }: TProjectInstructionArgs) {
142
180
  return Array.from(
143
181
  new Set(
144
- getAppAgentLinkDefinitions({ coreRoot, mode: 'standalone' }).map((linkDefinition) =>
145
- `/${normalizeProjectPathForGitignore(linkDefinition.projectPath)}`,
182
+ getAppAgentInstructionDefinitions({ coreRoot, mode: 'standalone' }).map((instructionDefinition) =>
183
+ `/${normalizeProjectPathForGitignore(instructionDefinition.projectPath)}`,
146
184
  ),
147
185
  ),
148
186
  );
149
187
  }
150
188
 
151
189
  export function renderProjectInstructionGitignoreBlock({ coreRoot }: TProjectInstructionArgs) {
152
- return renderInstructionGitignoreBlock({ linkDefinitions: getAppAgentLinkDefinitions({ coreRoot, mode: 'standalone' }) });
190
+ return renderInstructionGitignoreBlock({
191
+ instructionDefinitions: getAppAgentInstructionDefinitions({ coreRoot, mode: 'standalone' }),
192
+ });
153
193
  }
154
194
 
155
195
  export function inspectProjectAgentFiles({ appRoot }: { appRoot: string }): TProjectAgentFileInspection {
156
196
  const normalizedAppRoot = path.resolve(appRoot);
157
197
  const expectedAgentPaths = Array.from(
158
198
  new Set(
159
- standaloneAppAgentLinkDefinitions
160
- .map((linkDefinition) => linkDefinition.projectPath)
199
+ standaloneAppAgentInstructionDefinitions
200
+ .map((instructionDefinition) => instructionDefinition.projectPath)
161
201
  .filter((projectPath) => projectPath.endsWith('AGENTS.md')),
162
202
  ),
163
203
  );
@@ -187,42 +227,47 @@ export function inspectProjectAgentFiles({ appRoot }: { appRoot: string }): TPro
187
227
  - HELPERS
188
228
  ----------------------------------*/
189
229
 
190
- function getAppAgentLinkDefinitions({
230
+ function getAppAgentInstructionDefinitions({
191
231
  coreRoot,
192
232
  mode,
193
233
  }: TProjectInstructionArgs & { mode: 'monorepo' | 'standalone' }) {
194
234
  const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
195
- const sourceDefinitions = mode === 'monorepo' ? monorepoAppAgentLinkDefinitions : standaloneAppAgentLinkDefinitions;
235
+ const sourceDefinitions =
236
+ mode === 'monorepo' ? monorepoAppAgentInstructionDefinitions : standaloneAppAgentInstructionDefinitions;
196
237
 
197
- return resolveAgentLinkDefinitions({
238
+ return resolveAgentInstructionDefinitions({
198
239
  agentSourceRoot,
199
- linkDefinitions: sourceDefinitions,
240
+ instructionDefinitions: sourceDefinitions,
200
241
  });
201
242
  }
202
243
 
203
- function getRootAgentLinkDefinitions({ coreRoot }: TProjectInstructionArgs) {
204
- return resolveAgentLinkDefinitions({
244
+ function getRootAgentInstructionDefinitions({ coreRoot }: TProjectInstructionArgs) {
245
+ return resolveAgentInstructionDefinitions({
205
246
  agentSourceRoot: path.join(coreRoot, 'agents', 'project'),
206
- linkDefinitions: monorepoRootAgentLinkDefinitions,
247
+ instructionDefinitions: monorepoRootAgentInstructionDefinitions,
207
248
  });
208
249
  }
209
250
 
210
- function resolveAgentLinkDefinitions({
251
+ function resolveAgentInstructionDefinitions({
211
252
  agentSourceRoot,
212
- linkDefinitions,
253
+ instructionDefinitions,
213
254
  }: {
214
255
  agentSourceRoot: string;
215
- linkDefinitions: TAgentLinkDefinition[];
256
+ instructionDefinitions: TAgentInstructionDefinition[];
216
257
  }) {
217
- return linkDefinitions.map((linkDefinition) => ({
218
- ...linkDefinition,
219
- sourcePath: path.join(agentSourceRoot, linkDefinition.sourcePath),
258
+ return instructionDefinitions.map((instructionDefinition) => ({
259
+ ...instructionDefinition,
260
+ sourcePath: path.join(agentSourceRoot, instructionDefinition.sourcePath),
220
261
  }));
221
262
  }
222
263
 
223
- function renderInstructionGitignoreBlock({ linkDefinitions }: { linkDefinitions: TAgentLinkDefinition[] }) {
264
+ function renderInstructionGitignoreBlock({ instructionDefinitions }: { instructionDefinitions: TAgentInstructionDefinition[] }) {
224
265
  const entries = Array.from(
225
- new Set(linkDefinitions.map((linkDefinition) => `/${normalizeProjectPathForGitignore(linkDefinition.projectPath)}`)),
266
+ new Set(
267
+ instructionDefinitions.map(
268
+ (instructionDefinition) => `/${normalizeProjectPathForGitignore(instructionDefinition.projectPath)}`,
269
+ ),
270
+ ),
226
271
  );
227
272
 
228
273
  return [projectInstructionGitignoreBlockStart, ...entries, projectInstructionGitignoreBlockEnd].join('\n');
@@ -230,15 +275,17 @@ function renderInstructionGitignoreBlock({ linkDefinitions }: { linkDefinitions:
230
275
 
231
276
  function ensureInstructionGitignoreEntries({
232
277
  rootDir,
233
- linkDefinitions,
278
+ instructionDefinitions,
234
279
  }: {
235
280
  rootDir: string;
236
- linkDefinitions: TAgentLinkDefinition[];
281
+ instructionDefinitions: TAgentInstructionDefinition[];
237
282
  }) {
238
283
  const gitignoreFilepath = path.join(rootDir, '.gitignore');
239
284
  if (!pathEntryExists(gitignoreFilepath)) return false;
240
285
 
241
- const managedEntries = new Set(linkDefinitions.map((linkDefinition) => normalizeGitignoreEntry(linkDefinition.projectPath)));
286
+ const managedEntries = new Set(
287
+ instructionDefinitions.map((instructionDefinition) => normalizeGitignoreEntry(instructionDefinition.projectPath)),
288
+ );
242
289
  const lines = fs.readFileSync(gitignoreFilepath, 'utf8').split(/\r?\n/);
243
290
  const filteredLines: string[] = [];
244
291
  let insideManagedBlock = false;
@@ -246,12 +293,12 @@ function ensureInstructionGitignoreEntries({
246
293
  for (const line of lines) {
247
294
  const trimmedLine = line.trim();
248
295
 
249
- if (trimmedLine === projectInstructionGitignoreBlockStart) {
296
+ if (trimmedLine === projectInstructionGitignoreBlockStart || trimmedLine === legacyProjectInstructionGitignoreBlockStart) {
250
297
  insideManagedBlock = true;
251
298
  continue;
252
299
  }
253
300
 
254
- if (trimmedLine === projectInstructionGitignoreBlockEnd) {
301
+ if (trimmedLine === projectInstructionGitignoreBlockEnd || trimmedLine === legacyProjectInstructionGitignoreBlockEnd) {
255
302
  insideManagedBlock = false;
256
303
  continue;
257
304
  }
@@ -263,7 +310,7 @@ function ensureInstructionGitignoreEntries({
263
310
  }
264
311
 
265
312
  const baseContent = trimTrailingBlankLines(filteredLines).join('\n');
266
- const managedBlock = renderInstructionGitignoreBlock({ linkDefinitions });
313
+ const managedBlock = renderInstructionGitignoreBlock({ instructionDefinitions });
267
314
  const nextContent = baseContent ? `${baseContent}\n\n${managedBlock}\n` : `${managedBlock}\n`;
268
315
 
269
316
  if (nextContent === fs.readFileSync(gitignoreFilepath, 'utf8')) return false;
@@ -274,9 +321,9 @@ function ensureInstructionGitignoreEntries({
274
321
  return true;
275
322
  }
276
323
 
277
- function ensureSymlinks(
324
+ function ensureInstructionStubs(
278
325
  rootDir: string,
279
- linkDefinitions: TAgentLinkDefinition[],
326
+ instructionDefinitions: TAgentInstructionDefinition[],
280
327
  logPrefix: string,
281
328
  managedSourceRoot: string,
282
329
  {
@@ -286,8 +333,8 @@ function ensureSymlinks(
286
333
  dryRun: boolean;
287
334
  overwriteBlockedPaths: Set<string>;
288
335
  },
289
- ): TEnsureSymlinksResult {
290
- const result: TEnsureSymlinksResult = {
336
+ ): TEnsureInstructionStubsResult {
337
+ const result: TEnsureInstructionStubsResult = {
291
338
  blocked: [],
292
339
  created: [],
293
340
  overwritten: [],
@@ -295,24 +342,31 @@ function ensureSymlinks(
295
342
  updated: [],
296
343
  };
297
344
 
298
- for (const linkDefinition of linkDefinitions) {
299
- const projectFilepath = path.join(rootDir, linkDefinition.projectPath);
345
+ for (const instructionDefinition of instructionDefinitions) {
346
+ const projectFilepath = path.join(rootDir, instructionDefinition.projectPath);
300
347
  const projectParentDir = path.dirname(projectFilepath);
301
348
  const relativeProjectPath = path.relative(rootDir, projectFilepath) || '.';
302
349
 
303
- if (linkDefinition.ensureParentDir) fs.ensureDirSync(projectParentDir);
350
+ if (instructionDefinition.ensureParentDir) fs.ensureDirSync(projectParentDir);
304
351
  else if (!fs.existsSync(projectParentDir)) {
305
352
  result.skipped.push(relativeProjectPath);
306
353
  continue;
307
354
  }
308
355
 
309
- const sourceFilepath = linkDefinition.sourcePath;
356
+ const sourceFilepath = instructionDefinition.sourcePath;
310
357
  if (!fs.existsSync(sourceFilepath)) throw new Error(`Missing project instruction asset: ${sourceFilepath}`);
311
358
 
359
+ const stubContent = renderInstructionStub({
360
+ includeWorktreeBootstrap: instructionDefinition.includeWorktreeBootstrap === true,
361
+ projectFilepath,
362
+ sourceFilepath,
363
+ });
364
+
312
365
  const existingState = inspectExistingPath({
313
366
  managedSourceRoot,
314
367
  projectFilepath,
315
368
  sourceFilepath,
369
+ stubContent,
316
370
  });
317
371
 
318
372
  if (existingState.kind === 'match') {
@@ -326,57 +380,94 @@ function ensureSymlinks(
326
380
  continue;
327
381
  }
328
382
 
329
- const symlinkTarget = path.relative(projectParentDir, sourceFilepath);
330
-
331
383
  if (existingState.kind === 'managed-different') {
332
384
  if (!dryRun) {
333
- fs.unlinkSync(projectFilepath);
334
- fs.symlinkSync(symlinkTarget, projectFilepath);
385
+ fs.removeSync(projectFilepath);
386
+ fs.writeFileSync(projectFilepath, stubContent);
335
387
  }
336
388
  result.updated.push(relativeProjectPath);
337
- logVerbose(`${logPrefix} Updated ${relativeProjectPath} -> ${symlinkTarget}`);
389
+ logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
338
390
  continue;
339
391
  }
340
392
 
341
393
  if (existingState.kind === 'blocked') {
342
394
  if (!dryRun) {
343
395
  fs.removeSync(projectFilepath);
344
- fs.symlinkSync(symlinkTarget, projectFilepath);
396
+ fs.writeFileSync(projectFilepath, stubContent);
345
397
  }
346
398
  result.overwritten.push(relativeProjectPath);
347
- logVerbose(`${logPrefix} Replaced ${relativeProjectPath} -> ${symlinkTarget}`);
399
+ logVerbose(`${logPrefix} Replaced ${relativeProjectPath}`);
348
400
  continue;
349
401
  }
350
402
 
351
- if (!dryRun) fs.symlinkSync(symlinkTarget, projectFilepath);
403
+ if (!dryRun) fs.writeFileSync(projectFilepath, stubContent);
352
404
  result.created.push(relativeProjectPath);
353
- logVerbose(`${logPrefix} Created ${relativeProjectPath} -> ${symlinkTarget}`);
405
+ logVerbose(`${logPrefix} Created ${relativeProjectPath}`);
354
406
  }
355
407
 
356
408
  return result;
357
409
  }
358
410
 
411
+ function renderInstructionStub({
412
+ includeWorktreeBootstrap,
413
+ projectFilepath,
414
+ sourceFilepath,
415
+ }: {
416
+ includeWorktreeBootstrap: boolean;
417
+ projectFilepath: string;
418
+ sourceFilepath: string;
419
+ }) {
420
+ const sourcePath = normalizeProjectPathForGitignore(path.relative(path.dirname(projectFilepath), sourceFilepath));
421
+ const lines = [
422
+ ...(includeWorktreeBootstrap ? [...newProteumWorktreeBootstrapBlock, ''] : []),
423
+ managedInstructionStubHeader,
424
+ '',
425
+ managedInstructionStubIntro,
426
+ '',
427
+ 'Before reading or applying instructions from this file, read and follow the canonical Proteum instruction file at:',
428
+ '',
429
+ `\`${sourcePath}\``,
430
+ '',
431
+ 'Resolve that path relative to this file. Treat the canonical file as if its full contents were written here.',
432
+ '',
433
+ 'If the canonical file cannot be read, stop and run `npx proteum configure agents` before continuing.',
434
+ '',
435
+ ];
436
+
437
+ return lines.join('\n');
438
+ }
439
+
359
440
  function inspectExistingPath({
360
441
  managedSourceRoot,
361
442
  projectFilepath,
362
443
  sourceFilepath,
444
+ stubContent,
363
445
  }: {
364
446
  managedSourceRoot: string;
365
447
  projectFilepath: string;
366
448
  sourceFilepath: string;
449
+ stubContent: string;
367
450
  }) {
368
451
  if (!pathEntryExists(projectFilepath)) return { kind: 'missing' as const };
369
452
 
370
453
  const stats = fs.lstatSync(projectFilepath);
371
- if (!stats.isSymbolicLink()) return { kind: 'blocked' as const };
454
+ if (!stats.isSymbolicLink()) {
455
+ if (!stats.isFile()) return { kind: 'blocked' as const };
456
+
457
+ const existingContent = fs.readFileSync(projectFilepath, 'utf8');
458
+ if (existingContent === stubContent) return { kind: 'match' as const };
459
+ if (isManagedInstructionStub(existingContent)) return { kind: 'managed-different' as const };
460
+
461
+ return { kind: 'blocked' as const };
462
+ }
372
463
 
373
464
  const existingTarget = resolveSymlinkTarget(projectFilepath);
374
465
  const normalizedExistingTarget = normalizeAbsolutePath(existingTarget);
375
466
  const normalizedSourceFilepath = normalizeAbsolutePath(sourceFilepath);
376
467
  const normalizedManagedSourceRoot = normalizeAbsolutePath(managedSourceRoot);
377
468
 
378
- if (normalizedExistingTarget === normalizedSourceFilepath) return { kind: 'match' as const };
379
469
  if (
470
+ normalizedExistingTarget === normalizedSourceFilepath ||
380
471
  normalizedExistingTarget === normalizedManagedSourceRoot ||
381
472
  normalizedExistingTarget.startsWith(`${normalizedManagedSourceRoot}/`)
382
473
  )
@@ -385,15 +476,19 @@ function inspectExistingPath({
385
476
  return { kind: 'blocked' as const };
386
477
  }
387
478
 
479
+ function isManagedInstructionStub(content: string) {
480
+ return content.includes(`${managedInstructionStubHeader}\n\n${managedInstructionStubIntro}\n`);
481
+ }
482
+
388
483
  function resolveSymlinkTarget(projectFilepath: string) {
389
484
  const projectParentDir = path.dirname(projectFilepath);
390
485
  const rawTarget = fs.readlinkSync(projectFilepath);
391
486
  return path.resolve(projectParentDir, rawTarget);
392
487
  }
393
488
 
394
- function mergeSymlinkResults(
395
- result: TConfigureProjectAgentSymlinksResult,
396
- next: TEnsureSymlinksResult,
489
+ function mergeInstructionResults(
490
+ result: TConfigureProjectAgentInstructionsResult,
491
+ next: TEnsureInstructionStubsResult,
397
492
  rootDir: string,
398
493
  ) {
399
494
  result.created.push(...next.created.map((entry) => formatResultPath(rootDir, entry)));
@@ -2,10 +2,11 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
 
4
4
  import cli from '..';
5
- import Compiler from '../compiler';
6
5
  import { runProcess } from './runProcess';
7
6
 
8
- const tsconfigPaths = ['client/tsconfig.json', 'server/tsconfig.json', 'commands/tsconfig.json'];
7
+ const appConfigPaths = ['identity.config.ts', 'proteum.config.ts'];
8
+ const appTsconfigPaths = ['client/tsconfig.json', 'server/tsconfig.json', 'commands/tsconfig.json'];
9
+ const frameworkTsconfigPaths = ['client/app.tsconfig.json', 'server/app.tsconfig.json', 'cli/tsconfig.json'];
9
10
  const eslintConfigPaths = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
10
11
 
11
12
  const resolveInstalledBinary = (packageName: string, binName: string) => cli.paths.resolveBinary(packageName, binName);
@@ -15,6 +16,28 @@ const resolveExistingAppPaths = (paths: string[]) =>
15
16
  .map((relativePath) => ({ relativePath, absolutePath: path.join(cli.paths.appRoot, relativePath) }))
16
17
  .filter(({ absolutePath }) => fs.existsSync(absolutePath));
17
18
 
19
+ const isFrameworkCheckout = () => {
20
+ let packageJson: { name?: unknown } | undefined;
21
+
22
+ try {
23
+ packageJson = JSON.parse(fs.readFileSync(path.join(cli.paths.appRoot, 'package.json'), 'utf8'));
24
+ } catch {}
25
+
26
+ return packageJson?.name === 'proteum' && fs.existsSync(path.join(cli.paths.appRoot, 'cli', 'bin.js'));
27
+ };
28
+
29
+ const resolveTypecheckProjects = () => {
30
+ const appProjects = resolveExistingAppPaths(appTsconfigPaths);
31
+ if (appProjects.length > 0) return appProjects;
32
+
33
+ if (isFrameworkCheckout()) return resolveExistingAppPaths(frameworkTsconfigPaths);
34
+
35
+ return [];
36
+ };
37
+
38
+ export const hasAppConfig = () =>
39
+ appConfigPaths.every((relativePath) => fs.existsSync(path.join(cli.paths.appRoot, relativePath)));
40
+
18
41
  const getTypecheckEnv = () => {
19
42
  const existingNodeOptions = process.env.NODE_OPTIONS ?? '';
20
43
 
@@ -26,16 +49,21 @@ const getTypecheckEnv = () => {
26
49
  };
27
50
 
28
51
  export const refreshGeneratedTypings = async () => {
52
+ const { default: Compiler } = await import('../compiler');
29
53
  const compiler = new Compiler('dev');
30
54
 
31
55
  await compiler.refreshGeneratedTypings();
32
56
  };
33
57
 
34
58
  export const runAppTypecheck = async () => {
35
- const existingProjects = resolveExistingAppPaths(tsconfigPaths);
59
+ const existingProjects = resolveTypecheckProjects();
36
60
 
37
61
  if (existingProjects.length === 0)
38
- throw new Error(`No TypeScript app projects found. Expected one of: ${tsconfigPaths.join(', ')}.`);
62
+ throw new Error(
63
+ `No TypeScript projects found. Expected one of: ${[...appTsconfigPaths, ...frameworkTsconfigPaths].join(
64
+ ', ',
65
+ )}.`,
66
+ );
39
67
 
40
68
  const tsc = resolveInstalledBinary('typescript', 'tsc');
41
69
 
@@ -1,6 +1,6 @@
1
1
  # Dev Sessions
2
2
 
3
- Proteum ships a dev-only auth bootstrap command so `proteum verify browser`, Playwright runs, and local debugging can start from an authenticated state without driving the login UI.
3
+ Proteum ships dev-only auth bootstrap commands so `proteum verify browser`, `proteum e2e`, Playwright runs, and local debugging can start from an authenticated state without driving the login UI.
4
4
 
5
5
  ## When To Use It
6
6
 
@@ -31,10 +31,18 @@ proteum session admin@example.com --role ADMIN --port 3101
31
31
  proteum session god@example.com --role GOD --url http://localhost:3102 --json
32
32
  ```
33
33
 
34
+ Playwright wrapper mode:
35
+
36
+ ```bash
37
+ proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/admin.spec.ts
38
+ proteum e2e --url http://localhost:3101 --env FEATURE_FLAG=true --grep smoke
39
+ ```
40
+
34
41
  Behavior:
35
42
 
36
43
  - local mode refreshes generated artifacts, builds the dev output, starts a temporary local dev server, creates the session, prints the payload, and exits
37
44
  - remote mode talks to an already running `proteum dev` instance
45
+ - `proteum e2e` talks to an already running `proteum dev` instance when `--session-email` is present, then starts Playwright with `E2E_BASE_URL`, optional `E2E_PORT`, optional `E2E_AUTH_TOKEN`, and any explicit `--env` or `--env-file` values in the child process environment
38
46
  - the command requires an explicit email and optionally asserts a role before returning the session
39
47
  - the command is available only in dev mode
40
48
  - browser verification flows should keep browser state app-local and disposable through `proteum verify browser` or direct Playwright instead of reusing a shared temp profile
@@ -68,7 +76,7 @@ curl -H "$(jq -r '.curlCookieHeader' session.json)" http://localhost:3101/api/Au
68
76
  ## Agent Guidance
69
77
 
70
78
  - Prefer `proteum session` over UI login automation when the goal is to test or debug protected application behavior.
71
- - Prefer `proteum verify browser` for focused browser-visible verification. When lower-level control is required, use direct Playwright with a disposable profile.
79
+ - Prefer `proteum verify browser` for focused browser-visible verification, and `proteum e2e --port <port>` for targeted or full Playwright suites. When lower-level control is required, use direct Playwright with a disposable profile.
72
80
  - Use UI login automation only when the auth UX itself is the feature under test.
73
81
  - Pair it with `proteum diagnose` for a fast protected-route summary, `proteum perf request` for a one-request timing breakdown, then use `proteum trace` when you need lower-level request events.
74
82
  - Only the final verifier agent should usually run browser flows. Earlier agents should stay on `orient`, `verify owner`, `verify request`, and request-level diagnostics unless browser execution is required.
@@ -78,6 +86,7 @@ Typical flow:
78
86
  ```bash
79
87
  proteum orient /dashboard
80
88
  proteum session admin@example.com --role ADMIN --port 3101 --json > session.json
89
+ proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/dashboard.spec.ts
81
90
  proteum diagnose /dashboard --hit /dashboard --port 3101
82
91
  proteum perf request /dashboard --port 3101
83
92
  proteum trace latest --port 3101
@@ -53,6 +53,7 @@ proteum diagnose /api/Auth/CurrentUser --url http://127.0.0.1:3101
53
53
  proteum verify owner /api/Auth/CurrentUser
54
54
  proteum verify request /dashboard --port 3101
55
55
  proteum verify browser /dashboard --port 3101 --session-email admin@example.com --session-role ADMIN
56
+ proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/dashboard.spec.ts
56
57
 
57
58
  proteum perf top --since today
58
59
  proteum perf request /dashboard --port 3101
@@ -210,7 +211,7 @@ For AI coding agents or automation:
210
211
  2. Read `./.proteum/manifest.json` or run `proteum explain --json` only after you know which surface matters.
211
212
  3. Run `proteum doctor --json` and `proteum doctor --contracts --json` to inspect framework and generated-artifact diagnostics.
212
213
  4. Use `proteum verify owner <query>` or `proteum diagnose <path> --port <port>` for the smallest trustworthy runtime surface before broad checks.
213
- 5. Use `proteum verify browser` for browser-visible verification, and only drop to direct Playwright when request-level verification cannot reproduce the issue. Keep auth sourced from `proteum session`, and reserve browser flows for the final verifier agent unless they are the only trustworthy surface.
214
+ 5. Use `proteum verify browser` for browser-visible verification, or `proteum e2e --port <port>` for targeted/full Playwright suites. Only drop to direct Playwright when the Proteum wrapper cannot express the needed control. Keep auth sourced from Proteum session helpers, and reserve browser flows for the final verifier agent unless they are the only trustworthy surface.
214
215
  6. For performance, CPU, SQL, render, cache, or connected-boundary questions, use `proteum perf request <requestId|path>` against the same running dev server.
215
216
  7. Use `proteum trace ...` when you need lower-level event detail than `diagnose` or `perf` provides.
216
217
  8. Run global checks second, not first. Unrelated diagnostics should remain visible but non-blocking during focused verification unless strict global mode is required.
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.2.2",
4
+ "version": "2.2.6",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -6,7 +6,7 @@
6
6
  import path from 'path';
7
7
 
8
8
  // Core
9
- import { configureProjectAgentSymlinks } from '../cli/utils/agents';
9
+ import { configureProjectAgentInstructions } from '../cli/utils/agents';
10
10
 
11
11
  /*----------------------------------
12
12
  - TYPES
@@ -31,5 +31,5 @@ for (const projectRoot of projectRoots) {
31
31
  }
32
32
 
33
33
  console.log(`[update-codex-agents] Syncing project Codex assets in ${projectRoot}`);
34
- configureProjectAgentSymlinks({ appRoot: projectRoot, coreRoot: proteumRoot });
34
+ configureProjectAgentInstructions({ appRoot: projectRoot, coreRoot: proteumRoot });
35
35
  }