sinapse-ai 7.5.2 → 7.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.claude/hooks/enforce-architecture-first.cjs +144 -0
  2. package/.claude/hooks/enforce-delegation.cjs +142 -0
  3. package/.claude/hooks/enforce-story-gate.cjs +194 -0
  4. package/.claude/hooks/secret-scanning.cjs +155 -0
  5. package/.claude/hooks/write-path-validation.cjs +132 -0
  6. package/.claude/rules/agent-handoff.md +12 -0
  7. package/.claude/rules/cross-squad-routing.md +47 -0
  8. package/.claude/rules/hook-governance.md +61 -0
  9. package/.claude/rules/security-scanning.md +42 -0
  10. package/.sinapse-ai/cli/commands/health/index.js +237 -0
  11. package/.sinapse-ai/cli/commands/performance/index.js +157 -0
  12. package/.sinapse-ai/cli/commands/routing-intel/index.js +176 -0
  13. package/.sinapse-ai/development/templates/agent-handoff-tmpl.yaml +3 -0
  14. package/.sinapse-ai/install-manifest.yaml +17 -5
  15. package/bin/sinapse.js +34 -0
  16. package/package.json +1 -1
  17. package/squads/squad-claude/agents/swarm-orqx.md +28 -0
  18. package/squads/squad-claude/preferences/README.md +15 -0
  19. package/squads/squad-claude/squad.yaml +13 -0
  20. package/squads/squad-commercial/agents/commercial-orqx.md +44 -3
  21. package/squads/squad-commercial/squad.yaml +7 -0
  22. package/squads/squad-council/preferences/README.md +15 -0
  23. package/squads/squad-council/squad.yaml +7 -0
  24. package/squads/squad-cybersecurity/preferences/README.md +15 -0
  25. package/squads/squad-cybersecurity/squad.yaml +12 -0
  26. package/squads/squad-finance/preferences/README.md +15 -0
  27. package/squads/squad-growth/agents/growth-orqx.md +30 -0
  28. package/squads/squad-growth/squad.yaml +7 -0
  29. package/squads/squad-paidmedia/preferences/README.md +15 -0
  30. package/squads/squad-product/squad.yaml +7 -0
  31. package/squads/squad-storytelling/preferences/README.md +15 -0
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: Write Path Validation (CJS port)
6
+ *
7
+ * RULE: Documentation files should go to the correct paths per conventions.
8
+ * This hook WARNS (never blocks) when a doc path looks wrong.
9
+ *
10
+ * Protocol: always exit 0 (warn-only, never blocks).
11
+ *
12
+ * @module write-path-validation
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Configuration
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const PATH_RULES = [
23
+ {
24
+ namePatterns: [/session/i, /handoff/i, /^2\d{3}-\d{2}-\d{2}/],
25
+ expectedPath: 'docs/sessions/',
26
+ description: 'Session logs e handoffs → docs/sessions/YYYY-MM/',
27
+ },
28
+ {
29
+ namePatterns: [/architecture/i, /system-design/i, /infra/i],
30
+ expectedPath: 'docs/architecture/',
31
+ description: 'Docs de arquitetura → docs/architecture/',
32
+ excludePatterns: [/ARCHITECTURE_RULES/i],
33
+ },
34
+ {
35
+ namePatterns: [/guide/i, /tutorial/i, /how-to/i],
36
+ expectedPath: 'docs/guides/',
37
+ description: 'Guias e tutoriais → docs/guides/',
38
+ },
39
+ {
40
+ namePatterns: [/prd\.md$/i, /epic.*\.md$/i, /story.*\.md$/i],
41
+ expectedPath: 'docs/projects/',
42
+ description: 'PRDs, Epics, Stories → docs/projects/{project}/',
43
+ },
44
+ {
45
+ namePatterns: [/mind.*specific/i, /mind.*validation/i],
46
+ expectedPath: 'outputs/minds/',
47
+ description: 'Docs de mind → outputs/minds/{slug}/docs/',
48
+ },
49
+ ];
50
+
51
+ const ALWAYS_VALID = [
52
+ '.claude/', '.sinapse-ai/', '.sinapse-upstream/', 'squads/',
53
+ 'node_modules/', '.git/', 'app/', 'supabase/', 'outputs/',
54
+ ];
55
+
56
+ const DOC_EXTENSIONS = ['.md', '.mdx', '.txt', '.rst'];
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Helpers
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function projectRoot() {
63
+ return process.env.CLAUDE_PROJECT_DIR || process.cwd();
64
+ }
65
+
66
+ function relativize(filePath, root) {
67
+ if (filePath.startsWith(root)) return filePath.slice(root.length).replace(/^[/\\]+/, '');
68
+ return filePath;
69
+ }
70
+
71
+ function isAlwaysValid(rel) {
72
+ return ALWAYS_VALID.some((v) => rel.startsWith(v));
73
+ }
74
+
75
+ function isDocFile(rel) {
76
+ return DOC_EXTENSIONS.some((ext) => rel.endsWith(ext));
77
+ }
78
+
79
+ function checkRules(rel) {
80
+ const filename = path.basename(rel);
81
+ for (const rule of PATH_RULES) {
82
+ const matchesName = rule.namePatterns.some((p) => p.test(filename));
83
+ if (!matchesName) continue;
84
+
85
+ if (rule.excludePatterns && rule.excludePatterns.some((p) => p.test(filename))) continue;
86
+
87
+ if (!rel.startsWith(rule.expectedPath)) {
88
+ return { currentPath: rel, expectedPath: rule.expectedPath, description: rule.description };
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Main
96
+ // ---------------------------------------------------------------------------
97
+
98
+ function main() {
99
+ let input;
100
+ try {
101
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
102
+ } catch {
103
+ process.exit(0);
104
+ }
105
+
106
+ const toolName = input.tool_name || '';
107
+ if (toolName !== 'Write' && toolName !== 'Edit') process.exit(0);
108
+
109
+ const filePath = (input.tool_input || {}).file_path || '';
110
+ if (!filePath) process.exit(0);
111
+
112
+ const root = projectRoot();
113
+ const rel = relativize(filePath, root);
114
+
115
+ if (isAlwaysValid(rel)) process.exit(0);
116
+ if (!isDocFile(rel)) process.exit(0);
117
+
118
+ const violation = checkRules(rel);
119
+ if (!violation) process.exit(0);
120
+
121
+ // WARN only — never block
122
+ process.stderr.write(
123
+ `\nPATH WARNING: Document may be in the wrong location.\n` +
124
+ ` File: ${violation.currentPath}\n` +
125
+ ` Expected: ${violation.expectedPath}\n` +
126
+ ` Rule: ${violation.description}\n` +
127
+ ` NOTE: This is a WARNING only — the operation will proceed.\n`,
128
+ );
129
+ process.exit(0);
130
+ }
131
+
132
+ main();
@@ -44,6 +44,18 @@ The incoming agent receives:
44
44
  2. The **handoff artifact** from the previous agent (compact summary)
45
45
  3. **NOT** the previous agent's full persona/instructions/tool definitions
46
46
 
47
+ ### Scratchpad Protocol (v1.1)
48
+
49
+ **Before starting work**, the incoming agent MUST:
50
+ 1. Check if `.sinapse/scratchpad/{story-id}/` exists
51
+ 2. If yes, read ALL files in that directory (discoveries from previous agents)
52
+ 3. Use those insights to inform decisions (avoid rediscovering known issues)
53
+
54
+ **Before handing off**, the outgoing agent SHOULD:
55
+ 1. Write key discoveries to `.sinapse/scratchpad/{story-id}/{agent-id}.md`
56
+ 2. Include the scratchpad path in the handoff artifact `scratchpad_path` field
57
+ 3. Keep each file under 2KB (focused insights, not logs)
58
+
47
59
  ### Compaction Limits
48
60
 
49
61
  | Limit | Value |
@@ -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
+ }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * sinapse performance — Squad & Agent Performance Ranking
6
+ *
7
+ * Analyzes squad and agent completeness, ranking by:
8
+ * - Asset coverage (agents, tasks, KBs, workflows, templates, checklists)
9
+ * - Metadata completeness
10
+ * - Preferences tracking
11
+ * - Orchestrator enrichment level
12
+ *
13
+ * Usage:
14
+ * sinapse performance # Full ranking
15
+ * sinapse performance --json # JSON output
16
+ * sinapse performance --top 5 # Top 5 only
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ function findProjectRoot() {
23
+ let dir = process.cwd();
24
+ while (dir !== path.dirname(dir)) {
25
+ if (fs.existsSync(path.join(dir, '.sinapse-ai'))) return dir;
26
+ dir = path.dirname(dir);
27
+ }
28
+ return process.cwd();
29
+ }
30
+
31
+ function analyzeSquad(squadDir, squadName) {
32
+ const result = {
33
+ name: squadName,
34
+ agents: 0,
35
+ tasks: 0,
36
+ kbs: 0,
37
+ workflows: 0,
38
+ templates: 0,
39
+ checklists: 0,
40
+ hasMetadata: false,
41
+ hasPreferences: false,
42
+ orchestratorLines: 0,
43
+ score: 0,
44
+ };
45
+
46
+ // Count assets
47
+ const countMd = (subdir) => {
48
+ const dir = path.join(squadDir, subdir);
49
+ if (!fs.existsSync(dir)) return 0;
50
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
51
+ };
52
+
53
+ const countAny = (subdir) => {
54
+ const dir = path.join(squadDir, subdir);
55
+ if (!fs.existsSync(dir)) return 0;
56
+ return fs.readdirSync(dir).filter((f) => !f.startsWith('.')).length;
57
+ };
58
+
59
+ result.agents = countMd('agents');
60
+ result.tasks = countMd('tasks');
61
+ // Try both naming conventions
62
+ result.kbs = countAny('knowledge-bases') || countAny('knowledge-base');
63
+ result.workflows = countAny('workflows');
64
+ result.templates = countMd('templates') || countAny('templates');
65
+ result.checklists = countMd('checklists') || countAny('checklists');
66
+
67
+ // Metadata check
68
+ const yamlPath = path.join(squadDir, 'squad.yaml');
69
+ if (fs.existsSync(yamlPath)) {
70
+ const content = fs.readFileSync(yamlPath, 'utf8');
71
+ result.hasMetadata = /agents_count|total_files/.test(content);
72
+ }
73
+
74
+ // Preferences check
75
+ result.hasPreferences = fs.existsSync(path.join(squadDir, 'preferences'));
76
+
77
+ // Orchestrator enrichment
78
+ const orqxFile = fs.readdirSync(path.join(squadDir, 'agents')).find((f) => f.includes('-orqx'));
79
+ if (orqxFile) {
80
+ const content = fs.readFileSync(path.join(squadDir, 'agents', orqxFile), 'utf8');
81
+ result.orchestratorLines = content.split('\n').length;
82
+ }
83
+
84
+ // Calculate score (weighted)
85
+ result.score =
86
+ (result.agents >= 6 ? 15 : result.agents * 2.5) +
87
+ (result.tasks >= 50 ? 25 : result.tasks * 0.5) +
88
+ (result.kbs >= 8 ? 15 : result.kbs * 1.875) +
89
+ (result.workflows >= 3 ? 10 : result.workflows * 3.33) +
90
+ (result.templates >= 3 ? 10 : result.templates * 3.33) +
91
+ (result.checklists >= 2 ? 5 : result.checklists * 2.5) +
92
+ (result.hasMetadata ? 5 : 0) +
93
+ (result.hasPreferences ? 5 : 0) +
94
+ (result.orchestratorLines >= 100 ? 10 : result.orchestratorLines * 0.1);
95
+
96
+ result.score = Math.round(result.score * 10) / 10;
97
+
98
+ return result;
99
+ }
100
+
101
+ async function runPerformance(options = {}) {
102
+ const root = findProjectRoot();
103
+ const squadsDir = path.join(root, 'squads');
104
+
105
+ if (!fs.existsSync(squadsDir)) {
106
+ console.error('No squads/ directory found.');
107
+ process.exit(1);
108
+ }
109
+
110
+ const squadDirs = fs.readdirSync(squadsDir)
111
+ .filter((d) => d.startsWith('squad-') && fs.statSync(path.join(squadsDir, d)).isDirectory());
112
+
113
+ const rankings = squadDirs
114
+ .map((d) => analyzeSquad(path.join(squadsDir, d), d))
115
+ .sort((a, b) => b.score - a.score);
116
+
117
+ const limit = options.top || rankings.length;
118
+ const display = rankings.slice(0, limit);
119
+
120
+ if (options.json) {
121
+ console.log(JSON.stringify({ rankings: display, total: rankings.length }, null, 2));
122
+ return;
123
+ }
124
+
125
+ // Pretty output
126
+ console.log(`\n SINAPSE Performance Ranking — ${rankings.length} Squads`);
127
+ console.log(` ${'═'.repeat(60)}\n`);
128
+
129
+ const maxScore = rankings[0]?.score || 100;
130
+
131
+ display.forEach((sq, i) => {
132
+ const rank = i + 1;
133
+ const barLen = Math.round((sq.score / maxScore) * 20);
134
+ const bar = '█'.repeat(barLen) + '░'.repeat(20 - barLen);
135
+ const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : `#${rank}`;
136
+ const meta = sq.hasMetadata ? '✓' : '✗';
137
+ const pref = sq.hasPreferences ? '✓' : '✗';
138
+
139
+ console.log(` ${String(medal).padEnd(4)} ${sq.name.padEnd(25)} ${bar} ${sq.score.toFixed(1)}`);
140
+ console.log(` ${sq.agents}a ${sq.tasks}t ${sq.kbs}kb ${sq.workflows}wf ${sq.templates}tpl ${sq.checklists}ck | meta:${meta} pref:${pref} orqx:${sq.orchestratorLines}L`);
141
+ console.log('');
142
+ });
143
+ }
144
+
145
+ module.exports = { runPerformance };
146
+
147
+ if (require.main === module) {
148
+ const args = process.argv.slice(2);
149
+ const topIdx = args.indexOf('--top');
150
+ runPerformance({
151
+ json: args.includes('--json'),
152
+ top: topIdx >= 0 ? parseInt(args[topIdx + 1], 10) : undefined,
153
+ }).catch((err) => {
154
+ console.error(`Error: ${err.message}`);
155
+ process.exit(1);
156
+ });
157
+ }