clckernel 1.2.19 → 2.0.0
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/.agents/skills/clckernel_cli/SKILL.md +23 -6
- package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
- package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
- package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
- package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
- package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
- package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
- package/.agents/skills/clckernel_cli/reference/start.md +14 -4
- package/.agents/skills/clckernel_cli/reference/test.md +1 -1
- package/.agents/skills/clckernel_cli/reference/update.md +19 -19
- package/CHANGELOG.md +244 -0
- package/LICENSE +21 -0
- package/README.es.md +280 -51
- package/README.md +274 -50
- package/bin/cli.js +8 -0
- package/package.json +12 -2
- package/src/adapters/agent-os.js +161 -84
- package/src/adapters/astro.js +8 -6
- package/src/adapters/base.js +23 -38
- package/src/adapters/django.js +14 -13
- package/src/adapters/fastapi.js +16 -15
- package/src/adapters/go.js +11 -9
- package/src/adapters/laravel.js +12 -10
- package/src/adapters/nextjs.js +7 -5
- package/src/adapters/rails.js +13 -11
- package/src/adapters/rust.js +11 -9
- package/src/catalog.js +58 -49
- package/src/config.js +38 -240
- package/src/detector.js +243 -44
- package/src/doctor.js +18 -9
- package/src/generator.js +734 -302
- package/src/gitignore.js +60 -14
- package/src/index.js +257 -18
- package/src/io.js +409 -0
- package/src/known-hashes.json +309 -0
- package/src/onboarding.js +15 -0
- package/src/technologies/detector.js +1 -1
- package/src/technologies/guards/_shared.js +1 -1
- package/src/ui.js +233 -40
- package/src/updater.js +271 -38
- package/tools/_changes.js +279 -0
- package/tools/_changes.py +233 -0
- package/tools/_config.js +294 -0
- package/tools/_config.py +271 -0
- package/tools/_yaml.py +489 -0
- package/tools/audit.js +511 -111
- package/tools/audit.py +506 -128
- package/tools/celery_guard.py +13 -35
- package/tools/check_a11y.js +8 -28
- package/tools/check_api_contracts.js +13 -39
- package/tools/check_architecture.js +35 -44
- package/tools/check_architecture.py +172 -105
- package/tools/check_custom.js +108 -61
- package/tools/check_custom.py +108 -271
- package/tools/check_data_files.js +88 -0
- package/tools/check_db_efficiency.py +80 -65
- package/tools/check_hitl.js +112 -69
- package/tools/check_links.js +138 -0
- package/tools/check_migrations.py +143 -94
- package/tools/check_performance.js +35 -44
- package/tools/check_placeholders.js +104 -0
- package/tools/check_responsive.js +8 -28
- package/tools/check_scope.py +40 -31
- package/tools/check_seo.js +8 -28
- package/tools/check_skills.js +158 -0
- package/tools/check_storybook.js +8 -21
- package/tools/check_symlinks.js +40 -22
- package/tools/check_ui_reuse.js +17 -29
- package/tools/docker_guard.py +19 -41
- package/tools/postgres_guard.py +15 -36
- package/tools/redis_guard.py +13 -35
- package/tools/scan_secrets.js +134 -67
- package/tools/scan_secrets.py +163 -72
- package/tools/verify_tdd.js +867 -0
- package/tools/verify_tdd.py +407 -165
- package/tools/yaml.js +404 -132
- package/tools/check_domain_data.js +0 -100
- package/tools/check_test_calendar_sync.js +0 -111
package/src/generator.js
CHANGED
|
@@ -1,106 +1,507 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLC
|
|
2
|
+
* CLC Kernel — Universal Provisioning Engine
|
|
3
3
|
* Manifest-driven tool installation and dynamic pre-commit hook generation.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const {
|
|
9
|
-
const {
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
const { readProjectConfig, globMatcher } = require('./config.js');
|
|
10
|
+
const { createIO, runWithIO, activeIO, currentIO, registeredPaths, MANAGED_REGISTRY } = require('./io');
|
|
11
|
+
const { FIRST_SESSION_SECTION } = require('./onboarding');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Shell condition that decides at commit time whether an external tool runs.
|
|
15
|
+
* Adapters declare it as `availableIf`; a tool without one falls back to
|
|
16
|
+
* `command -v <extCLI or first word of the command>`. A compound condition
|
|
17
|
+
* (&&, ||, ;, |) is grouped in braces so the redirect silences all of it —
|
|
18
|
+
* `{ ...; }` is valid in both POSIX sh (husky hooks) and bash.
|
|
19
|
+
* @param {object} tool - External tool descriptor
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function availabilityCondition(tool) {
|
|
23
|
+
const cond = (tool.availableIf || `command -v ${tool.extCLI || tool.command.split(/\s+/)[0]}`).trim();
|
|
24
|
+
return /&&|\|\||;|\|/.test(cond) ? `{ ${cond}; }` : cond;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hook line for an external tool: run it when available (a failure aborts
|
|
29
|
+
* the commit under `set -e`), otherwise say it was skipped instead of
|
|
30
|
+
* omitting it silently.
|
|
31
|
+
* @param {object} tool - External tool descriptor
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function guardedCommand(tool, managed = []) {
|
|
35
|
+
return `if ${availabilityCondition(tool)} >/dev/null 2>&1; then ${withManagedExcludes(tool, managed)}; ` +
|
|
36
|
+
`else echo "clckernel: ${tool.name} not available, skipped"; fi`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Appends the harness's own files to a whole-project linter's exclusions.
|
|
41
|
+
* A tool opts in with `excludeManaged: { flag, extensions }` (e.g. ruff's
|
|
42
|
+
* --extend-exclude for .py); without managed files of those extensions the
|
|
43
|
+
* command is unchanged.
|
|
44
|
+
* @param {object} tool - External tool descriptor
|
|
45
|
+
* @param {string[]} managed - Paths listed in the registry
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function withManagedExcludes(tool, managed) {
|
|
49
|
+
if (!tool.excludeManaged) return tool.command;
|
|
50
|
+
const { flag, extensions } = tool.excludeManaged;
|
|
51
|
+
const files = managed.filter(f => extensions.some(ext => f.endsWith(ext)));
|
|
52
|
+
return files.length > 0 ? `${tool.command} ${flag} ${files.join(',')}` : tool.command;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reads the registry written by the last install/update (either format); []
|
|
57
|
+
* when absent or malformed.
|
|
16
58
|
* @param {string} targetDir - Target project root
|
|
17
|
-
* @
|
|
18
|
-
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
61
|
+
function readManagedRegistry(targetDir) {
|
|
62
|
+
return registeredPaths(targetDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The sorted paths of every file this run installs into tools/, the registry
|
|
67
|
+
* included: whole-project linters in the hook exclude them.
|
|
68
|
+
* @param {string[]} toolFiles - File names installed into tools/
|
|
69
|
+
* @returns {string[]}
|
|
70
|
+
*/
|
|
71
|
+
function managedToolPaths(toolFiles) {
|
|
72
|
+
return [...new Set([...toolFiles.map(f => `tools/${f}`), MANAGED_REGISTRY])].sort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Writes the registry (sdds/safe-update, decision 2): every kernel-owned file
|
|
77
|
+
* whose bytes are the kernel's after this run (tools/*, the hook, the skill),
|
|
78
|
+
* with its sha256. Protected files are left out: their content is the
|
|
79
|
+
* project's. The snapshot helpers (_changes.js/.py) drop these paths, and the
|
|
80
|
+
* registry itself, from the files a guard evaluates.
|
|
81
|
+
* @param {object} io - Active io session
|
|
82
|
+
* @param {string} targetDir - Target project root
|
|
83
|
+
*/
|
|
84
|
+
function writeManagedRegistry(io, targetDir) {
|
|
85
|
+
const { version } = require('../package.json');
|
|
86
|
+
const registry = { kernelVersion: version, files: io.managedEntries() };
|
|
87
|
+
io.writeFile(path.join(targetDir, MANAGED_REGISTRY), JSON.stringify(registry, null, 2) + '\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Builds the single tool manifest used both to copy guards into tools/ and to
|
|
92
|
+
* generate the pre-commit hook, so every installed guard is also executed.
|
|
93
|
+
* Technology tools (Celery, Redis, PostgreSQL, Docker) are inserted right
|
|
94
|
+
* before check_custom, keeping the user-defined rules last; when the adapter
|
|
95
|
+
* has no check_custom they are appended.
|
|
96
|
+
* The adapter receives the project root, so an adapter may add guards whose
|
|
97
|
+
* structure is present.
|
|
98
|
+
* @param {object} adapter - Adapter with getTools(targetDir?)
|
|
99
|
+
* @param {Array} [techTools] - Tools for detected technologies
|
|
100
|
+
* @param {string} [targetDir] - Project root
|
|
101
|
+
* @returns {Array} Merged tool manifest
|
|
102
|
+
*/
|
|
103
|
+
function buildToolManifest(adapter, techTools = [], targetDir) {
|
|
104
|
+
const frameworkTools = adapter.getTools(targetDir);
|
|
105
|
+
const checkCustomIdx = frameworkTools.findIndex(t => t.name === 'check_custom');
|
|
106
|
+
if (checkCustomIdx < 0) return [...frameworkTools, ...techTools];
|
|
107
|
+
return [
|
|
108
|
+
...frameworkTools.slice(0, checkCustomIdx),
|
|
109
|
+
...techTools,
|
|
110
|
+
...frameworkTools.slice(checkCustomIdx),
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A guard is a manifest entry the kernel copies into tools/ (not an external CLI). */
|
|
115
|
+
function isGuard(tool) {
|
|
116
|
+
return tool.language !== 'external' && !!tool.path;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The manifest an install or update works with (sdds/update-no-additions,
|
|
121
|
+
* decision 1). A fresh install keeps every guard. An update keeps only the
|
|
122
|
+
* guards the project has: those in the hashed registry plus those present in
|
|
123
|
+
* tools/ (a protected or project-owned file with a kernel guard's name is
|
|
124
|
+
* still installed); an old path-list registry, or none, means present files
|
|
125
|
+
* only. `addGuards` (names, or 'all') opts more in. Every other guard is
|
|
126
|
+
* recorded as available. Non-guard entries (external CLIs) are kept.
|
|
127
|
+
* @param {object} io - Active io session
|
|
128
|
+
* @param {Array} tools - Tool manifest
|
|
129
|
+
* @param {string[]|'all'} [addGuards] - Guards to add on update
|
|
130
|
+
* @returns {Array} The manifest to install and run in the hook
|
|
19
131
|
*/
|
|
20
|
-
function
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
132
|
+
function selectGuards(io, tools, addGuards) {
|
|
133
|
+
if (io.mode !== 'update') return tools;
|
|
134
|
+
const addAll = addGuards === 'all';
|
|
135
|
+
const add = new Set(Array.isArray(addGuards) ? addGuards : []);
|
|
136
|
+
const registered = new Set(io.registry.format === 'hashed' ? io.registry.paths : []);
|
|
137
|
+
return tools.filter(tool => {
|
|
138
|
+
if (!isGuard(tool)) return true;
|
|
139
|
+
const rel = `tools/${tool.path}`;
|
|
140
|
+
if (addAll || add.has(tool.name) || registered.has(rel) || io.exists(rel)) return true;
|
|
141
|
+
io.noteAvailable(tool.name);
|
|
25
142
|
return false;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Fallback files as guard descriptors (audit orchestrators excluded). */
|
|
147
|
+
function fallbackGuards(projectType) {
|
|
148
|
+
const files = projectType === 'unknown' ? GENERIC_FALLBACK_FILES : LEGACY_FALLBACK_FILES;
|
|
149
|
+
return files
|
|
150
|
+
.filter(file => !/^audit\./.test(file))
|
|
151
|
+
.map(file => ({ name: path.basename(file, path.extname(file)), language: path.extname(file).slice(1), path: file }));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Names of the guards the stack's manifest offers (what --add-guards
|
|
156
|
+
* accepts), in manifest order, once each.
|
|
157
|
+
* @param {string} targetDir - Project root
|
|
158
|
+
* @param {object} config - Detected config
|
|
159
|
+
* @returns {string[]}
|
|
160
|
+
*/
|
|
161
|
+
function manifestGuardNames(targetDir, config) {
|
|
162
|
+
const cfg = config || {};
|
|
163
|
+
let guards;
|
|
164
|
+
if (Array.isArray(cfg.parts) && cfg.parts.length > 0) {
|
|
165
|
+
guards = buildPartsManifest(cfg.parts, cfg.techTools || [], targetDir).guards.filter(isGuard);
|
|
166
|
+
} else if (cfg.adapter) {
|
|
167
|
+
guards = buildToolManifest(cfg.adapter, cfg.techTools || [], targetDir).filter(isGuard);
|
|
168
|
+
} else {
|
|
169
|
+
guards = fallbackGuards(cfg.projectType);
|
|
26
170
|
}
|
|
171
|
+
return [...new Set(guards.map(tool => tool.name))];
|
|
27
172
|
}
|
|
28
173
|
|
|
29
174
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
175
|
+
* Guard block every hook starts with (sdds/unified-discovery): the hook runs
|
|
176
|
+
* no guard itself. It calls the audit orchestrators with --hook, and they
|
|
177
|
+
* discover and run every guard once — the kernel guards installed for the
|
|
178
|
+
* project and the project guards (top-level tools/ guards the kernel does not
|
|
179
|
+
* own, and tools/guards/*) — exactly the set `clckernel audit` runs. Each
|
|
180
|
+
* orchestrator runs when it is installed (a Python stack needs no node); one
|
|
181
|
+
* that is missing hands its guards to the other, and with neither the commit
|
|
182
|
+
* fails. A failing guard or a crashing orchestrator aborts the commit under
|
|
183
|
+
* `set -e`. POSIX sh-safe.
|
|
184
|
+
*/
|
|
185
|
+
const ORCHESTRATOR_BLOCK = [
|
|
186
|
+
'# Guards: the audit orchestrators run every guard in tools/ once (kernel and project guards).',
|
|
187
|
+
'[ -f tools/audit.js ] || [ -f tools/audit.py ] || { echo "clckernel: no audit orchestrator in tools/" >&2; exit 1; }',
|
|
188
|
+
'if [ -f tools/audit.js ]; then node tools/audit.js --hook; fi',
|
|
189
|
+
'if [ -f tools/audit.py ]; then python3 tools/audit.py --hook; fi',
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
/** Value core.hooksPath takes when the harness owns the repo's hooks. */
|
|
193
|
+
const GITHOOKS_DIR = '.githooks';
|
|
194
|
+
|
|
195
|
+
function runGit(cwd, args) {
|
|
196
|
+
const res = spawnSync('git', args, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
197
|
+
return { ok: !res.error && res.status === 0, out: (res.stdout || '').trim() };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Points core.hooksPath at .githooks only when it is safe to do so:
|
|
202
|
+
* (a) targetDir is the root of its git repository, and (b) the current value
|
|
203
|
+
* is empty or already .githooks. Anything else (another hook manager such as
|
|
204
|
+
* Husky, a monorepo subdirectory, no repository at all) is left untouched and
|
|
205
|
+
* the manual command is printed instead.
|
|
39
206
|
* @param {string} targetDir - Target project root
|
|
207
|
+
* @returns {boolean} true when core.hooksPath is (now) .githooks
|
|
40
208
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
209
|
+
function configureHooksPath(targetDir) {
|
|
210
|
+
return configureHooksPathWithCommand(targetDir).active;
|
|
211
|
+
}
|
|
44
212
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
213
|
+
/**
|
|
214
|
+
* configureHooksPath, also returning the command that activates the hook
|
|
215
|
+
* when it was left inactive (sdds/onboarding: the Next steps repeat it).
|
|
216
|
+
* @param {string} targetDir - Target project root
|
|
217
|
+
* @returns {{ active: boolean, activate: string|null }}
|
|
218
|
+
*/
|
|
219
|
+
function configureHooksPathWithCommand(targetDir) {
|
|
220
|
+
const toplevel = runGit(targetDir, ['rev-parse', '--show-toplevel']);
|
|
221
|
+
if (!toplevel.ok || !toplevel.out) {
|
|
222
|
+
const activate = `git init && git config core.hooksPath ${GITHOOKS_DIR}`;
|
|
223
|
+
console.warn(
|
|
224
|
+
`Warning: ${targetDir} is not inside a git repository; core.hooksPath was not set.\n` +
|
|
225
|
+
` After \`git init\`, enable the hooks with: git config core.hooksPath ${GITHOOKS_DIR}`
|
|
226
|
+
);
|
|
227
|
+
return { active: false, activate };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const root = fs.realpathSync(toplevel.out);
|
|
231
|
+
if (root !== fs.realpathSync(targetDir)) {
|
|
232
|
+
const rel = path.join(path.relative(root, fs.realpathSync(targetDir)), GITHOOKS_DIR);
|
|
233
|
+
const activate = `git -C "${root}" config core.hooksPath ${rel}`;
|
|
234
|
+
console.warn(
|
|
235
|
+
`Warning: ${targetDir} is not the root of its git repository (${root}); core.hooksPath was not set.\n` +
|
|
236
|
+
` To use these hooks for the whole repository run: ${activate}`
|
|
237
|
+
);
|
|
238
|
+
return { active: false, activate };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Only the repository's own value means another hook manager owns it; a
|
|
242
|
+
// global/system value (a user-wide hooks dir) is overridden by a local one.
|
|
243
|
+
const current = localHooksPath(targetDir);
|
|
244
|
+
if (current === GITHOOKS_DIR) return { active: true, activate: null };
|
|
245
|
+
if (current) {
|
|
246
|
+
const activate = `git config core.hooksPath ${GITHOOKS_DIR}`;
|
|
247
|
+
console.warn(
|
|
248
|
+
`Warning: core.hooksPath is already set to "${current}" (another hook manager?); it was left unchanged.\n` +
|
|
249
|
+
` To run the CLC Kernel hooks instead run: ${activate}`
|
|
250
|
+
);
|
|
251
|
+
return { active: false, activate };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const inherited = runGit(targetDir, ['config', '--get', 'core.hooksPath']).out;
|
|
255
|
+
if (inherited && inherited !== GITHOOKS_DIR) {
|
|
256
|
+
console.warn(
|
|
257
|
+
`Note: a global core.hooksPath is configured ("${inherited}"). This repository gets its own local\n` +
|
|
258
|
+
` core.hooksPath=${GITHOOKS_DIR}, which overrides it here; hooks in "${inherited}" do not run for this repository.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const active = currentIO(targetDir).gitConfig(targetDir, 'core.hooksPath', GITHOOKS_DIR);
|
|
263
|
+
return { active, activate: active ? null : `git config core.hooksPath ${GITHOOKS_DIR}` };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The repository's own (local scope) core.hooksPath; '' when unset. */
|
|
267
|
+
function localHooksPath(targetDir) {
|
|
268
|
+
return runGit(targetDir, ['config', '--get', '--local', 'core.hooksPath']).out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** core.hooksPath values that mean Husky (v9: .husky/_, older: .husky) runs the hooks. */
|
|
272
|
+
const HUSKY_HOOKS_PATHS = new Set(['.husky', '.husky/_']);
|
|
49
273
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
274
|
+
/** True when Husky is the active hook manager of the repository at targetDir. */
|
|
275
|
+
function huskyActive(targetDir) {
|
|
276
|
+
const current = localHooksPath(targetDir).replace(/\/+$/, '');
|
|
277
|
+
return HUSKY_HOOKS_PATHS.has(current);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Writes the pre-commit hook where git will run it: .husky/pre-commit only when
|
|
282
|
+
* Husky is active for a frontend project; otherwise .githooks/pre-commit, with
|
|
283
|
+
* core.hooksPath set through configureHooksPath (and its safety checks).
|
|
284
|
+
* The orchestrator block (every guard) comes first, then the external tools.
|
|
285
|
+
* @param {string} targetDir - Target project root
|
|
286
|
+
* @param {boolean} isFront - Frontend project (may use Husky)
|
|
287
|
+
* @param {string[]} commands - External tool lines
|
|
288
|
+
* @param {string} banner - Text echoed when the hook starts
|
|
289
|
+
* @returns {{ file: string, active: boolean, activate: string|null }} Hook path
|
|
290
|
+
* relative to targetDir, whether git runs it and, when not, the command that
|
|
291
|
+
* activates it
|
|
292
|
+
*/
|
|
293
|
+
function writeHook(targetDir, isFront, commands, banner) {
|
|
294
|
+
const useHusky = isFront && huskyActive(targetDir);
|
|
295
|
+
const hookDir = path.join(targetDir, useHusky ? '.husky' : GITHOOKS_DIR);
|
|
296
|
+
const hookFile = path.join(hookDir, 'pre-commit');
|
|
54
297
|
|
|
55
298
|
const lines = [];
|
|
56
|
-
if (
|
|
299
|
+
if (useHusky) {
|
|
57
300
|
lines.push('#!/usr/bin/env sh');
|
|
58
|
-
|
|
301
|
+
// The husky bootstrap only exists with older Husky installs. Source it only
|
|
302
|
+
// then, so a generated hook never points at a missing file.
|
|
303
|
+
if (fs.existsSync(path.join(hookDir, '_', 'husky.sh'))) {
|
|
59
304
|
lines.push('. "$(dirname -- "$0")/_/husky.sh"');
|
|
60
305
|
}
|
|
61
|
-
lines.push('');
|
|
62
306
|
} else {
|
|
63
307
|
lines.push('#!/usr/bin/env bash');
|
|
64
|
-
lines.push(`echo "Running CLC Kernel ${adapter.name} Safeguards..."`);
|
|
65
|
-
lines.push('');
|
|
66
308
|
}
|
|
309
|
+
// Fail closed: any failing guard aborts the commit, not just the last one.
|
|
310
|
+
lines.push('set -e');
|
|
311
|
+
lines.push(`echo "${banner}"`);
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push(...ORCHESTRATOR_BLOCK);
|
|
314
|
+
lines.push(...commands);
|
|
315
|
+
|
|
316
|
+
// Generated per project: overwritten only when it still matches the
|
|
317
|
+
// registry's hash (sdds/safe-update, decision 3).
|
|
318
|
+
const file = path.posix.join(useHusky ? '.husky' : GITHOOKS_DIR, 'pre-commit');
|
|
319
|
+
const status = currentIO(targetDir).writeManaged(file, lines.join('\n') + '\n', { mode: '755' });
|
|
320
|
+
|
|
321
|
+
const { active, activate } = useHusky ? { active: true, activate: null } : configureHooksPathWithCommand(targetDir);
|
|
322
|
+
return { file, active, status, activate };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generates a pre-commit hook dynamically from the adapter's tool manifest.
|
|
327
|
+
* The hook goes to .husky/pre-commit only for a frontend project whose
|
|
328
|
+
* core.hooksPath already points at Husky; everything else uses .githooks.
|
|
329
|
+
*
|
|
330
|
+
* External CLIs (language === 'external', e.g. tsc, ruff, cargo) are wrapped in
|
|
331
|
+
* a runtime availability check (see guardedCommand): the hook runs them when
|
|
332
|
+
* the project has them at commit time and prints a "skipped" notice otherwise,
|
|
333
|
+
* so installing a tool later needs no `clckernel update`.
|
|
334
|
+
*
|
|
335
|
+
* @param {object} adapter - Adapter with getTools() and projectType
|
|
336
|
+
* @param {string} targetDir - Target project root
|
|
337
|
+
* @param {Array} [tools] - Resolved tool manifest (see buildToolManifest).
|
|
338
|
+
* Defaults to adapter.getTools() when omitted.
|
|
339
|
+
* @param {string[]} [managed] - Registered harness files (see writeManagedRegistry);
|
|
340
|
+
* defaults to the registry on disk
|
|
341
|
+
* @returns {{ file: string, active: boolean }} The hook written and whether git runs it
|
|
342
|
+
*/
|
|
343
|
+
function generateHookFromManifest(adapter, targetDir, tools = adapter.getTools(), managed = readManagedRegistry(targetDir)) {
|
|
344
|
+
const commands = tools
|
|
345
|
+
.filter(tool => tool.command && tool.language === 'external')
|
|
346
|
+
.map(tool => guardedCommand(tool, managed));
|
|
347
|
+
return writeHook(targetDir, adapter.projectType === 'frontend', commands, `Running CLC Kernel ${adapter.name} Safeguards...`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Single-quoted shell word (part dirs are already restricted to safe names). */
|
|
351
|
+
function shellQuote(value) {
|
|
352
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Hook line for an external tool of a part (sdds/fullstack-monorepo,
|
|
357
|
+
* decision 4): it runs inside the part's dir, in a subshell so the cd does
|
|
358
|
+
* not leak. A failing tool fails the subshell, and with it the hook under
|
|
359
|
+
* `set -e`; a missing one prints a skip notice naming the dir.
|
|
360
|
+
* @param {object} tool - External tool descriptor
|
|
361
|
+
* @param {string} dir - Part dir relative to the root ('.' for the root)
|
|
362
|
+
* @param {string[]} managed - Registered harness files (root-relative)
|
|
363
|
+
* @returns {string}
|
|
364
|
+
*/
|
|
365
|
+
function partGuardedCommand(tool, dir, managed = []) {
|
|
366
|
+
// Exclusions are relative to where the tool runs: only harness files inside the part apply.
|
|
367
|
+
const local = dir === '.' ? managed : managed
|
|
368
|
+
.filter(f => f.startsWith(`${dir}/`))
|
|
369
|
+
.map(f => f.slice(dir.length + 1));
|
|
370
|
+
const skip = `else echo "clckernel: ${tool.name} (${dir}) not available, skipped"; fi`;
|
|
371
|
+
const bin = dir === '.' ? null : nodeBinOf(tool);
|
|
372
|
+
if (bin) return `( cd ${shellQuote(dir)} && ${hoistedNodeBody(tool, bin, dir, local)} ${skip} )`;
|
|
373
|
+
const body = `if ${availabilityCondition(tool)} >/dev/null 2>&1; then ${withManagedExcludes(tool, local)}; ${skip}`;
|
|
374
|
+
return dir === '.' ? body : `( cd ${shellQuote(dir)} && ${body} )`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Name of the node_modules binary a Node-based external tool runs: its
|
|
379
|
+
* availableIf is `[ -x node_modules/.bin/<bin> ]` and its command `npx <bin> ...`.
|
|
380
|
+
* @param {object} tool - External tool descriptor
|
|
381
|
+
* @returns {string|null}
|
|
382
|
+
*/
|
|
383
|
+
function nodeBinOf(tool) {
|
|
384
|
+
const match = /^\[ -x node_modules\/\.bin\/([\w.@-]+) \]$/.exec((tool.availableIf || '').trim());
|
|
385
|
+
if (!match) return null;
|
|
386
|
+
return new RegExp(`^npx ${match[1].replace(/[.]/g, '\\.')}(\\s|$)`).test(tool.command) ? match[1] : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* `if/elif` branches of a part's Node tool (sdds/monorepo-gaps, gap 1): the
|
|
391
|
+
* part's own node_modules/.bin/<bin> first, then the one hoisted to the
|
|
392
|
+
* repository root by npm/yarn/pnpm workspaces. The branch runs the binary it
|
|
393
|
+
* found (not npx), with cwd = the part dir. The caller appends the else/fi.
|
|
394
|
+
* @param {object} tool - External tool descriptor (see nodeBinOf)
|
|
395
|
+
* @param {string} bin - Binary name
|
|
396
|
+
* @param {string} dir - Part dir relative to the root (not '.')
|
|
397
|
+
* @param {string[]} local - Managed files relative to the part dir
|
|
398
|
+
* @returns {string}
|
|
399
|
+
*/
|
|
400
|
+
function hoistedNodeBody(tool, bin, dir, local) {
|
|
401
|
+
const args = withManagedExcludes(tool, local).slice(`npx ${bin}`.length);
|
|
402
|
+
const up = dir.split('/').filter(Boolean).map(() => '..').join('/');
|
|
403
|
+
return [`node_modules/.bin/${bin}`, `${up}/node_modules/.bin/${bin}`]
|
|
404
|
+
.map((exe, i) => `${i === 0 ? 'if' : 'elif'} [ -x ${exe} ]; then ${exe}${args};`)
|
|
405
|
+
.join(' ');
|
|
406
|
+
}
|
|
67
407
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Tool manifest of a multi-part repo (sdds/fullstack-monorepo, decision 3):
|
|
410
|
+
* the guards of every part, once each (by file), with the technology tools
|
|
411
|
+
* before the user-defined rules (check_custom), then each part's external
|
|
412
|
+
* tools tagged with the part dir.
|
|
413
|
+
* @param {Array<{dir: string, adapter: object}>} parts - config.parts
|
|
414
|
+
* @param {Array} techTools - Tools for detected technologies
|
|
415
|
+
* @param {string} targetDir - Project root
|
|
416
|
+
* @returns {{ guards: Array, externals: Array<{tool: object, dir: string}> }}
|
|
417
|
+
*/
|
|
418
|
+
function buildPartsManifest(parts, techTools, targetDir) {
|
|
419
|
+
const guards = [];
|
|
420
|
+
const custom = [];
|
|
421
|
+
const externals = [];
|
|
422
|
+
const seen = new Set();
|
|
423
|
+
const add = (list, tool) => {
|
|
424
|
+
const key = tool.path || `${tool.name}:${tool.command}`;
|
|
425
|
+
if (seen.has(key)) return;
|
|
426
|
+
seen.add(key);
|
|
427
|
+
list.push(tool);
|
|
428
|
+
};
|
|
429
|
+
for (const part of parts) {
|
|
430
|
+
for (const tool of part.adapter.getTools(path.join(targetDir, part.dir))) {
|
|
431
|
+
if (tool.language === 'external') {
|
|
432
|
+
const key = `${part.dir}\0${tool.name}`;
|
|
433
|
+
if (!seen.has(key)) {
|
|
434
|
+
seen.add(key);
|
|
435
|
+
externals.push({ tool, dir: part.dir });
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
add(tool.name === 'check_custom' ? custom : guards, tool);
|
|
75
439
|
}
|
|
76
440
|
}
|
|
77
|
-
lines.push(tool.command);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (skipped.length > 0) {
|
|
81
|
-
lines.push('');
|
|
82
|
-
lines.push(`# Not run here (not a dependency of this project): ${skipped.join(', ')}`);
|
|
83
|
-
lines.push('# Install it and re-run `clckernel update` to enable it.');
|
|
84
441
|
}
|
|
442
|
+
for (const tool of techTools) add(guards, tool);
|
|
443
|
+
return { guards: [...guards, ...custom], externals };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Writes the hook of a multi-part repo: every guard once from the root, then
|
|
448
|
+
* each part's external tools in its dir. The hook goes to .husky only when a
|
|
449
|
+
* part is frontend and Husky is active (same rule as a single stack).
|
|
450
|
+
* @param {object} config - Multi-part config
|
|
451
|
+
* @param {string} targetDir - Project root
|
|
452
|
+
* @param {{ guards: Array, externals: Array }} manifest - From buildPartsManifest
|
|
453
|
+
* @param {string[]} managed - Registered harness files
|
|
454
|
+
* @returns {{ file: string, active: boolean }}
|
|
455
|
+
*/
|
|
456
|
+
function generatePartsHook(config, targetDir, manifest, managed) {
|
|
457
|
+
const commands = manifest.externals.map(({ tool, dir }) => partGuardedCommand(tool, dir, managed));
|
|
458
|
+
const isFront = config.parts.some(p => p.adapter.projectType === 'frontend');
|
|
459
|
+
const names = [...new Set(config.parts.map(p => p.adapter.name))].join(' + ');
|
|
460
|
+
return writeHook(targetDir, isFront, commands, `Running CLC Kernel ${names} Safeguards...`);
|
|
461
|
+
}
|
|
85
462
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
try { fs.chmodSync(hookFile, '755'); } catch (e) {}
|
|
463
|
+
/** Audit orchestrator that discovers guards of each language. */
|
|
464
|
+
const ORCHESTRATORS = { '.js': 'audit.js', '.py': 'audit.py' };
|
|
89
465
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Copies one audit orchestrator per guard language installed in tools/:
|
|
468
|
+
* audit.js discovers only .js guards and audit.py only .py ones, so a mixed
|
|
469
|
+
* set (e.g. Next.js + docker_guard.py) gets both. With no guards at all,
|
|
470
|
+
* audit.js is installed: Node is the one runtime known to exist.
|
|
471
|
+
* @param {string} targetDir - Target project root
|
|
472
|
+
* @param {string[]} copiedFiles - tools/ file names copied for this project
|
|
473
|
+
* @returns {string[]} File names written into tools/
|
|
474
|
+
*/
|
|
475
|
+
function installOrchestrators(targetDir, copiedFiles) {
|
|
476
|
+
const wanted = new Set(copiedFiles.map(f => ORCHESTRATORS[path.extname(f)]).filter(Boolean));
|
|
477
|
+
if (wanted.size === 0) wanted.add('audit.js');
|
|
478
|
+
const written = [];
|
|
479
|
+
for (const file of wanted) {
|
|
480
|
+
const src = path.join(__dirname, '..', 'tools', file);
|
|
481
|
+
if (!fs.existsSync(src)) continue;
|
|
482
|
+
currentIO(targetDir).copyManaged(src, `tools/${file}`, { mode: '755' });
|
|
483
|
+
written.push(file);
|
|
94
484
|
}
|
|
485
|
+
return written;
|
|
95
486
|
}
|
|
96
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Shared modules the diff-based guards import from their own directory, by
|
|
490
|
+
* guard language: the snapshot helper and the config reader it uses
|
|
491
|
+
* (sdds/config-foundation), so a config added after install works without
|
|
492
|
+
* `clckernel update`.
|
|
493
|
+
*/
|
|
494
|
+
const SNAPSHOT_HELPERS = { '.js': ['_changes.js', '_config.js', 'yaml.js'], '.py': ['_changes.py', '_config.py', '_yaml.py'] };
|
|
495
|
+
|
|
97
496
|
/**
|
|
98
497
|
* Installs tools from an adapter's manifest into the target tools/ directory.
|
|
99
498
|
* Only copies tools with valid descriptors. External CLIs are skipped (no file to copy).
|
|
100
|
-
*
|
|
499
|
+
* Copies the audit orchestrator(s) matching the installed guards' languages.
|
|
101
500
|
* @param {string} targetDir - Target project root
|
|
102
501
|
* @param {Array} manifest - Tool manifest from adapter.getTools()
|
|
103
502
|
* @param {object} config - Project config (projectType, framework, testRunner)
|
|
503
|
+
* @returns {{ installed: string[], written: string[] }} Names of the guards copied,
|
|
504
|
+
* and every file name written into tools/
|
|
104
505
|
*/
|
|
105
506
|
function installToolsFromManifest(targetDir, manifest, config) {
|
|
106
507
|
const sourceToolsDir = path.join(__dirname, '..', 'tools');
|
|
@@ -108,7 +509,7 @@ function installToolsFromManifest(targetDir, manifest, config) {
|
|
|
108
509
|
|
|
109
510
|
// tools/ must exist before anything is copied into it. Provisioning a project
|
|
110
511
|
// that has no tools/ yet used to throw ENOENT on the first copyFileSync.
|
|
111
|
-
|
|
512
|
+
currentIO(targetDir).mkdir(path.join(targetDir, 'tools'));
|
|
112
513
|
|
|
113
514
|
// Validate descriptors: skip + warn tools missing required fields
|
|
114
515
|
const validTools = manifest.filter(tool => {
|
|
@@ -119,34 +520,30 @@ function installToolsFromManifest(targetDir, manifest, config) {
|
|
|
119
520
|
return true;
|
|
120
521
|
});
|
|
121
522
|
|
|
122
|
-
// Always copy the audit orchestrator for the right language
|
|
123
|
-
const isNodeStack = config.projectType === 'frontend' || config.projectType === 'agent-os';
|
|
124
|
-
const orchestrator = isNodeStack ? 'audit.js' : 'audit.py';
|
|
125
|
-
const orchestratorSrc = path.join(sourceToolsDir, orchestrator);
|
|
126
|
-
const orchestratorDest = path.join(targetDir, 'tools', orchestrator);
|
|
127
|
-
if (fs.existsSync(orchestratorSrc)) {
|
|
128
|
-
fs.copyFileSync(orchestratorSrc, orchestratorDest);
|
|
129
|
-
try { fs.chmodSync(orchestratorDest, '755'); } catch (e) {}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
523
|
// Copy only custom tools with valid paths (skip external CLIs and empty paths)
|
|
524
|
+
const installed = [];
|
|
525
|
+
const copiedPaths = [];
|
|
133
526
|
validTools.forEach(tool => {
|
|
134
527
|
if (tool.language === 'external' || !tool.path) return;
|
|
135
528
|
const srcFile = path.join(sourceToolsDir, tool.path);
|
|
136
|
-
const destFile = path.join(targetDir, 'tools', tool.path);
|
|
137
529
|
if (fs.existsSync(srcFile)) {
|
|
138
|
-
|
|
139
|
-
|
|
530
|
+
currentIO(targetDir).copyManaged(srcFile, `tools/${tool.path}`, { mode: '755' });
|
|
531
|
+
installed.push(tool.name);
|
|
532
|
+
copiedPaths.push(tool.path);
|
|
140
533
|
} else {
|
|
141
534
|
console.warn(`Warning: tool "${tool.path}" declared by adapter but not found in bundled tools/. Skipping.`);
|
|
142
535
|
}
|
|
143
536
|
});
|
|
144
537
|
|
|
538
|
+
const written = [...copiedPaths, ...installOrchestrators(targetDir, copiedPaths)];
|
|
539
|
+
|
|
145
540
|
// check_custom.js parses the project YAML config, so the generated project
|
|
146
541
|
// must ship the parser alongside it.
|
|
147
542
|
if (validTools.some(tool => tool.path === 'check_custom.js')) {
|
|
148
|
-
installYamlModule(targetDir);
|
|
543
|
+
written.push(...installYamlModule(targetDir));
|
|
149
544
|
}
|
|
545
|
+
written.push(...installSnapshotHelpers(targetDir, copiedPaths));
|
|
546
|
+
return { installed, written };
|
|
150
547
|
}
|
|
151
548
|
|
|
152
549
|
/**
|
|
@@ -158,192 +555,250 @@ function installToolsFromManifest(targetDir, manifest, config) {
|
|
|
158
555
|
* harness provisions and owns.
|
|
159
556
|
*
|
|
160
557
|
* @param {string} targetDir - Target project root
|
|
558
|
+
* @returns {string[]} File names written into tools/
|
|
161
559
|
*/
|
|
162
560
|
function installYamlModule(targetDir) {
|
|
163
561
|
const yamlSrc = path.join(__dirname, '..', 'tools', 'yaml.js');
|
|
164
|
-
if (!fs.existsSync(yamlSrc)) return;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
562
|
+
if (!fs.existsSync(yamlSrc)) return [];
|
|
563
|
+
currentIO(targetDir).copyManaged(yamlSrc, 'tools/yaml.js');
|
|
564
|
+
return ['yaml.js'];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Copies the snapshot helper each installed guard imports as a sibling:
|
|
569
|
+
* _changes.js (with its config reader _config.js and yaml.js) when any JS file
|
|
570
|
+
* was copied into tools/, _changes.py (with _config.py and _yaml.py) when any
|
|
571
|
+
* Python file was (technology guards can bring Python guards to a Node
|
|
572
|
+
* stack). A stack without Python tools stays free of Python files. The `_`
|
|
573
|
+
* prefix keeps the helpers out of the audit orchestrators' guard discovery.
|
|
574
|
+
*
|
|
575
|
+
* @param {string} targetDir - Target project root
|
|
576
|
+
* @param {string[]} copiedFiles - tools/ file names copied for this project
|
|
577
|
+
* @returns {string[]} File names written into tools/
|
|
578
|
+
*/
|
|
579
|
+
function installSnapshotHelpers(targetDir, copiedFiles) {
|
|
580
|
+
const helpers = new Set(copiedFiles.flatMap(f => SNAPSHOT_HELPERS[path.extname(f)] || []));
|
|
581
|
+
const written = [];
|
|
582
|
+
for (const file of helpers) {
|
|
583
|
+
const src = path.join(__dirname, '..', 'tools', file);
|
|
584
|
+
if (!fs.existsSync(src)) continue;
|
|
585
|
+
currentIO(targetDir).copyManaged(src, `tools/${file}`);
|
|
586
|
+
written.push(file);
|
|
587
|
+
}
|
|
588
|
+
return written;
|
|
168
589
|
}
|
|
169
590
|
|
|
591
|
+
/** Generic harness for an unknown stack: Node is guaranteed (the user ran npx). */
|
|
592
|
+
const GENERIC_FALLBACK_FILES = ['audit.js', 'scan_secrets.js'];
|
|
593
|
+
|
|
594
|
+
/** Legacy fallback set, used when a stack was forced (frontend/backend) without an adapter. */
|
|
595
|
+
const LEGACY_FALLBACK_FILES = [
|
|
596
|
+
'audit.js', 'audit.py',
|
|
597
|
+
'scan_secrets.js', 'scan_secrets.py',
|
|
598
|
+
'check_custom.js', 'check_custom.py',
|
|
599
|
+
'check_responsive.js', 'check_seo.js',
|
|
600
|
+
'check_db_efficiency.py'
|
|
601
|
+
];
|
|
602
|
+
|
|
170
603
|
/**
|
|
171
604
|
* Installs a minimal fallback tool set when no adapter is provided.
|
|
172
|
-
*
|
|
605
|
+
* An unknown stack gets only the JS audit orchestrator and secret scan; a
|
|
606
|
+
* forced frontend/backend type keeps the legacy set.
|
|
173
607
|
* @param {string} targetDir - Target project root
|
|
608
|
+
* @param {string} [projectType] - Detected or forced project type
|
|
609
|
+
* @param {(file: string) => boolean} [keep] - Files to install (update: the selected guards)
|
|
610
|
+
* @returns {{ installed: string[], written: string[] }} Names of the guards copied
|
|
611
|
+
* (orchestrators excluded), and every file name written into tools/
|
|
174
612
|
*/
|
|
175
|
-
function installFallbackTools(targetDir) {
|
|
613
|
+
function installFallbackTools(targetDir, projectType, keep = () => true) {
|
|
176
614
|
const sourceToolsDir = path.join(__dirname, '..', 'tools');
|
|
177
|
-
const fallbackFiles =
|
|
178
|
-
'audit.js', 'audit.py',
|
|
179
|
-
'scan_secrets.js', 'scan_secrets.py',
|
|
180
|
-
'check_custom.js', 'check_custom.py',
|
|
181
|
-
'check_responsive.js', 'check_seo.js',
|
|
182
|
-
'check_db_efficiency.py'
|
|
183
|
-
];
|
|
615
|
+
const fallbackFiles = (projectType === 'unknown' ? GENERIC_FALLBACK_FILES : LEGACY_FALLBACK_FILES).filter(keep);
|
|
184
616
|
|
|
617
|
+
const installed = new Set();
|
|
618
|
+
const written = [];
|
|
185
619
|
fallbackFiles.forEach(file => {
|
|
186
620
|
const srcFile = path.join(sourceToolsDir, file);
|
|
187
|
-
const destFile = path.join(targetDir, 'tools', file);
|
|
188
621
|
if (fs.existsSync(srcFile)) {
|
|
189
|
-
|
|
190
|
-
|
|
622
|
+
currentIO(targetDir).copyManaged(srcFile, `tools/${file}`, { mode: '755' });
|
|
623
|
+
written.push(file);
|
|
624
|
+
const name = path.basename(file, path.extname(file));
|
|
625
|
+
if (name !== 'audit') installed.add(name);
|
|
191
626
|
}
|
|
192
627
|
});
|
|
193
628
|
|
|
194
629
|
if (fallbackFiles.includes('check_custom.js')) {
|
|
195
|
-
installYamlModule(targetDir);
|
|
630
|
+
written.push(...installYamlModule(targetDir));
|
|
196
631
|
}
|
|
632
|
+
written.push(...installSnapshotHelpers(targetDir, fallbackFiles));
|
|
633
|
+
return { installed: [...installed], written };
|
|
197
634
|
}
|
|
198
635
|
|
|
199
636
|
/**
|
|
200
|
-
* Generates a minimal generic pre-commit hook when no adapter is available
|
|
637
|
+
* Generates a minimal generic pre-commit hook when no adapter is available:
|
|
638
|
+
* the orchestrators run the installed fallback guards.
|
|
201
639
|
* @param {string} targetDir - Target project root
|
|
202
|
-
* @param {string} projectType - 'frontend' or '
|
|
640
|
+
* @param {string} projectType - 'frontend', 'backend' or 'unknown'
|
|
641
|
+
* @returns {{ file: string, active: boolean }} The hook written and whether git runs it
|
|
203
642
|
*/
|
|
204
643
|
function generateFallbackHook(targetDir, projectType) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (isFront) {
|
|
208
|
-
const hookDir = path.join(targetDir, '.husky');
|
|
209
|
-
fs.mkdirSync(hookDir, { recursive: true });
|
|
210
|
-
const hookFile = path.join(hookDir, 'pre-commit');
|
|
211
|
-
// Only source the husky bootstrap when the project actually has it.
|
|
212
|
-
const bootstrap = fs.existsSync(path.join(hookDir, '_', 'husky.sh'))
|
|
213
|
-
? '. "$(dirname -- "$0")/_/husky.sh"\n'
|
|
214
|
-
: '';
|
|
215
|
-
fs.writeFileSync(hookFile, `#!/usr/bin/env sh
|
|
216
|
-
${bootstrap}
|
|
217
|
-
node tools/scan_secrets.js || true
|
|
218
|
-
`, 'utf-8');
|
|
219
|
-
try { fs.chmodSync(hookFile, '755'); } catch (e) {}
|
|
220
|
-
} else {
|
|
221
|
-
const hookDir = path.join(targetDir, '.githooks');
|
|
222
|
-
fs.mkdirSync(hookDir, { recursive: true });
|
|
223
|
-
const hookFile = path.join(hookDir, 'pre-commit');
|
|
224
|
-
fs.writeFileSync(hookFile, `#!/usr/bin/env bash
|
|
225
|
-
echo "Running CLC Kernel Safeguards..."
|
|
226
|
-
python3 tools/scan_secrets.py || true
|
|
227
|
-
`, 'utf-8');
|
|
228
|
-
try { fs.chmodSync(hookFile, '755'); } catch (e) {}
|
|
229
|
-
try {
|
|
230
|
-
execSync('git config core.hooksPath .githooks', { cwd: targetDir, stdio: 'ignore' });
|
|
231
|
-
} catch (e) {}
|
|
232
|
-
}
|
|
644
|
+
return writeHook(targetDir, projectType === 'frontend', [], 'Running CLC Kernel Safeguards...');
|
|
233
645
|
}
|
|
234
646
|
|
|
235
647
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
648
|
+
* AGENTS.md of a multi-part repo (sdds/fullstack-monorepo, decision 6): one
|
|
649
|
+
* law with a table of the parts and each part's stack rules grouped.
|
|
650
|
+
* @param {object} config - Multi-part config
|
|
651
|
+
* @returns {string}
|
|
239
652
|
*/
|
|
240
|
-
|
|
241
|
-
'
|
|
242
|
-
'
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
653
|
+
function partsAgentsMd(config) {
|
|
654
|
+
const testRunner = "the part's test runner (see Parts)";
|
|
655
|
+
const rows = config.parts.map(p => `| \`${p.dir}\` | ${p.adapter.name} | ${p.adapter.testRunner} |`).join('\n');
|
|
656
|
+
const sections = config.parts.map((p, i) => `### 2.${i + 1} Core AI Safeguards — \`${p.dir}\` (${p.adapter.name})
|
|
657
|
+
${p.adapter.getSafeguards().map(s => `- ${s}`).join('\n')}`).join('\n\n');
|
|
658
|
+
return `# CLC Kernel ${config.projectType.toUpperCase()} (${config.parts.length} parts) — AGENTS
|
|
659
|
+
|
|
660
|
+
This document is the **authoritative law** for AI agents working in this repository.
|
|
661
|
+
Forged by **CLC Kernel: The AI Agent Governance Engine**.
|
|
662
|
+
|
|
663
|
+
> **RULE #0: MANDATORY EXECUTION OVERRIDE RULE (NO EXCEPTIONS)**
|
|
664
|
+
> Even when the user issues a direct or urgent fix request ("fix this bug", "fix this error", "quick fix"):
|
|
665
|
+
> YOU ARE STRICTLY FORBIDDEN from modifying source code directly without completing the full quality harness:
|
|
666
|
+
> 1. **Research & Root Cause Analysis:** Investigate tracebacks and inspect affected files before editing.
|
|
667
|
+
> 2. **TDD Verification (Red Phase):** Write a failing regression test first (${testRunner}).
|
|
668
|
+
> 3. **Clean Architecture Implementation (Green Phase):** Make the test pass maintaining layer isolation.
|
|
669
|
+
> 4. **Mandatory Educational Audit Gate:** Execute \`node tools/audit.js\` or \`python tools/audit.py\` and output the Educational Code Summary to stdout.
|
|
670
|
+
> NEVER declare success or skip verification commands for quick fixes.
|
|
249
671
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
672
|
+
## Parts
|
|
673
|
+
This repository has ${config.parts.length} code parts. The guards in \`tools/\` run once from the repository root; each part's external tools (linters, test runners) run inside the part's directory. A changed file's tests belong to the same part as the file.
|
|
674
|
+
|
|
675
|
+
| Directory | Stack | Test runner |
|
|
676
|
+
|---|---|---|
|
|
677
|
+
${rows}
|
|
678
|
+
|
|
679
|
+
## 1. The Loop (Every Task)
|
|
680
|
+
1. **Research** — Inspect codebase / docs before writing code.
|
|
681
|
+
2. **Plan** — Write an SDD under \`sdds/{change-name}/\`. SDDs live 100% locally and are gitignored.
|
|
682
|
+
3. **Test** (TDD) — Write failing test first (${testRunner}).
|
|
683
|
+
4. **Implement** — Make test pass.
|
|
684
|
+
5. **Verify & Audit** — Run \`node tools/audit.js --change {change-name}\` or \`python tools/audit.py --change {change-name}\` (report + receipt in \`sdds/{change-name}/\`; without \`--change\` nothing is written).
|
|
685
|
+
6. **DoD** — Lint, typecheck, tests, coverage, docs, memory.
|
|
686
|
+
7. **Commit** — Pre-commit hook runs automated guards.
|
|
687
|
+
8. **PR** — Generate PR body.
|
|
688
|
+
|
|
689
|
+
## 2. Core AI Safeguards (per part)
|
|
690
|
+
|
|
691
|
+
${sections}
|
|
692
|
+
|
|
693
|
+
${commandTable(testRunner)}
|
|
694
|
+
${FIRST_SESSION_SECTION}`;
|
|
254
695
|
}
|
|
255
696
|
|
|
256
697
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* Full layout/tools/hook materialization is the A-04 work unit; here only
|
|
262
|
-
* the metadata flows out.
|
|
263
|
-
* @param {string} targetDir - Target project root
|
|
264
|
-
* @param {object} configOrNull - Raw or resolved extended config
|
|
265
|
-
* @returns {object} The resolved config (metadata written to disk)
|
|
698
|
+
* The "Universal Command Interface" section shared by the generated AGENTS.md
|
|
699
|
+
* files (fallback and multi-part). `clckernel update` looks for its rows.
|
|
700
|
+
* @param {string} testRunner - Shown in the `clckernel test` row
|
|
701
|
+
* @returns {string}
|
|
266
702
|
*/
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
configOrNull.__clcForgeConfigPath === true ||
|
|
271
|
-
Array.isArray(configOrNull.__catalog);
|
|
272
|
-
const resolved = alreadyResolved ? configOrNull : resolveConfig(configOrNull, {});
|
|
273
|
-
|
|
274
|
-
const metadata = {
|
|
275
|
-
activeGuards: resolved.activeGuards,
|
|
276
|
-
phases: resolved.phases,
|
|
277
|
-
gateMode: resolved.gateMode,
|
|
278
|
-
effectiveSeverities: resolved.effectiveSeverities,
|
|
279
|
-
layers: resolved.layers,
|
|
280
|
-
scope: resolved.scope,
|
|
281
|
-
excludePaths: resolved.excludePaths,
|
|
282
|
-
rules: resolved.rules || [],
|
|
283
|
-
framework: resolved.framework,
|
|
284
|
-
projectType: resolved.projectType,
|
|
285
|
-
testRunner: resolved.testRunner,
|
|
286
|
-
__catalog: resolved.__catalog,
|
|
287
|
-
};
|
|
703
|
+
function commandTable(testRunner) {
|
|
704
|
+
return `## 3. Universal Command Interface (All LLMs & Agents)
|
|
705
|
+
When given a command matching \`clckernel <action>\` or \`/clckernel <action>\`, execute the deterministic state machine below:
|
|
288
706
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
707
|
+
| Command | Lifecycle Phase | Mandatory Execution Protocol |
|
|
708
|
+
|---|---|---|
|
|
709
|
+
| \`clckernel feature <name>\` | New Feature | 1. Research affected layers.<br>2. Author local spec in \`sdds/{name}/spec.md\`.<br>3. Present HIT review to user and wait for approval.<br>4. Write failing test (Red Phase).<br>5. Implement clean code (Green Phase).<br>6. Run \`node tools/audit.js --change {name}\` (or \`audit.py --change {name}\`). |
|
|
710
|
+
| \`clckernel fix <issue>\` | Bug Fix | 1. Investigate root cause (no code edits).<br>2. Reproduce with failing test (Red Phase).<br>3. Minimal surgical fix respecting Clean Arch (Green Phase).<br>4. Run audit gate without skipping. |
|
|
711
|
+
| \`clckernel test\` | Verification | Run test suite (\`${testRunner}\`) and verify clean pass (exit code 0). |
|
|
712
|
+
| \`clckernel audit\` | Verification | Run \`npx clckernel audit [dir] [--change <name>]\` (every installed orchestrator: \`tools/audit.js\` and/or \`tools/audit.py\`) and output the Educational Summary; only \`--change\` writes the report to \`sdds/<name>/\`. |
|
|
713
|
+
| \`clckernel doctor\` | Diagnostics | Run \`npx clckernel doctor [dir]\`: checks AGENTS.md, sdds/, tools/, the pre-commit hook and that the guard scripts load. |
|
|
714
|
+
| \`clckernel commit <message>\` | Version Control | Run full audit, verify tests pass, ensure no changes outside SDD scope, and commit with conventional commits (never add AI attribution). |
|
|
715
|
+
| \`clckernel guard create <name>\` | Safeguards | Scaffold a new guard script in the stack's native language in \`tools/guards/\`. |
|
|
716
|
+
| \`clckernel update\` | Maintenance | Non-destructive update: run \`npx clckernel update [dir]\` (\`--dry-run\` previews, \`--add-guards <a,b\\|all>\` adds guards); refreshes kernel tools, hook, playbooks & symlinks (preserves SDDs, project guards, edited files & \`.clckernel.yaml\`). |
|
|
717
|
+
| \`clckernel start\` | Setup | Scan codebase and propose a governance plan; if the harness is missing, install it with \`npx clckernel . --yes\`. \`.clckernel.yaml\` is optional: write it only for settings that differ from the defaults. |
|
|
718
|
+
`;
|
|
301
719
|
}
|
|
302
720
|
|
|
303
721
|
/**
|
|
304
|
-
* Generate a harness for a target project
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
722
|
+
* Generate a harness for a target project from its detected stack
|
|
723
|
+
* (`configOrNull`; null falls back to generic provisioning).
|
|
724
|
+
* The project's .clckernel.yaml is read first (sdds/config-foundation): an
|
|
725
|
+
* invalid one throws its ConfigError before anything is written. A caller
|
|
726
|
+
* that already read it (update, the CLI) passes `options.projectConfig`.
|
|
727
|
+
* @param {string} targetDir - Project root
|
|
728
|
+
* @param {object|null} configOrNull - Detected stack config
|
|
729
|
+
* @param {{ dryRun?: boolean, knownHashes?: object, addGuards?: string[]|'all',
|
|
730
|
+
* projectConfig?: { file: string|null, config: object, warnings: string[] } }} [options]
|
|
310
731
|
*/
|
|
311
|
-
function generateHarness(targetDir, configOrNull) {
|
|
312
|
-
|
|
313
|
-
|
|
732
|
+
function generateHarness(targetDir, configOrNull, options = {}) {
|
|
733
|
+
let projectConfig = options.projectConfig;
|
|
734
|
+
if (!projectConfig) {
|
|
735
|
+
projectConfig = readProjectConfig(targetDir);
|
|
736
|
+
for (const warning of projectConfig.warnings) console.warn(`Warning: ${warning}`);
|
|
314
737
|
}
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
//
|
|
738
|
+
const withConfig = { ...options, projectConfig };
|
|
739
|
+
// One io session per install (sdds/safe-update): reuse the caller's (update)
|
|
740
|
+
// or open one, real or dry-run.
|
|
741
|
+
const active = activeIO();
|
|
742
|
+
if (active) return generateHarnessWith(active, targetDir, configOrNull, withConfig);
|
|
743
|
+
const io = createIO({
|
|
744
|
+
root: targetDir,
|
|
745
|
+
dryRun: !!options.dryRun,
|
|
746
|
+
knownHashes: options.knownHashes,
|
|
747
|
+
exclude: globMatcher(projectConfig.config.update.exclude),
|
|
748
|
+
});
|
|
749
|
+
const result = runWithIO(io, () => generateHarnessWith(io, targetDir, configOrNull, withConfig));
|
|
750
|
+
if (result && typeof result === 'object' && !Array.isArray(result) && 'installedGuards' in result) {
|
|
751
|
+
return { ...result, plan: io.plan() };
|
|
752
|
+
}
|
|
753
|
+
return result;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function generateHarnessWith(io, targetDir, configOrNull, options = {}) {
|
|
757
|
+
// A null/absent config falls back to generic provisioning.
|
|
318
758
|
const config = configOrNull || {};
|
|
759
|
+
const { projectConfig } = options;
|
|
760
|
+
|
|
761
|
+
const multiPart = Array.isArray(config.parts) && config.parts.length > 0;
|
|
319
762
|
|
|
320
763
|
// Step 1: Provision directories + AGENTS.md
|
|
321
|
-
if (
|
|
764
|
+
if (multiPart) {
|
|
765
|
+
// Directories and .gitignore as any adapter provisions them; the law file
|
|
766
|
+
// is the multi-part one, never written over a project's own AGENTS.md.
|
|
767
|
+
config.parts[0].adapter.provision(targetDir, { ...config, skipAgentsMd: true });
|
|
768
|
+
if (!io.exists(path.join(targetDir, 'AGENTS.md'))) {
|
|
769
|
+
io.writeFile(path.join(targetDir, 'AGENTS.md'), partsAgentsMd(config));
|
|
770
|
+
}
|
|
771
|
+
} else if (config.adapter) {
|
|
322
772
|
config.adapter.provision(targetDir, config);
|
|
323
773
|
} else {
|
|
324
774
|
// Fallback: create directory structure + AGENTS.md
|
|
325
775
|
const isFront = config.projectType === 'frontend';
|
|
776
|
+
// Unknown stack: a Generic AGENTS.md with no stack-specific rules.
|
|
777
|
+
const isGeneric = config.projectType === 'unknown';
|
|
778
|
+
const frameworkLabel = isGeneric ? 'Generic' : (config.framework || 'Generic');
|
|
779
|
+
const testRunner = isGeneric ? "the project's test runner" : (config.testRunner || 'npm test');
|
|
326
780
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
781
|
+
io.mkdir(path.join(targetDir, 'docs', 'Journal'));
|
|
782
|
+
io.mkdir(path.join(targetDir, 'docs', 'Architecture'));
|
|
783
|
+
io.mkdir(path.join(targetDir, 'docs', 'Development'));
|
|
784
|
+
io.mkdir(path.join(targetDir, 'sdds'));
|
|
785
|
+
io.mkdir(path.join(targetDir, 'tools'));
|
|
332
786
|
|
|
333
787
|
const { ensureGitignoreEntries } = require('./gitignore');
|
|
334
788
|
ensureGitignoreEntries(targetDir);
|
|
335
|
-
|
|
789
|
+
const gitkeep = path.join(targetDir, 'sdds', '.gitkeep');
|
|
790
|
+
if (!io.exists(gitkeep)) io.writeFile(gitkeep, '');
|
|
336
791
|
|
|
337
|
-
const agentsContent = `# CLC Kernel ${
|
|
792
|
+
const agentsContent = `# CLC Kernel ${frameworkLabel} (${(config.projectType || 'unknown').toUpperCase()}) — AGENTS
|
|
338
793
|
|
|
339
794
|
This document is the **authoritative law** for AI agents working in this repository.
|
|
340
795
|
Forged by **CLC Kernel: The AI Agent Governance Engine**.
|
|
341
796
|
|
|
342
|
-
> **RULE #0: MANDATORY EXECUTION OVERRIDE RULE (
|
|
797
|
+
> **RULE #0: MANDATORY EXECUTION OVERRIDE RULE (NO EXCEPTIONS)**
|
|
343
798
|
> Even when the user issues a direct or urgent fix request ("fix this bug", "fix this error", "quick fix"):
|
|
344
799
|
> YOU ARE STRICTLY FORBIDDEN from modifying source code directly without completing the full quality harness:
|
|
345
800
|
> 1. **Research & Root Cause Analysis:** Investigate tracebacks and inspect affected files before editing.
|
|
346
|
-
> 2. **TDD Verification (Red Phase):** Write a failing regression test first (${
|
|
801
|
+
> 2. **TDD Verification (Red Phase):** Write a failing regression test first (${testRunner}).
|
|
347
802
|
> 3. **Clean Architecture Implementation (Green Phase):** Make the test pass maintaining layer isolation.
|
|
348
803
|
> 4. **Mandatory Educational Audit Gate:** Execute \`node tools/audit.js\` or \`python tools/audit.py\` and output the Educational Code Summary to stdout.
|
|
349
804
|
> NEVER declare success or skip verification commands for quick fixes.
|
|
@@ -351,9 +806,9 @@ Forged by **CLC Kernel: The AI Agent Governance Engine**.
|
|
|
351
806
|
## 1. The Loop (Every Task)
|
|
352
807
|
1. **Research** — Inspect codebase / docs before writing code.
|
|
353
808
|
2. **Plan** — Write an SDD under \`sdds/{change-name}/\`. SDDs live 100% locally and are gitignored.
|
|
354
|
-
3. **Test** (TDD) — Write failing test first (${
|
|
809
|
+
3. **Test** (TDD) — Write failing test first (${testRunner}).
|
|
355
810
|
4. **Implement** — Make test pass.
|
|
356
|
-
5. **Verify & Audit** — Run \`node tools/audit.js\` or \`python tools/audit.py
|
|
811
|
+
5. **Verify & Audit** — Run \`node tools/audit.js --change {change-name}\` or \`python tools/audit.py --change {change-name}\` (report + receipt in \`sdds/{change-name}/\`; without \`--change\` nothing is written).
|
|
357
812
|
6. **DoD** — Lint, typecheck, tests, coverage, docs, memory.
|
|
358
813
|
7. **Commit** — Pre-commit hook runs automated guards.
|
|
359
814
|
8. **PR** — Generate PR body.
|
|
@@ -369,86 +824,88 @@ ${isFront ? `- UI Component Reuse First (components/ui/ & semantic tokens)
|
|
|
369
824
|
- Next/Image & Tree-Shaking Guard
|
|
370
825
|
- Storybook Coverage Guard
|
|
371
826
|
- Educational Audit Gate
|
|
372
|
-
- Memory
|
|
373
|
-
-
|
|
374
|
-
-
|
|
827
|
+
- Memory protocol (agent-enforced; Engram / Graphify optional, not checked by a guard)` : isGeneric ? `- Wait for Audit Gate
|
|
828
|
+
- Secret Leak Guard (tools/scan_secrets.js, run by the pre-commit hook)
|
|
829
|
+
- Scope protocol (agent-enforced; not checked by a guard)
|
|
830
|
+
- TDD protocol (agent-enforced; no verifier installed for this stack)
|
|
831
|
+
- Memory protocol (agent-enforced; Engram / Graphify optional, not checked by a guard)` : `- Wait for Audit Gate
|
|
832
|
+
- Scope protocol (agent-enforced; not checked by a guard)
|
|
833
|
+
- TDD protocol (agent-enforced; no verifier installed for this stack)
|
|
375
834
|
- Secret Leak Guard
|
|
376
|
-
- Clean Architecture
|
|
377
|
-
-
|
|
378
|
-
-
|
|
379
|
-
- Memory
|
|
835
|
+
- Clean Architecture protocol (agent-enforced; no layer guard installed for this stack)
|
|
836
|
+
- N+1 Heuristic (ORM-like calls inside loops, Python AST)
|
|
837
|
+
- Migration Reversibility protocol (agent-enforced; no migration guard installed for this stack)
|
|
838
|
+
- Memory protocol (agent-enforced; Engram / Graphify optional, not checked by a guard)`}
|
|
380
839
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
| Command | Lifecycle Phase | Mandatory Execution Protocol |
|
|
385
|
-
|---|---|---|
|
|
386
|
-
| \`clckernel feature <name>\` | New Feature | 1. Research affected layers.<br>2. Author local spec in \`sdds/{name}/spec.md\`.<br>3. Present HIT review to user and wait for approval.<br>4. Write failing test (Red Phase).<br>5. Implement clean code (Green Phase).<br>6. Run \`node tools/audit.js\` (or \`audit.py\`). |
|
|
387
|
-
| \`clckernel fix <issue>\` | Bug Fix | 1. Investigate root cause (no code edits).<br>2. Reproduce with failing test (Red Phase).<br>3. Minimal surgical fix respecting Clean Arch (Green Phase).<br>4. Run audit gate without skipping. |
|
|
388
|
-
| \`clckernel test\` | Verification | Run test suite (\`${config.testRunner || 'npm test'}\`) and verify clean pass (exit code 0). |
|
|
389
|
-
| \`clckernel audit\` | Verification | Run \`node tools/audit.js\` or \`python tools/audit.py\` and output the Educational Summary. |
|
|
390
|
-
| \`clckernel doctor\` | Diagnostics | Check git pre-commit hooks, IDE symlinks, and AST engine integrity. |
|
|
391
|
-
| \`clckernel commit <message>\` | Version Control | Run full audit, verify tests pass, ensure no changes outside SDD scope, and commit with conventional commits (never add AI attribution). |
|
|
392
|
-
| \`clckernel guard create <name>\` | Safeguards | Scaffold a new AST guard in the stack's native language in \`tools/guards/\`. |
|
|
393
|
-
| \`clckernel update\` | Maintenance | Non-destructive update: refresh core tools, playbooks & symlinks (preserves SDDs & custom guards). |
|
|
394
|
-
| \`clckernel start\` | Setup | Scan codebase, propose governance plan, and materialize \`.clckernel.yaml\`. |
|
|
395
|
-
`;
|
|
840
|
+
${commandTable(testRunner)}
|
|
841
|
+
${FIRST_SESSION_SECTION}`;
|
|
396
842
|
// Guard: never overwrite an existing project-customized AGENTS.md.
|
|
397
|
-
// This prevents clckernel update from replacing
|
|
398
|
-
//
|
|
399
|
-
if (!
|
|
400
|
-
|
|
843
|
+
// This prevents clckernel update from replacing project-specific
|
|
844
|
+
// invariants the template cannot know about.
|
|
845
|
+
if (!io.exists(path.join(targetDir, 'AGENTS.md'))) {
|
|
846
|
+
io.writeFile(path.join(targetDir, 'AGENTS.md'), agentsContent);
|
|
401
847
|
}
|
|
402
848
|
|
|
403
849
|
}
|
|
404
850
|
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
851
|
+
// Steps 2-3: Install tools and generate the pre-commit hook from the same
|
|
852
|
+
// manifest, so every guard copied into tools/ is also run by the hook.
|
|
853
|
+
// The registry of everything written into tools/ is recorded before the hook
|
|
854
|
+
// is generated, so whole-project linters can exclude those files.
|
|
855
|
+
let installedGuards;
|
|
856
|
+
let hook;
|
|
857
|
+
// On update only the guards the project has (plus --add-guards) are
|
|
858
|
+
// installed and run (sdds/update-no-additions, decisions 1 and 4).
|
|
859
|
+
const { addGuards } = options;
|
|
860
|
+
if (multiPart) {
|
|
861
|
+
const full = buildPartsManifest(config.parts, config.techTools || [], targetDir);
|
|
862
|
+
const manifest = { ...full, guards: selectGuards(io, full.guards, addGuards) };
|
|
863
|
+
const { installed, written } = installToolsFromManifest(targetDir, manifest.guards, config);
|
|
864
|
+
installedGuards = installed;
|
|
865
|
+
hook = generatePartsHook(config, targetDir, manifest, managedToolPaths(written));
|
|
866
|
+
} else if (config.adapter) {
|
|
867
|
+
const manifest = selectGuards(io, buildToolManifest(config.adapter, config.techTools || [], targetDir), addGuards);
|
|
868
|
+
const { installed, written } = installToolsFromManifest(targetDir, manifest, config);
|
|
869
|
+
installedGuards = installed;
|
|
870
|
+
hook = generateHookFromManifest(config.adapter, targetDir, manifest, managedToolPaths(written));
|
|
425
871
|
} else {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
generateHookFromManifest(config.adapter, targetDir);
|
|
432
|
-
} else {
|
|
433
|
-
generateFallbackHook(targetDir, config.projectType);
|
|
872
|
+
const kept = new Set(selectGuards(io, fallbackGuards(config.projectType), addGuards).map(tool => tool.path));
|
|
873
|
+
const keep = file => /^audit\./.test(file) || kept.has(file);
|
|
874
|
+
const { installed } = installFallbackTools(targetDir, config.projectType, keep);
|
|
875
|
+
installedGuards = installed;
|
|
876
|
+
hook = generateFallbackHook(targetDir, config.projectType);
|
|
434
877
|
}
|
|
435
878
|
|
|
436
879
|
// Step 4: Install LLM Conversational CLI & Agent Context
|
|
437
|
-
installAgenticCLI(targetDir);
|
|
880
|
+
installAgenticCLI(targetDir, projectConfig.config.mirrors.list);
|
|
881
|
+
|
|
882
|
+
// Step 5: Record what the kernel owns now, with hashes.
|
|
883
|
+
writeManagedRegistry(io, targetDir);
|
|
884
|
+
|
|
885
|
+
return { installedGuards, hook };
|
|
438
886
|
}
|
|
439
887
|
|
|
888
|
+
/** AI IDE mirrors (symlinks to AGENTS.md) install creates when the config lists none. */
|
|
889
|
+
const DEFAULT_MIRRORS = [
|
|
890
|
+
'CLAUDE.md', // Cursor / Claude Desktop / Windsurf
|
|
891
|
+
'GEMINI.md', // Gemini / Project IDX
|
|
892
|
+
'.cursorrules', // Cursor legacy
|
|
893
|
+
'.cursor/rules/clckernel_context.mdc', // Cursor modern (MDC)
|
|
894
|
+
'.github/copilot-instructions.md', // GitHub Copilot
|
|
895
|
+
'.antigravity/rules.md' // Antigravity (Local Agent)
|
|
896
|
+
];
|
|
897
|
+
|
|
440
898
|
/**
|
|
441
|
-
*
|
|
442
|
-
*
|
|
899
|
+
* Installs the conversational CLI skill (SKILL.md and its playbooks) and the
|
|
900
|
+
* AI IDE mirrors of AGENTS.md: the config's mirrors.list, or DEFAULT_MIRRORS.
|
|
901
|
+
* @param {string} targetDir - Project root
|
|
902
|
+
* @param {string[]|null} [mirrors] - mirrors.list of the project config
|
|
443
903
|
*/
|
|
444
|
-
function installAgenticCLI(targetDir) {
|
|
904
|
+
function installAgenticCLI(targetDir, mirrors) {
|
|
905
|
+
const io = currentIO(targetDir);
|
|
445
906
|
// 1. Crear el Directorio de Skills
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
const skillRefDir = path.join(skillDir, 'reference');
|
|
449
|
-
const legacySkillRefDir = path.join(legacySkillDir, 'reference');
|
|
450
|
-
fs.mkdirSync(skillRefDir, { recursive: true });
|
|
451
|
-
fs.mkdirSync(legacySkillRefDir, { recursive: true });
|
|
907
|
+
const skillRel = '.agents/skills/clckernel_cli';
|
|
908
|
+
io.mkdir(path.join(targetDir, skillRel, 'reference'));
|
|
452
909
|
|
|
453
910
|
// 2. Inyectar el SKILL.md Maestro y Playbooks de Referencia
|
|
454
911
|
const bundledSkillDir = path.join(__dirname, '..', '.agents', 'skills', 'clckernel_cli');
|
|
@@ -456,8 +913,7 @@ function installAgenticCLI(targetDir) {
|
|
|
456
913
|
const bundledRefDir = path.join(bundledSkillDir, 'reference');
|
|
457
914
|
|
|
458
915
|
if (fs.existsSync(bundledSkillFile)) {
|
|
459
|
-
|
|
460
|
-
fs.copyFileSync(bundledSkillFile, path.join(legacySkillDir, 'SKILL.md'));
|
|
916
|
+
io.copyManaged(bundledSkillFile, `${skillRel}/SKILL.md`);
|
|
461
917
|
}
|
|
462
918
|
|
|
463
919
|
if (fs.existsSync(bundledRefDir)) {
|
|
@@ -465,53 +921,29 @@ function installAgenticCLI(targetDir) {
|
|
|
465
921
|
for (const pb of playbooks) {
|
|
466
922
|
const srcPb = path.join(bundledRefDir, pb);
|
|
467
923
|
if (fs.statSync(srcPb).isFile()) {
|
|
468
|
-
|
|
469
|
-
fs.copyFileSync(srcPb, path.join(legacySkillRefDir, pb));
|
|
924
|
+
io.copyManaged(srcPb, `${skillRel}/reference/${pb}`);
|
|
470
925
|
}
|
|
471
926
|
}
|
|
472
927
|
}
|
|
473
928
|
|
|
474
929
|
// 3. Crear Symlinks Dinámicos al AGENTS.md (El Single Source of Truth)
|
|
475
930
|
const sourceFile = 'AGENTS.md';
|
|
476
|
-
const symlinks =
|
|
477
|
-
'CLAUDE.md', // Cursor / Claude Desktop / Windsurf
|
|
478
|
-
'GEMINI.md', // Gemini / Project IDX
|
|
479
|
-
'.cursorrules', // Cursor legacy
|
|
480
|
-
'.cursor/rules/clckernel_context.mdc', // Cursor modern (MDC)
|
|
481
|
-
'.cursor/rules/clc_forge_context.mdc', // Cursor backward compat
|
|
482
|
-
'.github/copilot-instructions.md', // GitHub Copilot
|
|
483
|
-
'.antigravity/rules.md' // Antigravity (Local Agent)
|
|
484
|
-
];
|
|
931
|
+
const symlinks = Array.isArray(mirrors) ? mirrors : DEFAULT_MIRRORS;
|
|
485
932
|
|
|
486
933
|
symlinks.forEach(link => {
|
|
487
934
|
const linkPath = path.join(targetDir, link);
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
if (!fs.existsSync(linkDir)) {
|
|
491
|
-
fs.mkdirSync(linkDir, { recursive: true });
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const relPath = path.relative(linkDir, path.join(targetDir, sourceFile));
|
|
935
|
+
const relPath = path.relative(path.dirname(linkPath), path.join(targetDir, sourceFile));
|
|
495
936
|
|
|
496
937
|
try {
|
|
497
938
|
// Only replace an existing symlink (a stale mirror needs refreshing).
|
|
498
939
|
// A regular file belongs to the consumer: unlinking it would destroy
|
|
499
|
-
// their own rules, so leave it in place instead.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
existing = fs.lstatSync(linkPath);
|
|
503
|
-
} catch (err) {}
|
|
504
|
-
|
|
505
|
-
if (existing) {
|
|
506
|
-
if (!existing.isSymbolicLink()) return;
|
|
507
|
-
fs.unlinkSync(linkPath);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
fs.symlinkSync(relPath, linkPath, 'file');
|
|
940
|
+
// their own rules, so leave it in place instead. An update never
|
|
941
|
+
// creates a mirror the project does not have (sdds/update-no-additions).
|
|
942
|
+
io.mirror(relPath, linkPath);
|
|
511
943
|
} catch (e) {
|
|
512
944
|
// Ignorar fallos de permisos o symlinks existentes
|
|
513
945
|
}
|
|
514
946
|
});
|
|
515
947
|
}
|
|
516
948
|
|
|
517
|
-
module.exports = { generateHarness, generateHookFromManifest };
|
|
949
|
+
module.exports = { generateHarness, generateHookFromManifest, buildToolManifest, configureHooksPath, manifestGuardNames };
|