sinapse-ai 1.6.1 → 1.8.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/CLAUDE.md +5 -11
- package/.claude/hooks/README.md +14 -1
- package/.claude/hooks/code-intel-pretool.cjs +115 -0
- package/.claude/hooks/enforce-delegation.cjs +31 -3
- package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
- package/.claude/hooks/enforce-permission-mode.cjs +249 -0
- package/.claude/hooks/secret-scanning.cjs +34 -43
- package/.claude/hooks/synapse-engine.cjs +23 -23
- package/.claude/hooks/telemetry-post-tool.cjs +128 -0
- package/.claude/hooks/telemetry-stop.cjs +132 -0
- package/.claude/hooks/verify-packages.cjs +9 -2
- package/.claude/rules/documentation-first.md +1 -1
- package/.claude/rules/hook-governance.md +2 -0
- package/.sinapse-ai/cli/commands/health/index.js +24 -0
- package/.sinapse-ai/core/README.md +11 -0
- package/.sinapse-ai/core/config/config-loader.js +19 -0
- package/.sinapse-ai/core/config/merge-utils.js +8 -0
- package/.sinapse-ai/core/errors/constants.js +147 -0
- package/.sinapse-ai/core/errors/error-registry.js +176 -0
- package/.sinapse-ai/core/errors/index.js +50 -0
- package/.sinapse-ai/core/errors/serializer.js +147 -0
- package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
- package/.sinapse-ai/core/errors/utils.js +187 -0
- package/.sinapse-ai/core/execution/build-orchestrator.js +47 -49
- package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
- package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
- package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
- package/.sinapse-ai/core/execution/subagent-dispatcher.js +201 -60
- package/.sinapse-ai/core/execution/wave-executor.js +4 -1
- package/.sinapse-ai/core/grounding/README.md +71 -11
- package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
- package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
- package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
- package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
- package/.sinapse-ai/core/health-check/healers/index.js +40 -3
- package/.sinapse-ai/core/ideation/ideation-engine.js +212 -107
- package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
- package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
- package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
- package/.sinapse-ai/core/ids/index.js +30 -0
- package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
- package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
- package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
- package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
- package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
- package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
- package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
- package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
- package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
- package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
- package/.sinapse-ai/core/orchestration/master-orchestrator.js +150 -10
- package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
- package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
- package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
- package/.sinapse-ai/core/registry/registry-loader.js +71 -5
- package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
- package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
- package/.sinapse-ai/core/synapse/context/index.js +19 -0
- package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
- package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
- package/.sinapse-ai/core/synapse/engine.js +43 -3
- package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
- package/.sinapse-ai/core/utils/output-formatter.js +8 -290
- package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
- package/.sinapse-ai/core-config.yaml +68 -1
- package/.sinapse-ai/data/entity-registry.yaml +15082 -13618
- package/.sinapse-ai/data/registry-update-log.jsonl +143 -0
- package/.sinapse-ai/development/agents/developer.md +2 -0
- package/.sinapse-ai/development/agents/devops.md +9 -0
- package/.sinapse-ai/development/external-executors/README.md +18 -0
- package/.sinapse-ai/development/external-executors/codex.md +56 -0
- package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
- package/.sinapse-ai/development/scripts/squad/squad-downloader.js +169 -14
- package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
- package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
- package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
- package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
- package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +5 -8
- package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +6 -9
- package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
- package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
- package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
- package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
- package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
- package/.sinapse-ai/install-manifest.yaml +218 -114
- package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
- package/.sinapse-ai/scripts/pm.sh +18 -6
- package/bin/cli.js +17 -0
- package/bin/commands/agents.js +96 -0
- package/bin/commands/doctor.js +15 -0
- package/bin/commands/ideate.js +129 -0
- package/bin/commands/uninstall.js +40 -0
- package/bin/postinstall.js +50 -4
- package/bin/sinapse.js +146 -2
- package/bin/utils/secret-scanner-core.js +253 -0
- package/bin/utils/staged-secret-scan.js +106 -40
- package/docs/framework/collaboration-autonomy-plan.md +18 -18
- package/docs/guides/parallel-workflow.md +6 -6
- package/package.json +22 -5
- package/packages/installer/src/installer/git-hooks-installer.js +384 -0
- package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
- package/packages/installer/src/wizard/ide-config-generator.js +23 -0
- package/packages/installer/src/wizard/validators.js +38 -1
- package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
- package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
- package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
- package/scripts/eval-runner.js +422 -0
- package/scripts/generate-install-manifest.js +13 -9
- package/scripts/generate-synapse-runtime.js +51 -0
- package/scripts/regenerate-orqx-stubs.ps1 +6 -5
- package/scripts/validate-all.js +1 -0
- package/scripts/validate-evals.js +466 -0
- package/scripts/validate-schemas.js +539 -0
- package/scripts/validate-squad-orqx.js +9 -2
- package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
- package/squads/squad-brand/templates/client-delivery-template.md +1 -1
- package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
- package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
- package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
- package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
- package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
- package/docs/chrome-brain-upgrade-plan.md +0 -624
- package/docs/constitution-compliance.md +0 -87
- package/docs/mega-upgrade-orchestration-plan.md +0 -71
- package/docs/research-synthesis-for-upgrade.md +0 -511
- package/docs/security-audit-report.md +0 -306
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* @see Epic GEMINI-INT - Story 2: AI Provider Factory Pattern
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
11
|
const { AIProvider } = require('./ai-provider');
|
|
12
|
+
// runSafe (cross-spawn): resolves claude.cmd on Windows and delivers the prompt
|
|
13
|
+
// via stdin/argv — same hardened spawn path used by the SubagentDispatcher.
|
|
14
|
+
const { runSafe } = require('../../../core/utils/spawn-safe');
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Claude Code provider implementation
|
|
@@ -20,7 +23,7 @@ class ClaudeProvider extends AIProvider {
|
|
|
20
23
|
/**
|
|
21
24
|
* Create a Claude provider
|
|
22
25
|
* @param {Object} [config={}] - Provider configuration
|
|
23
|
-
* @param {string} [config.model
|
|
26
|
+
* @param {string} [config.model] - Model override; omitted → CLI default model
|
|
24
27
|
* @param {number} [config.timeout=300000] - Execution timeout
|
|
25
28
|
* @param {boolean} [config.dangerouslySkipPermissions=false] - Skip permission prompts
|
|
26
29
|
*/
|
|
@@ -31,7 +34,9 @@ class ClaudeProvider extends AIProvider {
|
|
|
31
34
|
timeout: config.timeout || 300000,
|
|
32
35
|
maxRetries: config.maxRetries || 3,
|
|
33
36
|
options: {
|
|
34
|
-
model:
|
|
37
|
+
// No hardcoded model: stale IDs break the CLI. Only pass --model when
|
|
38
|
+
// a caller explicitly configures one; otherwise use the CLI's default.
|
|
39
|
+
model: config.model || null,
|
|
35
40
|
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
36
41
|
...config,
|
|
37
42
|
},
|
|
@@ -82,59 +87,56 @@ class ClaudeProvider extends AIProvider {
|
|
|
82
87
|
args.push('--model', options.model || this.options.model);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
env: { ...process.env, ...options.env },
|
|
93
|
-
windowsHide: true,
|
|
94
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Write prompt via stdin to avoid shell injection
|
|
98
|
-
child.stdin.write(prompt);
|
|
99
|
-
child.stdin.end();
|
|
100
|
-
|
|
101
|
-
const timeoutId = setTimeout(() => {
|
|
102
|
-
child.kill('SIGTERM');
|
|
103
|
-
reject(new Error(`Claude execution timed out after ${timeout}ms`));
|
|
104
|
-
}, timeout);
|
|
105
|
-
|
|
106
|
-
child.stdout.on('data', (data) => {
|
|
107
|
-
stdout += data.toString();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
child.stderr.on('data', (data) => {
|
|
111
|
-
stderr += data.toString();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
child.on('close', (code) => {
|
|
115
|
-
clearTimeout(timeoutId);
|
|
116
|
-
const duration = Date.now() - startTime;
|
|
117
|
-
|
|
118
|
-
if (code === 0) {
|
|
119
|
-
resolve({
|
|
120
|
-
success: true,
|
|
121
|
-
output: stdout.trim(),
|
|
122
|
-
metadata: {
|
|
123
|
-
duration,
|
|
124
|
-
provider: 'claude',
|
|
125
|
-
model: options.model || this.options.model,
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
} else {
|
|
129
|
-
reject(new Error(`Claude exited with code ${code}: ${stderr || stdout}`));
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
child.on('error', (error) => {
|
|
134
|
-
clearTimeout(timeoutId);
|
|
135
|
-
reject(new Error(`Claude spawn error: ${error.message}`));
|
|
136
|
-
});
|
|
90
|
+
// runSafe: argv-based spawn via cross-spawn (resolves claude.cmd on Windows),
|
|
91
|
+
// prompt delivered through stdin — shell injection structurally impossible.
|
|
92
|
+
const result = await runSafe(this.command, args, {
|
|
93
|
+
cwd: workingDir,
|
|
94
|
+
env: { ...process.env, ...options.env },
|
|
95
|
+
timeout,
|
|
96
|
+
input: prompt,
|
|
137
97
|
});
|
|
98
|
+
|
|
99
|
+
const duration = Date.now() - startTime;
|
|
100
|
+
|
|
101
|
+
const stdout = (result.stdout || '').trim();
|
|
102
|
+
const stderr = (result.stderr || '').trim();
|
|
103
|
+
|
|
104
|
+
if (result.success) {
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
output: stdout,
|
|
108
|
+
metadata: {
|
|
109
|
+
duration,
|
|
110
|
+
provider: 'claude',
|
|
111
|
+
model: options.model || this.options.model || 'cli-default',
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (result.signal) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Claude killed by signal ${result.signal} (timeout ${timeout}ms?): ${stderr || stdout}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// User-environment hooks (SessionEnd etc.) can fail AFTER the model already
|
|
123
|
+
// printed its full response, poisoning the exit code. In --print mode a
|
|
124
|
+
// non-empty stdout + hook-related stderr means the work was done — accept it
|
|
125
|
+
// (with a warning) instead of discarding paid output and retrying.
|
|
126
|
+
if (stdout.length > 0 && /hook/i.test(stderr)) {
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
output: stdout,
|
|
130
|
+
metadata: {
|
|
131
|
+
duration,
|
|
132
|
+
provider: 'claude',
|
|
133
|
+
model: options.model || this.options.model || 'cli-default',
|
|
134
|
+
warning: `non-zero exit (${result.code}) caused by environment hook failure: ${stderr.slice(0, 200)}`,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw new Error(`Claude exited with code ${result.code}: ${stderr || stdout}`);
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @see Story 3.20 - PM Tool-Agnostic Integration (TR-3.20.3)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const { execFileSync } = require('child_process');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const yaml = require('js-yaml');
|
|
13
13
|
const { PMAdapter } = require('../../scripts/pm-adapter');
|
|
@@ -293,8 +293,10 @@ class GitHubProjectsAdapter extends PMAdapter {
|
|
|
293
293
|
* @returns {string} Command output
|
|
294
294
|
*/
|
|
295
295
|
_execGH(args) {
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
// Pass args as an argv array to execFileSync — no shell is spawned, so each
|
|
297
|
+
// argument is delivered to `gh` verbatim. This prevents shell-metacharacter
|
|
298
|
+
// injection (e.g. $(...), backticks, ;, &&, |) from story-derived title/body.
|
|
299
|
+
return execFileSync('gh', args, { encoding: 'utf8' });
|
|
298
300
|
}
|
|
299
301
|
|
|
300
302
|
/**
|
|
@@ -360,8 +362,8 @@ class GitHubProjectsAdapter extends PMAdapter {
|
|
|
360
362
|
|
|
361
363
|
const result = this._execGH([
|
|
362
364
|
'issue', 'create',
|
|
363
|
-
'--title',
|
|
364
|
-
'--body',
|
|
365
|
+
'--title', title,
|
|
366
|
+
'--body', body,
|
|
365
367
|
'--label', labels.join(','),
|
|
366
368
|
]);
|
|
367
369
|
|
|
@@ -383,8 +385,8 @@ class GitHubProjectsAdapter extends PMAdapter {
|
|
|
383
385
|
|
|
384
386
|
this._execGH([
|
|
385
387
|
'issue', 'edit', issueNumber.toString(),
|
|
386
|
-
'--title',
|
|
387
|
-
'--body',
|
|
388
|
+
'--title', title,
|
|
389
|
+
'--body', body,
|
|
388
390
|
]);
|
|
389
391
|
}
|
|
390
392
|
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gemini Commands Sync — generates .gemini/commands/*.toml from parsed agent data.
|
|
5
|
+
*
|
|
6
|
+
* Writes one TOML file per agent plus a sinapse-menu.toml launcher.
|
|
7
|
+
* Called by index.js after agents are parsed; the index.js caller is responsible
|
|
8
|
+
* for registering this as the Gemini IDE target handler.
|
|
9
|
+
*
|
|
10
|
+
* @story 5.1 - IDE Sync Expansion
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs-extra');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const FALLBACK_DESCRIPTION = 'Agente especializado SINAPSE';
|
|
17
|
+
const MAX_DESCRIPTION_CONTEXT = 120;
|
|
18
|
+
|
|
19
|
+
const MENU_ORDER = [
|
|
20
|
+
'sinapse-orqx',
|
|
21
|
+
'analyst',
|
|
22
|
+
'architect',
|
|
23
|
+
'data-engineer',
|
|
24
|
+
'developer',
|
|
25
|
+
'devops',
|
|
26
|
+
'project-lead',
|
|
27
|
+
'product-lead',
|
|
28
|
+
'quality-gate',
|
|
29
|
+
'sprint-lead',
|
|
30
|
+
'squad-creator',
|
|
31
|
+
'ux-design-expert',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Strip the sinapse- prefix so the slug is short and readable.
|
|
36
|
+
* @param {string} agentId
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function commandSlugForAgent(agentId) {
|
|
40
|
+
if (agentId.startsWith('sinapse-')) {
|
|
41
|
+
return agentId.replace(/^sinapse-/, '');
|
|
42
|
+
}
|
|
43
|
+
return agentId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return the Gemini slash-command name for an agent.
|
|
48
|
+
* Example: "developer" → "/sinapse-developer"
|
|
49
|
+
* @param {string} agentId
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function menuCommandName(agentId) {
|
|
53
|
+
return `/sinapse-${commandSlugForAgent(agentId)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Collapse internal whitespace and trim.
|
|
58
|
+
* @param {string} text
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function normalizeText(text) {
|
|
62
|
+
if (!text || typeof text !== 'string') return '';
|
|
63
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Truncate to maxLen characters with an ellipsis.
|
|
68
|
+
* @param {string} text
|
|
69
|
+
* @param {number} [maxLen]
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function truncateText(text, maxLen = MAX_DESCRIPTION_CONTEXT) {
|
|
73
|
+
if (!text || text.length <= maxLen) return text;
|
|
74
|
+
return `${text.slice(0, maxLen - 1).trimEnd()}…`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract a concise one-liner from a whenToUse block.
|
|
79
|
+
* Drops redirect/negative-guidance sections before truncating.
|
|
80
|
+
* @param {string} whenToUse
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function summarizeWhenToUse(whenToUse) {
|
|
84
|
+
const normalized = normalizeText(whenToUse);
|
|
85
|
+
if (!normalized) return '';
|
|
86
|
+
|
|
87
|
+
// Drop redirect/negative guidance sections that are useful for routing, not for menu labels.
|
|
88
|
+
const withoutNegativeSection = normalized.split(/\b(?:NOT\s+for|NÃO\s+para)\b/i)[0].trim();
|
|
89
|
+
const primary = withoutNegativeSection || normalized;
|
|
90
|
+
|
|
91
|
+
// Keep only the first sentence/chunk for concise autocomplete labels.
|
|
92
|
+
const firstChunk = primary.split(/[.;!?](?:\s|$)/)[0].trim();
|
|
93
|
+
return truncateText(firstChunk || primary);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Escape a string for embedding inside a TOML double-quoted scalar.
|
|
98
|
+
* @param {string} text
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function escapeTomlString(text) {
|
|
102
|
+
return String(text || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build a human-readable one-liner description for an agent entry.
|
|
107
|
+
* Prefers title + whenToUse summary; falls back to SINAPSE generic.
|
|
108
|
+
* @param {object} agent - Parsed agent object from agent-parser
|
|
109
|
+
* @returns {string}
|
|
110
|
+
*/
|
|
111
|
+
function buildAgentDescription(agent) {
|
|
112
|
+
const agentData = agent.agent || {};
|
|
113
|
+
const title = normalizeText(agentData.title);
|
|
114
|
+
const whenToUseSummary = summarizeWhenToUse(agentData.whenToUse);
|
|
115
|
+
|
|
116
|
+
if (title && whenToUseSummary) {
|
|
117
|
+
return `${title} (${whenToUseSummary})`;
|
|
118
|
+
}
|
|
119
|
+
if (title) {
|
|
120
|
+
return title;
|
|
121
|
+
}
|
|
122
|
+
if (whenToUseSummary) {
|
|
123
|
+
return whenToUseSummary;
|
|
124
|
+
}
|
|
125
|
+
return `Ativar agente SINAPSE ${agent.id}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build the activation prompt stored in each agent's TOML file.
|
|
130
|
+
* References the agent definition at .gemini/rules/SINAPSE/agents/{id}.md.
|
|
131
|
+
* @param {string} agentId
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
function buildAgentCommandPrompt(agentId) {
|
|
135
|
+
return [
|
|
136
|
+
`Ative o agente ${agentId}:`,
|
|
137
|
+
`1. Leia a definição completa em .gemini/rules/SINAPSE/agents/${agentId}.md`,
|
|
138
|
+
'2. Siga as activation-instructions do bloco YAML',
|
|
139
|
+
`3. Renderize o greeting via: node .sinapse-ai/development/scripts/generate-greeting.js ${agentId}`,
|
|
140
|
+
' Se shell nao disponivel, exiba o greeting de persona_profile.communication.greeting_levels.named',
|
|
141
|
+
'4. Mostre Quick Commands e aguarde input do usuario',
|
|
142
|
+
'Mantenha a persona até *exit.',
|
|
143
|
+
].join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build the TOML content for a single agent command file.
|
|
148
|
+
* @param {string} agentId
|
|
149
|
+
* @param {string} [description]
|
|
150
|
+
* @returns {{ filename: string, content: string, agentId: string, description: string }}
|
|
151
|
+
*/
|
|
152
|
+
function buildAgentCommandFile(agentId, description = FALLBACK_DESCRIPTION) {
|
|
153
|
+
const slug = commandSlugForAgent(agentId);
|
|
154
|
+
|
|
155
|
+
const prompt = buildAgentCommandPrompt(agentId);
|
|
156
|
+
const content = [
|
|
157
|
+
`description = "${escapeTomlString(description)}"`,
|
|
158
|
+
'prompt = """',
|
|
159
|
+
prompt,
|
|
160
|
+
'"""',
|
|
161
|
+
'',
|
|
162
|
+
].join('\n');
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
filename: `sinapse-${slug}.toml`,
|
|
166
|
+
content,
|
|
167
|
+
agentId,
|
|
168
|
+
description,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build the multi-line prompt that lists all agents in the launcher menu.
|
|
174
|
+
* @param {Array<{ agentId: string, description: string }>} commandFiles
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
177
|
+
function buildMenuPrompt(commandFiles) {
|
|
178
|
+
const lines = [
|
|
179
|
+
'Você está no launcher SINAPSE para Gemini.',
|
|
180
|
+
'',
|
|
181
|
+
'Mostre a lista de agentes abaixo em formato numerado, explicando em 1 linha quando usar cada um:',
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
let index = 1;
|
|
185
|
+
for (const commandFile of commandFiles) {
|
|
186
|
+
lines.push(`${index}. ${menuCommandName(commandFile.agentId)} - ${commandFile.description}`);
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push('No final, peça para o usuário escolher um número ou digitar o comando direto.');
|
|
192
|
+
return lines.join('\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build the sinapse-menu.toml launcher file that lists all agents.
|
|
197
|
+
* @param {Array<{ agentId: string, description: string }>} commandFiles
|
|
198
|
+
* @returns {{ filename: string, content: string }}
|
|
199
|
+
*/
|
|
200
|
+
function buildMenuCommandFile(commandFiles) {
|
|
201
|
+
const content = [
|
|
202
|
+
'description = "Menu rápido SINAPSE (lista agentes e orienta qual ativar)"',
|
|
203
|
+
'prompt = """',
|
|
204
|
+
buildMenuPrompt(commandFiles),
|
|
205
|
+
'"""',
|
|
206
|
+
'',
|
|
207
|
+
].join('\n');
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
filename: 'sinapse-menu.toml',
|
|
211
|
+
content,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Resolve the display order for agent IDs.
|
|
217
|
+
* MENU_ORDER entries come first (in declared order), extras are sorted alphabetically.
|
|
218
|
+
* @param {string[]} agentIds
|
|
219
|
+
* @returns {string[]}
|
|
220
|
+
*/
|
|
221
|
+
function resolveAgentOrder(agentIds) {
|
|
222
|
+
const unique = [...new Set(agentIds)];
|
|
223
|
+
const known = MENU_ORDER.filter((id) => unique.includes(id));
|
|
224
|
+
const extra = unique.filter((id) => !MENU_ORDER.includes(id)).sort();
|
|
225
|
+
return [...known, ...extra];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build the full set of TOML command files for all valid agents.
|
|
230
|
+
* Returns an array of { filename, content, agentId, description } entries,
|
|
231
|
+
* with sinapse-menu.toml prepended as the first item.
|
|
232
|
+
* @param {object[]} agents - Array of parsed agent objects from agent-parser
|
|
233
|
+
* @returns {Array<{ filename: string, content: string, agentId: string, description: string }>}
|
|
234
|
+
*/
|
|
235
|
+
function buildGeminiCommandFiles(agents) {
|
|
236
|
+
const validAgents = agents
|
|
237
|
+
.filter((agent) => !agent.error)
|
|
238
|
+
.map((agent) => ({
|
|
239
|
+
id: agent.id,
|
|
240
|
+
description: buildAgentDescription(agent),
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
const ordered = resolveAgentOrder(validAgents.map((agent) => agent.id));
|
|
244
|
+
const byId = new Map(validAgents.map((agent) => [agent.id, agent]));
|
|
245
|
+
const files = ordered.map((id) => {
|
|
246
|
+
const meta = byId.get(id);
|
|
247
|
+
const description = meta?.description || FALLBACK_DESCRIPTION;
|
|
248
|
+
return buildAgentCommandFile(id, description);
|
|
249
|
+
});
|
|
250
|
+
files.unshift(buildMenuCommandFile(files));
|
|
251
|
+
return files;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Write all Gemini command TOML files to {projectRoot}/.gemini/commands/.
|
|
256
|
+
* In dry-run mode the directory is not created and files are not written,
|
|
257
|
+
* but the return value still lists what would have been written.
|
|
258
|
+
* @param {object[]} agents - Parsed agents
|
|
259
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
260
|
+
* @param {{ dryRun?: boolean }} [options]
|
|
261
|
+
* @returns {{ commandsDir: string, files: Array<{ filename: string, path: string, content: string }> }}
|
|
262
|
+
*/
|
|
263
|
+
function syncGeminiCommands(agents, projectRoot, options = {}) {
|
|
264
|
+
const commandsDir = path.join(projectRoot, '.gemini', 'commands');
|
|
265
|
+
const files = buildGeminiCommandFiles(agents);
|
|
266
|
+
const written = [];
|
|
267
|
+
|
|
268
|
+
if (!options.dryRun) {
|
|
269
|
+
fs.ensureDirSync(commandsDir);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const file of files) {
|
|
273
|
+
const targetPath = path.join(commandsDir, file.filename);
|
|
274
|
+
if (!options.dryRun) {
|
|
275
|
+
fs.writeFileSync(targetPath, file.content, 'utf8');
|
|
276
|
+
}
|
|
277
|
+
written.push({
|
|
278
|
+
filename: path.join('commands', file.filename),
|
|
279
|
+
path: targetPath,
|
|
280
|
+
content: file.content,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { commandsDir, files: written };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
FALLBACK_DESCRIPTION,
|
|
289
|
+
MENU_ORDER,
|
|
290
|
+
commandSlugForAgent,
|
|
291
|
+
menuCommandName,
|
|
292
|
+
buildAgentDescription,
|
|
293
|
+
summarizeWhenToUse,
|
|
294
|
+
truncateText,
|
|
295
|
+
escapeTomlString,
|
|
296
|
+
buildGeminiCommandFiles,
|
|
297
|
+
syncGeminiCommands,
|
|
298
|
+
};
|
|
@@ -24,8 +24,14 @@ const yaml = require('js-yaml');
|
|
|
24
24
|
const { parseAllAgents } = require('./agent-parser');
|
|
25
25
|
const { generateAllRedirects, writeRedirects } = require('./redirect-generator');
|
|
26
26
|
const { validateAllIdes, formatValidationReport } = require('./validator');
|
|
27
|
+
const { syncGeminiCommands, buildGeminiCommandFiles } = require('./gemini-commands');
|
|
28
|
+
|
|
27
29
|
// Transformers
|
|
28
30
|
const claudeCodeTransformer = require('./transformers/claude-code');
|
|
31
|
+
const cursorTransformer = require('./transformers/cursor');
|
|
32
|
+
const antigravityTransformer = require('./transformers/antigravity');
|
|
33
|
+
const githubCopilotTransformer = require('./transformers/github-copilot');
|
|
34
|
+
const kimiTransformer = require(path.resolve(__dirname, 'transformers', 'kimi'));
|
|
29
35
|
|
|
30
36
|
// ANSI colors for output
|
|
31
37
|
const colors = {
|
|
@@ -62,6 +68,31 @@ function loadConfig(projectRoot) {
|
|
|
62
68
|
path: '.codex/agents',
|
|
63
69
|
format: 'full-markdown-yaml',
|
|
64
70
|
},
|
|
71
|
+
gemini: {
|
|
72
|
+
enabled: true,
|
|
73
|
+
path: '.gemini/rules/SINAPSE/agents',
|
|
74
|
+
format: 'full-markdown-yaml',
|
|
75
|
+
},
|
|
76
|
+
'github-copilot': {
|
|
77
|
+
enabled: true,
|
|
78
|
+
path: '.github/agents',
|
|
79
|
+
format: 'github-copilot',
|
|
80
|
+
},
|
|
81
|
+
cursor: {
|
|
82
|
+
enabled: true,
|
|
83
|
+
path: '.cursor/rules/agents',
|
|
84
|
+
format: 'condensed-rules',
|
|
85
|
+
},
|
|
86
|
+
antigravity: {
|
|
87
|
+
enabled: true,
|
|
88
|
+
path: '.antigravity/rules/agents',
|
|
89
|
+
format: 'cursor-style',
|
|
90
|
+
},
|
|
91
|
+
kimi: {
|
|
92
|
+
enabled: true,
|
|
93
|
+
path: '.kimi/skills',
|
|
94
|
+
format: 'kimi-skill',
|
|
95
|
+
},
|
|
65
96
|
},
|
|
66
97
|
redirects: {
|
|
67
98
|
'sinapse-developer': 'sinapse-orqx',
|
|
@@ -100,9 +131,54 @@ function loadConfig(projectRoot) {
|
|
|
100
131
|
function getTransformer(format) {
|
|
101
132
|
const transformers = {
|
|
102
133
|
'full-markdown-yaml': claudeCodeTransformer,
|
|
134
|
+
'condensed-rules': cursorTransformer,
|
|
135
|
+
'cursor-style': antigravityTransformer,
|
|
136
|
+
'github-copilot': githubCopilotTransformer,
|
|
137
|
+
'kimi-skill': kimiTransformer,
|
|
103
138
|
};
|
|
104
139
|
|
|
105
|
-
|
|
140
|
+
const transformer = transformers[format];
|
|
141
|
+
if (!transformer) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`No transformer registered for format '${format}'. ` +
|
|
144
|
+
`Register it in getTransformer() before adding a target with this format. ` +
|
|
145
|
+
`Available formats: ${Object.keys(transformers).join(', ')}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return transformer;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the primary file content for an agent.
|
|
153
|
+
* Allows IDE-specific transform variants (e.g. transformCommand) when present,
|
|
154
|
+
* falling back to the standard transform() otherwise.
|
|
155
|
+
* @param {object} transformer - Transformer module
|
|
156
|
+
* @param {object} agent - Parsed agent data
|
|
157
|
+
* @param {string} ideName - IDE name
|
|
158
|
+
* @returns {string} - Transformed content
|
|
159
|
+
*/
|
|
160
|
+
function transformPrimaryContent(transformer, agent, ideName) {
|
|
161
|
+
if (ideName === 'claude-code' && typeof transformer.transformCommand === 'function') {
|
|
162
|
+
return transformer.transformCommand(agent);
|
|
163
|
+
}
|
|
164
|
+
return transformer.transform(agent);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Guard against path traversal: returns true only when candidatePath resolves
|
|
169
|
+
* to a location strictly inside rootDir.
|
|
170
|
+
* @param {string} rootDir - Allowed root directory
|
|
171
|
+
* @param {string} candidatePath - Path to validate
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
function isPathInside(rootDir, candidatePath) {
|
|
175
|
+
const relativePath = path.relative(rootDir, candidatePath);
|
|
176
|
+
return (
|
|
177
|
+
relativePath !== '' &&
|
|
178
|
+
relativePath !== '..' &&
|
|
179
|
+
!relativePath.startsWith(`..${path.sep}`) &&
|
|
180
|
+
!path.isAbsolute(relativePath)
|
|
181
|
+
);
|
|
106
182
|
}
|
|
107
183
|
|
|
108
184
|
/**
|
|
@@ -153,9 +229,30 @@ function syncIde(agents, ideConfig, ideName, projectRoot, options) {
|
|
|
153
229
|
}
|
|
154
230
|
|
|
155
231
|
try {
|
|
156
|
-
const content = transformer
|
|
232
|
+
const content = transformPrimaryContent(transformer, agent, ideName);
|
|
157
233
|
const filename = transformer.getFilename(agent);
|
|
158
|
-
const
|
|
234
|
+
const targetRoot = path.resolve(result.targetDir);
|
|
235
|
+
|
|
236
|
+
// Kimi format uses subdirectories per skill: <skill-id>/SKILL.md
|
|
237
|
+
let targetPath;
|
|
238
|
+
if (ideConfig.format === 'kimi-skill' && transformer.getDirname) {
|
|
239
|
+
const dirname = transformer.getDirname(agent);
|
|
240
|
+
const skillDir = path.resolve(targetRoot, dirname);
|
|
241
|
+
targetPath = path.resolve(skillDir, filename);
|
|
242
|
+
|
|
243
|
+
if (!isPathInside(targetRoot, skillDir) || !isPathInside(targetRoot, targetPath)) {
|
|
244
|
+
throw new Error(`Unsafe Kimi output path for agent '${agent.id}'`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!options.dryRun) {
|
|
248
|
+
fs.ensureDirSync(skillDir);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
targetPath = path.resolve(targetRoot, filename);
|
|
252
|
+
if (!isPathInside(targetRoot, targetPath)) {
|
|
253
|
+
throw new Error(`Unsafe output path for agent '${agent.id}' in ${ideName}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
159
256
|
|
|
160
257
|
if (!options.dryRun) {
|
|
161
258
|
fs.writeFileSync(targetPath, content, 'utf8');
|
|
@@ -237,7 +334,13 @@ async function commandSync(options) {
|
|
|
237
334
|
|
|
238
335
|
const result = syncIde(agents, ideConfig, ideName, projectRoot, options);
|
|
239
336
|
|
|
240
|
-
|
|
337
|
+
// Gemini CLI: also sync slash launcher command files (.gemini/commands/*.toml)
|
|
338
|
+
if (ideName === 'gemini') {
|
|
339
|
+
const geminiCommands = syncGeminiCommands(agents, projectRoot, options);
|
|
340
|
+
result.commandFiles = geminiCommands.files;
|
|
341
|
+
} else {
|
|
342
|
+
result.commandFiles = [];
|
|
343
|
+
}
|
|
241
344
|
|
|
242
345
|
results.push(result);
|
|
243
346
|
|
|
@@ -339,9 +442,14 @@ async function commandValidate(options) {
|
|
|
339
442
|
if (agent.error) continue;
|
|
340
443
|
|
|
341
444
|
try {
|
|
342
|
-
const content = transformer
|
|
445
|
+
const content = transformPrimaryContent(transformer, agent, ideName);
|
|
343
446
|
const filename = transformer.getFilename(agent);
|
|
344
|
-
|
|
447
|
+
// Kimi format stores each skill in <skill-id>/SKILL.md — record nested path
|
|
448
|
+
const relPath =
|
|
449
|
+
ideConfig.format === 'kimi-skill' && transformer.getDirname
|
|
450
|
+
? path.join(transformer.getDirname(agent), filename)
|
|
451
|
+
: filename;
|
|
452
|
+
expectedFiles.push({ filename: relPath, content });
|
|
345
453
|
} catch (error) {
|
|
346
454
|
// Skip agents that fail to transform
|
|
347
455
|
}
|
|
@@ -366,6 +474,17 @@ async function commandValidate(options) {
|
|
|
366
474
|
targetDir: path.join(projectRoot, ideConfig.path),
|
|
367
475
|
};
|
|
368
476
|
|
|
477
|
+
// Gemini CLI command launcher files are synced under .gemini/commands/*.toml
|
|
478
|
+
if (ideName === 'gemini') {
|
|
479
|
+
const commandFiles = buildGeminiCommandFiles(agents).map((entry) => ({
|
|
480
|
+
filename: entry.filename,
|
|
481
|
+
content: entry.content,
|
|
482
|
+
}));
|
|
483
|
+
ideConfigs['gemini-commands'] = {
|
|
484
|
+
expectedFiles: commandFiles,
|
|
485
|
+
targetDir: path.join(projectRoot, '.gemini', 'commands'),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
369
488
|
}
|
|
370
489
|
|
|
371
490
|
// Validate
|
|
@@ -486,6 +605,8 @@ if (require.main === module) {
|
|
|
486
605
|
module.exports = {
|
|
487
606
|
loadConfig,
|
|
488
607
|
getTransformer,
|
|
608
|
+
transformPrimaryContent,
|
|
609
|
+
isPathInside,
|
|
489
610
|
syncIde,
|
|
490
611
|
commandSync,
|
|
491
612
|
commandValidate,
|