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.
- package/.claude/hooks/secret-scanning.cjs +155 -0
- package/.claude/rules/cross-squad-routing.md +47 -0
- package/.claude/rules/hook-governance.md +61 -0
- package/.claude/rules/security-scanning.md +42 -0
- package/.sinapse-ai/cli/commands/health/index.js +237 -0
- package/.sinapse-ai/cli/commands/performance/index.js +157 -0
- package/.sinapse-ai/cli/commands/routing-intel/index.js +176 -0
- package/.sinapse-ai/install-manifest.yaml +15 -3
- package/bin/sinapse.js +34 -0
- package/package.json +1 -1
- package/packages/installer/src/wizard/ide-config-generator.js +33 -1
- package/squads/squad-claude/agents/swarm-orqx.md +28 -0
- package/squads/squad-claude/preferences/README.md +15 -0
- package/squads/squad-claude/squad.yaml +13 -0
- package/squads/squad-commercial/agents/commercial-orqx.md +44 -3
- package/squads/squad-commercial/squad.yaml +7 -0
- package/squads/squad-council/preferences/README.md +15 -0
- package/squads/squad-council/squad.yaml +7 -0
- package/squads/squad-cybersecurity/preferences/README.md +15 -0
- package/squads/squad-cybersecurity/squad.yaml +12 -0
- package/squads/squad-finance/preferences/README.md +15 -0
- package/squads/squad-growth/agents/growth-orqx.md +30 -0
- package/squads/squad-growth/squad.yaml +7 -0
- package/squads/squad-paidmedia/preferences/README.md +15 -0
- package/squads/squad-product/squad.yaml +7 -0
- package/squads/squad-storytelling/preferences/README.md +15 -0
|
@@ -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
|
+
}
|