cabloy 5.1.192 → 5.1.194

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.
Files changed (47) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/package.json +1 -1
  4. package/repo-agent-governance/adapters/claude/contract-loop-gate.ts +15 -187
  5. package/repo-agent-governance/adapters/codex/contract-loop-gate.mjs +25 -0
  6. package/repo-agent-governance/adapters/codex/hooks.json +19 -0
  7. package/repo-agent-governance/adapters/cursor/contract-loop-gate.mjs +28 -0
  8. package/repo-agent-governance/adapters/cursor/hooks.json +19 -0
  9. package/repo-agent-governance/managed-assets.json +38 -10
  10. package/repo-agent-governance/manifest.json +24 -0
  11. package/repo-agent-governance/policies/repository.md +1 -1
  12. package/repo-agent-governance/scripts/governance.mjs +101 -2
  13. package/repo-agent-governance/scripts/pack-check.mjs +5 -0
  14. package/repo-agent-governance/skills/cabloy-contract-loop/SKILL.md +9 -2
  15. package/repo-agent-governance/skills/cabloy-contract-loop/references/verification-checklist.md +5 -3
  16. package/repo-agent-governance/tests/governance.test.mjs +306 -0
  17. package/repo-agent-governance/tools/contract-loop/core.mjs +30 -14
  18. package/repo-agent-governance/tools/contract-loop/hook-runtime.mjs +368 -0
  19. package/repo-docs/ai/agent-governance.md +8 -6
  20. package/repo-docs/ai/docs-skills-rules-mapping.md +4 -1
  21. package/repo-docs/ai/edition-consistency-checklist.md +1 -1
  22. package/repo-docs/ai/introduction.md +1 -1
  23. package/repo-docs/ai/repo-guidance.md +1 -1
  24. package/repo-docs/ai/rules-and-config.md +5 -3
  25. package/repo-docs/backend/menu-authorization.md +1 -0
  26. package/repo-docs/backend/menu-guide.md +18 -7
  27. package/repo-docs/blogs/cabloy-fullstack-resource-addressing/index.md +1 -0
  28. package/repo-docs/reference/package-map.md +1 -1
  29. package/repo-e2e/config/playwright.config.ts +1 -1
  30. package/repo-e2e/scripts/startE2eVona.ts +6 -2
  31. package/repo-e2e/specs/controller-boundary.spec.ts +76 -28
  32. package/vona/packages-vona/vona/package.json +1 -1
  33. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  34. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +2 -0
  35. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenu.ts +9 -3
  36. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuEligibility.ts +14 -0
  37. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuGroup.ts +9 -3
  38. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuSite.ts +10 -0
  39. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrMenu.ts +1 -1
  40. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrMenuGroup.ts +1 -1
  41. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuEligibility.test.ts +73 -13
  42. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuSite.test.ts +90 -0
  43. package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
  44. package/vona/src/suite-vendor/a-vona/modules/a-error/package.json +1 -1
  45. package/vona/src/suite-vendor/a-vona/modules/a-error/src/lib/onerror.ts +9 -0
  46. package/vona/src/suite-vendor/a-vona/modules/a-error/test/onerror.test.ts +92 -0
  47. package/vona/src/suite-vendor/a-vona/package.json +1 -1
@@ -0,0 +1,368 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import {
7
+ analyze,
8
+ analyzePath,
9
+ buildMessages,
10
+ isCodeFile,
11
+ isHighConfidenceReverseSource,
12
+ normalizePath,
13
+ resolveEdition,
14
+ toPosixPath,
15
+ } from './core.mjs';
16
+
17
+ const STATE_FILE = path.join(os.tmpdir(), 'cabloy-contract-loop-gate-state.json');
18
+ const AUTO_SYNC_WINDOW_SECONDS = 300;
19
+
20
+ function readText(filePath) {
21
+ try {
22
+ return readFileSync(filePath, 'utf8');
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function isDirectory(filePath) {
29
+ try {
30
+ return statSync(filePath).isDirectory();
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function canonicalPath(filePath) {
37
+ let current = filePath;
38
+ const missingSegments = [];
39
+ for (;;) {
40
+ try {
41
+ return path.resolve(realpathSync(current), ...missingSegments);
42
+ } catch {
43
+ const parent = path.dirname(current);
44
+ if (parent === current) return filePath;
45
+ missingSegments.unshift(path.basename(current));
46
+ current = parent;
47
+ }
48
+ }
49
+ }
50
+
51
+ function isInside(root, candidate) {
52
+ const relative = path.relative(canonicalPath(root), canonicalPath(candidate));
53
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..');
54
+ }
55
+
56
+ function loadState() {
57
+ if (!existsSync(STATE_FILE)) return {};
58
+ try {
59
+ return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
60
+ } catch {
61
+ return {};
62
+ }
63
+ }
64
+
65
+ function saveState(state) {
66
+ try {
67
+ writeFileSync(STATE_FILE, JSON.stringify(state), 'utf8');
68
+ } catch {
69
+ // State prevents duplicate convenience syncs only; it must not block guidance.
70
+ }
71
+ }
72
+
73
+ function syncFingerprint(filePath) {
74
+ try {
75
+ const stats = statSync(filePath, { bigint: true });
76
+ return `${filePath}:${stats.mtimeNs.toString()}`;
77
+ } catch {
78
+ return filePath;
79
+ }
80
+ }
81
+
82
+ function shouldSkipAutoSync(root, filePath) {
83
+ const state = loadState();
84
+ const entry = state[toPosixPath(root)];
85
+ return Boolean(
86
+ entry &&
87
+ entry.fingerprint === syncFingerprint(filePath) &&
88
+ Date.now() - entry.timestamp <= AUTO_SYNC_WINDOW_SECONDS * 1000,
89
+ );
90
+ }
91
+
92
+ function markAutoSync(root, filePath) {
93
+ const state = loadState();
94
+ state[toPosixPath(root)] = { fingerprint: syncFingerprint(filePath), timestamp: Date.now() };
95
+ saveState(state);
96
+ }
97
+
98
+ function runNpm(root, args) {
99
+ const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
100
+ return spawnSync(command, [...args], { cwd: root, encoding: 'utf8' });
101
+ }
102
+
103
+ function summarizeProcess(result) {
104
+ if (result.error) return result.error.message;
105
+ const combined = [result.stdout?.trim(), result.stderr?.trim()].filter(Boolean).join('\n');
106
+ const exitCode = result.status ?? 1;
107
+ if (!combined) return `command exited with code ${exitCode}`;
108
+ return `exit ${exitCode}: ${combined
109
+ .split(/\r?\n/)
110
+ .map(line => line.trim())
111
+ .filter(Boolean)
112
+ .slice(-3)
113
+ .join(' | ')}`;
114
+ }
115
+
116
+ function autoSyncReverse(root, filePath, edition) {
117
+ for (const command of edition.reverseAutoSyncCommands) {
118
+ const result = runNpm(root, command.args);
119
+ if (result.status !== 0) {
120
+ return {
121
+ kind: 'failure',
122
+ message: `Auto-sync failed during \`${command.display}\`: ${summarizeProcess(result)}`,
123
+ };
124
+ }
125
+ }
126
+ markAutoSync(root, filePath);
127
+ return {
128
+ kind: 'success',
129
+ message: `Auto-sync ran ${edition.reverseAutoSyncCommands
130
+ .map(command => `\`${command.display}\``)
131
+ .join(' and ')} for this ${edition.label} reverse-chain edit.`,
132
+ };
133
+ }
134
+
135
+ export function resolveReverseSyncOutcome(root, filePath, reverseReason) {
136
+ if (!reverseReason) return { kind: 'not-applicable', message: '' };
137
+ if (!isHighConfidenceReverseSource(filePath)) {
138
+ return {
139
+ kind: 'not-applicable',
140
+ message:
141
+ 'Auto-sync did not run because this reverse-chain signal came from the consumer side rather than a high-confidence frontend source edit.',
142
+ };
143
+ }
144
+ const resolution = resolveEdition(root);
145
+ if (resolution.kind !== 'resolved') {
146
+ return {
147
+ kind: 'not-applicable',
148
+ message: `Auto-sync did not run because ${resolution.kind === 'ambiguous' ? 'both Cabloy edition markers are present' : 'no Cabloy edition marker is present'}.`,
149
+ };
150
+ }
151
+ if (shouldSkipAutoSync(root, filePath)) {
152
+ return {
153
+ kind: 'skipped',
154
+ message:
155
+ 'Auto-sync skipped because the same reverse-source edit was already synced recently in this repo.',
156
+ };
157
+ }
158
+ return autoSyncReverse(root, filePath, resolution.edition);
159
+ }
160
+
161
+ export function readHookPayload() {
162
+ try {
163
+ return JSON.parse(readFileSync(0, 'utf8'));
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ export function extractClaudeEditedFilePath(payload) {
170
+ const filePath = payload?.tool_input?.file_path;
171
+ return typeof filePath === 'string' && filePath ? filePath : null;
172
+ }
173
+
174
+ export function extractCodexEditedFiles(payload) {
175
+ const command = payload?.tool_input?.command;
176
+ if (typeof command !== 'string') return [];
177
+ const entries = [];
178
+ let pendingUpdate = null;
179
+ const headerPattern = /^\*\*\* (Add|Update|Delete) File:(.+)$|^\*\*\* Move to:(.+)$/;
180
+
181
+ const flushUpdate = () => {
182
+ if (pendingUpdate) entries.push(pendingUpdate);
183
+ pendingUpdate = null;
184
+ };
185
+
186
+ for (const line of command.split(/\r?\n/)) {
187
+ const match = headerPattern.exec(line);
188
+ if (!match) continue;
189
+ const [, verb, sourcePath, destinationPath] = match;
190
+ if (destinationPath !== undefined) {
191
+ if (pendingUpdate) pendingUpdate.filePath = destinationPath.trim();
192
+ continue;
193
+ }
194
+ flushUpdate();
195
+ const filePath = sourcePath.trim();
196
+ if (!filePath) continue;
197
+ const operation = verb.toLowerCase();
198
+ if (operation === 'update') {
199
+ pendingUpdate = { operation, filePath };
200
+ } else {
201
+ entries.push({ operation, filePath });
202
+ }
203
+ }
204
+ flushUpdate();
205
+ return entries;
206
+ }
207
+
208
+ // Retain the path-only export for callers that only need the historical view.
209
+ export function extractCodexEditedFilePaths(payload) {
210
+ return extractCodexEditedFiles(payload).map(entry => entry.filePath);
211
+ }
212
+
213
+ export function extractCursorEditedFilePath(payload) {
214
+ if (!payload || typeof payload !== 'object') return null;
215
+ if (typeof payload.file_path === 'string' && payload.file_path) return payload.file_path;
216
+ const input = payload.tool_input;
217
+ if (!input || typeof input !== 'object') return null;
218
+ for (const key of ['path', 'file_path', 'target_file']) {
219
+ const value = input[key];
220
+ if (typeof value === 'string' && value) return value;
221
+ }
222
+ return null;
223
+ }
224
+
225
+ export function inferCursorHookEvent(payload) {
226
+ const eventName = payload?.hook_event_name;
227
+ if (
228
+ eventName === 'afterFileEdit' ||
229
+ eventName === 'afterTabFileEdit' ||
230
+ eventName === 'postToolUse'
231
+ ) {
232
+ return eventName;
233
+ }
234
+ if (typeof payload?.file_path === 'string' && payload.file_path && !payload.tool_name) {
235
+ return 'afterFileEdit';
236
+ }
237
+ return 'postToolUse';
238
+ }
239
+
240
+ export function formatClaudeHookOutput(message) {
241
+ return {
242
+ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: message },
243
+ systemMessage: message,
244
+ };
245
+ }
246
+
247
+ export function formatCodexHookOutput(message) {
248
+ return {
249
+ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: message },
250
+ systemMessage: message,
251
+ };
252
+ }
253
+
254
+ export function formatCursorHookOutput(message, eventName) {
255
+ if (eventName !== 'postToolUse') return null;
256
+ return { additional_context: message };
257
+ }
258
+
259
+ function resolvePathBase(root, candidate) {
260
+ if (typeof candidate !== 'string' || !candidate) return root;
261
+ const pathBase = normalizePath(root, candidate);
262
+ if (!pathBase || !isInside(root, pathBase) || !isDirectory(pathBase)) return root;
263
+ return pathBase;
264
+ }
265
+
266
+ function createInspectionFailure(root, filePath, operation) {
267
+ const relativePath = toPosixPath(path.relative(root, filePath));
268
+ return {
269
+ kind: 'failure',
270
+ filePath,
271
+ message: `Contract-loop gate: could not inspect edited source \`${relativePath}\` after this ${operation} patch; no contract-loop guidance or auto-sync ran.`,
272
+ };
273
+ }
274
+
275
+ function inspectEditedFile(root, rawPath, { operation = 'update', pathBase = root } = {}) {
276
+ const filePath = normalizePath(pathBase, rawPath);
277
+ if (!filePath || !isInside(root, filePath) || !isCodeFile(filePath)) return null;
278
+
279
+ const resolution = resolveEdition(root);
280
+ if (operation === 'delete') {
281
+ const result = analyzePath(filePath);
282
+ if (!result.forwardReason && !result.reverseReason) return null;
283
+ return { kind: 'signal', filePath, resolution, result };
284
+ }
285
+
286
+ const content = readText(filePath);
287
+ if (content === null) return createInspectionFailure(root, filePath, operation);
288
+ const result = analyze(
289
+ filePath,
290
+ content,
291
+ resolution.kind === 'resolved' ? resolution.edition : null,
292
+ );
293
+ if (!result.forwardReason && !result.reverseReason) return null;
294
+ return { kind: 'signal', filePath, resolution, result };
295
+ }
296
+
297
+ export function evaluateEditedFile(root, rawPath) {
298
+ const inspected = inspectEditedFile(root, rawPath);
299
+ if (!inspected || inspected.kind !== 'signal') return null;
300
+ const { filePath, resolution, result } = inspected;
301
+ const reverseSyncOutcome = resolveReverseSyncOutcome(root, filePath, result.reverseReason);
302
+ return {
303
+ filePath,
304
+ message: buildMessages(result, resolution, reverseSyncOutcome),
305
+ };
306
+ }
307
+
308
+ export function evaluateCodexEditedFiles(root, entries, payloadCwd) {
309
+ const pathBase = resolvePathBase(root, payloadCwd);
310
+ const seenEntries = new Set();
311
+ const inspected = [];
312
+ for (const entry of entries) {
313
+ if (!entry || typeof entry.filePath !== 'string') continue;
314
+ const operation = ['add', 'update', 'delete'].includes(entry.operation)
315
+ ? entry.operation
316
+ : 'update';
317
+ const normalizedPath = normalizePath(pathBase, entry.filePath);
318
+ if (!normalizedPath || !isInside(root, normalizedPath)) continue;
319
+ const key = `${operation}:${normalizedPath}`;
320
+ if (seenEntries.has(key)) continue;
321
+ seenEntries.add(key);
322
+ const item = inspectEditedFile(root, entry.filePath, { operation, pathBase });
323
+ if (item) inspected.push(item);
324
+ }
325
+
326
+ const autoSyncItem = inspected.find(
327
+ item =>
328
+ item.kind === 'signal' &&
329
+ item.result.reverseReason &&
330
+ isHighConfidenceReverseSource(item.filePath),
331
+ );
332
+ const autoSyncOutcome = autoSyncItem
333
+ ? resolveReverseSyncOutcome(root, autoSyncItem.filePath, autoSyncItem.result.reverseReason)
334
+ : null;
335
+
336
+ return inspected.map(item => {
337
+ if (item.kind === 'failure') return { filePath: item.filePath, message: item.message };
338
+
339
+ let reverseSyncOutcome;
340
+ if (item === autoSyncItem) {
341
+ reverseSyncOutcome = autoSyncOutcome;
342
+ } else if (item.result.reverseReason && isHighConfidenceReverseSource(item.filePath)) {
343
+ reverseSyncOutcome = {
344
+ kind: 'skipped',
345
+ message:
346
+ 'Auto-sync skipped because another high-confidence reverse-source file in this Codex edit was already handled.',
347
+ };
348
+ } else {
349
+ reverseSyncOutcome = resolveReverseSyncOutcome(
350
+ root,
351
+ item.filePath,
352
+ item.result.reverseReason,
353
+ );
354
+ }
355
+ return {
356
+ filePath: item.filePath,
357
+ message: buildMessages(item.result, item.resolution, reverseSyncOutcome),
358
+ };
359
+ });
360
+ }
361
+
362
+ // Keep the prior general API for direct callers. Codex now uses the operation-aware evaluator.
363
+ export function evaluateEditedFiles(root, rawPaths) {
364
+ return evaluateCodexEditedFiles(
365
+ root,
366
+ rawPaths.map(filePath => ({ operation: 'update', filePath })),
367
+ );
368
+ }
@@ -15,14 +15,16 @@ npm run agent:governance:check
15
15
 
16
16
  The committed repository adapter outputs are:
17
17
 
18
- | Agent surface | Generated repository assets |
19
- | ------------- | ---------------------------------------------------------------------------------------------- |
20
- | Claude Code | `CLAUDE.md`, `.claude/skills/`, `.claude/commands/`, `.claude/settings.json`, `.claude/hooks/` |
21
- | Codex | `AGENTS.md`, `.agents/skills/` |
22
- | Cursor | `.cursor/rules/cabloy-governance.mdc`, `.cursor/skills/` |
18
+ | Agent surface | Generated repository assets |
19
+ | ------------- | ------------------------------------------------------------------------------------------------ |
20
+ | Claude Code | `CLAUDE.md`, `.claude/skills/`, `.claude/commands/`, `.claude/settings.json`, `.claude/hooks/` |
21
+ | Codex | `AGENTS.md`, `.agents/skills/`, `.codex/hooks.json`, `.codex/hooks/` |
22
+ | Cursor | `.cursor/rules/cabloy-governance.mdc`, `.cursor/skills/`, `.cursor/hooks.json`, `.cursor/hooks/` |
23
23
 
24
24
  The same root rule and skill sources are copied to every listed adapter. Platform-specific discovery metadata is intentionally thin and lives in the adapter layer. The generated Codex and Cursor skill paths are configured adapter locations; their client discovery behavior requires a version-pinned external smoke check before it is claimed as supported.
25
25
 
26
+ Codex requires the user to trust project-local hooks before it runs `.codex/hooks.json`; that local trust state is not generated or committed by this repository. The repository launcher resolves a hook from nested working directories without Git once Codex invokes it, but Codex discovery remains client-version behavior. For non-Git projects opened from nested directories, configure local Codex `project_root_markers` to include `__CABLOY_BASIC__` and `__CABLOY_START__` alongside `.git`; do not generate that user setting from this repository.
27
+
26
28
  ### Package migration bridge
27
29
 
28
30
  The transition package deliberately excludes generated root adapters because the immediately preceding upgrader overwrites those paths before it starts the incoming initializer. This prevents that legacy path from silently replacing customized adapters. The package still includes canonical governance and `scripts/init.ts`; `npm run init` performs ownership-aware adoption for both a staged upgrade and a fresh project. A source checkout retains its committed generated adapters. This bridge is package-transition behavior, not a claim that agents generate instructions at startup.
@@ -31,7 +33,7 @@ The transition package deliberately excludes generated root adapters because the
31
33
 
32
34
  Rules, procedural skills, references, eval fixtures, contract-loop analysis, and ordinary deterministic repository tools are portable.
33
35
 
34
- Hooks, tool permissions, local settings, credentials, MCP registration, slash-command discovery, confirmation UI, and automatic command execution are not portable contracts. Claude Code retains the repository `PostToolUse` contract-loop convenience hook. Codex and Cursor receive the same advisory contract-loop guidance but must not be described as running the Claude hook or its automatic commands.
36
+ Hooks, tool permissions, local settings, credentials, MCP registration, slash-command discovery, confirmation UI, and automatic command execution are not portable contracts. Claude Code, Codex, and Cursor each have a platform-specific contract-loop convenience hook over the same portable classifier and reverse auto-sync runtime. Their hook payloads and stdout contracts differ, so each adapter must remain provider-specific even when it shares the same runtime.
35
37
 
36
38
  No adapter output owns `settings.local.json`, `CLAUDE.local.md`, worktree state, scheduled-task state, credentials, or arbitrary user-owned agent files.
37
39
 
@@ -81,6 +81,9 @@ Generated adapter locations include:
81
81
 
82
82
  - policy adapters: `CLAUDE.md`, `AGENTS.md`, and `.cursor/rules/`
83
83
  - Claude-specific command adapter: `.claude/commands/`
84
+ - Claude-specific hook adapter: `.claude/settings.json`, `.claude/hooks/`
85
+ - Codex-specific hook adapter: `.codex/hooks.json`, `.codex/hooks/`
86
+ - Cursor-specific hook adapter: `.cursor/hooks.json`, `.cursor/hooks/`
84
87
 
85
88
  Codex and Cursor adapter paths are configured output locations; verify active-client discovery externally before claiming that behavior is supported.
86
89
 
@@ -98,7 +101,7 @@ Canonical location:
98
101
 
99
102
  - `repo-agent-governance/skills/`
100
103
 
101
- Generated adapter locations include `.claude/skills/`, `.agents/skills/`, and `.cursor/skills/`. The Codex and Cursor locations are configured output paths, not verified client-discovery claims; validate the active client version externally before relying on discovery.
104
+ Generated adapter locations include `.claude/skills/`, `.agents/skills/`, `.codex/`, and `.cursor/skills/`. The Codex and Cursor locations are configured output paths, not verified client-discovery claims; validate the active client version externally before relying on discovery.
102
105
 
103
106
  Use skills for:
104
107
 
@@ -7,7 +7,7 @@ Use it whenever you add or revise:
7
7
  - edition-aware docs
8
8
  - `repo-agent-governance/` rules, commands, and skill bundles
9
9
  - generated Claude, Codex, or Cursor adapter outputs
10
- - platform-specific Claude commands, hooks, or settings when they are affected
10
+ - platform-specific Claude, Codex, or Cursor commands, hooks, or settings when they are affected
11
11
  - AI workflow playbooks
12
12
 
13
13
  ## The baseline story that should stay consistent
@@ -52,7 +52,7 @@ Use `repo-agent-governance/` as the authored source for portable repository rule
52
52
 
53
53
  ### Platform runtime integration
54
54
 
55
- `CLAUDE.md`, `.claude/commands/`, `.claude/skills/`, `AGENTS.md`, `.agents/skills/`, and `.cursor/` are adapter/runtime surfaces. Hooks, permissions, local settings, credentials, and provider-specific command discovery stay platform-specific rather than becoming shared framework facts.
55
+ `CLAUDE.md`, `.claude/commands/`, `.claude/skills/`, `AGENTS.md`, `.agents/skills/`, `.codex/`, and `.cursor/` are adapter/runtime surfaces. Hooks, permissions, local settings, credentials, and provider-specific command discovery stay platform-specific rather than becoming shared framework facts.
56
56
 
57
57
  ## AI reading paths
58
58
 
@@ -11,7 +11,7 @@ Check the root repository signals first:
11
11
  - `repo-docs-internal/`
12
12
  - `repo-specs/` when the task concerns a suite plan, WBS increment, acceptance record, or evidence
13
13
  - `repo-agent-governance/` canonical rules, skill bundles, and deterministic tools
14
- - generated platform adapter assets such as `CLAUDE.md`, `AGENTS.md`, `.claude/`, `.agents/`, and `.cursor/`
14
+ - generated platform adapter assets such as `CLAUDE.md`, `AGENTS.md`, `.claude/`, `.agents/`, `.codex/`, and `.cursor/`
15
15
 
16
16
  These surfaces tell the agent which repo it is in, which scripts are canonical, and where shared agent instructions are authored. Treat platform directories as generated adapters; regenerate them from `repo-agent-governance/` rather than hand-editing them. Inspect relevant `repo-docs-internal/` records as supporting context rather than as a gate to workflow guidance.
17
17
 
@@ -21,10 +21,12 @@ npm run test:agent-governance
21
21
  The following are committed generated outputs, not parallel authored knowledge sources:
22
22
 
23
23
  - Claude Code: `CLAUDE.md`, `.claude/commands/`, `.claude/skills/`, `.claude/settings.json`, `.claude/hooks/`
24
- - Codex: `AGENTS.md`, `.agents/skills/`
25
- - Cursor: `.cursor/rules/cabloy-governance.mdc`, `.cursor/skills/`
24
+ - Codex: `AGENTS.md`, `.agents/skills/`, `.codex/hooks.json`, `.codex/hooks/`
25
+ - Cursor: `.cursor/rules/cabloy-governance.mdc`, `.cursor/skills/`, `.cursor/hooks.json`, `.cursor/hooks/`
26
26
 
27
- The Codex and Cursor skill paths are configured generated adapter locations; verify client discovery with a version-pinned external smoke check before presenting it as supported behavior. The adapter model shares rules and procedures, not platform runtime behavior. Claude Code's contract-loop hook is a Claude-specific convenience integration; Codex and Cursor receive advisory guidance but do not claim the same automatic after-edit action.
27
+ The Codex and Cursor skill paths are configured generated adapter locations; verify client discovery with a version-pinned external smoke check before presenting it as supported behavior. The adapter model shares rules and procedures, not hook payload formats. Claude Code, Codex, and Cursor each register a platform-specific contract-loop convenience hook over the same portable runtime.
28
+
29
+ Codex runs project-local `.codex/hooks.json` only after the user trusts that hook definition. Hook trust is local state and remains outside generated governance ownership. The generated launcher is Git-free once Codex invokes it, but non-Git project discovery from a nested directory remains client-controlled; configure local Codex `project_root_markers` with Cabloy edition markers when that scenario must be supported.
28
30
 
29
31
  ## Local configuration and permissions
30
32
 
@@ -18,6 +18,7 @@ An SSR menu item can declare `roles` metadata:
18
18
  link: 'presetResource',
19
19
  roles: ['systemAdmin'],
20
20
  },
21
+ site: 'basic-siteadmin:admin',
21
22
  })
22
23
  ```
23
24
 
@@ -57,9 +57,16 @@ In the current repo implementation, the out-of-the-box menu controller is public
57
57
 
58
58
  ## SSR Site targeting
59
59
 
60
- `@SsrMenu(...)` and `@SsrMenuGroup(...)` use `site` to select the SSR Site onion(s) that receive the declaration. It is a navigation-composition filter, not a site-admission, role-visibility, or backend-authorization decision.
60
+ `@SsrMenu(...)` and `@SsrMenuGroup(...)` require an explicit `site` to select the SSR Site onion(s) that receive the declaration. It is a navigation-composition filter, not a site-admission, role-visibility, or backend-authorization decision.
61
61
 
62
- Use `site` for navigation owned by one SSR Site or deliberately exclusive to a known subset of sites:
62
+ - Use a site string or an array of site strings to target known SSR Sites.
63
+ - Omission, `undefined`, `null`, and empty or whitespace-only strings are invalid, including blank entries in an array.
64
+ - `site: []` is valid and explicitly means no sites receive the declaration.
65
+ - Sharing requires an explicit array of intended sites; there is no wildcard or implicit all-sites scope.
66
+ - Menu and group declarations are independently scoped. A menu does not inherit its group's `site`; declare the intended sites on both.
67
+ - `locale` remains optional: omitting it leaves the declaration unrestricted by locale.
68
+
69
+ Use a single site string for navigation owned by one SSR Site:
63
70
 
64
71
  ```typescript
65
72
  @SsrMenu({
@@ -69,11 +76,11 @@ Use `site` for navigation owned by one SSR Site or deliberately exclusive to a k
69
76
  roles: ['systemAdmin'],
70
77
  },
71
78
  // This Basic Admin-owned entry intentionally targets one SSR Site.
72
- site: ['basic-siteadmin:admin'],
79
+ site: 'basic-siteadmin:admin',
73
80
  })
74
81
  ```
75
82
 
76
- For a shared module declaration intended for every compatible SSR Admin site, omit `site`:
83
+ For a shared module declaration, list every intended compatible SSR Admin site explicitly:
77
84
 
78
85
  ```typescript
79
86
  @SsrMenu({
@@ -82,11 +89,15 @@ For a shared module declaration intended for every compatible SSR Admin site, om
82
89
  link: 'presetResource',
83
90
  roles: ['systemAdmin'],
84
91
  },
85
- // No `site`: the shared declaration is available to compatible SSR Admin sites.
92
+ // Share only with these explicitly selected SSR Sites.
93
+ site: ['basic-siteadmin:admin', 'commerce-siteadmin:commerceAdmin'],
86
94
  })
87
95
  ```
88
96
 
89
- Do not bind a shared Admin menu or menu group to the current/default Admin site merely to keep a new independent site's navigation narrow. Isolate an independent site through its enabled module composition and site-owned menus/groups. When a shared capability needs genuinely different navigation contracts per site, use an explicit, approved site-specific composition or owner instead of narrowing the shared declaration by default.
97
+ Choose the site list from the intended navigation contract, not merely the current/default Admin site. Isolate an independent site through its enabled module composition and site-owned menus/groups. When a shared capability needs genuinely different navigation contracts per site, use an explicit, approved site-specific composition or owner.
98
+
99
+ > [!WARNING]
100
+ > This is a breaking change for declarations that previously omitted `site` to reach all sites. Migrate each menu and group to an explicit site string or array, or use `site: []` when it should reach no sites. Add future shared sites to the array deliberately; omission is no longer a sharing mechanism.
90
101
 
91
102
  `site` is distinct from a role's `siteIds`, menu `roles` or role-menu associations, and Passport/RBAC authorization:
92
103
 
@@ -140,6 +151,6 @@ When editing SSR menu behavior, ask:
140
151
  2. is there a default fallback menu that should remain available?
141
152
  3. does the menu contract belong in backend API design, frontend route design, or both?
142
153
  4. does the active edition affect the menu structure or public path assumptions?
143
- 5. is this declaration site-owned or deliberately site-exclusive (specify `site`), or shared across compatible SSR Admin sites (omit `site`)?
154
+ 5. does every menu and group declare its own explicit `site` string or array, with all intended shared sites listed and `[]` used only when no sites should receive it?
144
155
 
145
156
  That helps AI keep menu behavior aligned with Cabloy’s shared SSR architecture.
@@ -219,6 +219,7 @@ The Student SSR menu supplies that context:
219
219
  },
220
220
  },
221
221
  },
222
+ site: ['basic-siteadmin:admin'],
222
223
  })
223
224
  export class SsrMenuStudent extends BeanBase {}
224
225
  ```
@@ -8,7 +8,7 @@ This monorepo is organized around a small set of stable top-level areas.
8
8
  - `lerna.json` — package topology
9
9
  - `repo-docs-internal/` — edition-local maintainer notes and ADRs
10
10
  - `repo-agent-governance/` — authored portable agent rules, root skill bundles, adapter definitions, and deterministic tools
11
- - `CLAUDE.md`, `AGENTS.md`, `.claude/`, `.agents/`, `.cursor/` — generated platform adapters plus platform-specific runtime integration
11
+ - `CLAUDE.md`, `AGENTS.md`, `.claude/`, `.agents/`, `.codex/`, `.cursor/` — generated platform adapters plus platform-specific runtime integration
12
12
  - `repo-docs/` — unified public docs
13
13
  - `repo-specs/` — suite-local product, technical, delivery, ATP, progress, observed-evidence, and ADR authority
14
14
 
@@ -21,7 +21,7 @@ export default defineConfig({
21
21
  : {
22
22
  command: 'node repo-e2e/scripts/startE2eVona.ts',
23
23
  cwd: E2E_ROOT_DIR,
24
- url: `${baseURL}/`,
24
+ url: `${baseURL}/health/ready`,
25
25
  timeout: 180_000,
26
26
  reuseExistingServer: false,
27
27
  stdout: 'pipe',
@@ -2,13 +2,17 @@ import type { ChildProcess } from 'node:child_process';
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
4
 
5
- import { E2E_ROOT_DIR } from './e2e.ts';
5
+ import { E2E_LOCAL_BASE_URL, E2E_ROOT_DIR } from './e2e.ts';
6
6
 
7
7
  const child: ChildProcess = spawn('npm', ['run', 'dev:one'], {
8
8
  cwd: E2E_ROOT_DIR,
9
9
  detached: process.platform !== 'win32',
10
10
  stdio: 'inherit',
11
- env: process.env,
11
+ env: {
12
+ ...process.env,
13
+ SERVER_SERVE_PROTOCOL: 'http',
14
+ SERVER_SERVE_HOST: E2E_LOCAL_BASE_URL.replace(/^https?:\/\//, ''),
15
+ },
12
16
  });
13
17
 
14
18
  const gracefulShutdownTimeout = 7000;