cabloy 5.1.191 → 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/.github/workflows/agent-governance.yml +39 -0
- package/.github/workflows/docs-pages.yml +1 -0
- package/.gitignore +3 -0
- package/CHANGELOG.md +14 -0
- package/package.json +11 -5
- package/repo-agent-governance/adapters/claude/contract-loop-gate.ts +27 -0
- 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/{.claude → repo-agent-governance}/commands/release.md +20 -20
- package/repo-agent-governance/managed-assets.json +915 -0
- package/repo-agent-governance/manifest.json +80 -0
- package/{CLAUDE.md → repo-agent-governance/policies/repository.md} +4 -2
- package/repo-agent-governance/scripts/governance.mjs +669 -0
- package/repo-agent-governance/scripts/pack-check.mjs +48 -0
- package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/SKILL.md +9 -2
- package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/references/contract-loop-map.md +4 -4
- package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/references/verification-checklist.md +5 -3
- package/{.claude → repo-agent-governance}/skills/cabloy-master-detail/SKILL.md +1 -1
- package/{.claude → repo-agent-governance}/skills/cabloy-module-removal/SKILL.md +1 -1
- package/{.claude → repo-agent-governance}/skills/cabloy-spec-execution/SKILL.md +1 -1
- package/{.claude → repo-agent-governance}/skills/cabloy-spec-execution/references/execution-protocol.md +8 -8
- package/{.claude → repo-agent-governance}/skills/cabloy-spec-execution/references/status-and-evidence.md +9 -9
- package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/SKILL.md +2 -2
- package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/references/repo-aware-discovery.md +7 -7
- package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/references/repo-specs-document-set.md +30 -30
- package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +9 -9
- package/{.claude → repo-agent-governance}/skills/cabloy-workflow/SKILL.md +7 -8
- package/{.claude → repo-agent-governance}/skills/cabloy-worktree-environment/SKILL.md +9 -9
- package/{.claude → repo-agent-governance}/skills/cabloy-zova-source-reading/SKILL.md +5 -5
- package/repo-agent-governance/tests/governance.test.mjs +569 -0
- package/repo-agent-governance/tools/contract-loop/check.mjs +71 -0
- package/repo-agent-governance/tools/contract-loop/core.mjs +205 -0
- package/repo-agent-governance/tools/contract-loop/hook-runtime.mjs +367 -0
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +858 -0
- package/{.claude/skills/cabloy-spec-generation/scripts → repo-agent-governance/tools/spec-charts}/generate-implementation-charts.test.mjs +95 -20
- package/repo-docs/.vitepress/config.mjs +1 -0
- package/repo-docs/ai/agent-governance.md +82 -0
- package/repo-docs/ai/cli-to-skill-map.md +3 -3
- package/repo-docs/ai/docs-skills-rules-mapping.md +30 -26
- package/repo-docs/ai/edition-consistency-checklist.md +8 -6
- package/repo-docs/ai/future-skill-roadmap.md +2 -2
- package/repo-docs/ai/introduction.md +7 -6
- package/repo-docs/ai/repo-guidance.md +3 -2
- package/repo-docs/ai/rules-and-config.md +25 -15
- package/repo-docs/ai/skills.md +7 -7
- package/repo-docs/ai/verification.md +2 -1
- package/repo-docs/fullstack/contract-loop-playbook.md +1 -1
- package/repo-docs/fullstack/quickstart.md +1 -1
- package/repo-docs/reference/package-map.md +3 -2
- package/repo-docs/reference/repo-scripts.md +17 -2
- package/scripts/bootstrapAgentGovernance.mjs +81 -0
- package/scripts/init.ts +1 -0
- package/scripts/release.ts +15 -0
- package/scripts/upgrade.ts +66 -7
- package/zova/pnpm-lock.yaml +3 -3
- package/.claude/hooks/contract-loop-gate.ts +0 -421
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.mjs +0 -385
- /package/{.claude → repo-agent-governance/adapters/claude}/settings.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-backend-scaffold/SKILL.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-backend-scaffold/evals/evals.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-backend-scaffold/references/backend-thread-map.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/evals/evals.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-domain-planning/SKILL.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-frontend-scaffold/SKILL.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-frontend-scaffold/evals/evals.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-frontend-scaffold/references/frontend-thread-map.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/SKILL.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/evals/evals.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/references/custom-renderer-demo-checklist.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/references/field-update-decision-tree.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/references/follow-up-checklist.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/references/verification-checklist.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-spec-execution/evals/evals.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/evals/evals.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-workflow/evals/evals.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-workflow/references/cli-strategy.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-workflow/references/edition-detection.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-worktree-environment/evals/evals.json +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-zova-source-reading/references/analysis-modes.md +0 -0
- /package/{.claude → repo-agent-governance}/skills/cabloy-zova-source-reading/references/core-reading-paths.md +0 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
14
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const SCRIPT_FILE = fileURLToPath(import.meta.url);
|
|
18
|
+
const EXECUTABLE_PATH = process.env.CABLOY_GOVERNANCE_EXECUTABLE
|
|
19
|
+
? resolve(process.env.CABLOY_GOVERNANCE_EXECUTABLE)
|
|
20
|
+
: SCRIPT_FILE;
|
|
21
|
+
export const ROOT_DIR = process.env.CABLOY_GOVERNANCE_ROOT
|
|
22
|
+
? resolve(process.env.CABLOY_GOVERNANCE_ROOT)
|
|
23
|
+
: resolve(dirname(SCRIPT_FILE), '..', '..');
|
|
24
|
+
export const GOVERNANCE_DIR = resolve(ROOT_DIR, 'repo-agent-governance');
|
|
25
|
+
const ACTIVE_SCRIPT_FILE = process.env.CABLOY_GOVERNANCE_ROOT
|
|
26
|
+
? resolve(GOVERNANCE_DIR, 'scripts/governance.mjs')
|
|
27
|
+
: SCRIPT_FILE;
|
|
28
|
+
const MANIFEST_PATH = resolve(GOVERNANCE_DIR, 'manifest.json');
|
|
29
|
+
const LOCK_PATH = resolve(GOVERNANCE_DIR, 'managed-assets.json');
|
|
30
|
+
const GENERATED_HEADER =
|
|
31
|
+
'<!-- Generated from repo-agent-governance/. Do not edit this adapter output directly. -->\n\n';
|
|
32
|
+
const CURSOR_HEADER =
|
|
33
|
+
'---\ndescription: Cabloy repository governance generated from repo-agent-governance.\nglobs: []\nalwaysApply: true\n---\n\n';
|
|
34
|
+
const ALLOWED_TARGET_ROOTS = [
|
|
35
|
+
'CLAUDE.md',
|
|
36
|
+
'AGENTS.md',
|
|
37
|
+
'.claude/',
|
|
38
|
+
'.agents/',
|
|
39
|
+
'.cursor/',
|
|
40
|
+
'.codex/',
|
|
41
|
+
];
|
|
42
|
+
const FORBIDDEN_TARGET_SEGMENTS = [
|
|
43
|
+
'settings.local.json',
|
|
44
|
+
'worktrees',
|
|
45
|
+
'CLAUDE.local.md',
|
|
46
|
+
'scheduled_tasks',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function normalizeText(value) {
|
|
50
|
+
return `${value.replace(/\r\n?/g, '\n').replace(/\n*$/, '')}\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sha256(value) {
|
|
54
|
+
return createHash('sha256').update(value).digest('hex');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toPosix(value) {
|
|
58
|
+
return value.split(sep).join('/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertSafeRelativePath(value, label) {
|
|
62
|
+
if (typeof value !== 'string' || !value || isAbsolute(value)) {
|
|
63
|
+
throw new Error(`${label} must be a non-empty relative path`);
|
|
64
|
+
}
|
|
65
|
+
const normalized = toPosix(value);
|
|
66
|
+
if (normalized.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
67
|
+
throw new Error(`${label} must not contain empty, dot, or parent segments: ${value}`);
|
|
68
|
+
}
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertInside(base, candidate, label) {
|
|
73
|
+
const result = relative(base, candidate);
|
|
74
|
+
if (result === '' || (!result.startsWith(`..${sep}`) && result !== '..' && !isAbsolute(result)))
|
|
75
|
+
return;
|
|
76
|
+
throw new Error(`${label} escapes its permitted directory: ${candidate}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assertAllowedTarget(target) {
|
|
80
|
+
const value = assertSafeRelativePath(target, 'Managed target');
|
|
81
|
+
const allowed = ALLOWED_TARGET_ROOTS.some(root =>
|
|
82
|
+
root.endsWith('/') ? value.startsWith(root) : value === root,
|
|
83
|
+
);
|
|
84
|
+
if (!allowed) {
|
|
85
|
+
throw new Error(`Managed target is outside the allowed roots: ${value}`);
|
|
86
|
+
}
|
|
87
|
+
const segments = value.split('/');
|
|
88
|
+
if (FORBIDDEN_TARGET_SEGMENTS.some(segment => segments.includes(segment))) {
|
|
89
|
+
throw new Error(`Managed target is forbidden: ${value}`);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requireObject(value, label) {
|
|
95
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
96
|
+
throw new Error(`${label} must be an object`);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function assertKeys(value, allowed, label) {
|
|
102
|
+
for (const key of Object.keys(value)) {
|
|
103
|
+
if (!allowed.includes(key)) throw new Error(`${label} has unsupported key: ${key}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readJson(path, label) {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`Unable to parse ${label}: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertFile(path, label) {
|
|
116
|
+
if (!existsSync(path) || !lstatSync(path).isFile())
|
|
117
|
+
throw new Error(`${label} is missing: ${path}`);
|
|
118
|
+
assertInside(GOVERNANCE_DIR, path, label);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readCanonical(source) {
|
|
122
|
+
const safeSource = assertSafeRelativePath(source, 'Canonical source');
|
|
123
|
+
const path = resolve(GOVERNANCE_DIR, safeSource);
|
|
124
|
+
assertFile(path, 'Canonical source');
|
|
125
|
+
return { source: safeSource, content: normalizeText(readFileSync(path, 'utf8')) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateSkill(content, source) {
|
|
129
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
130
|
+
if (!match) throw new Error(`Skill must begin with YAML front matter: ${source}`);
|
|
131
|
+
const lines = match[1].split('\n');
|
|
132
|
+
const names = lines.filter(line => line.startsWith('name: '));
|
|
133
|
+
const descriptions = lines.filter(line => line.startsWith('description: '));
|
|
134
|
+
if (
|
|
135
|
+
names.length !== 1 ||
|
|
136
|
+
descriptions.length !== 1 ||
|
|
137
|
+
!names[0].slice(6).trim() ||
|
|
138
|
+
!descriptions[0].slice(13).trim()
|
|
139
|
+
) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Skill front matter requires exactly one non-empty name and description: ${source}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return names[0].slice(6).trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function listFiles(directory, prefix = '') {
|
|
148
|
+
const results = [];
|
|
149
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
|
|
150
|
+
left.name.localeCompare(right.name),
|
|
151
|
+
)) {
|
|
152
|
+
if (entry.name === '.DS_Store') continue;
|
|
153
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
154
|
+
const path = resolve(directory, entry.name);
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
results.push(...listFiles(path, relativePath));
|
|
157
|
+
} else if (entry.isFile()) {
|
|
158
|
+
results.push(relativePath);
|
|
159
|
+
} else {
|
|
160
|
+
throw new Error(`Skill bundle contains an unsupported filesystem entry: ${path}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateManifest() {
|
|
167
|
+
const manifest = requireObject(
|
|
168
|
+
readJson(MANIFEST_PATH, 'governance manifest'),
|
|
169
|
+
'Governance manifest',
|
|
170
|
+
);
|
|
171
|
+
assertKeys(
|
|
172
|
+
manifest,
|
|
173
|
+
['schemaVersion', 'policy', 'skills', 'commands', 'claude', 'cursor', 'codex'],
|
|
174
|
+
'Governance manifest',
|
|
175
|
+
);
|
|
176
|
+
if (manifest.schemaVersion !== 1) throw new Error('Governance manifest must use schemaVersion 1');
|
|
177
|
+
|
|
178
|
+
const policy = requireObject(manifest.policy, 'Governance manifest policy');
|
|
179
|
+
assertKeys(policy, ['source', 'targets'], 'Governance manifest policy');
|
|
180
|
+
const policySource = readCanonical(policy.source);
|
|
181
|
+
if (!Array.isArray(policy.targets) || !policy.targets.length)
|
|
182
|
+
throw new Error('Policy targets must be a non-empty array');
|
|
183
|
+
|
|
184
|
+
const skills = requireObject(manifest.skills, 'Governance manifest skills');
|
|
185
|
+
assertKeys(skills, ['sourceDirectory', 'targets'], 'Governance manifest skills');
|
|
186
|
+
const skillsDirectory = assertSafeRelativePath(skills.sourceDirectory, 'Skill source directory');
|
|
187
|
+
const skillsPath = resolve(GOVERNANCE_DIR, skillsDirectory);
|
|
188
|
+
if (!existsSync(skillsPath) || !lstatSync(skillsPath).isDirectory())
|
|
189
|
+
throw new Error(`Skill source directory is missing: ${skillsDirectory}`);
|
|
190
|
+
assertInside(GOVERNANCE_DIR, skillsPath, 'Skill source directory');
|
|
191
|
+
if (!Array.isArray(skills.targets) || !skills.targets.length)
|
|
192
|
+
throw new Error('Skill targets must be a non-empty array');
|
|
193
|
+
|
|
194
|
+
if (!Array.isArray(manifest.commands)) throw new Error('Commands must be an array');
|
|
195
|
+
const commands = manifest.commands.map((command, index) => {
|
|
196
|
+
const item = requireObject(command, `Command ${index}`);
|
|
197
|
+
assertKeys(item, ['adapter', 'source', 'target'], `Command ${index}`);
|
|
198
|
+
return {
|
|
199
|
+
...item,
|
|
200
|
+
source: readCanonical(item.source).source,
|
|
201
|
+
target: assertAllowedTarget(item.target),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const claude = requireObject(manifest.claude, 'Claude manifest section');
|
|
206
|
+
assertKeys(claude, ['settings', 'hooks'], 'Claude manifest section');
|
|
207
|
+
const settings = requireObject(claude.settings, 'Claude settings manifest section');
|
|
208
|
+
assertKeys(settings, ['source', 'target'], 'Claude settings manifest section');
|
|
209
|
+
const hooks = Array.isArray(claude.hooks)
|
|
210
|
+
? claude.hooks
|
|
211
|
+
: (() => {
|
|
212
|
+
throw new Error('Claude hooks must be an array');
|
|
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
|
+
})();
|
|
232
|
+
|
|
233
|
+
const targets = new Set();
|
|
234
|
+
const claimTarget = target => {
|
|
235
|
+
const safeTarget = assertAllowedTarget(target);
|
|
236
|
+
if (targets.has(safeTarget))
|
|
237
|
+
throw new Error(`Managed target is declared more than once: ${safeTarget}`);
|
|
238
|
+
targets.add(safeTarget);
|
|
239
|
+
return safeTarget;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const normalizedPolicyTargets = policy.targets.map((item, index) => {
|
|
243
|
+
const target = requireObject(item, `Policy target ${index}`);
|
|
244
|
+
assertKeys(target, ['adapter', 'target'], `Policy target ${index}`);
|
|
245
|
+
if (!['claude', 'codex', 'cursor'].includes(target.adapter))
|
|
246
|
+
throw new Error(`Unsupported policy adapter: ${target.adapter}`);
|
|
247
|
+
return { adapter: target.adapter, target: claimTarget(target.target) };
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const normalizedSkillTargets = skills.targets.map((item, index) => {
|
|
251
|
+
const target = requireObject(item, `Skill target ${index}`);
|
|
252
|
+
assertKeys(target, ['adapter', 'targetDirectory'], `Skill target ${index}`);
|
|
253
|
+
if (!['claude', 'codex', 'cursor'].includes(target.adapter))
|
|
254
|
+
throw new Error(`Unsupported skill adapter: ${target.adapter}`);
|
|
255
|
+
return {
|
|
256
|
+
adapter: target.adapter,
|
|
257
|
+
targetDirectory: assertAllowedTarget(`${target.targetDirectory}/placeholder`).replace(
|
|
258
|
+
/\/placeholder$/,
|
|
259
|
+
'',
|
|
260
|
+
),
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
for (const command of commands) claimTarget(command.target);
|
|
265
|
+
const normalizedSettings = {
|
|
266
|
+
source: readCanonical(settings.source).source,
|
|
267
|
+
target: claimTarget(settings.target),
|
|
268
|
+
};
|
|
269
|
+
const normalizedHooks = hooks.map((item, index) => {
|
|
270
|
+
const hook = requireObject(item, `Claude hook ${index}`);
|
|
271
|
+
assertKeys(hook, ['source', 'target'], `Claude hook ${index}`);
|
|
272
|
+
return { source: readCanonical(hook.source).source, target: claimTarget(hook.target) };
|
|
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
|
+
});
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
manifest,
|
|
295
|
+
policy: {
|
|
296
|
+
source: policySource.source,
|
|
297
|
+
content: policySource.content,
|
|
298
|
+
targets: normalizedPolicyTargets,
|
|
299
|
+
},
|
|
300
|
+
skills: { directory: skillsDirectory, path: skillsPath, targets: normalizedSkillTargets },
|
|
301
|
+
commands,
|
|
302
|
+
claude: { settings: normalizedSettings, hooks: normalizedHooks },
|
|
303
|
+
cursor: { hooksJson: normalizedCursorHooksJson, hooks: normalizedCursorHooks },
|
|
304
|
+
codex: { hooksJson: normalizedCodexHooksJson, hooks: normalizedCodexHooks },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function policyForAdapter(content, adapter) {
|
|
309
|
+
if (adapter === 'cursor') return `${CURSOR_HEADER}${content}`;
|
|
310
|
+
return `${GENERATED_HEADER}${content}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildAssets() {
|
|
314
|
+
const config = validateManifest();
|
|
315
|
+
const assets = [];
|
|
316
|
+
const add = (target, content, source, adapter, category) => {
|
|
317
|
+
assets.push({ target, content: normalizeText(content), source, adapter, category });
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
for (const target of config.policy.targets) {
|
|
321
|
+
add(
|
|
322
|
+
target.target,
|
|
323
|
+
policyForAdapter(config.policy.content, target.adapter),
|
|
324
|
+
config.policy.source,
|
|
325
|
+
target.adapter,
|
|
326
|
+
'policy',
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const skillDirectories = readdirSync(config.skills.path, { withFileTypes: true })
|
|
331
|
+
.filter(entry => entry.isDirectory())
|
|
332
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
333
|
+
const skillNames = new Set();
|
|
334
|
+
for (const directory of skillDirectories) {
|
|
335
|
+
const skillPath = resolve(config.skills.path, directory.name);
|
|
336
|
+
const files = listFiles(skillPath);
|
|
337
|
+
if (!files.includes('SKILL.md'))
|
|
338
|
+
throw new Error(`Skill bundle is missing SKILL.md: ${directory.name}`);
|
|
339
|
+
const skillSource = `${config.skills.directory}/${directory.name}/SKILL.md`;
|
|
340
|
+
const skillName = validateSkill(readCanonical(skillSource).content, skillSource);
|
|
341
|
+
if (skillName !== directory.name)
|
|
342
|
+
throw new Error(`Skill directory and front-matter name must match: ${directory.name}`);
|
|
343
|
+
if (skillNames.has(skillName)) throw new Error(`Skill name is duplicated: ${skillName}`);
|
|
344
|
+
skillNames.add(skillName);
|
|
345
|
+
for (const skillTarget of config.skills.targets) {
|
|
346
|
+
for (const file of files) {
|
|
347
|
+
const source = `${config.skills.directory}/${directory.name}/${file}`;
|
|
348
|
+
const canonical = readCanonical(source);
|
|
349
|
+
add(
|
|
350
|
+
`${skillTarget.targetDirectory}/${directory.name}/${file}`,
|
|
351
|
+
canonical.content,
|
|
352
|
+
canonical.source,
|
|
353
|
+
skillTarget.adapter,
|
|
354
|
+
'skill',
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const command of config.commands) {
|
|
361
|
+
add(
|
|
362
|
+
command.target,
|
|
363
|
+
readCanonical(command.source).content,
|
|
364
|
+
command.source,
|
|
365
|
+
command.adapter,
|
|
366
|
+
'command',
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
add(
|
|
370
|
+
config.claude.settings.target,
|
|
371
|
+
readCanonical(config.claude.settings.source).content,
|
|
372
|
+
config.claude.settings.source,
|
|
373
|
+
'claude',
|
|
374
|
+
'runtime',
|
|
375
|
+
);
|
|
376
|
+
for (const hook of config.claude.hooks) {
|
|
377
|
+
add(hook.target, readCanonical(hook.source).content, hook.source, 'claude', 'runtime');
|
|
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
|
+
}
|
|
399
|
+
|
|
400
|
+
const targets = new Set();
|
|
401
|
+
for (const asset of assets) {
|
|
402
|
+
assertAllowedTarget(asset.target);
|
|
403
|
+
if (targets.has(asset.target)) throw new Error(`Generated target collision: ${asset.target}`);
|
|
404
|
+
targets.add(asset.target);
|
|
405
|
+
}
|
|
406
|
+
return assets.sort((left, right) => left.target.localeCompare(right.target));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function lockFromAssets(assets) {
|
|
410
|
+
return {
|
|
411
|
+
schemaVersion: 1,
|
|
412
|
+
assets: assets.map(asset => ({
|
|
413
|
+
adapter: asset.adapter,
|
|
414
|
+
category: asset.category,
|
|
415
|
+
sha256: sha256(asset.content),
|
|
416
|
+
source: asset.source,
|
|
417
|
+
target: asset.target,
|
|
418
|
+
})),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function lockText(assets) {
|
|
423
|
+
return `${JSON.stringify(lockFromAssets(assets), null, 2)}\n`;
|
|
424
|
+
}
|
|
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
|
+
|
|
451
|
+
function targetPath(target) {
|
|
452
|
+
const path = resolve(ROOT_DIR, target);
|
|
453
|
+
assertInside(ROOT_DIR, path, 'Managed target');
|
|
454
|
+
return path;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function writeAtomic(path, content) {
|
|
458
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
459
|
+
const temporary = `${path}.tmp-governance-${process.pid}`;
|
|
460
|
+
try {
|
|
461
|
+
writeFileSync(temporary, content);
|
|
462
|
+
renameSync(temporary, path);
|
|
463
|
+
} finally {
|
|
464
|
+
if (existsSync(temporary)) rmSync(temporary, { force: true });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function render() {
|
|
469
|
+
const assets = buildAssets();
|
|
470
|
+
const previousAssets = previousManagedAssets();
|
|
471
|
+
for (const asset of assets) {
|
|
472
|
+
const path = targetPath(asset.target);
|
|
473
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
474
|
+
if (existing !== asset.content) writeAtomic(path, asset.content);
|
|
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
|
+
}
|
|
484
|
+
const lock = lockText(assets);
|
|
485
|
+
const existingLock = existsSync(LOCK_PATH) ? readFileSync(LOCK_PATH, 'utf8') : null;
|
|
486
|
+
if (existingLock !== lock) writeAtomic(LOCK_PATH, lock);
|
|
487
|
+
return { assets };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function check() {
|
|
491
|
+
const assets = buildAssets();
|
|
492
|
+
const problems = [];
|
|
493
|
+
for (const asset of assets) {
|
|
494
|
+
const path = targetPath(asset.target);
|
|
495
|
+
if (!existsSync(path)) {
|
|
496
|
+
problems.push(`Missing managed output: ${asset.target}`);
|
|
497
|
+
} else if (readFileSync(path, 'utf8') !== asset.content) {
|
|
498
|
+
problems.push(`Stale managed output: ${asset.target}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const expectedLock = lockText(assets);
|
|
502
|
+
if (!existsSync(LOCK_PATH)) {
|
|
503
|
+
problems.push('Missing managed lock: repo-agent-governance/managed-assets.json');
|
|
504
|
+
} else if (readFileSync(LOCK_PATH, 'utf8') !== expectedLock) {
|
|
505
|
+
problems.push('Stale managed lock: repo-agent-governance/managed-assets.json');
|
|
506
|
+
}
|
|
507
|
+
return { assets, problems };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function readState(path) {
|
|
511
|
+
if (!existsSync(path)) return null;
|
|
512
|
+
try {
|
|
513
|
+
const state = JSON.parse(await readFile(path, 'utf8'));
|
|
514
|
+
if (state?.schemaVersion !== 1 || !Array.isArray(state.assets))
|
|
515
|
+
throw new Error('unsupported state shape');
|
|
516
|
+
const targets = new Set();
|
|
517
|
+
for (const asset of state.assets) {
|
|
518
|
+
if (
|
|
519
|
+
!asset ||
|
|
520
|
+
typeof asset !== 'object' ||
|
|
521
|
+
Object.keys(asset).some(key => !['target', 'sha256'].includes(key))
|
|
522
|
+
) {
|
|
523
|
+
throw new Error('invalid state asset shape');
|
|
524
|
+
}
|
|
525
|
+
const target = assertAllowedTarget(asset.target);
|
|
526
|
+
if (!/^[a-f0-9]{64}$/.test(asset.sha256) || targets.has(target)) {
|
|
527
|
+
throw new Error(`invalid state asset: ${asset.target}`);
|
|
528
|
+
}
|
|
529
|
+
targets.add(target);
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
schemaVersion: state.schemaVersion,
|
|
533
|
+
assets: state.assets.map(asset => ({
|
|
534
|
+
target: assertAllowedTarget(asset.target),
|
|
535
|
+
sha256: asset.sha256,
|
|
536
|
+
})),
|
|
537
|
+
};
|
|
538
|
+
} catch (error) {
|
|
539
|
+
throw new Error(`Unable to parse governance installer state: ${error.message}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function statePath() {
|
|
544
|
+
return resolve(ROOT_DIR, '.cabloy-agent-governance-state.json');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export async function adopt({ apply = false, force = [] } = {}) {
|
|
548
|
+
const assets = buildAssets();
|
|
549
|
+
const path = statePath();
|
|
550
|
+
const state = await readState(path);
|
|
551
|
+
const previous = new Map((state?.assets ?? []).map(asset => [asset.target, asset.sha256]));
|
|
552
|
+
const forceSet = new Set(force);
|
|
553
|
+
const created = [];
|
|
554
|
+
const updated = [];
|
|
555
|
+
const deleted = [];
|
|
556
|
+
const conflicts = [];
|
|
557
|
+
|
|
558
|
+
for (const asset of assets) {
|
|
559
|
+
const target = targetPath(asset.target);
|
|
560
|
+
if (!existsSync(target)) {
|
|
561
|
+
created.push(asset.target);
|
|
562
|
+
if (apply) writeAtomic(target, asset.content);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const current = readFileSync(target, 'utf8');
|
|
566
|
+
if (current === asset.content) continue;
|
|
567
|
+
if (
|
|
568
|
+
forceSet.has(asset.target) ||
|
|
569
|
+
(previous.has(asset.target) && previous.get(asset.target) === sha256(current))
|
|
570
|
+
) {
|
|
571
|
+
updated.push(asset.target);
|
|
572
|
+
if (apply) writeAtomic(target, asset.content);
|
|
573
|
+
} else {
|
|
574
|
+
conflicts.push(asset.target);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for (const [target, previousHash] of previous) {
|
|
579
|
+
if (assets.some(asset => asset.target === target)) continue;
|
|
580
|
+
const targetFile = targetPath(target);
|
|
581
|
+
if (!existsSync(targetFile)) continue;
|
|
582
|
+
if (sha256(readFileSync(targetFile, 'utf8')) === previousHash) {
|
|
583
|
+
deleted.push(target);
|
|
584
|
+
if (apply) rmSync(targetFile, { force: true });
|
|
585
|
+
} else {
|
|
586
|
+
conflicts.push(target);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (apply) {
|
|
591
|
+
const conflictSet = new Set(conflicts);
|
|
592
|
+
const retained = (state?.assets ?? []).filter(
|
|
593
|
+
asset => !deleted.includes(asset.target) && conflictSet.has(asset.target),
|
|
594
|
+
);
|
|
595
|
+
const adopted = assets
|
|
596
|
+
.filter(asset => !conflictSet.has(asset.target))
|
|
597
|
+
.map(asset => ({ target: asset.target, sha256: sha256(asset.content) }));
|
|
598
|
+
writeAtomic(
|
|
599
|
+
path,
|
|
600
|
+
`${JSON.stringify({ schemaVersion: 1, assets: [...retained, ...adopted].sort((left, right) => left.target.localeCompare(right.target)) }, null, 2)}\n`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
return { created, updated, deleted, conflicts };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export async function cleanFixture(path) {
|
|
607
|
+
await rm(path, { recursive: true, force: true });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function parseArgs(args) {
|
|
611
|
+
const command = args[0];
|
|
612
|
+
const options = args.slice(1);
|
|
613
|
+
if (!['render', 'check', 'adopt'].includes(command)) {
|
|
614
|
+
throw new Error('Usage: governance.mjs <render|check|adopt> [--apply] [--force <target>]');
|
|
615
|
+
}
|
|
616
|
+
return { command, options };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function main() {
|
|
620
|
+
const { command, options } = parseArgs(process.argv.slice(2));
|
|
621
|
+
if (command === 'render') {
|
|
622
|
+
const result = render();
|
|
623
|
+
// eslint-disable-next-line no-console
|
|
624
|
+
console.log(`Rendered ${result.assets.length} managed governance assets.`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (command === 'check') {
|
|
628
|
+
const result = check();
|
|
629
|
+
if (result.problems.length) {
|
|
630
|
+
console.error(`${result.problems.join('\n')}\nRun: npm run agent:governance:render`);
|
|
631
|
+
process.exitCode = 1;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// eslint-disable-next-line no-console
|
|
635
|
+
console.log(`Governance outputs are current (${result.assets.length} assets).`);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const force = [];
|
|
639
|
+
for (let index = 0; index < options.length; index++) {
|
|
640
|
+
if (options[index] === '--force') {
|
|
641
|
+
const target = options[++index];
|
|
642
|
+
if (!target) throw new Error('--force requires a managed target path');
|
|
643
|
+
force.push(assertAllowedTarget(target));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const result = await adopt({ apply: options.includes('--apply'), force });
|
|
647
|
+
for (const [label, paths] of Object.entries(result)) {
|
|
648
|
+
// eslint-disable-next-line no-console
|
|
649
|
+
if (paths.length) console.log(`${label}: ${paths.join(', ')}`);
|
|
650
|
+
}
|
|
651
|
+
if (!Object.values(result).some(paths => paths.length)) {
|
|
652
|
+
// eslint-disable-next-line no-console
|
|
653
|
+
console.log('No governance adoption changes are needed.');
|
|
654
|
+
}
|
|
655
|
+
if (result.conflicts.length) process.exitCode = 2;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (
|
|
659
|
+
resolve(process.argv[1] ?? '') === EXECUTABLE_PATH ||
|
|
660
|
+
(process.env.CABLOY_GOVERNANCE_ROOT &&
|
|
661
|
+
existsSync(ACTIVE_SCRIPT_FILE) &&
|
|
662
|
+
realpathSync(resolve(process.argv[1] ?? '')) === realpathSync(ACTIVE_SCRIPT_FILE))
|
|
663
|
+
) {
|
|
664
|
+
main().catch(error => {
|
|
665
|
+
// eslint-disable-next-line no-console
|
|
666
|
+
console.error(`Agent governance failed: ${error.message}`);
|
|
667
|
+
process.exitCode = 1;
|
|
668
|
+
});
|
|
669
|
+
}
|