sinapse-ai 1.7.0 → 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.
Files changed (96) hide show
  1. package/.claude/CLAUDE.md +5 -11
  2. package/.claude/hooks/README.md +14 -1
  3. package/.claude/hooks/code-intel-pretool.cjs +115 -0
  4. package/.claude/hooks/enforce-delegation.cjs +31 -3
  5. package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
  6. package/.claude/hooks/enforce-permission-mode.cjs +249 -0
  7. package/.claude/hooks/secret-scanning.cjs +34 -43
  8. package/.claude/hooks/synapse-engine.cjs +23 -23
  9. package/.claude/hooks/telemetry-post-tool.cjs +128 -0
  10. package/.claude/hooks/telemetry-stop.cjs +132 -0
  11. package/.claude/hooks/verify-packages.cjs +9 -2
  12. package/.claude/rules/hook-governance.md +2 -0
  13. package/.sinapse-ai/cli/commands/health/index.js +24 -0
  14. package/.sinapse-ai/core/README.md +11 -0
  15. package/.sinapse-ai/core/config/config-loader.js +19 -0
  16. package/.sinapse-ai/core/execution/build-orchestrator.js +4 -1
  17. package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
  18. package/.sinapse-ai/core/execution/subagent-dispatcher.js +126 -28
  19. package/.sinapse-ai/core/execution/wave-executor.js +4 -1
  20. package/.sinapse-ai/core/grounding/README.md +71 -11
  21. package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
  22. package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
  23. package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
  24. package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
  25. package/.sinapse-ai/core/health-check/healers/index.js +40 -3
  26. package/.sinapse-ai/core/ideation/ideation-engine.js +170 -121
  27. package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
  28. package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
  29. package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
  30. package/.sinapse-ai/core/ids/index.js +30 -0
  31. package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
  32. package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
  33. package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
  34. package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
  35. package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
  36. package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
  37. package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
  38. package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
  39. package/.sinapse-ai/core/orchestration/master-orchestrator.js +105 -7
  40. package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
  41. package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
  42. package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
  43. package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
  44. package/.sinapse-ai/core/utils/output-formatter.js +8 -290
  45. package/.sinapse-ai/core-config.yaml +49 -1
  46. package/.sinapse-ai/data/entity-registry.yaml +15081 -13735
  47. package/.sinapse-ai/data/registry-update-log.jsonl +86 -0
  48. package/.sinapse-ai/development/agents/developer.md +2 -0
  49. package/.sinapse-ai/development/agents/devops.md +9 -0
  50. package/.sinapse-ai/development/external-executors/README.md +18 -0
  51. package/.sinapse-ai/development/external-executors/codex.md +56 -0
  52. package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
  53. package/.sinapse-ai/development/scripts/squad/squad-downloader.js +54 -11
  54. package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
  55. package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
  56. package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
  57. package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
  58. package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +4 -7
  59. package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +4 -7
  60. package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
  61. package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
  62. package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
  63. package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
  64. package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
  65. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
  66. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
  67. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
  68. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
  69. package/.sinapse-ai/install-manifest.yaml +158 -90
  70. package/.sinapse-ai/scripts/pm.sh +18 -6
  71. package/bin/cli.js +17 -0
  72. package/bin/commands/agents.js +96 -0
  73. package/bin/commands/doctor.js +15 -0
  74. package/bin/commands/ideate.js +129 -0
  75. package/bin/commands/uninstall.js +40 -0
  76. package/bin/postinstall.js +50 -4
  77. package/bin/sinapse.js +146 -2
  78. package/bin/utils/secret-scanner-core.js +253 -0
  79. package/bin/utils/staged-secret-scan.js +106 -40
  80. package/package.json +13 -3
  81. package/packages/installer/src/installer/git-hooks-installer.js +384 -0
  82. package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
  83. package/packages/installer/src/wizard/ide-config-generator.js +23 -0
  84. package/packages/installer/src/wizard/validators.js +38 -1
  85. package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
  86. package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
  87. package/scripts/eval-runner.js +422 -0
  88. package/scripts/generate-install-manifest.js +13 -9
  89. package/scripts/generate-synapse-runtime.js +51 -0
  90. package/scripts/validate-all.js +1 -0
  91. package/scripts/validate-evals.js +466 -0
  92. package/scripts/validate-schemas.js +539 -0
  93. package/scripts/validate-squad-orqx.js +9 -2
  94. package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
  95. package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
  96. package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
@@ -318,45 +318,64 @@ If conflicts detected, fail with message:
318
318
  Resolve conflicts before pushing.
319
319
  ```
320
320
 
321
- ### 4. Run npm run lint (if script exists)
321
+ ### 4. Run Layer 1 Quality Gate (lint + test + typecheck) — CANONICAL
322
322
 
323
- ```javascript
324
- function runNpmScript(scriptName, projectRoot) {
325
- const packageJsonPath = path.join(projectRoot, 'package.json');
326
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
323
+ > **Do NOT reimplement lint/test/typecheck here.** The framework already ships
324
+ > the canonical Layer 1 pre-commit gate (`core/quality-gates/layer1-precommit.js`),
325
+ > exposed via the CLI. Calling it keeps a single source of truth for the quality
326
+ > checks (Constitution Art. I — CLI First) instead of forking the logic.
327
327
 
328
- if (!packageJson.scripts || !packageJson.scripts[scriptName]) {
329
- console.log(`⚠️ Script "${scriptName}" not found - skipping`);
330
- return { skipped: true };
331
- }
328
+ ```bash
329
+ sinapse qa run --layer=1
330
+ ```
332
331
 
332
+ Layer 1 runs **lint (ESLint), unit tests (Jest), and typecheck** — fast local
333
+ checks — and gracefully skips any check whose npm script is absent. Exit code:
334
+ `0` = PASS, non-zero = FAIL.
335
+
336
+ ```javascript
337
+ const { execSync } = require('child_process');
338
+
339
+ function runLayer1QualityGate(projectRoot) {
333
340
  try {
334
- execSync(`npm run ${scriptName}`, {
335
- cwd: projectRoot,
336
- stdio: 'inherit'
337
- });
338
- console.log(`✓ ${scriptName} PASSED`);
341
+ // The canonical 3-check Layer 1 gate. stdio:'inherit' streams its report.
342
+ execSync('sinapse qa run --layer=1', { cwd: projectRoot, stdio: 'inherit' });
343
+ console.log('✓ Layer 1 (lint + test + typecheck) PASSED');
339
344
  return { passed: true };
340
345
  } catch (error) {
341
- console.error(`❌ ${scriptName} FAILED`);
346
+ console.error('❌ Layer 1 quality gate FAILED');
342
347
  return { passed: false, error };
343
348
  }
344
349
  }
345
350
  ```
346
351
 
347
- ### 5. Run npm test (if script exists)
352
+ ### 5. Run npm run build (if script exists)
348
353
 
349
- Same logic as lint, but for `npm test`.
354
+ Build is outside Layer 1's scope (Layer 1 is the fast lint/test/typecheck pass),
355
+ so it stays a separate step. Skips gracefully when the `build` script is absent.
350
356
 
351
- ### 6. Run npm run typecheck (if script exists)
352
-
353
- Same logic as lint, but for `npm run typecheck`.
357
+ ```javascript
358
+ function runBuild(projectRoot) {
359
+ const packageJsonPath = path.join(projectRoot, 'package.json');
360
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
354
361
 
355
- ### 7. Run npm run build (if script exists)
362
+ if (!packageJson.scripts || !packageJson.scripts.build) {
363
+ console.log('⚠️ Script "build" not found - skipping');
364
+ return { skipped: true };
365
+ }
356
366
 
357
- Same logic as lint, but for `npm run build`.
367
+ try {
368
+ execSync('npm run build', { cwd: projectRoot, stdio: 'inherit' });
369
+ console.log('✓ build PASSED');
370
+ return { passed: true };
371
+ } catch (error) {
372
+ console.error('❌ build FAILED');
373
+ return { passed: false, error };
374
+ }
375
+ }
376
+ ```
358
377
 
359
- ### 8. Run CodeRabbit CLI Review (TR-3.14.12)
378
+ ### 6. Run CodeRabbit CLI Review (TR-3.14.12)
360
379
 
361
380
  ```javascript
362
381
  const { execSync } = require('child_process');
@@ -503,7 +522,7 @@ if (coderabbitResult.gateImpact === 'CONCERNS') {
503
522
  }
504
523
  ```
505
524
 
506
- ### 9. Run Security Scan (TR-3.14.11)
525
+ ### 7. Run Security Scan (TR-3.14.11)
507
526
 
508
527
  ```javascript
509
528
  const { execSync } = require('child_process');
@@ -630,7 +649,7 @@ function determineSecurityGate(results) {
630
649
  }
631
650
  ```
632
651
 
633
- ### 9.1 Impact Analysis (Code Intelligence — Advisory Only)
652
+ ### 7.1 Impact Analysis (Code Intelligence — Advisory Only)
634
653
 
635
654
  > **Added by:** Story NOG-7 (DevOps Pre-Push Impact Analysis)
636
655
  > **Behavior:** Advisory only — NEVER blocks push. Auto-skips if code intelligence unavailable.
@@ -688,7 +707,7 @@ Impact Analysis:
688
707
 
689
708
  ---
690
709
 
691
- ### 10. Verify Story Status (Optional - if using story-driven workflow)
710
+ ### 8. Verify Story Status (Optional - if using story-driven workflow)
692
711
 
693
712
  ```javascript
694
713
  function checkStoryStatus(storyPath) {
@@ -735,9 +754,7 @@ Mode: {framework-development | project-development}
735
754
  Quality Checks:
736
755
  ✓ No uncommitted changes
737
756
  ✓ No merge conflicts
738
- npm run lint PASSED
739
- ✓ npm test PASSED
740
- ✓ npm run typecheck PASSED
757
+ Layer 1 (lint+test+typecheck) PASSED (via `sinapse qa run --layer=1`)
741
758
  ✓ npm run build PASSED
742
759
  ✓ Security scan PASSED
743
760
  ⚠️ Story status SKIPPED (no story file)
@@ -1,11 +1,11 @@
1
1
  # Task: Update SINAPSE Framework
2
2
 
3
- > **Version:** 4.0.0
3
+ > **Version:** 5.2.0
4
4
  > **Created:** 2026-01-29
5
- > **Updated:** 2026-01-31
5
+ > **Updated:** 2026-06-15
6
6
  > **Type:** SYNC (git-native framework synchronization)
7
7
  > **Agent:** @devops (Pipeline) or @sinapse (Orion)
8
- > **Execution:** Simple bash script (~15 lines)
8
+ > **Execution:** Bash script (~150 lines)
9
9
 
10
10
  ## Purpose
11
11
 
@@ -39,15 +39,12 @@ function readStdin() {
39
39
  });
40
40
  }
41
41
 
42
+ // Story 10.47: delegate to the shared grounding config loader instead of
43
+ // duplicating the read+parse. The require is guarded so the hook stays
44
+ // fail-open even if the shared module is somehow absent at runtime.
42
45
  function loadConfig() {
43
46
  try {
44
- if (!fs.existsSync(CONFIG_PATH)) return null;
45
- const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
46
- let yaml;
47
- try { yaml = require('js-yaml'); } catch { return null; }
48
- const parsed = yaml.load(raw);
49
- if (!parsed || typeof parsed !== 'object') return null;
50
- return parsed;
47
+ return require('../core/grounding/config-loader.cjs').loadGroundingConfig(CONFIG_PATH);
51
48
  } catch {
52
49
  return null;
53
50
  }
@@ -66,15 +66,12 @@ function readStdin() {
66
66
  });
67
67
  }
68
68
 
69
+ // Story 10.47: delegate to the shared grounding config loader instead of
70
+ // duplicating the read+parse. The require is guarded so the hook stays
71
+ // fail-open even if the shared module is somehow absent at runtime.
69
72
  function loadConfig() {
70
73
  try {
71
- if (!fs.existsSync(CONFIG_PATH)) return null;
72
- const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
73
- let yaml;
74
- try { yaml = require('js-yaml'); } catch { return null; }
75
- const parsed = yaml.load(raw);
76
- if (!parsed || typeof parsed !== 'object') return null;
77
- return parsed;
74
+ return require('../core/grounding/config-loader.cjs').loadGroundingConfig(CONFIG_PATH);
78
75
  } catch {
79
76
  return null;
80
77
  }
@@ -54,15 +54,12 @@ function readStdin() {
54
54
  });
55
55
  }
56
56
 
57
+ // Story 10.47: delegate to the shared grounding config loader instead of
58
+ // duplicating the read+parse. The require is guarded so the hook stays
59
+ // fail-open even if the shared module is somehow absent at runtime.
57
60
  function loadConfig() {
58
61
  try {
59
- if (!fs.existsSync(CONFIG_PATH)) return null;
60
- const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
61
- let yaml;
62
- try { yaml = require('js-yaml'); } catch { return null; }
63
- const parsed = yaml.load(raw);
64
- if (!parsed || typeof parsed !== 'object') return null;
65
- return parsed;
62
+ return require('../core/grounding/config-loader.cjs').loadGroundingConfig(CONFIG_PATH);
66
63
  } catch {
67
64
  return null;
68
65
  }
@@ -42,7 +42,10 @@ const DEFAULT_CONFIG = {
42
42
  },
43
43
  },
44
44
  claude: {
45
- model: 'claude-3-5-sonnet',
45
+ // No hardcoded model: stale IDs get rejected by the CLI ("model may not
46
+ // exist"). null → omit --model and use the user's CLI default, which is
47
+ // always a valid, current model.
48
+ model: null,
46
49
  timeout: 300000,
47
50
  dangerouslySkipPermissions: false,
48
51
  },
@@ -7,8 +7,11 @@
7
7
  * @see Epic GEMINI-INT - Story 2: AI Provider Factory Pattern
8
8
  */
9
9
 
10
- const { spawn, execSync } = require('child_process');
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='claude-3-5-sonnet'] - Model to use
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: config.model || 'claude-3-5-sonnet',
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
- return new Promise((resolve, reject) => {
86
- let stdout = '';
87
- let stderr = '';
88
-
89
- // Spawn claude directly without shell interpolation (safer)
90
- const child = spawn(this.command, args, {
91
- cwd: workingDir,
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
  /**
@@ -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
+ };