proteum 2.5.8 → 2.5.10

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.
@@ -22,3 +22,35 @@ export type TDevSessionStartResponse = {
22
22
  export type TDevSessionErrorResponse = {
23
23
  error: string;
24
24
  };
25
+
26
+ export const devSessionLoginPath = '/__proteum/session/login';
27
+ export const devSessionStartPath = '/__proteum/session/start';
28
+
29
+ export const normalizeDevSessionRedirectPath = (value: string): string => {
30
+ const redirect = value.trim() || '/';
31
+ if (!redirect.startsWith('/') || redirect.startsWith('//') || redirect.startsWith('/\\') || /[\r\n]/.test(redirect)) {
32
+ throw new Error('Redirect must be a local absolute path such as /dashboard.');
33
+ }
34
+
35
+ return redirect;
36
+ };
37
+
38
+ export const buildDevSessionLoginUrl = ({
39
+ baseUrl,
40
+ email,
41
+ redirect,
42
+ role,
43
+ }: {
44
+ baseUrl: string;
45
+ email: string;
46
+ redirect: string;
47
+ role?: string;
48
+ }): string => {
49
+ const params = new URLSearchParams({
50
+ email,
51
+ redirect: normalizeDevSessionRedirectPath(redirect),
52
+ });
53
+ if (role?.trim()) params.set('role', role.trim());
54
+
55
+ return `${baseUrl.replace(/\/+$/, '')}${devSessionLoginPath}?${params.toString()}`;
56
+ };
@@ -36,7 +36,6 @@ type TErrorDetails = {
36
36
  export type ServerBug = {
37
37
  // Context
38
38
  hash: string;
39
- isDuplicate: boolean;
40
39
  date: Date; // Timestamp
41
40
  channelType?: string;
42
41
  channelId?: string;
@@ -96,11 +96,11 @@ Standard triggered reads:
96
96
  - Worktree Preflight (`cwd` inside `/.codex/worktrees/`, newly created Proteum worktree, or before editing in a Codex worktree): root contract fallback, then `npx proteum worktree init --source <source-app-root>` or the returned `--refresh` command, `npx proteum runtime status`, and tracked `npx proteum dev` for runtime-visible work.
97
97
  - Git lifecycle (`commit`, `and commit`, `stage`, `push`, `PR`, pull request): root contract fallback.
98
98
  - Before git writes after a bug fix, behavior change, decision change, or docs-relevant production change: `DOCUMENTATION.md`.
99
- - Before finishing production code changes: root contract fallback, `DOCUMENTATION.md`, `CODING_STYLE.md`, and touched area `AGENTS.md`.
99
+ - Before finishing production code changes: root contract fallback, `DOCUMENTATION.md`, `CODING_STYLE.md`, and touched area `AGENTS.md`, then the `CODING_STYLE.md` self-check on the final diff; a non-obvious decision without a why-comment is a defect.
100
100
  - Runtime-visible, request-time, router, SSR, browser, or controller behavior: root contract fallback plus `diagnostics.md`.
101
101
  - Bug fixes, regressions, incidents, broken public routes, auth/OAuth failures, integration failures, or production behavior fixes: `DOCUMENTATION.md`.
102
102
  - Non-trivial feature, product, business-rule, UX, copy, or docs changes: `DOCUMENTATION.md`.
103
- - Implementation edits: `CODING_STYLE.md` plus the matching area file from the routing table.
103
+ - Implementation edits: `CODING_STYLE.md` plus the matching area file from the routing table; record non-obvious decisions, workarounds, and constraints as why-comments while editing.
104
104
 
105
105
  `workflow_start`, `orient`, `route_candidates`, and MCP `instructions_resolve` should promote obvious triggered files into selected instruction previews; ambiguous conditional reads can remain in `readWhen`.
106
106
 
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.8",
4
+ "version": "2.5.10",
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,6 @@
6
6
  import { serialize } from 'v8';
7
7
  import { formatWithOptions } from 'util';
8
8
  import md5 from 'md5';
9
- import dayjs from 'dayjs';
10
9
  import stringify from 'fast-safe-stringify';
11
10
 
12
11
  // Npm
@@ -178,7 +177,6 @@ export default class Console {
178
177
  public logger!: Logger<ILogObj>;
179
178
  // Buffers
180
179
  public logs: TJsonLog[] = [];
181
- private reported: { [hash: string]: { times: number; last: Date } } = {};
182
180
 
183
181
  /*----------------------------------
184
182
  - LIFECYCLE
@@ -371,24 +369,9 @@ export default class Console {
371
369
  // Genertae unique error hash
372
370
  const hash = md5(inspection.stacktraces[0]);
373
371
 
374
- // Don't send the same error twice in a row (avoid email spamming)
375
- const lastReport = this.reported[hash];
376
- let isDuplicate = false;
377
- if (lastReport === undefined) {
378
- this.reported[hash] = { times: 0, last: new Date() };
379
-
380
- // If error older than 1 day
381
- } else if (dayjs(now).diff(dayjs(lastReport.last), 'day') > 1) {
382
- lastReport.times++;
383
- lastReport.last = now;
384
- } else {
385
- isDuplicate = true;
386
- }
387
-
388
372
  const bugReport: ServerBug = {
389
373
  // Context
390
374
  hash: hash,
391
- isDuplicate,
392
375
  date: now,
393
376
  channelType,
394
377
  channelId,
@@ -30,7 +30,13 @@ import type { TServerRouter } from '..';
30
30
  import type { TDevConsoleLogLevel } from '@common/dev/console';
31
31
  import type { TPerfGroupBy } from '@common/dev/performance';
32
32
  import type { TServerReadyConnectedProject } from '@common/dev/serverHotReload';
33
- import type { TDevSessionStartResponse, TDevSessionUserSummary } from '@common/dev/session';
33
+ import {
34
+ devSessionLoginPath,
35
+ devSessionStartPath,
36
+ normalizeDevSessionRedirectPath,
37
+ type TDevSessionStartResponse,
38
+ type TDevSessionUserSummary,
39
+ } from '@common/dev/session';
34
40
  import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
35
41
  import { explainSectionNames } from '@common/dev/diagnostics';
36
42
  import {
@@ -88,6 +94,16 @@ type TDevSessionAuthService = {
88
94
  createSession: (session: { email: string }, request: { id: string; res: express.Response }) => string;
89
95
  decodeSession: (session: { email: string }, req: express.Request) => Promise<TBasicUser | null>;
90
96
  };
97
+ type TDevSessionResult =
98
+ | {
99
+ ok: true;
100
+ response: TDevSessionStartResponse;
101
+ }
102
+ | {
103
+ ok: false;
104
+ error: string;
105
+ statusCode: number;
106
+ };
91
107
 
92
108
  const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPolicyOptions => {
93
109
  const directives: TContentSecurityPolicyDirectives = {
@@ -177,6 +193,77 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
177
193
  };
178
194
  }
179
195
 
196
+ private getSingleQueryValue(value: express.Request['query'][string]): string {
197
+ if (typeof value === 'string') return value.trim();
198
+ if (Array.isArray(value) && value.length === 1 && typeof value[0] === 'string') return value[0].trim();
199
+ return '';
200
+ }
201
+
202
+ private normalizeDevSessionRedirect(value: express.Request['query'][string]): string {
203
+ try {
204
+ return normalizeDevSessionRedirectPath(this.getSingleQueryValue(value));
205
+ } catch {
206
+ return '';
207
+ }
208
+ }
209
+
210
+ private isLocalDevSessionRequest(req: express.Request): boolean {
211
+ const hostname = req.hostname.toLowerCase();
212
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]';
213
+ }
214
+
215
+ private async createDevSessionResponse({
216
+ email,
217
+ req,
218
+ requiredRole,
219
+ res,
220
+ source,
221
+ }: {
222
+ email: string;
223
+ req: express.Request;
224
+ requiredRole: string;
225
+ res: express.Response;
226
+ source: 'login' | 'start';
227
+ }): Promise<TDevSessionResult> {
228
+ if (!email) return { ok: false, statusCode: 400, error: 'Email is required.' };
229
+
230
+ const auth = this.resolveDevSessionAuthService();
231
+ const user = await auth.decodeSession({ email }, req);
232
+
233
+ if (!user) return { ok: false, statusCode: 404, error: `No user could be resolved for "${email}".` };
234
+ if (requiredRole && !user.roles.includes(requiredRole)) {
235
+ return {
236
+ ok: false,
237
+ statusCode: 403,
238
+ error: `User "${email}" does not have required role "${requiredRole}".`,
239
+ };
240
+ }
241
+
242
+ const token = auth.createSession(
243
+ { email },
244
+ {
245
+ id: `proteum-session-${source}:${Date.now()}`,
246
+ res,
247
+ },
248
+ );
249
+ const issuedAt = new Date().toISOString();
250
+ const expiresAt = new Date(Date.now() + auth.config.jwt.expiration).toISOString();
251
+
252
+ return {
253
+ ok: true,
254
+ response: {
255
+ user: this.summarizeDevSessionUser(user),
256
+ session: {
257
+ token,
258
+ cookieName: 'authorization',
259
+ expiresInMs: auth.config.jwt.expiration,
260
+ issuedAt,
261
+ expiresAt,
262
+ },
263
+ },
264
+ };
265
+ }
266
+
180
267
  private async verifyConnectedProjectsBeforeStart(): Promise<TServerReadyConnectedProject[]> {
181
268
  const verifiedConnectedProjects: TServerReadyConnectedProject[] = [];
182
269
 
@@ -766,50 +853,60 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
766
853
  }
767
854
  });
768
855
 
769
- routes.post('/__proteum/session/start', async (req, res) => {
770
- const email = typeof req.body?.email === 'string' ? req.body.email.trim() : '';
771
- const requiredRole = typeof req.body?.role === 'string' ? req.body.role.trim() : '';
856
+ routes.get(devSessionLoginPath, async (req, res) => {
857
+ if (!this.isLocalDevSessionRequest(req)) {
858
+ res.status(403).send('Proteum session login is only available from localhost.');
859
+ return;
860
+ }
772
861
 
773
- if (!email) {
774
- res.status(400).json({ error: 'Email is required.' });
862
+ const email = this.getSingleQueryValue(req.query.email);
863
+ const requiredRole = this.getSingleQueryValue(req.query.role);
864
+ const redirect = this.normalizeDevSessionRedirect(req.query.redirect);
865
+
866
+ if (!redirect) {
867
+ res.status(400).send('Redirect must be a local absolute path.');
775
868
  return;
776
869
  }
777
870
 
778
871
  try {
779
- const auth = this.resolveDevSessionAuthService();
780
- const user = await auth.decodeSession({ email }, req);
872
+ const result = await this.createDevSessionResponse({
873
+ email,
874
+ req,
875
+ requiredRole,
876
+ res,
877
+ source: 'login',
878
+ });
781
879
 
782
- if (!user) {
783
- res.status(404).json({ error: `No user could be resolved for "${email}".` });
880
+ if (!result.ok) {
881
+ res.status(result.statusCode).send(result.error);
784
882
  return;
785
883
  }
786
884
 
787
- if (requiredRole && !user.roles.includes(requiredRole)) {
788
- res.status(403).json({ error: `User "${email}" does not have required role "${requiredRole}".` });
885
+ res.redirect(302, redirect);
886
+ } catch (error) {
887
+ res.status(500).send(error instanceof Error ? error.message : String(error));
888
+ }
889
+ });
890
+
891
+ routes.post(devSessionStartPath, async (req, res) => {
892
+ const email = typeof req.body?.email === 'string' ? req.body.email.trim() : '';
893
+ const requiredRole = typeof req.body?.role === 'string' ? req.body.role.trim() : '';
894
+
895
+ try {
896
+ const result = await this.createDevSessionResponse({
897
+ email,
898
+ req,
899
+ requiredRole,
900
+ res,
901
+ source: 'start',
902
+ });
903
+
904
+ if (!result.ok) {
905
+ res.status(result.statusCode).json({ error: result.error });
789
906
  return;
790
907
  }
791
908
 
792
- const token = auth.createSession(
793
- { email },
794
- {
795
- id: `proteum-session:${Date.now()}`,
796
- res,
797
- },
798
- );
799
- const issuedAt = new Date().toISOString();
800
- const expiresAt = new Date(Date.now() + auth.config.jwt.expiration).toISOString();
801
- const response: TDevSessionStartResponse = {
802
- user: this.summarizeDevSessionUser(user),
803
- session: {
804
- token,
805
- cookieName: 'authorization',
806
- expiresInMs: auth.config.jwt.expiration,
807
- issuedAt,
808
- expiresAt,
809
- },
810
- };
811
-
812
- res.json(response);
909
+ res.json(result.response);
813
910
  } catch (error) {
814
911
  res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
815
912
  }
@@ -108,10 +108,6 @@ test('project instruction sources require unit tests for applicable production c
108
108
  fs.readFileSync(path.join(projectAgentsRoot, 'AGENTS.md'), 'utf8'),
109
109
  /production changes must always add or update focused unit tests/,
110
110
  );
111
- assert.match(
112
- fs.readFileSync(path.join(projectAgentsRoot, 'root', 'AGENTS.md'), 'utf8'),
113
- /always add or update focused unit tests/,
114
- );
115
111
  assert.match(
116
112
  fs.readFileSync(path.join(projectAgentsRoot, 'tests', 'AGENTS.md'), 'utf8'),
117
113
  /For every production change, add or update focused unit tests/,
@@ -0,0 +1,33 @@
1
+ const assert = require('node:assert/strict');
2
+ const path = require('node:path');
3
+
4
+ const coreRoot = path.resolve(__dirname, '..');
5
+ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
6
+ process.env.TS_NODE_TRANSPILE_ONLY = '1';
7
+ require('ts-node/register/transpile-only');
8
+
9
+ const {
10
+ buildDevSessionLoginUrl,
11
+ normalizeDevSessionRedirectPath,
12
+ } = require('../common/dev/session.ts');
13
+
14
+ test('buildDevSessionLoginUrl creates a local browser handoff URL', () => {
15
+ const url = buildDevSessionLoginUrl({
16
+ baseUrl: 'http://127.0.0.1:3020/',
17
+ email: 'contact@gaetan-legac.fr',
18
+ redirect: '/projects/project-123?tab=radar',
19
+ role: 'GOD',
20
+ });
21
+
22
+ assert.equal(
23
+ url,
24
+ 'http://127.0.0.1:3020/__proteum/session/login?email=contact%40gaetan-legac.fr&redirect=%2Fprojects%2Fproject-123%3Ftab%3Dradar&role=GOD',
25
+ );
26
+ });
27
+
28
+ test('normalizeDevSessionRedirectPath rejects external redirects', () => {
29
+ assert.throws(() => normalizeDevSessionRedirectPath('https://example.com'), /local absolute path/);
30
+ assert.throws(() => normalizeDevSessionRedirectPath('//example.com'), /local absolute path/);
31
+ assert.throws(() => normalizeDevSessionRedirectPath('/\nadmin'), /local absolute path/);
32
+ assert.equal(normalizeDevSessionRedirectPath(''), '/');
33
+ });
@@ -0,0 +1,67 @@
1
+ const assert = require('node:assert/strict');
2
+ const path = require('node:path');
3
+
4
+ const coreRoot = path.resolve(__dirname, '..');
5
+ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
6
+ process.env.TS_NODE_TRANSPILE_ONLY = '1';
7
+
8
+ require('ts-node/register/transpile-only');
9
+
10
+ const {
11
+ isFrameworkSourceContext,
12
+ resolveServerExternalRequest,
13
+ } = require('../cli/compiler/server/externals.ts');
14
+
15
+ test('server external resolution prefers app dependencies outside framework source', () => {
16
+ const optionsSeen = [];
17
+ const resolved = resolveServerExternalRequest({
18
+ context: '/repo/node_modules/@klair/usecases/src/mcp/groups/workers',
19
+ frameworkRoots: ['/framework/core'],
20
+ request: '@prisma/client',
21
+ resolveRequest: (request, options) => {
22
+ optionsSeen.push(options);
23
+ assert.equal(request, '@prisma/client');
24
+ return options.preferApp
25
+ ? '/repo/node_modules/@prisma/client/default.js'
26
+ : '/framework/core/node_modules/@prisma/client/default.js';
27
+ },
28
+ });
29
+
30
+ assert.equal(resolved, '/repo/node_modules/@prisma/client/default.js');
31
+ assert.deepEqual(optionsSeen, [{ preferApp: true }]);
32
+ });
33
+
34
+ test('server external resolution prefers framework dependencies for framework source', () => {
35
+ const resolved = resolveServerExternalRequest({
36
+ context: '/framework/core/server',
37
+ frameworkRoots: ['/framework/core'],
38
+ request: 'express',
39
+ resolveRequest: (request, options) => {
40
+ assert.equal(request, 'express');
41
+ return options.preferApp
42
+ ? '/repo/node_modules/express/index.js'
43
+ : '/framework/core/node_modules/express/index.js';
44
+ },
45
+ });
46
+
47
+ assert.equal(resolved, '/framework/core/node_modules/express/index.js');
48
+ });
49
+
50
+ test('server external resolution falls back to the bare request when resolution fails', () => {
51
+ const resolved = resolveServerExternalRequest({
52
+ context: '/repo/server',
53
+ frameworkRoots: ['/framework/core'],
54
+ request: 'optional-peer',
55
+ resolveRequest: () => {
56
+ throw new Error('missing');
57
+ },
58
+ });
59
+
60
+ assert.equal(resolved, 'optional-peer');
61
+ });
62
+
63
+ test('framework source context matching handles exact and nested roots', () => {
64
+ assert.equal(isFrameworkSourceContext('/framework/core', ['/framework/core']), true);
65
+ assert.equal(isFrameworkSourceContext('/framework/core/server', ['/framework/core']), true);
66
+ assert.equal(isFrameworkSourceContext('/repo/node_modules/@klair/usecases', ['/framework/core']), false);
67
+ });
@@ -0,0 +1,104 @@
1
+ const assert = require('node:assert/strict');
2
+ const path = require('node:path');
3
+
4
+ const coreRoot = path.resolve(__dirname, '..');
5
+ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
6
+ process.env.TS_NODE_TRANSPILE_ONLY = '1';
7
+
8
+ require('ts-node/register/transpile-only');
9
+
10
+ const {
11
+ isUiSingletonRequest,
12
+ resolveUiSingletonAliases,
13
+ resolveUiSingletonServerExternalRequest,
14
+ } = require('../cli/compiler/common/uiSingletons.ts');
15
+
16
+ test('UI singleton matcher includes React and Preact runtime packages', () => {
17
+ assert.equal(isUiSingletonRequest('preact'), true);
18
+ assert.equal(isUiSingletonRequest('preact/hooks'), true);
19
+ assert.equal(isUiSingletonRequest('preact-render-to-string'), true);
20
+ assert.equal(isUiSingletonRequest('react'), true);
21
+ assert.equal(isUiSingletonRequest('react-dom/client'), true);
22
+ });
23
+
24
+ test('UI singleton matcher does not match unrelated React-prefixed packages', () => {
25
+ assert.equal(isUiSingletonRequest('react-number-format'), false);
26
+ assert.equal(isUiSingletonRequest('reactive-stream'), false);
27
+ assert.equal(isUiSingletonRequest('@radix-ui/react-popover'), false);
28
+ assert.equal(isUiSingletonRequest(undefined), false);
29
+ });
30
+
31
+ test('UI singleton aliases resolve React imports to Preact runtime requests', () => {
32
+ const seenRequests = [];
33
+ const seenPackageRoots = [];
34
+ const aliases = resolveUiSingletonAliases({
35
+ resolvePackageRoot: (packageName) => {
36
+ seenPackageRoots.push(packageName);
37
+ return `/app/node_modules/${packageName}`;
38
+ },
39
+ resolveRequest: (request) => {
40
+ seenRequests.push(request);
41
+ return `/app/node_modules/${request}`;
42
+ },
43
+ });
44
+
45
+ assert.equal(aliases['preact'], '/app/node_modules/preact');
46
+ assert.equal(aliases['preact$'], '/app/node_modules/preact');
47
+ assert.equal(aliases['preact/hooks$'], '/app/node_modules/preact/hooks');
48
+ assert.equal(aliases['react$'], '/app/node_modules/preact/compat');
49
+ assert.equal(aliases['react-dom$'], '/app/node_modules/preact/compat');
50
+ assert.equal(aliases['react-dom/client$'], '/app/node_modules/preact/compat/client');
51
+ assert.equal(aliases['react/jsx-runtime$'], '/app/node_modules/preact/jsx-runtime');
52
+ assert.equal(aliases['preact-render-to-string'], '/app/node_modules/preact-render-to-string');
53
+ assert.equal(aliases['preact-render-to-string$'], '/app/node_modules/preact-render-to-string');
54
+ assert.deepEqual(seenPackageRoots, ['preact', 'preact-render-to-string']);
55
+ assert.equal(
56
+ Object.keys(aliases).indexOf('preact/jsx-dev-runtime$') < Object.keys(aliases).indexOf('preact'),
57
+ true,
58
+ );
59
+ assert.deepEqual(
60
+ seenRequests.filter((request) => request === 'preact-render-to-string'),
61
+ ['preact-render-to-string'],
62
+ );
63
+ });
64
+
65
+ test('UI singleton server externals resolve Preact and React compat requests from the app', () => {
66
+ const seenRequests = [];
67
+ const resolveRequest = (request) => {
68
+ seenRequests.push(request);
69
+ return `/app/node_modules/${request}`;
70
+ };
71
+
72
+ assert.equal(resolveUiSingletonServerExternalRequest('preact', resolveRequest), '/app/node_modules/preact');
73
+ assert.equal(
74
+ resolveUiSingletonServerExternalRequest('preact/hooks', resolveRequest),
75
+ '/app/node_modules/preact/hooks',
76
+ );
77
+ assert.equal(
78
+ resolveUiSingletonServerExternalRequest('react', resolveRequest),
79
+ '/app/node_modules/preact/compat',
80
+ );
81
+ assert.equal(
82
+ resolveUiSingletonServerExternalRequest('react/jsx-runtime', resolveRequest),
83
+ '/app/node_modules/preact/jsx-runtime',
84
+ );
85
+ assert.equal(
86
+ resolveUiSingletonServerExternalRequest('react-dom/client', resolveRequest),
87
+ '/app/node_modules/preact/compat/client',
88
+ );
89
+ assert.deepEqual(seenRequests, [
90
+ 'preact',
91
+ 'preact/hooks',
92
+ 'preact/compat',
93
+ 'preact/jsx-runtime',
94
+ 'preact/compat/client',
95
+ ]);
96
+ });
97
+
98
+ test('UI singleton server externals keep the SSR renderer compiled', () => {
99
+ const resolveRequest = () => {
100
+ throw new Error('should not resolve');
101
+ };
102
+
103
+ assert.equal(resolveUiSingletonServerExternalRequest('preact-render-to-string', resolveRequest), undefined);
104
+ });
@@ -1,14 +0,0 @@
1
- # Proteum App-Root Addendum
2
-
3
- This file is the app-root-only addendum for a Proteum app that lives inside a larger monorepo.
4
- Keep the reusable Proteum contract in the nearest ancestor root `AGENTS.md`, and keep this file only for instructions that depend on the current directory being the Proteum app root.
5
- Role: keep only app-root workflow and local project-semantic rules here.
6
- Do not put here: reusable Proteum architecture contracts, shared verification rules, documentation-driven coding workflow, diagnostics workflow, optimization checklists, coding-style details, or area-specific rules already covered by broader `AGENTS.md` files or the root-level `DOCUMENTATION.md`, `diagnostics.md`, `optimizations.md`, and `CODING_STYLE.md`.
7
-
8
- ## App-Root Triggers
9
-
10
- - If you are working in a newly created Proteum worktree, before following the rest of these instructions:
11
- - Run `npx proteum worktree init --source <source-app-root>`.
12
- - Read and acknowledge the applicable `AGENTS.md` files.
13
- - Run the dev server with the task-safe elevated-permissions launch workflow from the reusable root `AGENTS.md`, keep it running so user can see the results by himself, and print the live server URL as a clickable Markdown link. If `proteum dev` reports blocked instruction paths, resolve them before continuing.
14
- - If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, use root-level `DOCUMENTATION.md` to choose the smallest relevant `./docs/` pack before editing. If a dev server is already running, print the live dev server URL as a clickable Markdown link.