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.
Files changed (71) hide show
  1. package/AGENTS.md +124 -0
  2. package/README.es.md +166 -0
  3. package/README.md +167 -0
  4. package/bin/cli.js +12 -0
  5. package/docs/Journal/001-adr-clckernel-governance.md +18 -0
  6. package/package.json +44 -0
  7. package/src/adapters/astro.js +67 -0
  8. package/src/adapters/base.js +109 -0
  9. package/src/adapters/django.js +70 -0
  10. package/src/adapters/fastapi.js +69 -0
  11. package/src/adapters/go.js +50 -0
  12. package/src/adapters/laravel.js +52 -0
  13. package/src/adapters/nextjs.js +72 -0
  14. package/src/adapters/rails.js +54 -0
  15. package/src/adapters/rust.js +51 -0
  16. package/src/catalog.js +217 -0
  17. package/src/config.js +250 -0
  18. package/src/detector.js +85 -0
  19. package/src/doctor.js +125 -0
  20. package/src/generator.js +473 -0
  21. package/src/index.js +52 -0
  22. package/src/technologies/detector.js +243 -0
  23. package/src/technologies/guards/_shared.js +66 -0
  24. package/src/ui.js +101 -0
  25. package/src/yaml.js +173 -0
  26. package/test/adapters/astro.test.js +102 -0
  27. package/test/adapters/base.test.js +86 -0
  28. package/test/adapters/django.test.js +95 -0
  29. package/test/adapters/fastapi.test.js +88 -0
  30. package/test/adapters/go.test.js +81 -0
  31. package/test/adapters/laravel.test.js +106 -0
  32. package/test/adapters/nextjs.test.js +113 -0
  33. package/test/adapters/rails.test.js +108 -0
  34. package/test/adapters/rust.test.js +83 -0
  35. package/test/catalog.test.js +170 -0
  36. package/test/config.test.js +512 -0
  37. package/test/detector.test.js +345 -0
  38. package/test/doctor.test.js +302 -0
  39. package/test/generator.test.js +419 -0
  40. package/test/guards_new.test.js +95 -0
  41. package/test/helpers.js +36 -0
  42. package/test/integration/cli-flow.test.js +270 -0
  43. package/test/technologies/celery_guard.test.js +185 -0
  44. package/test/technologies/detector.test.js +342 -0
  45. package/test/technologies/docker_guard.test.js +187 -0
  46. package/test/technologies/integration.test.js +123 -0
  47. package/test/technologies/postgres_guard.test.js +87 -0
  48. package/test/technologies/redis_guard.test.js +128 -0
  49. package/tools/audit.js +219 -0
  50. package/tools/audit.py +273 -0
  51. package/tools/celery_guard.py +209 -0
  52. package/tools/check_a11y.js +109 -0
  53. package/tools/check_api_contracts.js +139 -0
  54. package/tools/check_architecture.js +139 -0
  55. package/tools/check_architecture.py +264 -0
  56. package/tools/check_custom.js +163 -0
  57. package/tools/check_custom.py +395 -0
  58. package/tools/check_db_efficiency.py +190 -0
  59. package/tools/check_migrations.py +223 -0
  60. package/tools/check_performance.js +142 -0
  61. package/tools/check_responsive.js +131 -0
  62. package/tools/check_scope.py +180 -0
  63. package/tools/check_seo.js +139 -0
  64. package/tools/check_storybook.js +108 -0
  65. package/tools/check_ui_reuse.js +135 -0
  66. package/tools/docker_guard.py +210 -0
  67. package/tools/postgres_guard.py +192 -0
  68. package/tools/redis_guard.py +197 -0
  69. package/tools/scan_secrets.js +109 -0
  70. package/tools/scan_secrets.py +153 -0
  71. 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 };
@@ -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 };