proteum 2.5.2 → 2.5.3

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 CHANGED
@@ -65,7 +65,7 @@ npx prisma migrate dev --config ./prisma.config.ts --name <migration name>
65
65
  - Keep the developer-facing contract synchronized when framework work changes CLI commands, profiler capabilities, or the `proteum dev` banner. Update the live surfaces together in the same pass: CLI command/help definitions, profiler panels and dev-only endpoints, banner text/examples, and the most relevant agent docs that describe them, especially `AGENTS.md`, `agents/project/AGENTS.md`, `agents/project/root/AGENTS.md`, `agents/project/app-root/AGENTS.md`, `agents/project/diagnostics.md`, and any narrower `agents/project/**/AGENTS.md` file that mentions the changed workflow.
66
66
  - Proteum MCP contract: `proteum mcp` is the machine-scope router agents register once, and `proteum dev` exposes each app runtime at `/__proteum/mcp`. `proteum dev` ensures one managed machine MCP daemon is running; do not start a second managed daemon. Agents should start with MCP `workflow_start` using `cwd` or a known `projectId`; ambiguous routing or offline app candidates use `project_resolve { cwd }`, and follow-up live app tools require the returned `projectId`. Dev-hosted app tools are already rooted to their own runtime. Keep MCP tools/resources compact, typed, capped, paginated for full trace detail, and read-only unless a future task explicitly expands the mutation contract. The database diagnostic exception is still read-only: MCP `db_query` and CLI `proteum db query` allow one capped `SELECT`, `SHOW`, or `EXPLAIN` statement only and return rows plus elapsed milliseconds. MCP payloads are compact single-line `proteum-mcp-v1` JSON, not pretty-printed human output. Do not implement MCP tools as thin CLI process wrappers when the data is available through manifest readers, tracked sessions, or dev runtime registries.
67
67
  - Keep the same-system trace contract explicit when request instrumentation changes: `TRACE_*` controls the retained dev trace store plus the trace/perf CLI, dev-only HTTP endpoints, and bottom profiler, while `ENABLE_PROFILER` enables the reduced request-local `request.profiling` snapshot and `request.finished` hook payload without retaining finished requests globally unless dev trace is also enabled.
68
- - Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
68
+ - Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files before the dev loop begins.
69
69
  - Keep core changes aligned with the explicit controller/page architecture in `agents/project/root/AGENTS.md` and its standalone composition in `agents/project/AGENTS.md`.
70
70
  - Prefer removing framework magic when the same result can be expressed with explicit contracts, generated code, or typed context.
71
71
  - Apply the pruning rules from `agents/project/optimizations.md`, especially for webpack plugins, Babel plugins, aliases, helpers, runtime services, and npm packages that are not meaningfully used by both apps.
package/README.md CHANGED
@@ -403,7 +403,7 @@ Proteum ships with a compact CLI focused on the real app lifecycle:
403
403
  | `proteum e2e` | Run Playwright with Proteum-managed `E2E_*` values instead of shell-leading env assignments |
404
404
  | `proteum verify` | Validate targeted changed-file checks, focused owner/request/browser workflows, or the full framework reference-app pass |
405
405
  | `proteum init` | Scaffold a new Proteum app with built-in deterministic templates |
406
- | `proteum configure agents` | Interactively configure tracked Proteum instruction files for standalone or monorepo apps |
406
+ | `proteum configure agents` | Interactively configure tracked Proteum instruction files and Claude aliases |
407
407
  | `proteum create` | Scaffold a page, controller, command, route, or root service inside an app |
408
408
  | `proteum worktree` | Create or initialize Codex worktrees with a machine-readable bootstrap marker |
409
409
 
@@ -419,7 +419,7 @@ proteum build --prod --analyze
419
419
  proteum build --prod --analyze --analyze-serve --analyze-port auto
420
420
  ```
421
421
 
422
- Only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. `proteum dev` is the only command that clears the interactive terminal before rendering its live session UI, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys, and prints connected app names plus successful connected `/ping` checks in the server-ready banner. Every `proteum dev` start ensures tracked Proteum instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
422
+ Only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. `proteum dev` is the only command that clears the interactive terminal before rendering its live session UI, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys, and prints connected app names plus successful connected `/ping` checks in the server-ready banner. Every `proteum dev` start ensures tracked Proteum instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files before the dev loop begins.
423
423
 
424
424
  Useful inspection commands:
425
425
 
@@ -467,7 +467,7 @@ proteum create controller Founder/projects --method list
467
467
  proteum create service Conversion/Plans
468
468
  ```
469
469
 
470
- `proteum configure agents` writes a compact managed `# Proteum Instructions` router plus the task-specific instruction files that router points to. Standalone mode writes root documents into the app root; monorepo mode writes shared root documents such as `AGENTS.md`, `DOCUMENTATION.md`, `CODING_STYLE.md`, `diagnostics.md`, and `optimizations.md` into the chosen monorepo root and keeps only app-local instruction files in the Proteum app root. It preserves content outside managed sections and asks before replacing directories or foreign symlinks. If you decline, that path is left untouched.
470
+ `proteum configure agents` writes a compact managed `# Proteum Instructions` router plus the task-specific instruction files that router points to. Standalone mode writes root documents into the app root; monorepo mode writes shared root documents such as `AGENTS.md`, `DOCUMENTATION.md`, `CODING_STYLE.md`, `diagnostics.md`, and `optimizations.md` into the chosen monorepo root and keeps only app-local instruction files in the Proteum app root. For each generated `AGENTS.md`, it creates a sibling `CLAUDE.md` symlink pointing to `AGENTS.md`. It preserves content outside managed sections and asks before replacing directories, foreign symlinks, or unrelated files. If you decline, that path is left untouched.
471
471
 
472
472
  Every `proteum dev` start runs the same idempotent instruction check. It updates missing or stale managed sections automatically and prompts only when a blocked path would need to be replaced.
473
473
 
@@ -74,7 +74,7 @@ Managed compact root routers must use trigger -> canonical instruction file refe
74
74
  - If the current app depends on local `file:` connected projects, boot every connected producer app too, each with its own task-scoped session file and free port, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or verifying the consumer app.
75
75
  - During `npx proteum dev`, the app exposes the read-only Proteum MCP runtime endpoint at `/__proteum/mcp`; use it for repeated agent reads instead of spawning equivalent diagnostics commands. For route/page/controller ownership, prefer MCP `workflow_start`, `route_candidates { projectId, query }`, or `explain_summary { projectId, query }` over broad `npx proteum explain --routes --controllers --full` dumps.
76
76
  - For browser validation, use the browser MCP against the running app. Keep Playwright inside `npx proteum e2e --port <port>` for targeted/full end-to-end suites. Bootstrap protected browser MCP state with `npx proteum session`; bootstrap protected E2E runs with `npx proteum e2e --session-email <email> --session-role <role>`.
77
- - Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
77
+ - Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files before the dev loop begins.
78
78
 
79
79
  ### Before Finishing
80
80
 
@@ -18,7 +18,7 @@ This file is the canonical source of truth for diagnostics, temporary instrument
18
18
 
19
19
  - For long-lived dev reproductions, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file, inspect `npx proteum runtime status` first, then use its exact next action so occupied router/HMR ports and untracked same-app runtimes are handled without page-body probes. After the server is ready, print the live server URL as a clickable Markdown link.
20
20
  - Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
21
- - Only the bare `npx proteum build` and bare `npx proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `npx proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `npx proteum dev` clears the interactive terminal before rendering and reports connected app names plus successful connected `/ping` checks in the ready banner; keep that in mind when capturing or comparing command logs during diagnosis. Every `npx proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
21
+ - Only the bare `npx proteum build` and bare `npx proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `npx proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `npx proteum dev` clears the interactive terminal before rendering and reports connected app names plus successful connected `/ping` checks in the ready banner; keep that in mind when capturing or comparing command logs during diagnosis. Every `npx proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files before the dev loop begins.
22
22
  - During `npx proteum dev`, the running app exposes the read-only Proteum MCP transport at `/__proteum/mcp`. Use it for runtime-adjacent agent reads instead of repeatedly spawning equivalent CLI diagnostics.
23
23
  - If machine MCP routing fails, run `npx proteum mcp status` and `npx proteum runtime status` from the intended app root. If no live session exists, use the exact MCP offline or runtime-status next action so occupied router/HMR ports are avoided. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. Do not `curl` normal page routes to identify which app owns a port; use runtime status or Proteum dev-only endpoints. 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, and do not start a second managed MCP daemon. Do not run diagnose, trace, or perf reads while runtime health is unreachable. Then retry MCP `workflow_start` and use the returned `projectId`.
24
24
  - For ownership or repo discovery questions, start with MCP `workflow_start`; use MCP `route_candidates { projectId, query }`, MCP `orient { projectId, query }`, and MCP `explain_summary { projectId, query }` only when the bootstrap owner summary is insufficient. Use `npx proteum orient <query>` or `npx proteum explain owner <query>` only when MCP is unavailable or terminal evidence is required.
@@ -59,7 +59,7 @@ Managed compact root routers must use trigger -> canonical instruction file refe
59
59
  - If the current app depends on local `file:` connected projects, boot every connected producer app too, each with its own task-scoped session file and free port, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or verifying the consumer app.
60
60
  - During `npx proteum dev`, the app exposes the read-only Proteum MCP runtime endpoint at `/__proteum/mcp`; use it for repeated agent reads instead of spawning equivalent diagnostics commands. For route/page/controller ownership, prefer MCP `workflow_start`, `route_candidates { projectId, query }`, or `explain_summary { projectId, query }` over broad `npx proteum explain --routes --controllers --full` dumps.
61
61
  - For browser validation, use the browser MCP against the running app. Keep Playwright inside `npx proteum e2e --port <port>` for targeted/full end-to-end suites. Bootstrap protected browser MCP state with `npx proteum session`; bootstrap protected E2E runs with `npx proteum e2e --session-email <email> --session-role <role>`.
62
- - Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section before the dev loop begins.
62
+ - Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the welcome banner. Terminal `proteum mcp` may print a compact central MCP ready banner when it starts or reuses the managed daemon. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. Every `proteum dev` start ensures tracked instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files before the dev loop begins.
63
63
 
64
64
  ### Before Finishing
65
65
 
@@ -74,7 +74,7 @@ const promptBlockedOverwritePaths = async (blockedPaths: string[]) => {
74
74
  console.info(await renderWarning('Proteum found existing paths that block managed instruction updates.'));
75
75
  console.info(
76
76
  [
77
- 'Choose whether to overwrite each path with a tracked Proteum instruction file:',
77
+ 'Choose whether to overwrite each path with a Proteum-managed instruction path:',
78
78
  ...blockedPaths.map((entry) => `- ${entry}`),
79
79
  ].join('\n'),
80
80
  );
@@ -163,7 +163,7 @@ export const runConfigureAgentsWizard = async ({
163
163
  : undefined;
164
164
  console.info(
165
165
  [
166
- await renderTitle('PROTEUM CONFIGURE AGENTS', 'Configure tracked Proteum instruction files.'),
166
+ await renderTitle('PROTEUM CONFIGURE AGENTS', 'Configure tracked Proteum instruction files and Claude aliases.'),
167
167
  renderRows([{ label: 'app', value: appRoot === process.cwd() ? '.' : appRoot }]),
168
168
  ].join('\n\n'),
169
169
  );
@@ -201,8 +201,8 @@ export const runConfigureAgentsWizard = async ({
201
201
  await renderStep(
202
202
  '[1/1]',
203
203
  isMonorepo
204
- ? `Writing monorepo-aware instruction files using ${monorepoRoot}.`
205
- : 'Writing standalone instruction files.',
204
+ ? `Writing monorepo-aware instruction files and Claude aliases using ${monorepoRoot}.`
205
+ : 'Writing standalone instruction files and Claude aliases.',
206
206
  ),
207
207
  );
208
208
 
@@ -214,7 +214,7 @@ export const runConfigureAgentsWizard = async ({
214
214
  });
215
215
  const sections = renderConfigureResultSections(result);
216
216
 
217
- console.info(await renderSuccess('Proteum-managed instruction files are configured.'));
217
+ console.info(await renderSuccess('Proteum-managed instruction files and Claude aliases are configured.'));
218
218
 
219
219
  if (sections.length > 0) console.info(`\n${sections.join('\n\n')}`);
220
220
  };
@@ -153,7 +153,7 @@ const promptBlockedAgentInstructionOverwrites = async (blockedPaths: string[]) =
153
153
  if (cli.args.json === true || !process.stdin.isTTY || !process.stdout.isTTY) {
154
154
  throw new UsageError(
155
155
  [
156
- 'Proteum could not update managed instruction files because existing paths are blocked:',
156
+ 'Proteum could not update managed instruction paths because existing paths are blocked:',
157
157
  ...blockedPaths.map((entry) => `- ${entry}`),
158
158
  'Run `proteum configure agents` in an interactive terminal to choose which paths can be replaced.',
159
159
  ].join('\n'),
@@ -163,7 +163,7 @@ const promptBlockedAgentInstructionOverwrites = async (blockedPaths: string[]) =
163
163
  console.info(await renderWarning('Proteum found existing paths that block managed instruction updates.'));
164
164
  console.info(
165
165
  [
166
- 'Choose whether to overwrite each blocked path with a tracked Proteum instruction file:',
166
+ 'Choose whether to overwrite each blocked path with a Proteum-managed instruction path:',
167
167
  ...blockedPaths.map((entry) => `- ${entry}`),
168
168
  ].join('\n'),
169
169
  );
@@ -212,7 +212,7 @@ const ensureProjectAgentInstructions = async () => {
212
212
 
213
213
  throw new UsageError(
214
214
  [
215
- 'Proteum could not update all managed instruction files because these paths were left blocked:',
215
+ 'Proteum could not update all managed instruction paths because these paths were left blocked:',
216
216
  ...result.blocked.map((entry) => `- ${entry}`),
217
217
  ].join('\n'),
218
218
  );
@@ -116,7 +116,7 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
116
116
  configure: {
117
117
  name: 'configure',
118
118
  category: 'Project scaffolding',
119
- summary: 'Interactively configure tracked Proteum instruction files for a standalone app or monorepo app root.',
119
+ summary: 'Interactively configure tracked Proteum instruction files and Claude aliases for an app.',
120
120
  usage: 'proteum configure agents',
121
121
  bestFor:
122
122
  'Creating or switching the tracked instruction layout intentionally while keeping Proteum-owned instructions embedded in managed sections.',
@@ -128,8 +128,9 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
128
128
  ],
129
129
  notes: [
130
130
  'This command is interactive. It asks whether the current Proteum app belongs to a monorepo and, if so, which ancestor path should receive the reusable root instruction files.',
131
- 'Standalone mode writes tracked instruction files into the current Proteum app root.',
131
+ 'Standalone mode writes tracked instruction files into the current Proteum app root and creates `CLAUDE.md` symlinks beside each `AGENTS.md`.',
132
132
  'Monorepo mode writes reusable root documents such as `AGENTS.md`, `DOCUMENTATION.md`, `CODING_STYLE.md`, `diagnostics.md`, and `optimizations.md` into the chosen monorepo root, then writes only app-root and area instruction files into the current Proteum app root.',
133
+ 'Every generated `CLAUDE.md` is a sibling symlink pointing to `AGENTS.md`.',
133
134
  'Every managed instruction file contains a `# Proteum Instructions` section with the full embedded Proteum project instruction corpus.',
134
135
  'Existing content outside `# Proteum Instructions` is preserved. Directories and foreign symlinks are replaced only after confirmation.',
135
136
  ],
@@ -139,7 +139,7 @@ export const renderCliOverview = async ({
139
139
  indent: ' ',
140
140
  nextIndent: ' ',
141
141
  }),
142
- wrapText('Before the dev loop starts, `proteum dev` ensures tracked instruction files contain the current managed `# Proteum Instructions` section.', {
142
+ wrapText('Before the dev loop starts, `proteum dev` ensures tracked instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files.', {
143
143
  indent: ' ',
144
144
  nextIndent: ' ',
145
145
  }),
@@ -60,6 +60,9 @@ const managedInstructionSectionHeader = '# Proteum Instructions';
60
60
  const managedInstructionSectionStart = '<!-- proteum-instructions:start -->';
61
61
  const managedInstructionSectionEnd = '<!-- proteum-instructions:end -->';
62
62
  const managedInstructionSectionIntro = 'This section is managed by `proteum configure agents`.';
63
+ const agentInstructionFilename = 'AGENTS.md';
64
+ const claudeInstructionFilename = 'CLAUDE.md';
65
+ const claudeInstructionPointerContent = `@${agentInstructionFilename}`;
63
66
 
64
67
  const sharedRootDocumentInstructionDefinitions: TAgentInstructionDefinition[] = [
65
68
  { projectPath: 'DOCUMENTATION.md', content: 'source' },
@@ -243,6 +246,45 @@ function getRootAgentInstructionDefinitions() {
243
246
  return monorepoRootAgentInstructionDefinitions.map((instructionDefinition) => ({ ...instructionDefinition }));
244
247
  }
245
248
 
249
+ function createEmptyInstructionResult(): TEnsureInstructionFilesResult {
250
+ return {
251
+ blocked: [],
252
+ created: [],
253
+ overwritten: [],
254
+ removed: [],
255
+ skipped: [],
256
+ updated: [],
257
+ };
258
+ }
259
+
260
+ function mergeEnsureInstructionResults(result: TEnsureInstructionFilesResult, next: TEnsureInstructionFilesResult) {
261
+ result.created.push(...next.created);
262
+ result.overwritten.push(...next.overwritten);
263
+ result.removed.push(...next.removed);
264
+ result.updated.push(...next.updated);
265
+ result.skipped.push(...next.skipped);
266
+ result.blocked.push(...next.blocked);
267
+ }
268
+
269
+ function getManagedInstructionProjectPaths(instructionDefinitions: TAgentInstructionDefinition[]) {
270
+ const projectPaths: string[] = [];
271
+
272
+ for (const instructionDefinition of instructionDefinitions) {
273
+ projectPaths.push(instructionDefinition.projectPath);
274
+
275
+ const claudeProjectPath = getClaudeCompanionProjectPath(instructionDefinition.projectPath);
276
+ if (claudeProjectPath) projectPaths.push(claudeProjectPath);
277
+ }
278
+
279
+ return projectPaths;
280
+ }
281
+
282
+ function getClaudeCompanionProjectPath(projectPath: string) {
283
+ if (path.basename(projectPath).toLowerCase() !== agentInstructionFilename.toLowerCase()) return undefined;
284
+
285
+ return path.join(path.dirname(projectPath), claudeInstructionFilename);
286
+ }
287
+
246
288
  function removeInstructionGitignoreEntries({
247
289
  rootDir,
248
290
  instructionDefinitions,
@@ -254,7 +296,7 @@ function removeInstructionGitignoreEntries({
254
296
  if (!pathEntryExists(gitignoreFilepath)) return false;
255
297
 
256
298
  const managedEntries = new Set(
257
- instructionDefinitions.map((instructionDefinition) => normalizeGitignoreEntry(instructionDefinition.projectPath)),
299
+ getManagedInstructionProjectPaths(instructionDefinitions).map((projectPath) => normalizeGitignoreEntry(projectPath)),
258
300
  );
259
301
  const lines = fs.readFileSync(gitignoreFilepath, 'utf8').split(/\r?\n/);
260
302
  const filteredLines: string[] = [];
@@ -341,12 +383,28 @@ function ensureInstructionFiles(
341
383
  : upsertManagedInstructionSection(existingState.content, instructionContent);
342
384
  if (nextContent === existingState.content) {
343
385
  result.skipped.push(relativeProjectPath);
386
+ ensureClaudeCompanionIfNeeded({
387
+ dryRun,
388
+ instructionDefinition,
389
+ logPrefix,
390
+ overwriteBlockedPaths,
391
+ result,
392
+ rootDir,
393
+ });
344
394
  continue;
345
395
  }
346
396
 
347
397
  if (!dryRun) fs.writeFileSync(projectFilepath, nextContent);
348
398
  result.updated.push(relativeProjectPath);
349
399
  logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
400
+ ensureClaudeCompanionIfNeeded({
401
+ dryRun,
402
+ instructionDefinition,
403
+ logPrefix,
404
+ overwriteBlockedPaths,
405
+ result,
406
+ rootDir,
407
+ });
350
408
  continue;
351
409
  }
352
410
 
@@ -357,6 +415,14 @@ function ensureInstructionFiles(
357
415
  }
358
416
  result.updated.push(relativeProjectPath);
359
417
  logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
418
+ ensureClaudeCompanionIfNeeded({
419
+ dryRun,
420
+ instructionDefinition,
421
+ logPrefix,
422
+ overwriteBlockedPaths,
423
+ result,
424
+ rootDir,
425
+ });
360
426
  continue;
361
427
  }
362
428
 
@@ -373,12 +439,28 @@ function ensureInstructionFiles(
373
439
  }
374
440
  result.overwritten.push(relativeProjectPath);
375
441
  logVerbose(`${logPrefix} Replaced ${relativeProjectPath}`);
442
+ ensureClaudeCompanionIfNeeded({
443
+ dryRun,
444
+ instructionDefinition,
445
+ logPrefix,
446
+ overwriteBlockedPaths,
447
+ result,
448
+ rootDir,
449
+ });
376
450
  continue;
377
451
  }
378
452
 
379
453
  if (!dryRun) fs.writeFileSync(projectFilepath, instructionContent);
380
454
  result.created.push(relativeProjectPath);
381
455
  logVerbose(`${logPrefix} Created ${relativeProjectPath}`);
456
+ ensureClaudeCompanionIfNeeded({
457
+ dryRun,
458
+ instructionDefinition,
459
+ logPrefix,
460
+ overwriteBlockedPaths,
461
+ result,
462
+ rootDir,
463
+ });
382
464
  }
383
465
 
384
466
  return result;
@@ -422,6 +504,13 @@ function removeManagedInstructionFiles(
422
504
  if (!dryRun) fs.removeSync(projectFilepath);
423
505
  result.removed.push(relativeProjectPath);
424
506
  logVerbose(`${logPrefix} Removed retired app-root ${relativeProjectPath}`);
507
+ removeClaudeCompanionIfNeeded({
508
+ dryRun,
509
+ instructionDefinition,
510
+ logPrefix,
511
+ result,
512
+ rootDir,
513
+ });
425
514
  continue;
426
515
  }
427
516
 
@@ -437,6 +526,13 @@ function removeManagedInstructionFiles(
437
526
  if (!dryRun) fs.removeSync(projectFilepath);
438
527
  result.removed.push(relativeProjectPath);
439
528
  logVerbose(`${logPrefix} Removed retired app-root ${relativeProjectPath}`);
529
+ removeClaudeCompanionIfNeeded({
530
+ dryRun,
531
+ instructionDefinition,
532
+ logPrefix,
533
+ result,
534
+ rootDir,
535
+ });
440
536
  continue;
441
537
  }
442
538
 
@@ -452,6 +548,179 @@ function removeManagedInstructionFiles(
452
548
  return result;
453
549
  }
454
550
 
551
+ function ensureClaudeCompanionIfNeeded({
552
+ dryRun,
553
+ instructionDefinition,
554
+ logPrefix,
555
+ overwriteBlockedPaths,
556
+ result,
557
+ rootDir,
558
+ }: {
559
+ dryRun: boolean;
560
+ instructionDefinition: TAgentInstructionDefinition;
561
+ logPrefix: string;
562
+ overwriteBlockedPaths: Set<string>;
563
+ result: TEnsureInstructionFilesResult;
564
+ rootDir: string;
565
+ }) {
566
+ const claudeProjectPath = getClaudeCompanionProjectPath(instructionDefinition.projectPath);
567
+ if (!claudeProjectPath) return;
568
+
569
+ mergeEnsureInstructionResults(
570
+ result,
571
+ ensureClaudeInstructionSymlink({
572
+ claudeProjectPath,
573
+ dryRun,
574
+ logPrefix,
575
+ overwriteBlockedPaths,
576
+ rootDir,
577
+ }),
578
+ );
579
+ }
580
+
581
+ function ensureClaudeInstructionSymlink({
582
+ claudeProjectPath,
583
+ dryRun,
584
+ logPrefix,
585
+ overwriteBlockedPaths,
586
+ rootDir,
587
+ }: {
588
+ claudeProjectPath: string;
589
+ dryRun: boolean;
590
+ logPrefix: string;
591
+ overwriteBlockedPaths: Set<string>;
592
+ rootDir: string;
593
+ }): TEnsureInstructionFilesResult {
594
+ const result = createEmptyInstructionResult();
595
+ const claudeFilepath = path.join(rootDir, claudeProjectPath);
596
+ const claudeParentDir = path.dirname(claudeFilepath);
597
+ const relativeClaudePath = path.relative(rootDir, claudeFilepath) || '.';
598
+
599
+ if (!fs.existsSync(claudeParentDir)) {
600
+ result.skipped.push(relativeClaudePath);
601
+ return result;
602
+ }
603
+
604
+ const existingState = inspectExistingClaudePath(claudeFilepath);
605
+
606
+ if (existingState.kind === 'correct') {
607
+ result.skipped.push(relativeClaudePath);
608
+ return result;
609
+ }
610
+
611
+ if (existingState.kind === 'missing') {
612
+ if (!dryRun) fs.symlinkSync(agentInstructionFilename, claudeFilepath);
613
+ result.created.push(relativeClaudePath);
614
+ logVerbose(`${logPrefix} Created ${relativeClaudePath}`);
615
+ return result;
616
+ }
617
+
618
+ if (existingState.kind === 'managed-different') {
619
+ if (!dryRun) {
620
+ fs.removeSync(claudeFilepath);
621
+ fs.symlinkSync(agentInstructionFilename, claudeFilepath);
622
+ }
623
+ result.updated.push(relativeClaudePath);
624
+ logVerbose(`${logPrefix} Updated ${relativeClaudePath}`);
625
+ return result;
626
+ }
627
+
628
+ const normalizedClaudeFilepath = normalizeAbsolutePath(claudeFilepath);
629
+ if (!overwriteBlockedPaths.has(normalizedClaudeFilepath)) {
630
+ result.blocked.push(relativeClaudePath);
631
+ return result;
632
+ }
633
+
634
+ if (!dryRun) {
635
+ fs.removeSync(claudeFilepath);
636
+ fs.symlinkSync(agentInstructionFilename, claudeFilepath);
637
+ }
638
+ result.overwritten.push(relativeClaudePath);
639
+ logVerbose(`${logPrefix} Replaced ${relativeClaudePath}`);
640
+
641
+ return result;
642
+ }
643
+
644
+ function removeClaudeCompanionIfNeeded({
645
+ dryRun,
646
+ instructionDefinition,
647
+ logPrefix,
648
+ result,
649
+ rootDir,
650
+ }: {
651
+ dryRun: boolean;
652
+ instructionDefinition: TAgentInstructionDefinition;
653
+ logPrefix: string;
654
+ result: TEnsureInstructionFilesResult;
655
+ rootDir: string;
656
+ }) {
657
+ const claudeProjectPath = getClaudeCompanionProjectPath(instructionDefinition.projectPath);
658
+ if (!claudeProjectPath) return;
659
+
660
+ mergeEnsureInstructionResults(
661
+ result,
662
+ removeClaudeInstructionSymlink({
663
+ claudeProjectPath,
664
+ dryRun,
665
+ logPrefix,
666
+ rootDir,
667
+ }),
668
+ );
669
+ }
670
+
671
+ function removeClaudeInstructionSymlink({
672
+ claudeProjectPath,
673
+ dryRun,
674
+ logPrefix,
675
+ rootDir,
676
+ }: {
677
+ claudeProjectPath: string;
678
+ dryRun: boolean;
679
+ logPrefix: string;
680
+ rootDir: string;
681
+ }): TEnsureInstructionFilesResult {
682
+ const result = createEmptyInstructionResult();
683
+ const claudeFilepath = path.join(rootDir, claudeProjectPath);
684
+ const relativeClaudePath = path.relative(rootDir, claudeFilepath) || '.';
685
+
686
+ if (!pathEntryExists(claudeFilepath)) return result;
687
+
688
+ const existingState = inspectExistingClaudePath(claudeFilepath);
689
+ if (existingState.kind === 'correct' || existingState.kind === 'managed-different') {
690
+ if (!dryRun) fs.removeSync(claudeFilepath);
691
+ result.removed.push(relativeClaudePath);
692
+ logVerbose(`${logPrefix} Removed retired app-root ${relativeClaudePath}`);
693
+ return result;
694
+ }
695
+
696
+ if (existingState.kind === 'blocked') result.skipped.push(relativeClaudePath);
697
+
698
+ return result;
699
+ }
700
+
701
+ function inspectExistingClaudePath(claudeFilepath: string) {
702
+ if (!pathEntryExists(claudeFilepath)) return { kind: 'missing' as const };
703
+
704
+ const stats = fs.lstatSync(claudeFilepath);
705
+ const claudeParentDir = path.dirname(claudeFilepath);
706
+ const agentFilepath = path.join(claudeParentDir, agentInstructionFilename);
707
+
708
+ if (stats.isSymbolicLink()) {
709
+ const rawTarget = fs.readlinkSync(claudeFilepath);
710
+ const resolvedTarget = path.resolve(claudeParentDir, rawTarget);
711
+ if (normalizeAbsolutePath(resolvedTarget) !== normalizeAbsolutePath(agentFilepath)) return { kind: 'blocked' as const };
712
+
713
+ return rawTarget === agentInstructionFilename ? { kind: 'correct' as const } : { kind: 'managed-different' as const };
714
+ }
715
+
716
+ if (!stats.isFile()) return { kind: 'blocked' as const };
717
+
718
+ const content = fs.readFileSync(claudeFilepath, 'utf8');
719
+ if (content.trim() === claudeInstructionPointerContent) return { kind: 'managed-different' as const };
720
+
721
+ return { kind: 'blocked' as const };
722
+ }
723
+
455
724
  function inspectExistingPath({
456
725
  managedSourceRoot,
457
726
  projectFilepath,
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.5.2",
4
+ "version": "2.5.3",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -15,6 +15,23 @@ const writeFile = (filepath, content) => {
15
15
  fs.writeFileSync(filepath, content);
16
16
  };
17
17
 
18
+ const assertClaudeSymlink = (root, relativeDir = '') => {
19
+ const linkPath = path.join(root, relativeDir, 'CLAUDE.md');
20
+ const stats = fs.lstatSync(linkPath);
21
+
22
+ assert.equal(stats.isSymbolicLink(), true, `${linkPath} should be a symlink`);
23
+ assert.equal(fs.readlinkSync(linkPath), 'AGENTS.md');
24
+ };
25
+
26
+ const pathEntryExists = (filepath) => {
27
+ try {
28
+ fs.lstatSync(filepath);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ };
34
+
18
35
  const makeTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-agents-'));
19
36
 
20
37
  const createCoreFixture = () => {
@@ -53,6 +70,7 @@ const createAppFixture = () => {
53
70
  'node_modules',
54
71
  '# Proteum-managed instruction files',
55
72
  '/AGENTS.md',
73
+ '/CLAUDE.md',
56
74
  '/CODING_STYLE.md',
57
75
  '/DOCUMENTATION.md',
58
76
  '# End Proteum-managed instruction files',
@@ -137,9 +155,17 @@ test('standalone configure creates tracked instruction files with routing contra
137
155
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md')), true);
138
156
  assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /Journey rule/);
139
157
  assert.doesNotMatch(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
158
+ assertClaudeSymlink(appRoot);
159
+ assertClaudeSymlink(appRoot, 'client');
160
+ assertClaudeSymlink(appRoot, 'client/pages');
161
+ assertClaudeSymlink(appRoot, 'server/routes');
162
+ assertClaudeSymlink(appRoot, 'server/services');
163
+ assertClaudeSymlink(appRoot, 'tests');
164
+ assertClaudeSymlink(appRoot, 'tests/e2e');
140
165
  assert.doesNotMatch(agentsContent, /Before reading or applying instructions from this file/);
141
166
  assert.doesNotMatch(gitignoreContent, /Proteum-managed instruction files/);
142
167
  assert.doesNotMatch(gitignoreContent, /^\/AGENTS\.md$/m);
168
+ assert.doesNotMatch(gitignoreContent, /^\/CLAUDE\.md$/m);
143
169
  assert.doesNotMatch(gitignoreContent, /^\/DOCUMENTATION\.md$/m);
144
170
  });
145
171
 
@@ -255,13 +281,20 @@ test('monorepo configure writes root and app instruction files', () => {
255
281
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'AGENTS.md'), 'utf8'), /## Source: tests\/e2e\/AGENTS\.md/);
256
282
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: tests\/e2e\/REAL_WORLD_JOURNEY_TESTS\.md/);
257
283
  assert.doesNotMatch(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
284
+ assertClaudeSymlink(monorepoRoot);
285
+ assertClaudeSymlink(monorepoRoot, 'tests');
286
+ assertClaudeSymlink(monorepoRoot, 'tests/e2e');
258
287
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'AGENTS.md')), false);
259
288
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), false);
289
+ assert.equal(pathEntryExists(path.join(appRoot, 'tests', 'CLAUDE.md')), false);
290
+ assert.equal(pathEntryExists(path.join(appRoot, 'tests', 'e2e', 'CLAUDE.md')), false);
260
291
  const appAgentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
261
292
  assert.match(appAgentsContent, /## Agent Routing Contract/);
262
293
  assert.doesNotMatch(appAgentsContent, /## Known Proteum Apps/);
263
294
  assert.doesNotMatch(appAgentsContent, /Do not start `npx proteum dev` from this root/);
264
295
  assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
296
+ assertClaudeSymlink(appRoot);
297
+ assertClaudeSymlink(appRoot, 'client');
265
298
  assert.equal(fs.existsSync(path.join(appRoot, 'CODING_STYLE.md')), false);
266
299
  assert.equal(fs.existsSync(path.join(appRoot, 'DOCUMENTATION.md')), false);
267
300
  assert.equal(fs.existsSync(path.join(appRoot, 'diagnostics.md')), false);
@@ -331,6 +364,45 @@ test('configure migrates legacy managed symlinks to tracked files', () => {
331
364
  assert.equal(result.updated.some((entry) => entry.endsWith('/AGENTS.md')), true);
332
365
  assert.equal(stats.isSymbolicLink(), false);
333
366
  assert.match(content, /# Proteum Instructions/);
367
+ assertClaudeSymlink(appRoot);
368
+ });
369
+
370
+ test('configure migrates one-line Claude pointer files to symlinks', () => {
371
+ const coreRoot = createCoreFixture();
372
+ const appRoot = createAppFixture();
373
+ const linkPath = path.join(appRoot, 'CLAUDE.md');
374
+
375
+ writeFile(linkPath, '@AGENTS.md\n');
376
+
377
+ const result = configureProjectAgentInstructions({ appRoot, coreRoot });
378
+
379
+ assert.equal(result.updated.some((entry) => entry.endsWith('/CLAUDE.md')), true);
380
+ assertClaudeSymlink(appRoot);
381
+ });
382
+
383
+ test('configure reports blocked Claude companion paths unless overwrite is allowed', () => {
384
+ const coreRoot = createCoreFixture();
385
+ const appRoot = createAppFixture();
386
+ const blockedPath = path.join(appRoot, 'CLAUDE.md');
387
+
388
+ writeFile(blockedPath, '# Local Claude Notes\n\n- Keep this local rule.\n');
389
+
390
+ const preview = configureProjectAgentInstructions({ appRoot, coreRoot, dryRun: true });
391
+ assert.equal(preview.blocked.some((entry) => entry.endsWith('/CLAUDE.md')), true);
392
+
393
+ const blockedResult = configureProjectAgentInstructions({ appRoot, coreRoot });
394
+ assert.equal(blockedResult.blocked.some((entry) => entry.endsWith('/CLAUDE.md')), true);
395
+ assert.equal(fs.lstatSync(blockedPath).isFile(), true);
396
+ assert.match(fs.readFileSync(blockedPath, 'utf8'), /Keep this local rule/);
397
+
398
+ const result = configureProjectAgentInstructions({
399
+ appRoot,
400
+ coreRoot,
401
+ overwriteBlockedPaths: [blockedPath],
402
+ });
403
+
404
+ assert.equal(result.overwritten.some((entry) => entry.endsWith('/CLAUDE.md')), true);
405
+ assertClaudeSymlink(appRoot);
334
406
  });
335
407
 
336
408
  test('configure reports blocked paths unless overwrite is allowed', () => {