cabloy 5.1.192 → 5.1.193

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/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.192
1
+ 5.1.193
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.193
4
+
5
+ ### Features
6
+
7
+ - Add support for Cursor and Codex agents.
8
+ - Update agent capabilities and integrations.
9
+
3
10
  ## 5.1.192
4
11
 
5
12
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.192",
3
+ "version": "5.1.193",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -1,198 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import type { SpawnSyncReturns } from 'node:child_process';
3
-
4
- import { spawnSync } from 'node:child_process';
5
- import { readFileSync, statSync, writeFileSync, existsSync } from 'node:fs';
6
- import os from 'node:os';
7
- import path from 'node:path';
2
+ import { dirname, resolve } from 'node:path';
8
3
  import { fileURLToPath } from 'node:url';
9
4
 
10
5
  import {
11
- analyze,
12
- buildMessages,
13
- isCodeFile,
14
- isHighConfidenceReverseSource,
15
- normalizePath,
16
- resolveEdition,
17
- toPosixPath,
18
- } from '../../repo-agent-governance/tools/contract-loop/core.mjs';
19
-
20
- interface EditionConfig {
21
- id: 'basic' | 'start';
22
- label: string;
23
- reverseAutoSyncCommands: ReadonlyArray<{ args: readonly string[]; display: string }>;
24
- }
25
-
26
- interface HookPayload {
27
- tool_input?: {
28
- file_path?: string;
29
- };
30
- }
31
-
32
- interface SyncStateEntry {
33
- fingerprint: string;
34
- timestamp: number;
35
- }
36
-
37
- type ReverseSyncOutcome =
38
- | { kind: 'not-applicable'; message: string }
39
- | { kind: 'skipped'; message: string }
40
- | { kind: 'success'; message: string }
41
- | { kind: 'failure'; message: string };
6
+ evaluateEditedFile,
7
+ extractClaudeEditedFilePath,
8
+ formatClaudeHookOutput,
9
+ readHookPayload,
10
+ } from '../../repo-agent-governance/tools/contract-loop/hook-runtime.mjs';
42
11
 
43
- type SyncState = Record<string, SyncStateEntry>;
44
-
45
- const SCRIPT_FILE = fileURLToPath(import.meta.url);
46
- const ROOT = path.resolve(path.dirname(SCRIPT_FILE), '../..');
47
- const ROOT_KEY = toPosixPath(ROOT);
48
- const STATE_FILE = path.join(os.tmpdir(), 'cabloy-contract-loop-gate-state.json');
49
- const AUTO_SYNC_WINDOW_SECONDS = 300;
50
-
51
- function readText(filePath: string): string | null {
52
- try {
53
- return readFileSync(filePath, 'utf8');
54
- } catch {
55
- return null;
56
- }
57
- }
58
-
59
- function loadState(): SyncState {
60
- if (!existsSync(STATE_FILE)) return {};
61
- try {
62
- return JSON.parse(readFileSync(STATE_FILE, 'utf8')) as SyncState;
63
- } catch {
64
- return {};
65
- }
66
- }
67
-
68
- function saveState(state: SyncState): void {
69
- try {
70
- writeFileSync(STATE_FILE, JSON.stringify(state), 'utf8');
71
- } catch {
72
- // State prevents duplicate convenience syncs only; it must not block guidance.
73
- }
74
- }
75
-
76
- function syncFingerprint(filePath: string): string {
77
- try {
78
- const stats = statSync(filePath, { bigint: true });
79
- return `${filePath}:${stats.mtimeNs.toString()}`;
80
- } catch {
81
- return filePath;
82
- }
83
- }
84
-
85
- function shouldSkipAutoSync(filePath: string): boolean {
86
- const state = loadState();
87
- const entry = state[ROOT_KEY];
88
- return Boolean(
89
- entry &&
90
- entry.fingerprint === syncFingerprint(filePath) &&
91
- Date.now() - entry.timestamp <= AUTO_SYNC_WINDOW_SECONDS * 1000,
92
- );
93
- }
94
-
95
- function markAutoSync(filePath: string): void {
96
- const state = loadState();
97
- state[ROOT_KEY] = { fingerprint: syncFingerprint(filePath), timestamp: Date.now() };
98
- saveState(state);
99
- }
100
-
101
- function runNpm(args: readonly string[]): SpawnSyncReturns<string> {
102
- const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
103
- return spawnSync(command, [...args], { cwd: ROOT, encoding: 'utf8' });
104
- }
105
-
106
- function summarizeProcess(result: SpawnSyncReturns<string>): string {
107
- if (result.error) return result.error.message;
108
- const combined = [result.stdout?.trim(), result.stderr?.trim()].filter(Boolean).join('\n');
109
- const exitCode = result.status ?? 1;
110
- if (!combined) return `command exited with code ${exitCode}`;
111
- return `exit ${exitCode}: ${combined
112
- .split(/\r?\n/)
113
- .map(line => line.trim())
114
- .filter(Boolean)
115
- .slice(-3)
116
- .join(' | ')}`;
117
- }
118
-
119
- function autoSyncReverse(filePath: string, edition: EditionConfig): ReverseSyncOutcome {
120
- for (const command of edition.reverseAutoSyncCommands) {
121
- const result = runNpm(command.args);
122
- if (result.status !== 0) {
123
- return {
124
- kind: 'failure',
125
- message: `Auto-sync failed during \`${command.display}\`: ${summarizeProcess(result)}`,
126
- };
127
- }
128
- }
129
- markAutoSync(filePath);
130
- return {
131
- kind: 'success',
132
- message: `Auto-sync ran ${edition.reverseAutoSyncCommands
133
- .map(command => `\`${command.display}\``)
134
- .join(' and ')} for this ${edition.label} reverse-chain edit.`,
135
- };
136
- }
137
-
138
- function resolveReverseSyncOutcome(
139
- filePath: string,
140
- reverseReason: string | null,
141
- ): ReverseSyncOutcome {
142
- if (!reverseReason) return { kind: 'not-applicable', message: '' };
143
- if (!isHighConfidenceReverseSource(filePath)) {
144
- return {
145
- kind: 'not-applicable',
146
- message:
147
- 'Auto-sync did not run because this reverse-chain signal came from the consumer side rather than a high-confidence frontend source edit.',
148
- };
149
- }
150
- const resolution = resolveEdition(ROOT);
151
- if (resolution.kind !== 'resolved') {
152
- return {
153
- kind: 'not-applicable',
154
- message: `Auto-sync did not run because ${resolution.kind === 'ambiguous' ? 'both Cabloy edition markers are present' : 'no Cabloy edition marker is present'}.`,
155
- };
156
- }
157
- if (shouldSkipAutoSync(filePath)) {
158
- return {
159
- kind: 'skipped',
160
- message:
161
- 'Auto-sync skipped because the same reverse-source edit was already synced recently in this repo.',
162
- };
163
- }
164
- return autoSyncReverse(filePath, resolution.edition);
165
- }
12
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
166
13
 
167
14
  function runClaudeHook(): number {
168
- let payload: HookPayload;
169
- try {
170
- payload = JSON.parse(readFileSync(0, 'utf8')) as HookPayload;
171
- } catch {
172
- return 0;
173
- }
174
- const filePath = normalizePath(ROOT, payload.tool_input?.file_path);
175
- if (!filePath || !isCodeFile(filePath)) return 0;
176
- const content = readText(filePath);
177
- if (content === null) return 0;
178
-
179
- const resolution = resolveEdition(ROOT);
180
- const result = analyze(
181
- filePath,
182
- content,
183
- resolution.kind === 'resolved' ? resolution.edition : null,
184
- );
185
- if (!result.forwardReason && !result.reverseReason) return 0;
186
-
187
- const reverseSyncOutcome = resolveReverseSyncOutcome(filePath, result.reverseReason);
188
- const message = buildMessages(result, resolution, reverseSyncOutcome);
15
+ // Cursor can import this Claude-compatible bridge alongside its native hook. Its native
16
+ // Cursor adapter owns that event, so avoid duplicate context and reverse auto-sync work.
17
+ if (process.env.CURSOR_PROJECT_DIR) return 0;
18
+ const payload = readHookPayload();
19
+ if (!payload) return 0;
20
+ const evaluated = evaluateEditedFile(ROOT, extractClaudeEditedFilePath(payload));
21
+ if (!evaluated) return 0;
189
22
  // eslint-disable-next-line
190
- console.log(
191
- JSON.stringify({
192
- hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: message },
193
- systemMessage: message,
194
- }),
195
- );
23
+ console.log(JSON.stringify(formatClaudeHookOutput(evaluated.message)));
196
24
  return 0;
197
25
  }
198
26
 
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import {
6
+ evaluateCodexEditedFiles,
7
+ extractCodexEditedFiles,
8
+ formatCodexHookOutput,
9
+ readHookPayload,
10
+ } from '../../repo-agent-governance/tools/contract-loop/hook-runtime.mjs';
11
+
12
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
13
+
14
+ function runCodexHook() {
15
+ const payload = readHookPayload();
16
+ if (!payload) return 0;
17
+ const evaluated = evaluateCodexEditedFiles(ROOT, extractCodexEditedFiles(payload), payload.cwd);
18
+ if (!evaluated.length) return 0;
19
+ const message = evaluated.map(item => item.message).join('\n\n');
20
+ // eslint-disable-next-line
21
+ console.log(JSON.stringify(formatCodexHookOutput(message)));
22
+ return 0;
23
+ }
24
+
25
+ process.exit(runCodexHook());
@@ -0,0 +1,19 @@
1
+ {
2
+ "description": "Cabloy repository lifecycle hooks generated from repo-agent-governance.",
3
+ "hooks": {
4
+ "PostToolUse": [
5
+ {
6
+ "matcher": "apply_patch|Edit|Write",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node -e \"const fs=require('node:fs'),path=require('node:path'),{pathToFileURL}=require('node:url');let d=process.cwd();for(;;){const h=path.join(d,'.codex','hooks','contract-loop-gate.mjs');if(fs.existsSync(h)){import(pathToFileURL(h).href);break}const p=path.dirname(d);if(p===d)break;d=p}\"",
11
+ "commandWindows": "node -e \"const fs=require('node:fs'),path=require('node:path'),{pathToFileURL}=require('node:url');let d=process.cwd();for(;;){const h=path.join(d,'.codex','hooks','contract-loop-gate.mjs');if(fs.existsSync(h)){import(pathToFileURL(h).href);break}const p=path.dirname(d);if(p===d)break;d=p}\"",
12
+ "timeout": 600,
13
+ "statusMessage": "Checking contract-loop gate"
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import {
6
+ evaluateEditedFile,
7
+ extractCursorEditedFilePath,
8
+ formatCursorHookOutput,
9
+ inferCursorHookEvent,
10
+ readHookPayload,
11
+ } from '../../repo-agent-governance/tools/contract-loop/hook-runtime.mjs';
12
+
13
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
14
+
15
+ function runCursorHook() {
16
+ const payload = readHookPayload();
17
+ if (!payload) return 0;
18
+ const evaluated = evaluateEditedFile(ROOT, extractCursorEditedFilePath(payload));
19
+ if (!evaluated) return 0;
20
+ const output = formatCursorHookOutput(evaluated.message, inferCursorHookEvent(payload));
21
+ if (output) {
22
+ // eslint-disable-next-line
23
+ console.log(JSON.stringify(output));
24
+ }
25
+ return 0;
26
+ }
27
+
28
+ process.exit(runCursorHook());
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "postToolUse": [
5
+ {
6
+ "command": "node .cursor/hooks/contract-loop-gate.mjs",
7
+ "matcher": "Write",
8
+ "timeout": 600
9
+ }
10
+ ],
11
+ "afterTabFileEdit": [
12
+ {
13
+ "command": "node .cursor/hooks/contract-loop-gate.mjs",
14
+ "matcher": "TabWrite",
15
+ "timeout": 600
16
+ }
17
+ ]
18
+ }
19
+ }
@@ -53,14 +53,14 @@
53
53
  {
54
54
  "adapter": "codex",
55
55
  "category": "skill",
56
- "sha256": "c8dcc0ce6b27b9bac4197e600f1444162dba1a520d05791b38f9197917034c93",
56
+ "sha256": "004d81369d47f539092bf054cf5604f3185805391110553f349dd53247bed386",
57
57
  "source": "skills/cabloy-contract-loop/references/verification-checklist.md",
58
58
  "target": ".agents/skills/cabloy-contract-loop/references/verification-checklist.md"
59
59
  },
60
60
  {
61
61
  "adapter": "codex",
62
62
  "category": "skill",
63
- "sha256": "02daa2135016d46f021c7df7d47d64af8cf64371d9f30e59390a9e63aca165fc",
63
+ "sha256": "897a2bda7df448cc2f8468c9e23a090f0bd16bcd49fbb30427c95b11340b64c8",
64
64
  "source": "skills/cabloy-contract-loop/SKILL.md",
65
65
  "target": ".agents/skills/cabloy-contract-loop/SKILL.md"
66
66
  },
@@ -291,7 +291,7 @@
291
291
  {
292
292
  "adapter": "claude",
293
293
  "category": "runtime",
294
- "sha256": "13c431fcea31168c7bce53a742d3d21484f1b1346744a15f8a20cb4ceb0e9cce",
294
+ "sha256": "47be90e9311379a981b3a5e01a6ed33bf51c9653a3b4ebf95426253db44ffcb4",
295
295
  "source": "adapters/claude/contract-loop-gate.ts",
296
296
  "target": ".claude/hooks/contract-loop-gate.ts"
297
297
  },
@@ -354,14 +354,14 @@
354
354
  {
355
355
  "adapter": "claude",
356
356
  "category": "skill",
357
- "sha256": "c8dcc0ce6b27b9bac4197e600f1444162dba1a520d05791b38f9197917034c93",
357
+ "sha256": "004d81369d47f539092bf054cf5604f3185805391110553f349dd53247bed386",
358
358
  "source": "skills/cabloy-contract-loop/references/verification-checklist.md",
359
359
  "target": ".claude/skills/cabloy-contract-loop/references/verification-checklist.md"
360
360
  },
361
361
  {
362
362
  "adapter": "claude",
363
363
  "category": "skill",
364
- "sha256": "02daa2135016d46f021c7df7d47d64af8cf64371d9f30e59390a9e63aca165fc",
364
+ "sha256": "897a2bda7df448cc2f8468c9e23a090f0bd16bcd49fbb30427c95b11340b64c8",
365
365
  "source": "skills/cabloy-contract-loop/SKILL.md",
366
366
  "target": ".claude/skills/cabloy-contract-loop/SKILL.md"
367
367
  },
@@ -582,10 +582,38 @@
582
582
  "source": "skills/cabloy-zova-source-reading/SKILL.md",
583
583
  "target": ".claude/skills/cabloy-zova-source-reading/SKILL.md"
584
584
  },
585
+ {
586
+ "adapter": "codex",
587
+ "category": "runtime",
588
+ "sha256": "0d5fcf1261dab374778df9a970e99dda7afedbfe3b9819feb38562ec8c7c65d5",
589
+ "source": "adapters/codex/hooks.json",
590
+ "target": ".codex/hooks.json"
591
+ },
592
+ {
593
+ "adapter": "codex",
594
+ "category": "runtime",
595
+ "sha256": "3f3fcc1824f18bcdc2040d65932a47cb74b477ad21670dae957de61540af0154",
596
+ "source": "adapters/codex/contract-loop-gate.mjs",
597
+ "target": ".codex/hooks/contract-loop-gate.mjs"
598
+ },
599
+ {
600
+ "adapter": "cursor",
601
+ "category": "runtime",
602
+ "sha256": "c619d001ff60ee8f5f0e5aab6fa3fa73d218f2e89d1aa3e62e451f8580290c54",
603
+ "source": "adapters/cursor/hooks.json",
604
+ "target": ".cursor/hooks.json"
605
+ },
606
+ {
607
+ "adapter": "cursor",
608
+ "category": "runtime",
609
+ "sha256": "da224fb581fde615273af8db4e2bfca513233a83e69a0803442dfec86bbd6ba6",
610
+ "source": "adapters/cursor/contract-loop-gate.mjs",
611
+ "target": ".cursor/hooks/contract-loop-gate.mjs"
612
+ },
585
613
  {
586
614
  "adapter": "cursor",
587
615
  "category": "policy",
588
- "sha256": "f7db85ff63f6a36f19ce9c990a2d2c3a3f715043986cd0bef224cad330b3b826",
616
+ "sha256": "f0a6dd9005443bdc194e8a1cd8e8f303a139f9bb838205d68d722379f6889b4f",
589
617
  "source": "policies/repository.md",
590
618
  "target": ".cursor/rules/cabloy-governance.mdc"
591
619
  },
@@ -641,14 +669,14 @@
641
669
  {
642
670
  "adapter": "cursor",
643
671
  "category": "skill",
644
- "sha256": "c8dcc0ce6b27b9bac4197e600f1444162dba1a520d05791b38f9197917034c93",
672
+ "sha256": "004d81369d47f539092bf054cf5604f3185805391110553f349dd53247bed386",
645
673
  "source": "skills/cabloy-contract-loop/references/verification-checklist.md",
646
674
  "target": ".cursor/skills/cabloy-contract-loop/references/verification-checklist.md"
647
675
  },
648
676
  {
649
677
  "adapter": "cursor",
650
678
  "category": "skill",
651
- "sha256": "02daa2135016d46f021c7df7d47d64af8cf64371d9f30e59390a9e63aca165fc",
679
+ "sha256": "897a2bda7df448cc2f8468c9e23a090f0bd16bcd49fbb30427c95b11340b64c8",
652
680
  "source": "skills/cabloy-contract-loop/SKILL.md",
653
681
  "target": ".cursor/skills/cabloy-contract-loop/SKILL.md"
654
682
  },
@@ -872,14 +900,14 @@
872
900
  {
873
901
  "adapter": "codex",
874
902
  "category": "policy",
875
- "sha256": "baaab679e04af3e386cb0b22dd5f69ad47bd19acf569f319c69c44afdb70c06a",
903
+ "sha256": "c87071f6f34adaaaf25aac5cb41d9ff10daea1743a31bd3344517d68a3d4082a",
876
904
  "source": "policies/repository.md",
877
905
  "target": "AGENTS.md"
878
906
  },
879
907
  {
880
908
  "adapter": "claude",
881
909
  "category": "policy",
882
- "sha256": "baaab679e04af3e386cb0b22dd5f69ad47bd19acf569f319c69c44afdb70c06a",
910
+ "sha256": "c87071f6f34adaaaf25aac5cb41d9ff10daea1743a31bd3344517d68a3d4082a",
883
911
  "source": "policies/repository.md",
884
912
  "target": "CLAUDE.md"
885
913
  }
@@ -52,5 +52,29 @@
52
52
  "target": ".claude/hooks/contract-loop-gate.ts"
53
53
  }
54
54
  ]
55
+ },
56
+ "cursor": {
57
+ "hooksJson": {
58
+ "source": "adapters/cursor/hooks.json",
59
+ "target": ".cursor/hooks.json"
60
+ },
61
+ "hooks": [
62
+ {
63
+ "source": "adapters/cursor/contract-loop-gate.mjs",
64
+ "target": ".cursor/hooks/contract-loop-gate.mjs"
65
+ }
66
+ ]
67
+ },
68
+ "codex": {
69
+ "hooksJson": {
70
+ "source": "adapters/codex/hooks.json",
71
+ "target": ".codex/hooks.json"
72
+ },
73
+ "hooks": [
74
+ {
75
+ "source": "adapters/codex/contract-loop-gate.mjs",
76
+ "target": ".codex/hooks/contract-loop-gate.mjs"
77
+ }
78
+ ]
55
79
  }
56
80
  }
@@ -22,7 +22,7 @@ Always perform this detection before making UI-sensitive assumptions, choosing f
22
22
  - `repo-e2e/` contains the end-to-end test project.
23
23
  - `repo-observability/` contains local observability infrastructure.
24
24
  - `repo-agent-governance/` is the authored source for portable agent rules, root skills, adapters, and deterministic governance tools.
25
- - `.claude/`, `.agents/`, and `.cursor/` contain generated platform adapters plus platform-specific runtime integration.
25
+ - `.claude/`, `.agents/`, `.codex/`, and `.cursor/` contain generated platform adapters plus platform-specific runtime integration.
26
26
 
27
27
  ## Preferred workflow
28
28
 
@@ -31,7 +31,14 @@ const GENERATED_HEADER =
31
31
  '<!-- Generated from repo-agent-governance/. Do not edit this adapter output directly. -->\n\n';
32
32
  const CURSOR_HEADER =
33
33
  '---\ndescription: Cabloy repository governance generated from repo-agent-governance.\nglobs: []\nalwaysApply: true\n---\n\n';
34
- const ALLOWED_TARGET_ROOTS = ['CLAUDE.md', 'AGENTS.md', '.claude/', '.agents/', '.cursor/'];
34
+ const ALLOWED_TARGET_ROOTS = [
35
+ 'CLAUDE.md',
36
+ 'AGENTS.md',
37
+ '.claude/',
38
+ '.agents/',
39
+ '.cursor/',
40
+ '.codex/',
41
+ ];
35
42
  const FORBIDDEN_TARGET_SEGMENTS = [
36
43
  'settings.local.json',
37
44
  'worktrees',
@@ -163,7 +170,7 @@ function validateManifest() {
163
170
  );
164
171
  assertKeys(
165
172
  manifest,
166
- ['schemaVersion', 'policy', 'skills', 'commands', 'claude'],
173
+ ['schemaVersion', 'policy', 'skills', 'commands', 'claude', 'cursor', 'codex'],
167
174
  'Governance manifest',
168
175
  );
169
176
  if (manifest.schemaVersion !== 1) throw new Error('Governance manifest must use schemaVersion 1');
@@ -204,6 +211,24 @@ function validateManifest() {
204
211
  : (() => {
205
212
  throw new Error('Claude hooks must be an array');
206
213
  })();
214
+ const cursor = requireObject(manifest.cursor, 'Cursor manifest section');
215
+ assertKeys(cursor, ['hooksJson', 'hooks'], 'Cursor manifest section');
216
+ const cursorHooksJson = requireObject(cursor.hooksJson, 'Cursor hooks.json manifest section');
217
+ assertKeys(cursorHooksJson, ['source', 'target'], 'Cursor hooks.json manifest section');
218
+ const cursorHooks = Array.isArray(cursor.hooks)
219
+ ? cursor.hooks
220
+ : (() => {
221
+ throw new Error('Cursor hooks must be an array');
222
+ })();
223
+ const codex = requireObject(manifest.codex, 'Codex manifest section');
224
+ assertKeys(codex, ['hooksJson', 'hooks'], 'Codex manifest section');
225
+ const codexHooksJson = requireObject(codex.hooksJson, 'Codex hooks.json manifest section');
226
+ assertKeys(codexHooksJson, ['source', 'target'], 'Codex hooks.json manifest section');
227
+ const codexHooks = Array.isArray(codex.hooks)
228
+ ? codex.hooks
229
+ : (() => {
230
+ throw new Error('Codex hooks must be an array');
231
+ })();
207
232
 
208
233
  const targets = new Set();
209
234
  const claimTarget = target => {
@@ -246,6 +271,24 @@ function validateManifest() {
246
271
  assertKeys(hook, ['source', 'target'], `Claude hook ${index}`);
247
272
  return { source: readCanonical(hook.source).source, target: claimTarget(hook.target) };
248
273
  });
274
+ const normalizedCursorHooksJson = {
275
+ source: readCanonical(cursorHooksJson.source).source,
276
+ target: claimTarget(cursorHooksJson.target),
277
+ };
278
+ const normalizedCursorHooks = cursorHooks.map((item, index) => {
279
+ const hook = requireObject(item, `Cursor hook ${index}`);
280
+ assertKeys(hook, ['source', 'target'], `Cursor hook ${index}`);
281
+ return { source: readCanonical(hook.source).source, target: claimTarget(hook.target) };
282
+ });
283
+ const normalizedCodexHooksJson = {
284
+ source: readCanonical(codexHooksJson.source).source,
285
+ target: claimTarget(codexHooksJson.target),
286
+ };
287
+ const normalizedCodexHooks = codexHooks.map((item, index) => {
288
+ const hook = requireObject(item, `Codex hook ${index}`);
289
+ assertKeys(hook, ['source', 'target'], `Codex hook ${index}`);
290
+ return { source: readCanonical(hook.source).source, target: claimTarget(hook.target) };
291
+ });
249
292
 
250
293
  return {
251
294
  manifest,
@@ -257,6 +300,8 @@ function validateManifest() {
257
300
  skills: { directory: skillsDirectory, path: skillsPath, targets: normalizedSkillTargets },
258
301
  commands,
259
302
  claude: { settings: normalizedSettings, hooks: normalizedHooks },
303
+ cursor: { hooksJson: normalizedCursorHooksJson, hooks: normalizedCursorHooks },
304
+ codex: { hooksJson: normalizedCodexHooksJson, hooks: normalizedCodexHooks },
260
305
  };
261
306
  }
262
307
 
@@ -331,6 +376,26 @@ function buildAssets() {
331
376
  for (const hook of config.claude.hooks) {
332
377
  add(hook.target, readCanonical(hook.source).content, hook.source, 'claude', 'runtime');
333
378
  }
379
+ add(
380
+ config.cursor.hooksJson.target,
381
+ readCanonical(config.cursor.hooksJson.source).content,
382
+ config.cursor.hooksJson.source,
383
+ 'cursor',
384
+ 'runtime',
385
+ );
386
+ for (const hook of config.cursor.hooks) {
387
+ add(hook.target, readCanonical(hook.source).content, hook.source, 'cursor', 'runtime');
388
+ }
389
+ add(
390
+ config.codex.hooksJson.target,
391
+ readCanonical(config.codex.hooksJson.source).content,
392
+ config.codex.hooksJson.source,
393
+ 'codex',
394
+ 'runtime',
395
+ );
396
+ for (const hook of config.codex.hooks) {
397
+ add(hook.target, readCanonical(hook.source).content, hook.source, 'codex', 'runtime');
398
+ }
334
399
 
335
400
  const targets = new Set();
336
401
  for (const asset of assets) {
@@ -358,6 +423,31 @@ function lockText(assets) {
358
423
  return `${JSON.stringify(lockFromAssets(assets), null, 2)}\n`;
359
424
  }
360
425
 
426
+ function previousManagedAssets() {
427
+ if (!existsSync(LOCK_PATH)) return [];
428
+ try {
429
+ const lock = readJson(LOCK_PATH, 'managed governance lock');
430
+ if (lock?.schemaVersion !== 1 || !Array.isArray(lock.assets)) return [];
431
+ const targets = new Set();
432
+ return lock.assets.map(asset => {
433
+ if (
434
+ !asset ||
435
+ typeof asset !== 'object' ||
436
+ typeof asset.target !== 'string' ||
437
+ !/^[a-f0-9]{64}$/.test(asset.sha256)
438
+ ) {
439
+ throw new Error('invalid managed asset');
440
+ }
441
+ const target = assertAllowedTarget(asset.target);
442
+ if (targets.has(target)) throw new Error('duplicate managed asset');
443
+ targets.add(target);
444
+ return { target, sha256: asset.sha256 };
445
+ });
446
+ } catch {
447
+ return [];
448
+ }
449
+ }
450
+
361
451
  function targetPath(target) {
362
452
  const path = resolve(ROOT_DIR, target);
363
453
  assertInside(ROOT_DIR, path, 'Managed target');
@@ -377,11 +467,20 @@ function writeAtomic(path, content) {
377
467
 
378
468
  export function render() {
379
469
  const assets = buildAssets();
470
+ const previousAssets = previousManagedAssets();
380
471
  for (const asset of assets) {
381
472
  const path = targetPath(asset.target);
382
473
  const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
383
474
  if (existing !== asset.content) writeAtomic(path, asset.content);
384
475
  }
476
+ const activeTargets = new Set(assets.map(asset => asset.target));
477
+ for (const asset of previousAssets) {
478
+ if (activeTargets.has(asset.target)) continue;
479
+ const path = targetPath(asset.target);
480
+ if (existsSync(path) && sha256(readFileSync(path, 'utf8')) === asset.sha256) {
481
+ rmSync(path, { force: true });
482
+ }
483
+ }
385
484
  const lock = lockText(assets);
386
485
  const existingLock = existsSync(LOCK_PATH) ? readFileSync(LOCK_PATH, 'utf8') : null;
387
486
  if (existingLock !== lock) writeAtomic(LOCK_PATH, lock);
@@ -16,6 +16,11 @@ const required = [
16
16
  'repo-agent-governance/managed-assets.json',
17
17
  'repo-agent-governance/scripts/governance.mjs',
18
18
  'repo-agent-governance/tools/contract-loop/core.mjs',
19
+ 'repo-agent-governance/tools/contract-loop/hook-runtime.mjs',
20
+ 'repo-agent-governance/adapters/cursor/hooks.json',
21
+ 'repo-agent-governance/adapters/cursor/contract-loop-gate.mjs',
22
+ 'repo-agent-governance/adapters/codex/hooks.json',
23
+ 'repo-agent-governance/adapters/codex/contract-loop-gate.mjs',
19
24
  ];
20
25
  const forbidden = [
21
26
  '.cabloy-agent-governance-state.json',