cabloy 5.1.191 → 5.1.192

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 (82) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/agent-governance.yml +39 -0
  3. package/.github/workflows/docs-pages.yml +1 -0
  4. package/.gitignore +3 -0
  5. package/CHANGELOG.md +7 -0
  6. package/package.json +11 -5
  7. package/repo-agent-governance/adapters/claude/contract-loop-gate.ts +199 -0
  8. package/{.claude → repo-agent-governance}/commands/release.md +20 -20
  9. package/repo-agent-governance/managed-assets.json +887 -0
  10. package/repo-agent-governance/manifest.json +56 -0
  11. package/{CLAUDE.md → repo-agent-governance/policies/repository.md} +4 -2
  12. package/repo-agent-governance/scripts/governance.mjs +570 -0
  13. package/repo-agent-governance/scripts/pack-check.mjs +43 -0
  14. package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/references/contract-loop-map.md +4 -4
  15. package/{.claude → repo-agent-governance}/skills/cabloy-master-detail/SKILL.md +1 -1
  16. package/{.claude → repo-agent-governance}/skills/cabloy-module-removal/SKILL.md +1 -1
  17. package/{.claude → repo-agent-governance}/skills/cabloy-spec-execution/SKILL.md +1 -1
  18. package/{.claude → repo-agent-governance}/skills/cabloy-spec-execution/references/execution-protocol.md +8 -8
  19. package/{.claude → repo-agent-governance}/skills/cabloy-spec-execution/references/status-and-evidence.md +9 -9
  20. package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/SKILL.md +2 -2
  21. package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/references/repo-aware-discovery.md +7 -7
  22. package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/references/repo-specs-document-set.md +30 -30
  23. package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +9 -9
  24. package/{.claude → repo-agent-governance}/skills/cabloy-workflow/SKILL.md +7 -8
  25. package/{.claude → repo-agent-governance}/skills/cabloy-worktree-environment/SKILL.md +9 -9
  26. package/{.claude → repo-agent-governance}/skills/cabloy-zova-source-reading/SKILL.md +5 -5
  27. package/repo-agent-governance/tests/governance.test.mjs +295 -0
  28. package/repo-agent-governance/tools/contract-loop/check.mjs +71 -0
  29. package/repo-agent-governance/tools/contract-loop/core.mjs +189 -0
  30. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +858 -0
  31. package/{.claude/skills/cabloy-spec-generation/scripts → repo-agent-governance/tools/spec-charts}/generate-implementation-charts.test.mjs +95 -20
  32. package/repo-docs/.vitepress/config.mjs +1 -0
  33. package/repo-docs/ai/agent-governance.md +80 -0
  34. package/repo-docs/ai/cli-to-skill-map.md +3 -3
  35. package/repo-docs/ai/docs-skills-rules-mapping.md +27 -26
  36. package/repo-docs/ai/edition-consistency-checklist.md +8 -6
  37. package/repo-docs/ai/future-skill-roadmap.md +2 -2
  38. package/repo-docs/ai/introduction.md +7 -6
  39. package/repo-docs/ai/repo-guidance.md +3 -2
  40. package/repo-docs/ai/rules-and-config.md +23 -15
  41. package/repo-docs/ai/skills.md +7 -7
  42. package/repo-docs/ai/verification.md +2 -1
  43. package/repo-docs/fullstack/contract-loop-playbook.md +1 -1
  44. package/repo-docs/fullstack/quickstart.md +1 -1
  45. package/repo-docs/reference/package-map.md +3 -2
  46. package/repo-docs/reference/repo-scripts.md +17 -2
  47. package/scripts/bootstrapAgentGovernance.mjs +81 -0
  48. package/scripts/init.ts +1 -0
  49. package/scripts/release.ts +15 -0
  50. package/scripts/upgrade.ts +66 -7
  51. package/zova/pnpm-lock.yaml +3 -3
  52. package/.claude/hooks/contract-loop-gate.ts +0 -421
  53. package/.claude/scheduled_tasks.lock +0 -1
  54. package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.mjs +0 -385
  55. /package/{.claude → repo-agent-governance/adapters/claude}/settings.json +0 -0
  56. /package/{.claude → repo-agent-governance}/skills/cabloy-backend-scaffold/SKILL.md +0 -0
  57. /package/{.claude → repo-agent-governance}/skills/cabloy-backend-scaffold/evals/evals.json +0 -0
  58. /package/{.claude → repo-agent-governance}/skills/cabloy-backend-scaffold/references/backend-thread-map.md +0 -0
  59. /package/{.claude → repo-agent-governance}/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +0 -0
  60. /package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/SKILL.md +0 -0
  61. /package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/evals/evals.json +0 -0
  62. /package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md +0 -0
  63. /package/{.claude → repo-agent-governance}/skills/cabloy-contract-loop/references/verification-checklist.md +0 -0
  64. /package/{.claude → repo-agent-governance}/skills/cabloy-domain-planning/SKILL.md +0 -0
  65. /package/{.claude → repo-agent-governance}/skills/cabloy-frontend-scaffold/SKILL.md +0 -0
  66. /package/{.claude → repo-agent-governance}/skills/cabloy-frontend-scaffold/evals/evals.json +0 -0
  67. /package/{.claude → repo-agent-governance}/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +0 -0
  68. /package/{.claude → repo-agent-governance}/skills/cabloy-frontend-scaffold/references/frontend-thread-map.md +0 -0
  69. /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/SKILL.md +0 -0
  70. /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/evals/evals.json +0 -0
  71. /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/references/custom-renderer-demo-checklist.md +0 -0
  72. /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/references/field-update-decision-tree.md +0 -0
  73. /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/references/follow-up-checklist.md +0 -0
  74. /package/{.claude → repo-agent-governance}/skills/cabloy-resource-field-update/references/verification-checklist.md +0 -0
  75. /package/{.claude → repo-agent-governance}/skills/cabloy-spec-execution/evals/evals.json +0 -0
  76. /package/{.claude → repo-agent-governance}/skills/cabloy-spec-generation/evals/evals.json +0 -0
  77. /package/{.claude → repo-agent-governance}/skills/cabloy-workflow/evals/evals.json +0 -0
  78. /package/{.claude → repo-agent-governance}/skills/cabloy-workflow/references/cli-strategy.md +0 -0
  79. /package/{.claude → repo-agent-governance}/skills/cabloy-workflow/references/edition-detection.md +0 -0
  80. /package/{.claude → repo-agent-governance}/skills/cabloy-worktree-environment/evals/evals.json +0 -0
  81. /package/{.claude → repo-agent-governance}/skills/cabloy-zova-source-reading/references/analysis-modes.md +0 -0
  82. /package/{.claude → repo-agent-governance}/skills/cabloy-zova-source-reading/references/core-reading-paths.md +0 -0
@@ -0,0 +1,56 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "policy": {
4
+ "source": "policies/repository.md",
5
+ "targets": [
6
+ {
7
+ "adapter": "claude",
8
+ "target": "CLAUDE.md"
9
+ },
10
+ {
11
+ "adapter": "codex",
12
+ "target": "AGENTS.md"
13
+ },
14
+ {
15
+ "adapter": "cursor",
16
+ "target": ".cursor/rules/cabloy-governance.mdc"
17
+ }
18
+ ]
19
+ },
20
+ "skills": {
21
+ "sourceDirectory": "skills",
22
+ "targets": [
23
+ {
24
+ "adapter": "claude",
25
+ "targetDirectory": ".claude/skills"
26
+ },
27
+ {
28
+ "adapter": "codex",
29
+ "targetDirectory": ".agents/skills"
30
+ },
31
+ {
32
+ "adapter": "cursor",
33
+ "targetDirectory": ".cursor/skills"
34
+ }
35
+ ]
36
+ },
37
+ "commands": [
38
+ {
39
+ "adapter": "claude",
40
+ "source": "commands/release.md",
41
+ "target": ".claude/commands/release.md"
42
+ }
43
+ ],
44
+ "claude": {
45
+ "settings": {
46
+ "source": "adapters/claude/settings.json",
47
+ "target": ".claude/settings.json"
48
+ },
49
+ "hooks": [
50
+ {
51
+ "source": "adapters/claude/contract-loop-gate.ts",
52
+ "target": ".claude/hooks/contract-loop-gate.ts"
53
+ }
54
+ ]
55
+ }
56
+ }
@@ -21,7 +21,8 @@ Always perform this detection before making UI-sensitive assumptions, choosing f
21
21
  - `repo-specs/` contains product and business specifications, delivery plans, and suite-local ADRs.
22
22
  - `repo-e2e/` contains the end-to-end test project.
23
23
  - `repo-observability/` contains local observability infrastructure.
24
- - `.claude/` contains Claude commands, skills, and settings.
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
26
 
26
27
  ## Preferred workflow
27
28
 
@@ -66,7 +67,8 @@ Before inventing a custom implementation path:
66
67
  - Default to establishing such query state during render. Use `disableSuspenseOnInit: true` only for relatively stable query-backed state when you want to skip the init-time `query.suspense()` kick; it does not prevent query creation, fetches, or hydration-time rendering. If strict readiness is needed later, wait explicitly at the interaction boundary.
67
68
  - For render-driving async state, read the current query-owned reactive surface (`query.data` or a model-derived projection); an awaited `query.refetch()` result is valid only for one-shot interaction or orchestration and must not become a parallel ongoing controller/render state owner.
68
69
  - In SSR, keep server HTML and the client's hydration-time initial render equivalent. When server rendering intentionally omits private, cookie-unavailable, or browser-only state, keep the same neutral shell or placeholder through hydration and defer its query/load/render branch to an explicit post-hydration, admission, mounted, or interaction boundary.
69
- - Keep repo-wide AI rules in `CLAUDE.md` short and durable; put branching Zova analysis workflows in `.claude/skills/`.
70
+ - Keep portable repo-wide AI rules short and durable in `repo-agent-governance/policies/`; put branching root workflows in `repo-agent-governance/skills/` and regenerate platform adapters instead of hand-editing them.
71
+ - Treat hooks, permissions, local settings, credentials, MCP registration, command discovery, and automatic command execution as platform-specific integrations, not portable rule or skill capabilities.
70
72
  - After implementation, do not launch broad reviews automatically; when review is needed, scope it to the current diff and report only high-confidence issues.
71
73
  - Do not modify shared environment identity or ports merely to bypass a busy resource. For intentional parallel worktree setup, follow `repo-docs/fullstack/parallel-worktree-environment.md`; otherwise wait for the shared resource or ask the user. Create or change worktree-local environment overrides only through the explicitly invoked `cabloy-worktree-environment` skill and its confirmation phase, and only in `vona/env/.env.local` and `zova/env/.env.local`; never modify flavor-, mode-, app-mode-, or runtime-specific `.env.*.local` files. The skill derives the standard `APP_NAME`, `SERVER_LISTEN_PORT`, `DEV_SERVER_PORT`, `DEV_SERVER_HMR_PORT`, and API-derived `API_BASE_URL` tuple only from Git worktree metadata and fixed port baselines; never read or expose `.env*` content while recommending values. Admin and Web are alternative commands using this shared tuple and must not run concurrently in one worktree; use another linked worktree for concurrent use. Detect the active edition before choosing scripts, and never run `npm run init` as an automatic follow-up.
72
74
  - For SSR theme-sensitive frontend work, detect the active edition marker and UI library before making assumptions. Cabloy Basic currently means DaisyUI + Tailwind CSS assumptions; Cabloy Start currently means Vuetify assumptions.
@@ -0,0 +1,570 @@
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 = ['CLAUDE.md', 'AGENTS.md', '.claude/', '.agents/', '.cursor/'];
35
+ const FORBIDDEN_TARGET_SEGMENTS = [
36
+ 'settings.local.json',
37
+ 'worktrees',
38
+ 'CLAUDE.local.md',
39
+ 'scheduled_tasks',
40
+ ];
41
+
42
+ function normalizeText(value) {
43
+ return `${value.replace(/\r\n?/g, '\n').replace(/\n*$/, '')}\n`;
44
+ }
45
+
46
+ function sha256(value) {
47
+ return createHash('sha256').update(value).digest('hex');
48
+ }
49
+
50
+ function toPosix(value) {
51
+ return value.split(sep).join('/');
52
+ }
53
+
54
+ function assertSafeRelativePath(value, label) {
55
+ if (typeof value !== 'string' || !value || isAbsolute(value)) {
56
+ throw new Error(`${label} must be a non-empty relative path`);
57
+ }
58
+ const normalized = toPosix(value);
59
+ if (normalized.split('/').some(part => !part || part === '.' || part === '..')) {
60
+ throw new Error(`${label} must not contain empty, dot, or parent segments: ${value}`);
61
+ }
62
+ return normalized;
63
+ }
64
+
65
+ function assertInside(base, candidate, label) {
66
+ const result = relative(base, candidate);
67
+ if (result === '' || (!result.startsWith(`..${sep}`) && result !== '..' && !isAbsolute(result)))
68
+ return;
69
+ throw new Error(`${label} escapes its permitted directory: ${candidate}`);
70
+ }
71
+
72
+ function assertAllowedTarget(target) {
73
+ const value = assertSafeRelativePath(target, 'Managed target');
74
+ const allowed = ALLOWED_TARGET_ROOTS.some(root =>
75
+ root.endsWith('/') ? value.startsWith(root) : value === root,
76
+ );
77
+ if (!allowed) {
78
+ throw new Error(`Managed target is outside the allowed roots: ${value}`);
79
+ }
80
+ const segments = value.split('/');
81
+ if (FORBIDDEN_TARGET_SEGMENTS.some(segment => segments.includes(segment))) {
82
+ throw new Error(`Managed target is forbidden: ${value}`);
83
+ }
84
+ return value;
85
+ }
86
+
87
+ function requireObject(value, label) {
88
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
89
+ throw new Error(`${label} must be an object`);
90
+ }
91
+ return value;
92
+ }
93
+
94
+ function assertKeys(value, allowed, label) {
95
+ for (const key of Object.keys(value)) {
96
+ if (!allowed.includes(key)) throw new Error(`${label} has unsupported key: ${key}`);
97
+ }
98
+ }
99
+
100
+ function readJson(path, label) {
101
+ try {
102
+ return JSON.parse(readFileSync(path, 'utf8'));
103
+ } catch (error) {
104
+ throw new Error(`Unable to parse ${label}: ${error.message}`);
105
+ }
106
+ }
107
+
108
+ function assertFile(path, label) {
109
+ if (!existsSync(path) || !lstatSync(path).isFile())
110
+ throw new Error(`${label} is missing: ${path}`);
111
+ assertInside(GOVERNANCE_DIR, path, label);
112
+ }
113
+
114
+ function readCanonical(source) {
115
+ const safeSource = assertSafeRelativePath(source, 'Canonical source');
116
+ const path = resolve(GOVERNANCE_DIR, safeSource);
117
+ assertFile(path, 'Canonical source');
118
+ return { source: safeSource, content: normalizeText(readFileSync(path, 'utf8')) };
119
+ }
120
+
121
+ function validateSkill(content, source) {
122
+ const match = content.match(/^---\n([\s\S]*?)\n---\n/);
123
+ if (!match) throw new Error(`Skill must begin with YAML front matter: ${source}`);
124
+ const lines = match[1].split('\n');
125
+ const names = lines.filter(line => line.startsWith('name: '));
126
+ const descriptions = lines.filter(line => line.startsWith('description: '));
127
+ if (
128
+ names.length !== 1 ||
129
+ descriptions.length !== 1 ||
130
+ !names[0].slice(6).trim() ||
131
+ !descriptions[0].slice(13).trim()
132
+ ) {
133
+ throw new Error(
134
+ `Skill front matter requires exactly one non-empty name and description: ${source}`,
135
+ );
136
+ }
137
+ return names[0].slice(6).trim();
138
+ }
139
+
140
+ function listFiles(directory, prefix = '') {
141
+ const results = [];
142
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
143
+ left.name.localeCompare(right.name),
144
+ )) {
145
+ if (entry.name === '.DS_Store') continue;
146
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
147
+ const path = resolve(directory, entry.name);
148
+ if (entry.isDirectory()) {
149
+ results.push(...listFiles(path, relativePath));
150
+ } else if (entry.isFile()) {
151
+ results.push(relativePath);
152
+ } else {
153
+ throw new Error(`Skill bundle contains an unsupported filesystem entry: ${path}`);
154
+ }
155
+ }
156
+ return results;
157
+ }
158
+
159
+ function validateManifest() {
160
+ const manifest = requireObject(
161
+ readJson(MANIFEST_PATH, 'governance manifest'),
162
+ 'Governance manifest',
163
+ );
164
+ assertKeys(
165
+ manifest,
166
+ ['schemaVersion', 'policy', 'skills', 'commands', 'claude'],
167
+ 'Governance manifest',
168
+ );
169
+ if (manifest.schemaVersion !== 1) throw new Error('Governance manifest must use schemaVersion 1');
170
+
171
+ const policy = requireObject(manifest.policy, 'Governance manifest policy');
172
+ assertKeys(policy, ['source', 'targets'], 'Governance manifest policy');
173
+ const policySource = readCanonical(policy.source);
174
+ if (!Array.isArray(policy.targets) || !policy.targets.length)
175
+ throw new Error('Policy targets must be a non-empty array');
176
+
177
+ const skills = requireObject(manifest.skills, 'Governance manifest skills');
178
+ assertKeys(skills, ['sourceDirectory', 'targets'], 'Governance manifest skills');
179
+ const skillsDirectory = assertSafeRelativePath(skills.sourceDirectory, 'Skill source directory');
180
+ const skillsPath = resolve(GOVERNANCE_DIR, skillsDirectory);
181
+ if (!existsSync(skillsPath) || !lstatSync(skillsPath).isDirectory())
182
+ throw new Error(`Skill source directory is missing: ${skillsDirectory}`);
183
+ assertInside(GOVERNANCE_DIR, skillsPath, 'Skill source directory');
184
+ if (!Array.isArray(skills.targets) || !skills.targets.length)
185
+ throw new Error('Skill targets must be a non-empty array');
186
+
187
+ if (!Array.isArray(manifest.commands)) throw new Error('Commands must be an array');
188
+ const commands = manifest.commands.map((command, index) => {
189
+ const item = requireObject(command, `Command ${index}`);
190
+ assertKeys(item, ['adapter', 'source', 'target'], `Command ${index}`);
191
+ return {
192
+ ...item,
193
+ source: readCanonical(item.source).source,
194
+ target: assertAllowedTarget(item.target),
195
+ };
196
+ });
197
+
198
+ const claude = requireObject(manifest.claude, 'Claude manifest section');
199
+ assertKeys(claude, ['settings', 'hooks'], 'Claude manifest section');
200
+ const settings = requireObject(claude.settings, 'Claude settings manifest section');
201
+ assertKeys(settings, ['source', 'target'], 'Claude settings manifest section');
202
+ const hooks = Array.isArray(claude.hooks)
203
+ ? claude.hooks
204
+ : (() => {
205
+ throw new Error('Claude hooks must be an array');
206
+ })();
207
+
208
+ const targets = new Set();
209
+ const claimTarget = target => {
210
+ const safeTarget = assertAllowedTarget(target);
211
+ if (targets.has(safeTarget))
212
+ throw new Error(`Managed target is declared more than once: ${safeTarget}`);
213
+ targets.add(safeTarget);
214
+ return safeTarget;
215
+ };
216
+
217
+ const normalizedPolicyTargets = policy.targets.map((item, index) => {
218
+ const target = requireObject(item, `Policy target ${index}`);
219
+ assertKeys(target, ['adapter', 'target'], `Policy target ${index}`);
220
+ if (!['claude', 'codex', 'cursor'].includes(target.adapter))
221
+ throw new Error(`Unsupported policy adapter: ${target.adapter}`);
222
+ return { adapter: target.adapter, target: claimTarget(target.target) };
223
+ });
224
+
225
+ const normalizedSkillTargets = skills.targets.map((item, index) => {
226
+ const target = requireObject(item, `Skill target ${index}`);
227
+ assertKeys(target, ['adapter', 'targetDirectory'], `Skill target ${index}`);
228
+ if (!['claude', 'codex', 'cursor'].includes(target.adapter))
229
+ throw new Error(`Unsupported skill adapter: ${target.adapter}`);
230
+ return {
231
+ adapter: target.adapter,
232
+ targetDirectory: assertAllowedTarget(`${target.targetDirectory}/placeholder`).replace(
233
+ /\/placeholder$/,
234
+ '',
235
+ ),
236
+ };
237
+ });
238
+
239
+ for (const command of commands) claimTarget(command.target);
240
+ const normalizedSettings = {
241
+ source: readCanonical(settings.source).source,
242
+ target: claimTarget(settings.target),
243
+ };
244
+ const normalizedHooks = hooks.map((item, index) => {
245
+ const hook = requireObject(item, `Claude hook ${index}`);
246
+ assertKeys(hook, ['source', 'target'], `Claude hook ${index}`);
247
+ return { source: readCanonical(hook.source).source, target: claimTarget(hook.target) };
248
+ });
249
+
250
+ return {
251
+ manifest,
252
+ policy: {
253
+ source: policySource.source,
254
+ content: policySource.content,
255
+ targets: normalizedPolicyTargets,
256
+ },
257
+ skills: { directory: skillsDirectory, path: skillsPath, targets: normalizedSkillTargets },
258
+ commands,
259
+ claude: { settings: normalizedSettings, hooks: normalizedHooks },
260
+ };
261
+ }
262
+
263
+ function policyForAdapter(content, adapter) {
264
+ if (adapter === 'cursor') return `${CURSOR_HEADER}${content}`;
265
+ return `${GENERATED_HEADER}${content}`;
266
+ }
267
+
268
+ function buildAssets() {
269
+ const config = validateManifest();
270
+ const assets = [];
271
+ const add = (target, content, source, adapter, category) => {
272
+ assets.push({ target, content: normalizeText(content), source, adapter, category });
273
+ };
274
+
275
+ for (const target of config.policy.targets) {
276
+ add(
277
+ target.target,
278
+ policyForAdapter(config.policy.content, target.adapter),
279
+ config.policy.source,
280
+ target.adapter,
281
+ 'policy',
282
+ );
283
+ }
284
+
285
+ const skillDirectories = readdirSync(config.skills.path, { withFileTypes: true })
286
+ .filter(entry => entry.isDirectory())
287
+ .sort((left, right) => left.name.localeCompare(right.name));
288
+ const skillNames = new Set();
289
+ for (const directory of skillDirectories) {
290
+ const skillPath = resolve(config.skills.path, directory.name);
291
+ const files = listFiles(skillPath);
292
+ if (!files.includes('SKILL.md'))
293
+ throw new Error(`Skill bundle is missing SKILL.md: ${directory.name}`);
294
+ const skillSource = `${config.skills.directory}/${directory.name}/SKILL.md`;
295
+ const skillName = validateSkill(readCanonical(skillSource).content, skillSource);
296
+ if (skillName !== directory.name)
297
+ throw new Error(`Skill directory and front-matter name must match: ${directory.name}`);
298
+ if (skillNames.has(skillName)) throw new Error(`Skill name is duplicated: ${skillName}`);
299
+ skillNames.add(skillName);
300
+ for (const skillTarget of config.skills.targets) {
301
+ for (const file of files) {
302
+ const source = `${config.skills.directory}/${directory.name}/${file}`;
303
+ const canonical = readCanonical(source);
304
+ add(
305
+ `${skillTarget.targetDirectory}/${directory.name}/${file}`,
306
+ canonical.content,
307
+ canonical.source,
308
+ skillTarget.adapter,
309
+ 'skill',
310
+ );
311
+ }
312
+ }
313
+ }
314
+
315
+ for (const command of config.commands) {
316
+ add(
317
+ command.target,
318
+ readCanonical(command.source).content,
319
+ command.source,
320
+ command.adapter,
321
+ 'command',
322
+ );
323
+ }
324
+ add(
325
+ config.claude.settings.target,
326
+ readCanonical(config.claude.settings.source).content,
327
+ config.claude.settings.source,
328
+ 'claude',
329
+ 'runtime',
330
+ );
331
+ for (const hook of config.claude.hooks) {
332
+ add(hook.target, readCanonical(hook.source).content, hook.source, 'claude', 'runtime');
333
+ }
334
+
335
+ const targets = new Set();
336
+ for (const asset of assets) {
337
+ assertAllowedTarget(asset.target);
338
+ if (targets.has(asset.target)) throw new Error(`Generated target collision: ${asset.target}`);
339
+ targets.add(asset.target);
340
+ }
341
+ return assets.sort((left, right) => left.target.localeCompare(right.target));
342
+ }
343
+
344
+ function lockFromAssets(assets) {
345
+ return {
346
+ schemaVersion: 1,
347
+ assets: assets.map(asset => ({
348
+ adapter: asset.adapter,
349
+ category: asset.category,
350
+ sha256: sha256(asset.content),
351
+ source: asset.source,
352
+ target: asset.target,
353
+ })),
354
+ };
355
+ }
356
+
357
+ function lockText(assets) {
358
+ return `${JSON.stringify(lockFromAssets(assets), null, 2)}\n`;
359
+ }
360
+
361
+ function targetPath(target) {
362
+ const path = resolve(ROOT_DIR, target);
363
+ assertInside(ROOT_DIR, path, 'Managed target');
364
+ return path;
365
+ }
366
+
367
+ function writeAtomic(path, content) {
368
+ mkdirSync(dirname(path), { recursive: true });
369
+ const temporary = `${path}.tmp-governance-${process.pid}`;
370
+ try {
371
+ writeFileSync(temporary, content);
372
+ renameSync(temporary, path);
373
+ } finally {
374
+ if (existsSync(temporary)) rmSync(temporary, { force: true });
375
+ }
376
+ }
377
+
378
+ export function render() {
379
+ const assets = buildAssets();
380
+ for (const asset of assets) {
381
+ const path = targetPath(asset.target);
382
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
383
+ if (existing !== asset.content) writeAtomic(path, asset.content);
384
+ }
385
+ const lock = lockText(assets);
386
+ const existingLock = existsSync(LOCK_PATH) ? readFileSync(LOCK_PATH, 'utf8') : null;
387
+ if (existingLock !== lock) writeAtomic(LOCK_PATH, lock);
388
+ return { assets };
389
+ }
390
+
391
+ export function check() {
392
+ const assets = buildAssets();
393
+ const problems = [];
394
+ for (const asset of assets) {
395
+ const path = targetPath(asset.target);
396
+ if (!existsSync(path)) {
397
+ problems.push(`Missing managed output: ${asset.target}`);
398
+ } else if (readFileSync(path, 'utf8') !== asset.content) {
399
+ problems.push(`Stale managed output: ${asset.target}`);
400
+ }
401
+ }
402
+ const expectedLock = lockText(assets);
403
+ if (!existsSync(LOCK_PATH)) {
404
+ problems.push('Missing managed lock: repo-agent-governance/managed-assets.json');
405
+ } else if (readFileSync(LOCK_PATH, 'utf8') !== expectedLock) {
406
+ problems.push('Stale managed lock: repo-agent-governance/managed-assets.json');
407
+ }
408
+ return { assets, problems };
409
+ }
410
+
411
+ async function readState(path) {
412
+ if (!existsSync(path)) return null;
413
+ try {
414
+ const state = JSON.parse(await readFile(path, 'utf8'));
415
+ if (state?.schemaVersion !== 1 || !Array.isArray(state.assets))
416
+ throw new Error('unsupported state shape');
417
+ const targets = new Set();
418
+ for (const asset of state.assets) {
419
+ if (
420
+ !asset ||
421
+ typeof asset !== 'object' ||
422
+ Object.keys(asset).some(key => !['target', 'sha256'].includes(key))
423
+ ) {
424
+ throw new Error('invalid state asset shape');
425
+ }
426
+ const target = assertAllowedTarget(asset.target);
427
+ if (!/^[a-f0-9]{64}$/.test(asset.sha256) || targets.has(target)) {
428
+ throw new Error(`invalid state asset: ${asset.target}`);
429
+ }
430
+ targets.add(target);
431
+ }
432
+ return {
433
+ schemaVersion: state.schemaVersion,
434
+ assets: state.assets.map(asset => ({
435
+ target: assertAllowedTarget(asset.target),
436
+ sha256: asset.sha256,
437
+ })),
438
+ };
439
+ } catch (error) {
440
+ throw new Error(`Unable to parse governance installer state: ${error.message}`);
441
+ }
442
+ }
443
+
444
+ function statePath() {
445
+ return resolve(ROOT_DIR, '.cabloy-agent-governance-state.json');
446
+ }
447
+
448
+ export async function adopt({ apply = false, force = [] } = {}) {
449
+ const assets = buildAssets();
450
+ const path = statePath();
451
+ const state = await readState(path);
452
+ const previous = new Map((state?.assets ?? []).map(asset => [asset.target, asset.sha256]));
453
+ const forceSet = new Set(force);
454
+ const created = [];
455
+ const updated = [];
456
+ const deleted = [];
457
+ const conflicts = [];
458
+
459
+ for (const asset of assets) {
460
+ const target = targetPath(asset.target);
461
+ if (!existsSync(target)) {
462
+ created.push(asset.target);
463
+ if (apply) writeAtomic(target, asset.content);
464
+ continue;
465
+ }
466
+ const current = readFileSync(target, 'utf8');
467
+ if (current === asset.content) continue;
468
+ if (
469
+ forceSet.has(asset.target) ||
470
+ (previous.has(asset.target) && previous.get(asset.target) === sha256(current))
471
+ ) {
472
+ updated.push(asset.target);
473
+ if (apply) writeAtomic(target, asset.content);
474
+ } else {
475
+ conflicts.push(asset.target);
476
+ }
477
+ }
478
+
479
+ for (const [target, previousHash] of previous) {
480
+ if (assets.some(asset => asset.target === target)) continue;
481
+ const targetFile = targetPath(target);
482
+ if (!existsSync(targetFile)) continue;
483
+ if (sha256(readFileSync(targetFile, 'utf8')) === previousHash) {
484
+ deleted.push(target);
485
+ if (apply) rmSync(targetFile, { force: true });
486
+ } else {
487
+ conflicts.push(target);
488
+ }
489
+ }
490
+
491
+ if (apply) {
492
+ const conflictSet = new Set(conflicts);
493
+ const retained = (state?.assets ?? []).filter(
494
+ asset => !deleted.includes(asset.target) && conflictSet.has(asset.target),
495
+ );
496
+ const adopted = assets
497
+ .filter(asset => !conflictSet.has(asset.target))
498
+ .map(asset => ({ target: asset.target, sha256: sha256(asset.content) }));
499
+ writeAtomic(
500
+ path,
501
+ `${JSON.stringify({ schemaVersion: 1, assets: [...retained, ...adopted].sort((left, right) => left.target.localeCompare(right.target)) }, null, 2)}\n`,
502
+ );
503
+ }
504
+ return { created, updated, deleted, conflicts };
505
+ }
506
+
507
+ export async function cleanFixture(path) {
508
+ await rm(path, { recursive: true, force: true });
509
+ }
510
+
511
+ function parseArgs(args) {
512
+ const command = args[0];
513
+ const options = args.slice(1);
514
+ if (!['render', 'check', 'adopt'].includes(command)) {
515
+ throw new Error('Usage: governance.mjs <render|check|adopt> [--apply] [--force <target>]');
516
+ }
517
+ return { command, options };
518
+ }
519
+
520
+ async function main() {
521
+ const { command, options } = parseArgs(process.argv.slice(2));
522
+ if (command === 'render') {
523
+ const result = render();
524
+ // eslint-disable-next-line no-console
525
+ console.log(`Rendered ${result.assets.length} managed governance assets.`);
526
+ return;
527
+ }
528
+ if (command === 'check') {
529
+ const result = check();
530
+ if (result.problems.length) {
531
+ console.error(`${result.problems.join('\n')}\nRun: npm run agent:governance:render`);
532
+ process.exitCode = 1;
533
+ return;
534
+ }
535
+ // eslint-disable-next-line no-console
536
+ console.log(`Governance outputs are current (${result.assets.length} assets).`);
537
+ return;
538
+ }
539
+ const force = [];
540
+ for (let index = 0; index < options.length; index++) {
541
+ if (options[index] === '--force') {
542
+ const target = options[++index];
543
+ if (!target) throw new Error('--force requires a managed target path');
544
+ force.push(assertAllowedTarget(target));
545
+ }
546
+ }
547
+ const result = await adopt({ apply: options.includes('--apply'), force });
548
+ for (const [label, paths] of Object.entries(result)) {
549
+ // eslint-disable-next-line no-console
550
+ if (paths.length) console.log(`${label}: ${paths.join(', ')}`);
551
+ }
552
+ if (!Object.values(result).some(paths => paths.length)) {
553
+ // eslint-disable-next-line no-console
554
+ console.log('No governance adoption changes are needed.');
555
+ }
556
+ if (result.conflicts.length) process.exitCode = 2;
557
+ }
558
+
559
+ if (
560
+ resolve(process.argv[1] ?? '') === EXECUTABLE_PATH ||
561
+ (process.env.CABLOY_GOVERNANCE_ROOT &&
562
+ existsSync(ACTIVE_SCRIPT_FILE) &&
563
+ realpathSync(resolve(process.argv[1] ?? '')) === realpathSync(ACTIVE_SCRIPT_FILE))
564
+ ) {
565
+ main().catch(error => {
566
+ // eslint-disable-next-line no-console
567
+ console.error(`Agent governance failed: ${error.message}`);
568
+ process.exitCode = 1;
569
+ });
570
+ }
@@ -0,0 +1,43 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ const output = execFileSync('npm', ['pack', '--json', '--dry-run', '--ignore-scripts'], {
5
+ encoding: 'utf8',
6
+ });
7
+ const packageInfo = JSON.parse(output)[0];
8
+ const files = new Set(packageInfo.files.map(entry => entry.path));
9
+ const managedAdapters = JSON.parse(
10
+ readFileSync('repo-agent-governance/managed-assets.json', 'utf8'),
11
+ ).assets.map(asset => asset.target);
12
+ const required = [
13
+ 'scripts/bootstrapAgentGovernance.mjs',
14
+ 'scripts/init.ts',
15
+ 'repo-agent-governance/manifest.json',
16
+ 'repo-agent-governance/managed-assets.json',
17
+ 'repo-agent-governance/scripts/governance.mjs',
18
+ 'repo-agent-governance/tools/contract-loop/core.mjs',
19
+ ];
20
+ const forbidden = [
21
+ '.cabloy-agent-governance-state.json',
22
+ '.claude/scheduled_tasks.lock',
23
+ '.claude/settings.local.json',
24
+ ];
25
+ for (const path of required) {
26
+ if (!files.has(path)) throw new Error(`npm package is missing governance asset: ${path}`);
27
+ }
28
+ // A pre-governance upgrader overwrites manifest-managed outputs before it invokes the
29
+ // incoming initializer. Keep every managed adapter out of this transition package so the
30
+ // initializer can adopt canonical outputs without destroying a customized legacy adapter.
31
+ for (const path of managedAdapters) {
32
+ if (files.has(path)) {
33
+ throw new Error(`npm package must exclude transition adapter output: ${path}`);
34
+ }
35
+ }
36
+ for (const path of forbidden) {
37
+ if (files.has(path))
38
+ throw new Error(`npm package must not contain local governance state: ${path}`);
39
+ }
40
+ // eslint-disable-next-line no-console
41
+ console.log(
42
+ `Governance package surface is valid (${required.length} canonical assets; ${managedAdapters.length} transition adapters excluded).`,
43
+ );