sinapse-ai 7.6.0 → 7.7.1

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.
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: Secret Scanning
6
+ *
7
+ * RULE: Detect and block potential secrets/credentials from being written to files.
8
+ *
9
+ * Protocol (Claude Code PreToolUse):
10
+ * exit 0 → allow
11
+ * exit 2 → block (message shown to model via stderr)
12
+ *
13
+ * Fail-open: if parsing fails, allow.
14
+ *
15
+ * @module secret-scanning
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Secret Patterns — ordered by severity
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const SECRET_PATTERNS = [
26
+ // API Keys & Tokens
27
+ { name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/ },
28
+ { name: 'AWS Secret Key', pattern: /(?:aws_secret_access_key|secret_key)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/i },
29
+ { name: 'GitHub Token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },
30
+ { name: 'GitHub OAuth', pattern: /gho_[A-Za-z0-9_]{36,}/ },
31
+ { name: 'Slack Token', pattern: /xox[bpors]-[0-9]{10,}-[A-Za-z0-9-]+/ },
32
+ { name: 'Stripe Key', pattern: /[sr]k_(live|test)_[A-Za-z0-9]{20,}/ },
33
+ { name: 'OpenAI Key', pattern: /sk-[A-Za-z0-9]{20,}/ },
34
+ { name: 'Anthropic Key', pattern: /sk-ant-[A-Za-z0-9-]{20,}/ },
35
+ { name: 'Supabase Key', pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{50,}/ },
36
+ { name: 'Google API Key', pattern: /AIza[0-9A-Za-z_-]{35}/ },
37
+ { name: 'Vercel Token', pattern: /vercel_[A-Za-z0-9]{20,}/ },
38
+
39
+ // Private Keys
40
+ { name: 'RSA Private Key', pattern: /-----BEGIN RSA PRIVATE KEY-----/ },
41
+ { name: 'SSH Private Key', pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/ },
42
+ { name: 'PGP Private Key', pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/ },
43
+ { name: 'EC Private Key', pattern: /-----BEGIN EC PRIVATE KEY-----/ },
44
+
45
+ // Connection Strings
46
+ { name: 'DB Connection String', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@[^/\s]+/i },
47
+ { name: 'Supabase DB URL', pattern: /postgresql:\/\/postgres\.[A-Za-z0-9]+:[^@]+@/i },
48
+
49
+ // Generic Patterns (broader, lower confidence)
50
+ { name: 'Hardcoded Password', pattern: /(?:password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]/i },
51
+ { name: 'Bearer Token', pattern: /[Bb]earer\s+[A-Za-z0-9_\-.]{20,}/ },
52
+ { name: 'Basic Auth', pattern: /[Bb]asic\s+[A-Za-z0-9+/=]{20,}/ },
53
+ ];
54
+
55
+ /** Files that are expected to contain secret-like patterns */
56
+ const EXEMPT_PATHS = [
57
+ '.env.example', '.env.template', '.env.sample',
58
+ 'node_modules/', '.git/',
59
+ '.claude/hooks/', // Hook scripts may reference patterns
60
+ 'test/', 'tests/', '__tests__/',
61
+ '.sinapse-ai/core/', // Framework core may have validators
62
+ ];
63
+
64
+ /** File extensions to scan */
65
+ const SCANNABLE_EXTENSIONS = [
66
+ '.ts', '.tsx', '.js', '.jsx', '.cjs', '.mjs',
67
+ '.json', '.yaml', '.yml', '.toml',
68
+ '.env', '.sh', '.bash', '.py',
69
+ '.md', '.txt', '.cfg', '.conf', '.ini',
70
+ ];
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Helpers
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function projectRoot() {
77
+ return process.env.CLAUDE_PROJECT_DIR || process.cwd();
78
+ }
79
+
80
+ function relativize(filePath, root) {
81
+ const normalized = filePath.replace(/\\/g, '/');
82
+ const normalizedRoot = root.replace(/\\/g, '/');
83
+ if (normalized.startsWith(normalizedRoot)) {
84
+ return normalized.slice(normalizedRoot.length).replace(/^\/+/, '');
85
+ }
86
+ return normalized;
87
+ }
88
+
89
+ function isExempt(rel) {
90
+ return EXEMPT_PATHS.some((ep) => rel.includes(ep));
91
+ }
92
+
93
+ function isScannable(rel) {
94
+ return SCANNABLE_EXTENSIONS.some((ext) => rel.endsWith(ext));
95
+ }
96
+
97
+ function scanForSecrets(content) {
98
+ const findings = [];
99
+ for (const { name, pattern } of SECRET_PATTERNS) {
100
+ if (pattern.test(content)) {
101
+ findings.push(name);
102
+ }
103
+ }
104
+ return findings;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Main
109
+ // ---------------------------------------------------------------------------
110
+
111
+ function main() {
112
+ let input;
113
+ try {
114
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
115
+ } catch {
116
+ process.exit(0); // fail-open
117
+ }
118
+
119
+ const toolName = input.tool_name || '';
120
+ if (toolName !== 'Write' && toolName !== 'Edit') {
121
+ process.exit(0);
122
+ }
123
+
124
+ const toolInput = input.tool_input || {};
125
+ const filePath = toolInput.file_path || '';
126
+ if (!filePath) process.exit(0);
127
+
128
+ const root = projectRoot();
129
+ const rel = relativize(filePath, root);
130
+
131
+ if (isExempt(rel)) process.exit(0);
132
+ if (!isScannable(rel)) process.exit(0);
133
+
134
+ // Scan content being written
135
+ const content = toolInput.content || toolInput.new_string || '';
136
+ if (!content) process.exit(0);
137
+
138
+ const findings = scanForSecrets(content);
139
+ if (findings.length === 0) process.exit(0);
140
+
141
+ // BLOCK
142
+ process.stderr.write(
143
+ `\nSECRET SCANNING BLOCK: Potential secrets detected!\n` +
144
+ `File: ${rel}\n` +
145
+ `Found: ${findings.join(', ')}\n` +
146
+ `\n` +
147
+ `DO NOT commit secrets to code. Instead:\n` +
148
+ ` - Use environment variables (.env) for local dev\n` +
149
+ ` - Use .env.example with placeholder values for templates\n` +
150
+ ` - Use secret managers for production (Supabase Vault, etc.)\n`,
151
+ );
152
+ process.exit(2);
153
+ }
154
+
155
+ main();
@@ -0,0 +1,47 @@
1
+ # Cross-Squad Routing Rules
2
+
3
+ > Applies to Imperator (sinapse-orqx) and ALL squad orchestrators (*-orqx).
4
+
5
+ ## Single-Squad Requests
6
+
7
+ When a request maps cleanly to one domain, route directly:
8
+
9
+ ```
10
+ "Crie um headline" → @copy-orqx → @headline-specialist
11
+ "Audite a marca" → @brand-orqx → @brand-auditor
12
+ "Otimize SEO" → @growth-orqx → @seo-specialist
13
+ ```
14
+
15
+ ## Multi-Squad Patterns
16
+
17
+ For requests spanning multiple domains, use established patterns:
18
+
19
+ | Pattern | Squads | Trigger |
20
+ |---------|--------|---------|
21
+ | `brand_launch` | brand + design + content + copy + animations | New brand from scratch |
22
+ | `go_to_market` | product + commercial + content + paidmedia + growth | Launch product/service |
23
+ | `strategic_pivot` | council + research + finance + product | Major direction change |
24
+ | `full_digital_presence` | brand + design + content + animations + growth + paidmedia | Complete digital setup |
25
+ | `security_compliance_audit` | cybersecurity + research | Security assessment |
26
+ | `content_campaign` | content + copy + growth + paidmedia | Multi-channel campaign |
27
+ | `course_launch` | courses + content + copy + commercial | Course/workshop launch |
28
+
29
+ ## Routing Priority
30
+
31
+ 1. **Exact match** — Request clearly belongs to one squad
32
+ 2. **Pattern match** — Request matches a known multi-squad pattern
33
+ 3. **Diagnostic** — Unclear request → Imperator diagnoses before routing
34
+
35
+ ## Handoff Between Squads
36
+
37
+ When work flows from one squad to another:
38
+ 1. Outgoing squad writes deliverables to `docs/` or project files
39
+ 2. Outgoing orchestrator generates handoff artifact
40
+ 3. Incoming orchestrator receives artifact + reads deliverables
41
+ 4. Incoming orchestrator routes to appropriate specialist
42
+
43
+ ## Anti-Patterns
44
+
45
+ - Never have two squads working on the same file simultaneously
46
+ - Never skip the orchestrator (user → specialist directly) for multi-squad work
47
+ - Never route to a squad without providing context from the previous squad
@@ -0,0 +1,61 @@
1
+ # Hook Governance Rules
2
+
3
+ > Applies to ALL agents. Hooks are the enforcement layer of the Constitution.
4
+
5
+ ## Active Hook Registry
6
+
7
+ ### PreToolUse — Bash
8
+ | Hook | Purpose | Behavior |
9
+ |------|---------|----------|
10
+ | `enforce-git-push-authority.sh` | Art. II — Only @devops can push | BLOCK (deny) |
11
+ | `sql-governance.py` | Security — Block dangerous SQL | BLOCK (exit 2) |
12
+ | `enforce-delegation.cjs` | Art. VIII — Orchestrators can't execute | BLOCK (exit 2) |
13
+
14
+ ### PreToolUse — Write|Edit
15
+ | Hook | Purpose | Behavior |
16
+ |------|---------|----------|
17
+ | `enforce-architecture-first.cjs` | Art. III — Docs before protected code | BLOCK (exit 2) |
18
+ | `write-path-validation.cjs` | Convention — Warn wrong doc paths | WARN (exit 0) |
19
+ | `enforce-story-gate.cjs` | Art. III — Story required for code | BLOCK (exit 2) |
20
+ | `slug-validation.py` | Convention — Validate naming | WARN (exit 0) |
21
+ | `mind-clone-governance.py` | Cloning — DNA required | BLOCK (exit 2) |
22
+ | `enforce-delegation.cjs` | Art. VIII — Orchestrators can't execute | BLOCK (exit 2) |
23
+
24
+ ### PreToolUse — Read
25
+ | Hook | Purpose | Behavior |
26
+ |------|---------|----------|
27
+ | `read-protection.py` | Security — Control sensitive file access | WARN (exit 0) |
28
+
29
+ ### UserPromptSubmit
30
+ | Hook | Purpose | Behavior |
31
+ |------|---------|----------|
32
+ | `synapse-wrapper.cjs` | SYNAPSE context injection | ALLOW (exit 0) |
33
+
34
+ ### PreCompact
35
+ | Hook | Purpose | Behavior |
36
+ |------|---------|----------|
37
+ | `precompact-wrapper.cjs` | Session digest before compaction | ALLOW (exit 0) |
38
+
39
+ ## Hook Design Principles
40
+
41
+ 1. **Fail-open** — If a hook crashes or can't parse input, exit 0 (allow)
42
+ 2. **Fast** — Each hook must complete in < 5 seconds
43
+ 3. **Silent on success** — Only output on block or warning
44
+ 4. **Deterministic** — Same input always produces same output
45
+ 5. **No side effects** — Hooks read state but don't modify it
46
+
47
+ ## Adding New Hooks
48
+
49
+ 1. Create script in `.claude/hooks/` (prefer CJS for portability)
50
+ 2. Add entry to `.claude/settings.json` under appropriate event
51
+ 3. Document in this file (hook-governance.md)
52
+ 4. Test with mock JSON via stdin
53
+ 5. Verify fail-open behavior with invalid input
54
+
55
+ ## Exit Code Protocol
56
+
57
+ | Code | Meaning | Effect |
58
+ |------|---------|--------|
59
+ | 0 | Allow | Operation proceeds |
60
+ | 2 | Block | Operation denied, message shown to model |
61
+ | Other | Ignored | Operation proceeds (treated as 0) |
@@ -0,0 +1,42 @@
1
+ # Security Scanning Rules
2
+
3
+ > Applies to ALL agents writing code or configuration files.
4
+
5
+ ## Secret Detection
6
+
7
+ NEVER commit files containing:
8
+ - API keys, tokens, or passwords in plaintext
9
+ - `.env` files with real values (use `.env.example` with placeholders)
10
+ - OAuth credentials (`access_token`, `refresh_token`, `client_secret`)
11
+ - Private keys (RSA, SSH, PGP)
12
+ - Database connection strings with credentials
13
+ - Webhook URLs with embedded tokens
14
+
15
+ ## Path Traversal Prevention
16
+
17
+ When handling file paths from user input or external sources:
18
+ - Reject paths containing `..` segments
19
+ - Reject absolute paths outside project root
20
+ - Normalize paths before validation (`path.resolve()` then check prefix)
21
+ - Never construct paths with string concatenation from untrusted input
22
+
23
+ ## SQL Injection Prevention
24
+
25
+ - Always use parameterized queries — never string interpolation
26
+ - Supabase RPC functions must use `$1, $2` parameter placeholders
27
+ - Edge functions must validate and sanitize all query parameters
28
+ - `sql-governance.py` hook blocks dangerous SQL patterns automatically
29
+
30
+ ## Dependency Security
31
+
32
+ - Review new dependencies before adding (`npm audit`)
33
+ - Prefer well-maintained packages (>1K weekly downloads, recent updates)
34
+ - Pin exact versions in production dependencies
35
+ - Never install packages with known critical vulnerabilities
36
+
37
+ ## Hooks Enforcement
38
+
39
+ Active security hooks in `.claude/settings.json`:
40
+ - `sql-governance.py` — blocks dangerous SQL in Bash commands
41
+ - `read-protection.py` — controls access to sensitive config files
42
+ - `enforce-architecture-first.cjs` — requires docs before protected code paths
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * sinapse health — Framework Health Analytics
6
+ *
7
+ * Analyzes the health of a SINAPSE installation:
8
+ * - Hook connectivity (wired vs orphaned)
9
+ * - Squad completeness (metadata, preferences, KBs)
10
+ * - Rule coverage
11
+ * - Agent authority compliance
12
+ * - Skill activation coverage
13
+ *
14
+ * Usage:
15
+ * sinapse health # Full health report
16
+ * sinapse health --json # JSON output
17
+ * sinapse health --fix # Auto-fix common issues
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ function findProjectRoot() {
24
+ let dir = process.cwd();
25
+ while (dir !== path.dirname(dir)) {
26
+ if (fs.existsSync(path.join(dir, '.sinapse-ai'))) return dir;
27
+ dir = path.dirname(dir);
28
+ }
29
+ return process.cwd();
30
+ }
31
+
32
+ function checkHooks(root) {
33
+ const results = { score: 0, max: 0, issues: [] };
34
+ const settingsPath = path.join(root, '.claude', 'settings.json');
35
+
36
+ results.max += 3;
37
+ if (!fs.existsSync(settingsPath)) {
38
+ results.issues.push({ severity: 'critical', msg: '.claude/settings.json not found' });
39
+ return results;
40
+ }
41
+
42
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
43
+ const hooks = settings.hooks || {};
44
+
45
+ // Check PreToolUse exists
46
+ if (hooks.PreToolUse && hooks.PreToolUse.length > 0) {
47
+ results.score += 1;
48
+ } else {
49
+ results.issues.push({ severity: 'high', msg: 'No PreToolUse hooks configured' });
50
+ }
51
+
52
+ // Check UserPromptSubmit exists
53
+ if (hooks.UserPromptSubmit && hooks.UserPromptSubmit.length > 0) {
54
+ results.score += 1;
55
+ } else {
56
+ results.issues.push({ severity: 'medium', msg: 'No UserPromptSubmit hooks configured' });
57
+ }
58
+
59
+ // Count total connected hooks
60
+ let connected = 0;
61
+ Object.values(hooks).forEach((matchers) =>
62
+ matchers.forEach((m) => (m.hooks || []).forEach(() => connected++)),
63
+ );
64
+
65
+ // Check hook files exist
66
+ const hooksDir = path.join(root, '.claude', 'hooks');
67
+ if (fs.existsSync(hooksDir)) {
68
+ const hookFiles = fs.readdirSync(hooksDir).filter((f) => f.endsWith('.cjs') || f.endsWith('.sh') || f.endsWith('.py'));
69
+ const orphaned = hookFiles.length - connected;
70
+ if (orphaned <= 5) results.score += 1; // Some orphaned is OK (utilities, legacy)
71
+ else results.issues.push({ severity: 'low', msg: `${orphaned} hook files not connected to settings.json` });
72
+ }
73
+
74
+ results.connected = connected;
75
+ return results;
76
+ }
77
+
78
+ function checkSquads(root) {
79
+ const results = { score: 0, max: 0, issues: [], squads: [] };
80
+ const squadsDir = path.join(root, 'squads');
81
+
82
+ if (!fs.existsSync(squadsDir)) {
83
+ results.issues.push({ severity: 'medium', msg: 'squads/ directory not found' });
84
+ return results;
85
+ }
86
+
87
+ const squadDirs = fs.readdirSync(squadsDir).filter((d) => d.startsWith('squad-') && fs.statSync(path.join(squadsDir, d)).isDirectory());
88
+
89
+ for (const squad of squadDirs) {
90
+ const dir = path.join(squadsDir, squad);
91
+ const checks = { name: squad, metadata: false, preferences: false, agents: 0, tasks: 0 };
92
+ results.max += 2;
93
+
94
+ // Check metadata
95
+ const yamlPath = path.join(dir, 'squad.yaml');
96
+ if (fs.existsSync(yamlPath)) {
97
+ const content = fs.readFileSync(yamlPath, 'utf8');
98
+ checks.metadata = /agents_count|total_files/.test(content);
99
+ if (checks.metadata) results.score += 1;
100
+ else results.issues.push({ severity: 'low', msg: `${squad}: missing metadata in squad.yaml` });
101
+ }
102
+
103
+ // Check preferences
104
+ checks.preferences = fs.existsSync(path.join(dir, 'preferences'));
105
+ if (checks.preferences) results.score += 1;
106
+ else results.issues.push({ severity: 'low', msg: `${squad}: missing preferences/ directory` });
107
+
108
+ // Count assets
109
+ const agentsDir = path.join(dir, 'agents');
110
+ const tasksDir = path.join(dir, 'tasks');
111
+ checks.agents = fs.existsSync(agentsDir) ? fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md')).length : 0;
112
+ checks.tasks = fs.existsSync(tasksDir) ? fs.readdirSync(tasksDir).filter((f) => f.endsWith('.md')).length : 0;
113
+
114
+ results.squads.push(checks);
115
+ }
116
+
117
+ return results;
118
+ }
119
+
120
+ function checkRules(root) {
121
+ const results = { score: 0, max: 1, issues: [] };
122
+ const rulesDir = path.join(root, '.claude', 'rules');
123
+
124
+ if (!fs.existsSync(rulesDir)) {
125
+ results.issues.push({ severity: 'high', msg: '.claude/rules/ not found' });
126
+ return results;
127
+ }
128
+
129
+ const rules = fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md'));
130
+ if (rules.length >= 13) results.score += 1;
131
+ else results.issues.push({ severity: 'medium', msg: `Only ${rules.length} rules (recommended: 16+)` });
132
+
133
+ results.count = rules.length;
134
+ return results;
135
+ }
136
+
137
+ function checkSkills(root) {
138
+ const results = { score: 0, max: 1, issues: [] };
139
+ const skillsDir = path.join(root, '.claude', 'skills');
140
+
141
+ if (!fs.existsSync(skillsDir)) {
142
+ results.issues.push({ severity: 'medium', msg: '.claude/skills/ not found' });
143
+ return results;
144
+ }
145
+
146
+ const skills = fs.readdirSync(skillsDir).filter((f) => f.endsWith('.md') || fs.statSync(path.join(skillsDir, f)).isDirectory());
147
+ // Check for path-activated skills
148
+ let pathActivated = 0;
149
+ for (const skill of skills) {
150
+ const skillPath = path.join(skillsDir, skill);
151
+ if (fs.statSync(skillPath).isFile()) {
152
+ const content = fs.readFileSync(skillPath, 'utf8');
153
+ if (/^paths:/m.test(content)) pathActivated++;
154
+ }
155
+ }
156
+
157
+ if (pathActivated >= 3) results.score += 1;
158
+ else results.issues.push({ severity: 'low', msg: `Only ${pathActivated} path-activated skills (recommended: 5+)` });
159
+
160
+ results.total = skills.length;
161
+ results.pathActivated = pathActivated;
162
+ return results;
163
+ }
164
+
165
+ async function runHealth(options = {}) {
166
+ const root = findProjectRoot();
167
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
168
+
169
+ const hookResults = checkHooks(root);
170
+ const squadResults = checkSquads(root);
171
+ const ruleResults = checkRules(root);
172
+ const skillResults = checkSkills(root);
173
+
174
+ const totalScore = hookResults.score + squadResults.score + ruleResults.score + skillResults.score;
175
+ const totalMax = hookResults.max + squadResults.max + ruleResults.max + skillResults.max;
176
+ const percentage = Math.round((totalScore / totalMax) * 100);
177
+
178
+ const allIssues = [
179
+ ...hookResults.issues,
180
+ ...squadResults.issues,
181
+ ...ruleResults.issues,
182
+ ...skillResults.issues,
183
+ ];
184
+
185
+ if (options.json) {
186
+ console.log(JSON.stringify({
187
+ version: pkg.version,
188
+ health: percentage,
189
+ score: `${totalScore}/${totalMax}`,
190
+ hooks: { connected: hookResults.connected, score: `${hookResults.score}/${hookResults.max}` },
191
+ squads: { count: squadResults.squads.length, score: `${squadResults.score}/${squadResults.max}` },
192
+ rules: { count: ruleResults.count, score: `${ruleResults.score}/${ruleResults.max}` },
193
+ skills: { total: skillResults.total, pathActivated: skillResults.pathActivated, score: `${skillResults.score}/${skillResults.max}` },
194
+ issues: allIssues,
195
+ }, null, 2));
196
+ return;
197
+ }
198
+
199
+ // Pretty output
200
+ const bar = (score, max) => {
201
+ const filled = Math.round((score / max) * 10);
202
+ return '█'.repeat(filled) + '░'.repeat(10 - filled);
203
+ };
204
+
205
+ console.log(`\n SINAPSE Health Report — v${pkg.version}`);
206
+ console.log(` ${'═'.repeat(50)}\n`);
207
+ console.log(` Overall Health: ${percentage}% ${bar(totalScore, totalMax)} (${totalScore}/${totalMax})\n`);
208
+ console.log(` Hooks: ${bar(hookResults.score, hookResults.max)} ${hookResults.connected || 0} connected`);
209
+ console.log(` Squads: ${bar(squadResults.score, squadResults.max)} ${squadResults.squads.length} squads`);
210
+ console.log(` Rules: ${bar(ruleResults.score, ruleResults.max)} ${ruleResults.count || 0} rules`);
211
+ console.log(` Skills: ${bar(skillResults.score, skillResults.max)} ${skillResults.pathActivated || 0} path-activated\n`);
212
+
213
+ if (allIssues.length > 0) {
214
+ console.log(` Issues (${allIssues.length}):`);
215
+ for (const issue of allIssues.slice(0, 10)) {
216
+ const icon = issue.severity === 'critical' ? '🔴' : issue.severity === 'high' ? '🟠' : issue.severity === 'medium' ? '🟡' : '🔵';
217
+ console.log(` ${icon} ${issue.msg}`);
218
+ }
219
+ if (allIssues.length > 10) console.log(` ... and ${allIssues.length - 10} more`);
220
+ console.log('');
221
+ } else {
222
+ console.log(' ✅ No issues found!\n');
223
+ }
224
+ }
225
+
226
+ module.exports = { runHealth };
227
+
228
+ if (require.main === module) {
229
+ const args = process.argv.slice(2);
230
+ runHealth({
231
+ json: args.includes('--json'),
232
+ fix: args.includes('--fix'),
233
+ }).catch((err) => {
234
+ console.error(`Error: ${err.message}`);
235
+ process.exit(1);
236
+ });
237
+ }