clckernel 1.2.6
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.md +124 -0
- package/README.es.md +166 -0
- package/README.md +167 -0
- package/bin/cli.js +12 -0
- package/docs/Journal/001-adr-clckernel-governance.md +18 -0
- package/package.json +44 -0
- package/src/adapters/astro.js +67 -0
- package/src/adapters/base.js +109 -0
- package/src/adapters/django.js +70 -0
- package/src/adapters/fastapi.js +69 -0
- package/src/adapters/go.js +50 -0
- package/src/adapters/laravel.js +52 -0
- package/src/adapters/nextjs.js +72 -0
- package/src/adapters/rails.js +54 -0
- package/src/adapters/rust.js +51 -0
- package/src/catalog.js +217 -0
- package/src/config.js +250 -0
- package/src/detector.js +85 -0
- package/src/doctor.js +125 -0
- package/src/generator.js +473 -0
- package/src/index.js +52 -0
- package/src/technologies/detector.js +243 -0
- package/src/technologies/guards/_shared.js +66 -0
- package/src/ui.js +101 -0
- package/src/yaml.js +173 -0
- package/test/adapters/astro.test.js +102 -0
- package/test/adapters/base.test.js +86 -0
- package/test/adapters/django.test.js +95 -0
- package/test/adapters/fastapi.test.js +88 -0
- package/test/adapters/go.test.js +81 -0
- package/test/adapters/laravel.test.js +106 -0
- package/test/adapters/nextjs.test.js +113 -0
- package/test/adapters/rails.test.js +108 -0
- package/test/adapters/rust.test.js +83 -0
- package/test/catalog.test.js +170 -0
- package/test/config.test.js +512 -0
- package/test/detector.test.js +345 -0
- package/test/doctor.test.js +302 -0
- package/test/generator.test.js +419 -0
- package/test/guards_new.test.js +95 -0
- package/test/helpers.js +36 -0
- package/test/integration/cli-flow.test.js +270 -0
- package/test/technologies/celery_guard.test.js +185 -0
- package/test/technologies/detector.test.js +342 -0
- package/test/technologies/docker_guard.test.js +187 -0
- package/test/technologies/integration.test.js +123 -0
- package/test/technologies/postgres_guard.test.js +87 -0
- package/test/technologies/redis_guard.test.js +128 -0
- package/tools/audit.js +219 -0
- package/tools/audit.py +273 -0
- package/tools/celery_guard.py +209 -0
- package/tools/check_a11y.js +109 -0
- package/tools/check_api_contracts.js +139 -0
- package/tools/check_architecture.js +139 -0
- package/tools/check_architecture.py +264 -0
- package/tools/check_custom.js +163 -0
- package/tools/check_custom.py +395 -0
- package/tools/check_db_efficiency.py +190 -0
- package/tools/check_migrations.py +223 -0
- package/tools/check_performance.js +142 -0
- package/tools/check_responsive.js +131 -0
- package/tools/check_scope.py +180 -0
- package/tools/check_seo.js +139 -0
- package/tools/check_storybook.js +108 -0
- package/tools/check_ui_reuse.js +135 -0
- package/tools/docker_guard.py +210 -0
- package/tools/postgres_guard.py +192 -0
- package/tools/redis_guard.py +197 -0
- package/tools/scan_secrets.js +109 -0
- package/tools/scan_secrets.py +153 -0
- package/tools/verify_tdd.py +212 -0
package/src/doctor.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Kernel — Health Doctor & Functional Validator
|
|
3
|
+
* Scans a target repository to verify safeguards, AGENTS.md, docs structure,
|
|
4
|
+
* git hooks, tool functionality, and audit orchestrator alignment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
const { colors } = require('./ui');
|
|
11
|
+
|
|
12
|
+
function runDoctor(targetDir) {
|
|
13
|
+
console.log(`\n${colors.bold}${colors.cyan}CLC KERNEL DOCTOR — HEALTH CHECK${colors.reset}\n`);
|
|
14
|
+
console.log(` Target Repository: ${colors.cyan}${targetDir}${colors.reset}\n`);
|
|
15
|
+
|
|
16
|
+
const checks = [];
|
|
17
|
+
|
|
18
|
+
// === Structural checks ===
|
|
19
|
+
const hasAgentsMd = fs.existsSync(path.join(targetDir, 'AGENTS.md'));
|
|
20
|
+
checks.push({ name: 'AGENTS.md law document', status: hasAgentsMd });
|
|
21
|
+
|
|
22
|
+
const hasSdds = fs.existsSync(path.join(targetDir, 'sdds'));
|
|
23
|
+
checks.push({ name: 'sdds/ local spec directory', status: hasSdds });
|
|
24
|
+
|
|
25
|
+
const hasJournal = fs.existsSync(path.join(targetDir, 'docs', 'Journal'));
|
|
26
|
+
checks.push({ name: 'docs/Journal/ ADR directory', status: hasJournal });
|
|
27
|
+
|
|
28
|
+
const toolsDir = path.join(targetDir, 'tools');
|
|
29
|
+
const hasTools = fs.existsSync(toolsDir) && fs.readdirSync(toolsDir).length > 0;
|
|
30
|
+
checks.push({ name: 'tools/ safeguard suite', status: hasTools });
|
|
31
|
+
|
|
32
|
+
const hasHusky = fs.existsSync(path.join(targetDir, '.husky', 'pre-commit'));
|
|
33
|
+
const hasGithooks = fs.existsSync(path.join(targetDir, '.githooks', 'pre-commit'));
|
|
34
|
+
checks.push({ name: 'pre-commit git hooks', status: hasHusky || hasGithooks });
|
|
35
|
+
|
|
36
|
+
// === Tool functionality validation ===
|
|
37
|
+
if (fs.existsSync(toolsDir)) {
|
|
38
|
+
const toolFiles = fs.readdirSync(toolsDir);
|
|
39
|
+
|
|
40
|
+
toolFiles.forEach(file => {
|
|
41
|
+
const toolPath = path.join(toolsDir, file);
|
|
42
|
+
|
|
43
|
+
// Load-test JS guard scripts via require()
|
|
44
|
+
if (file.endsWith('.js') && file.startsWith('check_')) {
|
|
45
|
+
try {
|
|
46
|
+
require(toolPath);
|
|
47
|
+
checks.push({ name: `tool/${file} loads`, status: true });
|
|
48
|
+
} catch (e) {
|
|
49
|
+
checks.push({ name: `tool/${file} loads`, status: false, detail: e.message });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse-test Python guard scripts via ast.parse()
|
|
54
|
+
if (file.endsWith('.py') && file.startsWith('check_')) {
|
|
55
|
+
try {
|
|
56
|
+
execSync(
|
|
57
|
+
`python3 -c "import ast; ast.parse(open('${toolPath}').read())"`,
|
|
58
|
+
{ stdio: 'pipe' }
|
|
59
|
+
);
|
|
60
|
+
checks.push({ name: `tool/${file} parses`, status: true });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
checks.push({ name: `tool/${file} parses`, status: false, detail: e.message });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// === Audit orchestrator import alignment ===
|
|
69
|
+
if (fs.existsSync(toolsDir)) {
|
|
70
|
+
// Check JS audit orchestrator imports
|
|
71
|
+
const auditJs = path.join(toolsDir, 'audit.js');
|
|
72
|
+
if (fs.existsSync(auditJs)) {
|
|
73
|
+
const content = fs.readFileSync(auditJs, 'utf-8');
|
|
74
|
+
const imports = content.match(/require\(['"]\.\/(\w+)['"]\)/g) || [];
|
|
75
|
+
imports.forEach(imp => {
|
|
76
|
+
const moduleName = imp.match(/\.\/(\w+)/)[1];
|
|
77
|
+
const candidates = [`${moduleName}.js`, `${moduleName}.py`];
|
|
78
|
+
const exists = candidates.some(c => fs.existsSync(path.join(toolsDir, c)));
|
|
79
|
+
checks.push({
|
|
80
|
+
name: `audit.js import ./${moduleName}`,
|
|
81
|
+
status: exists,
|
|
82
|
+
detail: exists ? undefined : 'Module not found in tools/',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check Python audit orchestrator imports
|
|
88
|
+
const auditPy = path.join(toolsDir, 'audit.py');
|
|
89
|
+
if (fs.existsSync(auditPy)) {
|
|
90
|
+
const content = fs.readFileSync(auditPy, 'utf-8');
|
|
91
|
+
const imports = content.match(/from tools\.(\w+)/g) || [];
|
|
92
|
+
imports.forEach(imp => {
|
|
93
|
+
const moduleName = imp.match(/from tools\.(\w+)/)[1];
|
|
94
|
+
const candidates = [`${moduleName}.py`, `${moduleName}.js`];
|
|
95
|
+
const exists = candidates.some(c => fs.existsSync(path.join(toolsDir, c)));
|
|
96
|
+
checks.push({
|
|
97
|
+
name: `audit.py import tools.${moduleName}`,
|
|
98
|
+
status: exists,
|
|
99
|
+
detail: exists ? undefined : 'Module not found in tools/',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// === Results ===
|
|
106
|
+
let passedCount = 0;
|
|
107
|
+
checks.forEach(({ name, status, detail }) => {
|
|
108
|
+
if (status) {
|
|
109
|
+
passedCount++;
|
|
110
|
+
console.log(` ${colors.emerald}✔ [PASS]${colors.reset} ${name}`);
|
|
111
|
+
} else {
|
|
112
|
+
const detailMsg = detail ? ` — ${detail}` : '';
|
|
113
|
+
console.log(` ${colors.red}✖ [FAIL]${colors.reset} ${name}${colors.gray}${detailMsg}${colors.reset}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
console.log('\n---------------------------------------------------------------');
|
|
118
|
+
if (passedCount === checks.length) {
|
|
119
|
+
console.log(` ${colors.bgEmerald} HEALTHY ${colors.reset} ${colors.emerald}${colors.bold}Repository meets 100% of the CLC Kernel standard.${colors.reset}\n`);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` ${colors.amber} ${checks.length - passedCount} issue(s) detected. Run \`npx clckernel\` to repair.${colors.reset}\n`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { runDoctor };
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — Universal Provisioning Engine
|
|
3
|
+
* Manifest-driven tool installation and dynamic pre-commit hook generation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const { resolveConfig } = require('./config.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a pre-commit hook dynamically from the adapter's tool manifest.
|
|
13
|
+
* Frontend projects use .husky; backend projects use .githooks.
|
|
14
|
+
* @param {object} adapter - Adapter with getTools() and projectType
|
|
15
|
+
* @param {string} targetDir - Target project root
|
|
16
|
+
*/
|
|
17
|
+
function generateHookFromManifest(adapter, targetDir) {
|
|
18
|
+
const tools = adapter.getTools();
|
|
19
|
+
const isFront = adapter.projectType === 'frontend';
|
|
20
|
+
|
|
21
|
+
const hookDir = isFront
|
|
22
|
+
? path.join(targetDir, '.husky')
|
|
23
|
+
: path.join(targetDir, '.githooks');
|
|
24
|
+
const hookFile = path.join(hookDir, 'pre-commit');
|
|
25
|
+
|
|
26
|
+
const lines = [];
|
|
27
|
+
if (isFront) {
|
|
28
|
+
lines.push('#!/usr/bin/env sh');
|
|
29
|
+
lines.push('. "$(dirname -- "$0")/_/husky.sh"');
|
|
30
|
+
lines.push('');
|
|
31
|
+
} else {
|
|
32
|
+
lines.push('#!/usr/bin/env bash');
|
|
33
|
+
lines.push(`echo "Running CLC Kernel ${adapter.name} Safeguards..."`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
tools.forEach(tool => {
|
|
38
|
+
if (!tool.command) return;
|
|
39
|
+
lines.push(tool.command);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
43
|
+
fs.writeFileSync(hookFile, lines.join('\n'), 'utf-8');
|
|
44
|
+
try { fs.chmodSync(hookFile, '755'); } catch (e) {}
|
|
45
|
+
|
|
46
|
+
if (!isFront) {
|
|
47
|
+
try {
|
|
48
|
+
execSync('git config core.hooksPath .githooks', { cwd: targetDir, stdio: 'ignore' });
|
|
49
|
+
} catch (e) {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Installs tools from an adapter's manifest into the target tools/ directory.
|
|
55
|
+
* Only copies tools with valid descriptors. External CLIs are skipped (no file to copy).
|
|
56
|
+
* Always copies the audit orchestrator for the project type.
|
|
57
|
+
* @param {string} targetDir - Target project root
|
|
58
|
+
* @param {Array} manifest - Tool manifest from adapter.getTools()
|
|
59
|
+
* @param {object} config - Project config (projectType, framework, testRunner)
|
|
60
|
+
*/
|
|
61
|
+
function installToolsFromManifest(targetDir, manifest, config) {
|
|
62
|
+
const sourceToolsDir = path.join(__dirname, '..', 'tools');
|
|
63
|
+
const isFront = config.projectType === 'frontend';
|
|
64
|
+
|
|
65
|
+
// Validate descriptors: skip + warn tools missing required fields
|
|
66
|
+
const validTools = manifest.filter(tool => {
|
|
67
|
+
if (!tool.name || !tool.language || !tool.command) {
|
|
68
|
+
console.warn(`Warning: tool "${tool.name || '(unnamed)'}" skipped - missing name, language, or command`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Always copy the audit orchestrator for the right language
|
|
75
|
+
const orchestrator = isFront ? 'audit.js' : 'audit.py';
|
|
76
|
+
const orchestratorSrc = path.join(sourceToolsDir, orchestrator);
|
|
77
|
+
const orchestratorDest = path.join(targetDir, 'tools', orchestrator);
|
|
78
|
+
if (fs.existsSync(orchestratorSrc)) {
|
|
79
|
+
fs.copyFileSync(orchestratorSrc, orchestratorDest);
|
|
80
|
+
try { fs.chmodSync(orchestratorDest, '755'); } catch (e) {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Copy only custom tools with valid paths (skip external CLIs and empty paths)
|
|
84
|
+
validTools.forEach(tool => {
|
|
85
|
+
if (tool.language === 'external' || !tool.path) return;
|
|
86
|
+
const srcFile = path.join(sourceToolsDir, tool.path);
|
|
87
|
+
const destFile = path.join(targetDir, 'tools', tool.path);
|
|
88
|
+
if (fs.existsSync(srcFile)) {
|
|
89
|
+
fs.copyFileSync(srcFile, destFile);
|
|
90
|
+
try { fs.chmodSync(destFile, '755'); } catch (e) {}
|
|
91
|
+
} else {
|
|
92
|
+
console.warn(`Warning: tool "${tool.path}" declared by adapter but not found in bundled tools/. Skipping.`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// check_custom.js requires the shared YAML parser (../src/yaml.js),
|
|
97
|
+
// so the generated project must ship it too.
|
|
98
|
+
if (validTools.some(tool => tool.path === 'check_custom.js')) {
|
|
99
|
+
installYamlModule(targetDir, sourceToolsDir);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Copies the zero-dependency YAML parser into a generated project's src/.
|
|
105
|
+
* Required by tools/check_custom.js (require('../src/yaml.js')).
|
|
106
|
+
* @param {string} targetDir - Target project root
|
|
107
|
+
* @param {string} sourceToolsDir - Bundled tools/ directory of this package
|
|
108
|
+
*/
|
|
109
|
+
function installYamlModule(targetDir, sourceToolsDir) {
|
|
110
|
+
const yamlSrc = path.join(sourceToolsDir, '..', 'src', 'yaml.js');
|
|
111
|
+
if (!fs.existsSync(yamlSrc)) return;
|
|
112
|
+
const yamlDest = path.join(targetDir, 'src', 'yaml.js');
|
|
113
|
+
fs.mkdirSync(path.join(targetDir, 'src'), { recursive: true });
|
|
114
|
+
fs.copyFileSync(yamlSrc, yamlDest);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Installs a minimal fallback tool set when no adapter is provided.
|
|
119
|
+
* Copies audit.js, audit.py, scan_secrets.js, scan_secrets.py.
|
|
120
|
+
* @param {string} targetDir - Target project root
|
|
121
|
+
*/
|
|
122
|
+
function installFallbackTools(targetDir) {
|
|
123
|
+
const sourceToolsDir = path.join(__dirname, '..', 'tools');
|
|
124
|
+
const fallbackFiles = [
|
|
125
|
+
'audit.js', 'audit.py',
|
|
126
|
+
'scan_secrets.js', 'scan_secrets.py',
|
|
127
|
+
'check_custom.js', 'check_custom.py',
|
|
128
|
+
'check_responsive.js', 'check_seo.js',
|
|
129
|
+
'check_db_efficiency.py'
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
fallbackFiles.forEach(file => {
|
|
133
|
+
const srcFile = path.join(sourceToolsDir, file);
|
|
134
|
+
const destFile = path.join(targetDir, 'tools', file);
|
|
135
|
+
if (fs.existsSync(srcFile)) {
|
|
136
|
+
fs.copyFileSync(srcFile, destFile);
|
|
137
|
+
try { fs.chmodSync(destFile, '755'); } catch (e) {}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (fallbackFiles.includes('check_custom.js')) {
|
|
142
|
+
installYamlModule(targetDir, sourceToolsDir);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generates a minimal generic pre-commit hook when no adapter is available.
|
|
148
|
+
* @param {string} targetDir - Target project root
|
|
149
|
+
* @param {string} projectType - 'frontend' or 'backend'
|
|
150
|
+
*/
|
|
151
|
+
function generateFallbackHook(targetDir, projectType) {
|
|
152
|
+
const isFront = projectType === 'frontend';
|
|
153
|
+
|
|
154
|
+
if (isFront) {
|
|
155
|
+
const hookDir = path.join(targetDir, '.husky');
|
|
156
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
157
|
+
const hookFile = path.join(hookDir, 'pre-commit');
|
|
158
|
+
fs.writeFileSync(hookFile, `#!/usr/bin/env sh
|
|
159
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
160
|
+
|
|
161
|
+
node tools/scan_secrets.js || true
|
|
162
|
+
`, 'utf-8');
|
|
163
|
+
try { fs.chmodSync(hookFile, '755'); } catch (e) {}
|
|
164
|
+
} else {
|
|
165
|
+
const hookDir = path.join(targetDir, '.githooks');
|
|
166
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
167
|
+
const hookFile = path.join(hookDir, 'pre-commit');
|
|
168
|
+
fs.writeFileSync(hookFile, `#!/usr/bin/env bash
|
|
169
|
+
echo "Running CLC Kernel Safeguards..."
|
|
170
|
+
python3 tools/scan_secrets.py || true
|
|
171
|
+
`, 'utf-8');
|
|
172
|
+
try { fs.chmodSync(hookFile, '755'); } catch (e) {}
|
|
173
|
+
try {
|
|
174
|
+
execSync('git config core.hooksPath .githooks', { cwd: targetDir, stdio: 'ignore' });
|
|
175
|
+
} catch (e) {}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extended-schema keys that route a config through the config-driven path
|
|
181
|
+
* (design §1.4). `rules` intentionally excluded: a rules-only YAML keeps the
|
|
182
|
+
* legacy adapter/fallback path (check_custom reads rules at audit time).
|
|
183
|
+
*/
|
|
184
|
+
const EXTENDED_CONFIG_KEYS = [
|
|
185
|
+
'active_guards',
|
|
186
|
+
'phases',
|
|
187
|
+
'gate_mode',
|
|
188
|
+
'severities',
|
|
189
|
+
'layers',
|
|
190
|
+
'scope',
|
|
191
|
+
'exclude_paths',
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
/** True when the object carries extended-schema keys (a config-path config). */
|
|
195
|
+
function isExtendedConfig(config) {
|
|
196
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return false;
|
|
197
|
+
return EXTENDED_CONFIG_KEYS.some((k) => Object.prototype.hasOwnProperty.call(config, k));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Config-driven path (Slice A, minimal A-03 integration): resolve the raw
|
|
202
|
+
* config (or reuse an already-resolved one marked by index.js/A-05) and emit
|
|
203
|
+
* the resolved harness metadata to `.clckernel.resolved.json` and
|
|
204
|
+
* `.clc-forge.resolved.json` (deterministic: stable key order, 2-space JSON).
|
|
205
|
+
* Full layout/tools/hook materialization is the A-04 work unit; here only
|
|
206
|
+
* the metadata flows out.
|
|
207
|
+
* @param {string} targetDir - Target project root
|
|
208
|
+
* @param {object} configOrNull - Raw or resolved extended config
|
|
209
|
+
* @returns {object} The resolved config (metadata written to disk)
|
|
210
|
+
*/
|
|
211
|
+
function generateConfigPathHarness(targetDir, configOrNull) {
|
|
212
|
+
const alreadyResolved =
|
|
213
|
+
configOrNull.__clcKernelConfigPath === true ||
|
|
214
|
+
configOrNull.__clcForgeConfigPath === true ||
|
|
215
|
+
Array.isArray(configOrNull.__catalog);
|
|
216
|
+
const resolved = alreadyResolved ? configOrNull : resolveConfig(configOrNull, {});
|
|
217
|
+
|
|
218
|
+
const metadata = {
|
|
219
|
+
activeGuards: resolved.activeGuards,
|
|
220
|
+
phases: resolved.phases,
|
|
221
|
+
gateMode: resolved.gateMode,
|
|
222
|
+
effectiveSeverities: resolved.effectiveSeverities,
|
|
223
|
+
layers: resolved.layers,
|
|
224
|
+
scope: resolved.scope,
|
|
225
|
+
excludePaths: resolved.excludePaths,
|
|
226
|
+
rules: resolved.rules || [],
|
|
227
|
+
framework: resolved.framework,
|
|
228
|
+
projectType: resolved.projectType,
|
|
229
|
+
testRunner: resolved.testRunner,
|
|
230
|
+
__catalog: resolved.__catalog,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
234
|
+
fs.writeFileSync(
|
|
235
|
+
path.join(targetDir, '.clckernel.resolved.json'),
|
|
236
|
+
JSON.stringify(metadata, null, 2) + '\n',
|
|
237
|
+
'utf-8'
|
|
238
|
+
);
|
|
239
|
+
fs.writeFileSync(
|
|
240
|
+
path.join(targetDir, '.clc-forge.resolved.json'),
|
|
241
|
+
JSON.stringify(metadata, null, 2) + '\n',
|
|
242
|
+
'utf-8'
|
|
243
|
+
);
|
|
244
|
+
return resolved;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Generate a harness for a target project.
|
|
249
|
+
* When the optional `configOrNull` is null (or an adapter-detected config
|
|
250
|
+
* without extended-schema keys) the legacy path runs UNCHANGED. A config
|
|
251
|
+
* carrying extended-schema keys (or the `__clcKernelConfigPath` marker set
|
|
252
|
+
* by index.js) routes to the config-driven path — which for A-03 emits the
|
|
253
|
+
* resolved metadata; full materialization lands in A-04.
|
|
254
|
+
*/
|
|
255
|
+
function generateHarness(targetDir, configOrNull) {
|
|
256
|
+
if (configOrNull && (configOrNull.__clcKernelConfigPath === true || configOrNull.__clcForgeConfigPath === true || isExtendedConfig(configOrNull))) {
|
|
257
|
+
return generateConfigPathHarness(targetDir, configOrNull);
|
|
258
|
+
}
|
|
259
|
+
// Legacy path: a null/absent config falls back to generic provisioning
|
|
260
|
+
// (byte-identical for every existing caller, which always passes a full
|
|
261
|
+
// detected config — defaults only kick in for a bare null).
|
|
262
|
+
const config = configOrNull || {};
|
|
263
|
+
|
|
264
|
+
// Step 1: Provision directories + AGENTS.md
|
|
265
|
+
if (config.adapter) {
|
|
266
|
+
config.adapter.provision(targetDir, config);
|
|
267
|
+
} else {
|
|
268
|
+
// Fallback: create directory structure + AGENTS.md
|
|
269
|
+
const isFront = config.projectType === 'frontend';
|
|
270
|
+
|
|
271
|
+
fs.mkdirSync(path.join(targetDir, 'docs', 'Journal'), { recursive: true });
|
|
272
|
+
fs.mkdirSync(path.join(targetDir, 'docs', 'Architecture'), { recursive: true });
|
|
273
|
+
fs.mkdirSync(path.join(targetDir, 'docs', 'Development'), { recursive: true });
|
|
274
|
+
fs.mkdirSync(path.join(targetDir, 'sdds'), { recursive: true });
|
|
275
|
+
fs.mkdirSync(path.join(targetDir, 'tools'), { recursive: true });
|
|
276
|
+
|
|
277
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
278
|
+
let gitignoreContent = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
|
|
279
|
+
if (!gitignoreContent.includes('sdds/*')) {
|
|
280
|
+
gitignoreContent += '\n\n# Local Spec-Driven Development (SDD) files\nsdds/*\n!sdds/.gitkeep\nopenspec/*\n!openspec/.gitkeep\n.ruff_cache/\n';
|
|
281
|
+
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
|
|
282
|
+
}
|
|
283
|
+
fs.writeFileSync(path.join(targetDir, 'sdds', '.gitkeep'), '', 'utf-8');
|
|
284
|
+
|
|
285
|
+
const agentsContent = `# CLC Kernel ${config.framework || 'Generic'} (${(config.projectType || 'unknown').toUpperCase()}) — AGENTS
|
|
286
|
+
|
|
287
|
+
This document is the **authoritative law** for AI agents working in this repository.
|
|
288
|
+
Forged by **CLC Kernel: The AI Agent Governance Engine**.
|
|
289
|
+
|
|
290
|
+
> **RULE #0: MANDATORY EXECUTION OVERRIDE RULE (UNBYPASSABLE)**
|
|
291
|
+
> Even when the user issues a direct or urgent fix request ("fix this bug", "fix this error", "quick fix"):
|
|
292
|
+
> YOU ARE STRICTLY FORBIDDEN from modifying source code directly without completing the full quality harness:
|
|
293
|
+
> 1. **Research & Root Cause Analysis:** Investigate tracebacks and inspect affected files before editing.
|
|
294
|
+
> 2. **TDD Verification (Red Phase):** Write a failing regression test first (${config.testRunner || 'npm test'}).
|
|
295
|
+
> 3. **Clean Architecture Implementation (Green Phase):** Make the test pass maintaining layer isolation.
|
|
296
|
+
> 4. **Mandatory Educational Audit Gate:** Execute \`node tools/audit.js\` or \`python tools/audit.py\` and output the Educational Code Summary to stdout.
|
|
297
|
+
> NEVER declare success or skip verification commands for quick fixes.
|
|
298
|
+
|
|
299
|
+
## 1. The Loop (Every Task)
|
|
300
|
+
1. **Research** — Inspect codebase / docs before writing code.
|
|
301
|
+
2. **Plan** — Write an SDD under \`sdds/{change-name}/\`. SDDs live 100% locally and are gitignored.
|
|
302
|
+
3. **Test** (TDD) — Write failing test first (${config.testRunner || 'npm test'}).
|
|
303
|
+
4. **Implement** — Make test pass.
|
|
304
|
+
5. **Verify & Audit** — Run \`node tools/audit.js\` or \`python tools/audit.py\`.
|
|
305
|
+
6. **DoD** — Lint, typecheck, tests, coverage, docs, memory.
|
|
306
|
+
7. **Commit** — Pre-commit hook runs automated guards.
|
|
307
|
+
8. **PR** — Generate PR body.
|
|
308
|
+
|
|
309
|
+
## 2. Core AI Safeguards
|
|
310
|
+
${isFront ? `- UI Component Reuse First (components/ui/ & semantic tokens)
|
|
311
|
+
- Responsive & Mobile-First Adaptability Guard
|
|
312
|
+
- SEO, GEO & Web Performance Guard
|
|
313
|
+
- Next.js Clean Architecture Guard
|
|
314
|
+
- Secret & Client Exposure Guard
|
|
315
|
+
- Accessibility & ARIA Guard (A11y)
|
|
316
|
+
- Zod API Contract Guard
|
|
317
|
+
- Next/Image & Tree-Shaking Guard
|
|
318
|
+
- Storybook Coverage Guard
|
|
319
|
+
- Educational Audit Gate
|
|
320
|
+
- Memory Guard (Engram / Graphify)` : `- Wait for Audit Gate
|
|
321
|
+
- Scope Guardrail
|
|
322
|
+
- Real TDD Validation (Red -> Green)
|
|
323
|
+
- Secret Leak Guard
|
|
324
|
+
- Clean Architecture AST Guard
|
|
325
|
+
- Database Efficiency & N+1 Performance Guard
|
|
326
|
+
- Database Migration Idempotency Guard
|
|
327
|
+
- Memory Guard (Engram / Graphify)`}
|
|
328
|
+
`;
|
|
329
|
+
fs.writeFileSync(path.join(targetDir, 'AGENTS.md'), agentsContent, 'utf-8');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Step 2: Install tools (manifest-driven or fallback)
|
|
333
|
+
if (config.adapter) {
|
|
334
|
+
const frameworkTools = config.adapter.getTools();
|
|
335
|
+
const techTools = config.techTools || [];
|
|
336
|
+
|
|
337
|
+
// Merge order: [frameworkTools prefix, ...techTools, check_custom + suffix]
|
|
338
|
+
// Split around check_custom to insert tech tools before it
|
|
339
|
+
const checkCustomIdx = frameworkTools.findIndex(t => t.name === 'check_custom');
|
|
340
|
+
const mergedManifest = checkCustomIdx >= 0
|
|
341
|
+
? [
|
|
342
|
+
...frameworkTools.slice(0, checkCustomIdx),
|
|
343
|
+
...techTools,
|
|
344
|
+
...frameworkTools.slice(checkCustomIdx)
|
|
345
|
+
]
|
|
346
|
+
: [
|
|
347
|
+
...frameworkTools,
|
|
348
|
+
...techTools
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
installToolsFromManifest(targetDir, mergedManifest, config);
|
|
352
|
+
} else {
|
|
353
|
+
installFallbackTools(targetDir);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Step 3: Generate pre-commit hook (manifest-driven or fallback)
|
|
357
|
+
if (config.adapter) {
|
|
358
|
+
generateHookFromManifest(config.adapter, targetDir);
|
|
359
|
+
} else {
|
|
360
|
+
generateFallbackHook(targetDir, config.projectType);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Step 4: Install LLM Conversational CLI & Agent Context
|
|
364
|
+
installAgenticCLI(targetDir);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Instala la CLI Conversacional (SKILL.md) y genera los symlinks universales
|
|
369
|
+
* para que todos los LLMs (Cursor, Claude, Gemini) absorban el AGENTS.md.
|
|
370
|
+
*/
|
|
371
|
+
function installAgenticCLI(targetDir) {
|
|
372
|
+
// 1. Crear el Directorio de Skills
|
|
373
|
+
const skillDir = path.join(targetDir, '.agents', 'skills', 'clckernel_cli');
|
|
374
|
+
const legacySkillDir = path.join(targetDir, '.agents', 'skills', 'clc_forge_cli');
|
|
375
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
376
|
+
fs.mkdirSync(legacySkillDir, { recursive: true });
|
|
377
|
+
|
|
378
|
+
// 2. Inyectar el SKILL.md Maestro
|
|
379
|
+
const skillContent = `---
|
|
380
|
+
name: clckernel_cli
|
|
381
|
+
description: Conversational CLI Orchestrator for the CLC Kernel AI Agent Governance Engine.
|
|
382
|
+
triggers:
|
|
383
|
+
- clckernel start
|
|
384
|
+
- clckernel create_guard
|
|
385
|
+
- clckernel remove_guard
|
|
386
|
+
- clckernel add_phase
|
|
387
|
+
- clckernel remove_phase
|
|
388
|
+
- clc_forge start
|
|
389
|
+
- clc_forge create_guard
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
# 🤖 CLC Kernel — Conversational CLI
|
|
393
|
+
|
|
394
|
+
You are the internal runtime engine of CLC Kernel (AIUP Orchestrator).
|
|
395
|
+
DO NOT suggest terminal bash commands for these triggers; YOU are the execution environment. Your job is to guide the user through the agent governance lifecycle via chat, dynamically discovering the environment, and manipulating configuration files.
|
|
396
|
+
|
|
397
|
+
## 🚀 INTENT: \`clckernel start\` (or \`clc_forge start\`)
|
|
398
|
+
Execute this exact sequence without skipping steps:
|
|
399
|
+
|
|
400
|
+
### Phase 1: Silent Discovery
|
|
401
|
+
Scan the current project using your file-reading capabilities. Identify the primary tech stack by looking for signature files (e.g., \`package.json\`, \`Gemfile\`, \`Cargo.toml\`, \`go.mod\`).
|
|
402
|
+
*Golden Rule: Do not assume the language. Read the files to establish context.*
|
|
403
|
+
|
|
404
|
+
### Phase 2: Interview & Proposal
|
|
405
|
+
Introduce yourself as CLC Kernel and state the detected stack.
|
|
406
|
+
Propose a standard governance plan for that ecosystem (SDD + TDD + stack-specific Safeguards).
|
|
407
|
+
ASK the user directly:
|
|
408
|
+
1. "Should we activate these standard phases, or do you want to define custom ones?"
|
|
409
|
+
2. "Do you have any specific security needs that require a custom Guard?"
|
|
410
|
+
**STOP.** Wait for the user's response.
|
|
411
|
+
|
|
412
|
+
### Phase 3: Materialization
|
|
413
|
+
Based on user approval, generate and write the \`.clckernel.yaml\` file in the project root.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## 🛠️ INTENT: \`clckernel create_guard\`
|
|
418
|
+
1. Ask the user what behavior they want to audit or block. **STOP.** Wait for technical details.
|
|
419
|
+
2. Based on the detected stack, write the linter/guard script in the **NATIVE ECOSYSTEM LANGUAGE** (Ruby for Rails, Go \`ast\` for Golang, Python \`ast\` for FastAPI, JS for Node). Save it in \`tools/guards/\`.
|
|
420
|
+
3. Update the \`.clckernel.yaml\` file to include the new Guard.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 🗑️ INTENT: \`clckernel remove_guard\`
|
|
425
|
+
1. Ask which guard to remove and if the script should be deleted. **STOP.** Wait for response.
|
|
426
|
+
2. Update \`.clckernel.yaml\` and delete the script from \`tools/guards/\` if requested.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 🔄 INTENT: \`clckernel add_phase\` / \`clckernel remove_phase\`
|
|
431
|
+
1. Ask for details of the lifecycle phase to add or remove. **STOP.** Wait for response.
|
|
432
|
+
2. Update the \`execution_phases\` block in \`.clckernel.yaml\`, preserving the logical sequence.
|
|
433
|
+
`;
|
|
434
|
+
|
|
435
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent, 'utf-8');
|
|
436
|
+
fs.writeFileSync(path.join(legacySkillDir, 'SKILL.md'), skillContent, 'utf-8');
|
|
437
|
+
|
|
438
|
+
// 3. Crear Symlinks Dinámicos al AGENTS.md (El Single Source of Truth)
|
|
439
|
+
const sourceFile = 'AGENTS.md';
|
|
440
|
+
const symlinks = [
|
|
441
|
+
'CLAUDE.md', // Cursor / Claude Desktop / Windsurf
|
|
442
|
+
'GEMINI.md', // Gemini / Project IDX
|
|
443
|
+
'.cursorrules', // Cursor legacy
|
|
444
|
+
'.cursor/rules/clckernel_context.mdc', // Cursor modern (MDC)
|
|
445
|
+
'.cursor/rules/clc_forge_context.mdc', // Cursor backward compat
|
|
446
|
+
'.github/copilot-instructions.md', // GitHub Copilot
|
|
447
|
+
'.antigravity/rules.md' // Antigravity (Local Agent)
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
symlinks.forEach(link => {
|
|
451
|
+
const linkPath = path.join(targetDir, link);
|
|
452
|
+
const linkDir = path.dirname(linkPath);
|
|
453
|
+
|
|
454
|
+
if (!fs.existsSync(linkDir)) {
|
|
455
|
+
fs.mkdirSync(linkDir, { recursive: true });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const relPath = path.relative(linkDir, path.join(targetDir, sourceFile));
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
try {
|
|
462
|
+
if (fs.lstatSync(linkPath)) {
|
|
463
|
+
fs.unlinkSync(linkPath);
|
|
464
|
+
}
|
|
465
|
+
} catch (err) {}
|
|
466
|
+
fs.symlinkSync(relPath, linkPath, 'file');
|
|
467
|
+
} catch (e) {
|
|
468
|
+
// Ignorar fallos de permisos o symlinks existentes
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = { generateHarness, generateHookFromManifest };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Harness — Main Orchestrator Controller
|
|
3
|
+
* Coordinates auto-discovery, UI rendering, generator engine, and doctor checks.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { autoDetectStack } = require('./detector');
|
|
8
|
+
const { printBanner, printBox, promptOptions, printSuccess, colors } = require('./ui');
|
|
9
|
+
const { generateHarness } = require('./generator');
|
|
10
|
+
const { runDoctor } = require('./doctor');
|
|
11
|
+
|
|
12
|
+
async function runCli() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args[0];
|
|
15
|
+
|
|
16
|
+
let targetDir = process.cwd();
|
|
17
|
+
if (command && !command.startsWith('-') && command !== 'doctor') {
|
|
18
|
+
targetDir = path.resolve(command);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (command === 'doctor' || args.includes('--doctor')) {
|
|
22
|
+
if (args[1]) targetDir = path.resolve(args[1]);
|
|
23
|
+
runDoctor(targetDir);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 1. Print Pro ASCII Banner
|
|
28
|
+
printBanner();
|
|
29
|
+
|
|
30
|
+
// 2. Auto-Detect Stack
|
|
31
|
+
const detected = autoDetectStack(targetDir);
|
|
32
|
+
|
|
33
|
+
// 3. Render Formatted Configuration Box
|
|
34
|
+
printBox('🔍 CONFIGURACIÓN DETECTADA AUTOMÁTICAMENTE', [
|
|
35
|
+
{ label: '📦 Tipo de Proyecto', value: detected.projectType.toUpperCase(), badgeColor: colors.amber },
|
|
36
|
+
{ label: '🚀 Framework', value: detected.framework, badgeColor: colors.emerald },
|
|
37
|
+
{ label: '🎨 Estilos & UI', value: `${detected.styling} (${detected.uiLibrary})`, badgeColor: colors.violet },
|
|
38
|
+
{ label: '🧪 Test Runner', value: detected.testRunner, badgeColor: colors.cyan },
|
|
39
|
+
{ label: '🔒 Git Hooks', value: detected.gitHooks, badgeColor: colors.blue }
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// 4. Prompt User for Confirmation or Override
|
|
43
|
+
const finalConfig = await promptOptions(detected);
|
|
44
|
+
|
|
45
|
+
// 5. Generate Harness & Safeguards
|
|
46
|
+
generateHarness(targetDir, finalConfig);
|
|
47
|
+
|
|
48
|
+
// 6. Render Pro Success Card
|
|
49
|
+
printSuccess(targetDir, finalConfig.projectType === 'frontend', finalConfig);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { runCli };
|