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 +1 -1
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/repo-agent-governance/adapters/claude/contract-loop-gate.ts +15 -187
- package/repo-agent-governance/adapters/codex/contract-loop-gate.mjs +25 -0
- package/repo-agent-governance/adapters/codex/hooks.json +19 -0
- package/repo-agent-governance/adapters/cursor/contract-loop-gate.mjs +28 -0
- package/repo-agent-governance/adapters/cursor/hooks.json +19 -0
- package/repo-agent-governance/managed-assets.json +38 -10
- package/repo-agent-governance/manifest.json +24 -0
- package/repo-agent-governance/policies/repository.md +1 -1
- package/repo-agent-governance/scripts/governance.mjs +101 -2
- package/repo-agent-governance/scripts/pack-check.mjs +5 -0
- package/repo-agent-governance/skills/cabloy-contract-loop/SKILL.md +9 -2
- package/repo-agent-governance/skills/cabloy-contract-loop/references/verification-checklist.md +5 -3
- package/repo-agent-governance/tests/governance.test.mjs +274 -0
- package/repo-agent-governance/tools/contract-loop/core.mjs +30 -14
- package/repo-agent-governance/tools/contract-loop/hook-runtime.mjs +367 -0
- package/repo-docs/ai/agent-governance.md +8 -6
- package/repo-docs/ai/docs-skills-rules-mapping.md +4 -1
- package/repo-docs/ai/edition-consistency-checklist.md +1 -1
- package/repo-docs/ai/introduction.md +1 -1
- package/repo-docs/ai/repo-guidance.md +1 -1
- package/repo-docs/ai/rules-and-config.md +5 -3
- package/repo-docs/reference/package-map.md +1 -1
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.193
|
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,198 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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 = [
|
|
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',
|