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.
- package/.claude/hooks/enforce-architecture-first.cjs +144 -0
- package/.claude/hooks/enforce-delegation.cjs +142 -0
- package/.claude/hooks/enforce-story-gate.cjs +194 -0
- package/.claude/hooks/secret-scanning.cjs +155 -0
- package/.claude/hooks/write-path-validation.cjs +132 -0
- package/.claude/rules/agent-handoff.md +12 -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/development/templates/agent-handoff-tmpl.yaml +3 -0
- package/.sinapse-ai/install-manifest.yaml +17 -5
- package/bin/sinapse.js +34 -0
- package/package.json +1 -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,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
|
+
}
|