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.
Files changed (131) 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/documentation-first.md +1 -1
  13. package/.claude/rules/hook-governance.md +2 -0
  14. package/.sinapse-ai/cli/commands/health/index.js +24 -0
  15. package/.sinapse-ai/core/README.md +11 -0
  16. package/.sinapse-ai/core/config/config-loader.js +19 -0
  17. package/.sinapse-ai/core/config/merge-utils.js +8 -0
  18. package/.sinapse-ai/core/errors/constants.js +147 -0
  19. package/.sinapse-ai/core/errors/error-registry.js +176 -0
  20. package/.sinapse-ai/core/errors/index.js +50 -0
  21. package/.sinapse-ai/core/errors/serializer.js +147 -0
  22. package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
  23. package/.sinapse-ai/core/errors/utils.js +187 -0
  24. package/.sinapse-ai/core/execution/build-orchestrator.js +47 -49
  25. package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
  26. package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
  27. package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
  28. package/.sinapse-ai/core/execution/subagent-dispatcher.js +201 -60
  29. package/.sinapse-ai/core/execution/wave-executor.js +4 -1
  30. package/.sinapse-ai/core/grounding/README.md +71 -11
  31. package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
  32. package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
  33. package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
  34. package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
  35. package/.sinapse-ai/core/health-check/healers/index.js +40 -3
  36. package/.sinapse-ai/core/ideation/ideation-engine.js +212 -107
  37. package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
  38. package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
  39. package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
  40. package/.sinapse-ai/core/ids/index.js +30 -0
  41. package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
  42. package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
  43. package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
  44. package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
  45. package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
  46. package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
  47. package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
  48. package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
  49. package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
  50. package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
  51. package/.sinapse-ai/core/orchestration/master-orchestrator.js +150 -10
  52. package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
  53. package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
  54. package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
  55. package/.sinapse-ai/core/registry/registry-loader.js +71 -5
  56. package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
  57. package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
  58. package/.sinapse-ai/core/synapse/context/index.js +19 -0
  59. package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
  60. package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
  61. package/.sinapse-ai/core/synapse/engine.js +43 -3
  62. package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
  63. package/.sinapse-ai/core/utils/output-formatter.js +8 -290
  64. package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
  65. package/.sinapse-ai/core-config.yaml +68 -1
  66. package/.sinapse-ai/data/entity-registry.yaml +15082 -13618
  67. package/.sinapse-ai/data/registry-update-log.jsonl +143 -0
  68. package/.sinapse-ai/development/agents/developer.md +2 -0
  69. package/.sinapse-ai/development/agents/devops.md +9 -0
  70. package/.sinapse-ai/development/external-executors/README.md +18 -0
  71. package/.sinapse-ai/development/external-executors/codex.md +56 -0
  72. package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
  73. package/.sinapse-ai/development/scripts/squad/squad-downloader.js +169 -14
  74. package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
  75. package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
  76. package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
  77. package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
  78. package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +5 -8
  79. package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +6 -9
  80. package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
  81. package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
  82. package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
  83. package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
  84. package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
  85. package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
  86. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
  87. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
  88. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
  89. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
  90. package/.sinapse-ai/install-manifest.yaml +218 -114
  91. package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
  92. package/.sinapse-ai/scripts/pm.sh +18 -6
  93. package/bin/cli.js +17 -0
  94. package/bin/commands/agents.js +96 -0
  95. package/bin/commands/doctor.js +15 -0
  96. package/bin/commands/ideate.js +129 -0
  97. package/bin/commands/uninstall.js +40 -0
  98. package/bin/postinstall.js +50 -4
  99. package/bin/sinapse.js +146 -2
  100. package/bin/utils/secret-scanner-core.js +253 -0
  101. package/bin/utils/staged-secret-scan.js +106 -40
  102. package/docs/framework/collaboration-autonomy-plan.md +18 -18
  103. package/docs/guides/parallel-workflow.md +6 -6
  104. package/package.json +22 -5
  105. package/packages/installer/src/installer/git-hooks-installer.js +384 -0
  106. package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
  107. package/packages/installer/src/wizard/ide-config-generator.js +23 -0
  108. package/packages/installer/src/wizard/validators.js +38 -1
  109. package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
  110. package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
  111. package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
  112. package/scripts/eval-runner.js +422 -0
  113. package/scripts/generate-install-manifest.js +13 -9
  114. package/scripts/generate-synapse-runtime.js +51 -0
  115. package/scripts/regenerate-orqx-stubs.ps1 +6 -5
  116. package/scripts/validate-all.js +1 -0
  117. package/scripts/validate-evals.js +466 -0
  118. package/scripts/validate-schemas.js +539 -0
  119. package/scripts/validate-squad-orqx.js +9 -2
  120. package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
  121. package/squads/squad-brand/templates/client-delivery-template.md +1 -1
  122. package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
  123. package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
  124. package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
  125. package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
  126. package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
  127. package/docs/chrome-brain-upgrade-plan.md +0 -624
  128. package/docs/constitution-compliance.md +0 -87
  129. package/docs/mega-upgrade-orchestration-plan.md +0 -71
  130. package/docs/research-synthesis-for-upgrade.md +0 -511
  131. package/docs/security-audit-report.md +0 -306
@@ -9,26 +9,45 @@
9
9
  */
10
10
 
11
11
  const EventEmitter = require('events');
12
- const { spawn } = require('child_process');
13
12
  const _path = require('path');
13
+ const { runSafe } = require('../utils/spawn-safe');
14
14
 
15
- // Import AI Provider Factory
15
+ // epic: orchestration-consolidation, F2 — resolves any of the 189 agent ids to
16
+ // its real persona on disk (squads/ + framework agents). Optional: degrades to a
17
+ // generic prompt if the module is missing.
18
+ let SquadAgentResolver;
19
+ try {
20
+ SquadAgentResolver = require('../registry/squad-agent-resolver');
21
+ } catch {
22
+ SquadAgentResolver = null;
23
+ }
24
+
25
+ // Import AI Provider Factory (factory module directly — the index barrel
26
+ // requires a removed gemini-provider and would throw on load).
16
27
  let AIProviderFactory;
17
28
  try {
18
- AIProviderFactory = require('../../infrastructure/integrations/ai-providers');
29
+ AIProviderFactory = require('../../infrastructure/integrations/ai-providers/ai-provider-factory');
19
30
  } catch {
20
- AIProviderFactory = null;
31
+ try {
32
+ AIProviderFactory = require('../../infrastructure/integrations/ai-providers');
33
+ } catch {
34
+ AIProviderFactory = null;
35
+ }
21
36
  }
22
37
 
23
38
  // Import dependencies with fallbacks
24
39
  let MemoryQuery, GotchasMemory;
25
40
  try {
26
- MemoryQuery = require('../memory/memory-query');
41
+ const memoryQueryModule = require('../memory/memory-query');
42
+ MemoryQuery = memoryQueryModule.MemoryQuery || memoryQueryModule;
43
+ if (typeof MemoryQuery !== 'function') MemoryQuery = null;
27
44
  } catch {
28
45
  MemoryQuery = null;
29
46
  }
30
47
  try {
31
- GotchasMemory = require('../memory/gotchas-memory');
48
+ const gotchasModule = require('../memory/gotchas-memory');
49
+ GotchasMemory = gotchasModule.GotchasMemory || gotchasModule;
50
+ if (typeof GotchasMemory !== 'function') GotchasMemory = null;
32
51
  } catch {
33
52
  GotchasMemory = null;
34
53
  }
@@ -94,16 +113,42 @@ class SubagentDispatcher extends EventEmitter {
94
113
  this.maxRetries = config.maxRetries || 2;
95
114
  this.retryDelay = config.retryDelay || 2000;
96
115
 
97
- // Dependencies
98
- this.memoryQuery = config.memoryQuery || (MemoryQuery ? new MemoryQuery() : null);
99
- this.gotchasMemory = config.gotchasMemory || (GotchasMemory ? new GotchasMemory() : null);
116
+ // Claude CLI execution timeout (per spawn), 10 min default
117
+ this.claudeTimeout = config.claudeTimeout || 10 * 60 * 1000;
100
118
 
101
- // Dispatch log
119
+ // Dispatch log (initialized before optional deps — _tryConstruct logs failures)
102
120
  this.dispatchLog = [];
103
121
  this.maxLogSize = 100;
104
122
 
105
- // Root path for project
123
+ // Root path for project (resolved before memory deps — they need it)
106
124
  this.rootPath = config.rootPath || process.cwd();
125
+
126
+ // Agent persona resolver (F2): make every squad/framework agent addressable.
127
+ this.agentResolver =
128
+ config.agentResolver ||
129
+ (SquadAgentResolver ? new SquadAgentResolver(this.rootPath) : null);
130
+
131
+ // Dependencies (never let optional memory enrichment break the dispatcher)
132
+ this.memoryQuery = config.memoryQuery || this._tryConstruct(MemoryQuery, this.rootPath);
133
+ this.gotchasMemory = config.gotchasMemory || this._tryConstruct(GotchasMemory, this.rootPath);
134
+ }
135
+
136
+ /**
137
+ * Safely construct an optional dependency. Memory enrichment is best-effort:
138
+ * a broken/missing memory module must never break real agent dispatch.
139
+ * @param {Function|null} Ctor - Constructor (or null when unavailable)
140
+ * @param {string} rootPath - Project root passed to the constructor
141
+ * @returns {Object|null} - Instance or null
142
+ * @private
143
+ */
144
+ _tryConstruct(Ctor, rootPath) {
145
+ if (typeof Ctor !== 'function') return null;
146
+ try {
147
+ return new Ctor(rootPath);
148
+ } catch (error) {
149
+ this.log?.('optional_dependency_failed', { dep: Ctor.name, error: error.message });
150
+ return null;
151
+ }
107
152
  }
108
153
 
109
154
  /**
@@ -134,7 +179,13 @@ class SubagentDispatcher extends EventEmitter {
134
179
 
135
180
  // Enrich context
136
181
  const enrichedContext = await this.enrichContext(task, context);
137
- dispatchRecord.contextSize = JSON.stringify(enrichedContext).length;
182
+ // Context may contain non-serializable/circular values injected by callers;
183
+ // size accounting is best-effort and must never break dispatch.
184
+ try {
185
+ dispatchRecord.contextSize = JSON.stringify(enrichedContext).length;
186
+ } catch {
187
+ dispatchRecord.contextSize = -1;
188
+ }
138
189
 
139
190
  // Execute with retries
140
191
  let lastError = null;
@@ -211,6 +262,12 @@ class SubagentDispatcher extends EventEmitter {
211
262
  return this.agentMapping[task.type.toLowerCase()];
212
263
  }
213
264
 
265
+ // F2: if the task names a real agent id directly (any of the 189 squad/
266
+ // framework personas), honor it instead of inferring a generic one.
267
+ if (this.agentResolver && task.name && this.agentResolver.has(task.name)) {
268
+ return `@${this.agentResolver.resolve(task.name).id}`;
269
+ }
270
+
214
271
  // Check task tags
215
272
  if (task.tags && Array.isArray(task.tags)) {
216
273
  for (const tag of task.tags) {
@@ -303,6 +360,42 @@ class SubagentDispatcher extends EventEmitter {
303
360
  }
304
361
  }
305
362
 
363
+ /**
364
+ * Resolve the fallback provider name for a given primary.
365
+ * Reads ai_providers.fallback / primary from the factory config instead of
366
+ * the old hardcoded claude<->gemini pair. Falls back safely when config or
367
+ * factory is unavailable.
368
+ * @param {string} primaryName - The provider that just failed/was unavailable.
369
+ * @returns {string|null} - Fallback provider name, or null if none distinct.
370
+ */
371
+ getFallbackProviderName(primaryName) {
372
+ let config = null;
373
+ if (AIProviderFactory && typeof AIProviderFactory.getConfig === 'function') {
374
+ try {
375
+ config = AIProviderFactory.getConfig();
376
+ } catch (error) {
377
+ this.log('fallback_config_error', { error: error.message });
378
+ }
379
+ }
380
+
381
+ const providers = config?.ai_providers || {};
382
+ const configuredFallback = providers.fallback;
383
+ const configuredPrimary = providers.primary;
384
+
385
+ // Prefer explicit fallback when it differs from the failing provider.
386
+ if (configuredFallback && configuredFallback !== primaryName) {
387
+ return configuredFallback;
388
+ }
389
+
390
+ // If the failing provider IS the configured fallback, try the primary.
391
+ if (configuredPrimary && configuredPrimary !== primaryName) {
392
+ return configuredPrimary;
393
+ }
394
+
395
+ // Last-resort safe default: never return the same provider that just failed.
396
+ return primaryName === 'claude' ? null : 'claude';
397
+ }
398
+
306
399
  /**
307
400
  * Enrich context with memory and gotchas
308
401
  * @param {Object} task - Task being dispatched
@@ -399,11 +492,25 @@ class SubagentDispatcher extends EventEmitter {
399
492
 
400
493
  // Try to use AI Provider Factory if available
401
494
  if (this.multiProviderEnabled && AIProviderFactory) {
402
- return this.executeWithProvider(prompt, providerName, task);
495
+ return this.executeWithProvider(prompt, providerName, task, agentId);
403
496
  }
404
497
 
405
498
  // Fallback to direct Claude CLI
406
- return this.executeClaude(prompt);
499
+ return this.executeClaude(prompt, agentId);
500
+ }
501
+
502
+ /**
503
+ * Build the env for a spawned agent process. Declares the active agent via
504
+ * SINAPSE_ACTIVE_AGENT so the autonomous path is observable AND the Article
505
+ * VIII delegation hook can enforce when the spawned agent is an orchestrator.
506
+ * (Interactive chat activations don't go through here — they're opt-in.)
507
+ * @param {string} agentId - Resolved agent id (with or without '@')
508
+ * @returns {Object} env additions
509
+ * @private
510
+ */
511
+ _agentEnv(agentId) {
512
+ const id = String(agentId || '').replace(/^@/, '').trim();
513
+ return id ? { SINAPSE_ACTIVE_AGENT: id } : {};
407
514
  }
408
515
 
409
516
  /**
@@ -413,7 +520,7 @@ class SubagentDispatcher extends EventEmitter {
413
520
  * @param {Object} task - Original task for context
414
521
  * @returns {Promise<Object>} - Execution result
415
522
  */
416
- async executeWithProvider(prompt, providerName, task) {
523
+ async executeWithProvider(prompt, providerName, task, agentId) {
417
524
  const startTime = Date.now();
418
525
 
419
526
  // Get primary provider
@@ -422,7 +529,7 @@ class SubagentDispatcher extends EventEmitter {
422
529
  if (!provider) {
423
530
  this.log('provider_unavailable', { provider: providerName });
424
531
  // Fallback to legacy Claude execution
425
- return this.executeClaude(prompt);
532
+ return this.executeClaude(prompt, agentId);
426
533
  }
427
534
 
428
535
  // Check availability
@@ -431,21 +538,21 @@ class SubagentDispatcher extends EventEmitter {
431
538
  if (!isAvailable) {
432
539
  this.log('provider_not_available', { provider: providerName });
433
540
 
434
- // Try fallback provider
435
- const fallbackName = providerName === 'claude' ? 'gemini' : 'claude';
436
- const fallback = this.getAIProvider(fallbackName);
541
+ // Try fallback provider (config-driven, not hardcoded)
542
+ const fallbackName = this.getFallbackProviderName(providerName);
543
+ const fallback = fallbackName ? this.getAIProvider(fallbackName) : null;
437
544
 
438
545
  if (fallback && (await fallback.checkAvailability())) {
439
546
  this.log('using_fallback_provider', { original: providerName, fallback: fallbackName });
440
- return this.executeWithSingleProvider(fallback, prompt, task);
547
+ return this.executeWithSingleProvider(fallback, prompt, task, agentId);
441
548
  }
442
549
 
443
550
  // Last resort: legacy Claude
444
- return this.executeClaude(prompt);
551
+ return this.executeClaude(prompt, agentId);
445
552
  }
446
553
 
447
554
  // Execute with selected provider
448
- return this.executeWithSingleProvider(provider, prompt, task);
555
+ return this.executeWithSingleProvider(provider, prompt, task, agentId);
449
556
  }
450
557
 
451
558
  /**
@@ -455,10 +562,11 @@ class SubagentDispatcher extends EventEmitter {
455
562
  * @param {Object} task - Original task
456
563
  * @returns {Promise<Object>} - Execution result
457
564
  */
458
- async executeWithSingleProvider(provider, prompt, task) {
565
+ async executeWithSingleProvider(provider, prompt, task, agentId) {
459
566
  try {
460
567
  const response = await provider.executeWithRetry(prompt, {
461
568
  workingDir: this.rootPath,
569
+ env: this._agentEnv(agentId),
462
570
  });
463
571
 
464
572
  this.emit('provider_execution_complete', {
@@ -591,7 +699,29 @@ class SubagentDispatcher extends EventEmitter {
591
699
  * @returns {string} - Formatted prompt
592
700
  */
593
701
  buildPrompt(agentId, task, context) {
594
- let prompt = `You are ${agentId}, a specialized agent in the SINAPSE framework.\n\n`;
702
+ let prompt = '';
703
+
704
+ // F2: inject the FULL real persona when the agent is known on disk. This is
705
+ // the difference between "act as a generic dev" and "act as Nimbus, the cloud
706
+ // security engineer with these exact frameworks". Falls back to the one-liner
707
+ // only when the agent is unknown or the resolver is unavailable.
708
+ let personaLoaded = false;
709
+ if (this.agentResolver) {
710
+ const persona = this.agentResolver.loadPersona(agentId);
711
+ if (persona && persona.trim().length > 0) {
712
+ prompt += '## Your Agent Definition (adopt this persona fully)\n\n';
713
+ prompt += persona.trim();
714
+ prompt += '\n\n---\n\n';
715
+ personaLoaded = true;
716
+ }
717
+ }
718
+ if (!personaLoaded) {
719
+ prompt += `You are ${agentId}, a specialized agent in the SINAPSE framework.\n\n`;
720
+ }
721
+
722
+ // Always state the role label so the dispatched run knows which agent it is,
723
+ // on top of (or instead of) the injected persona.
724
+ prompt += `## Acting as\n${agentId}\n\n`;
595
725
 
596
726
  prompt += '## Task\n';
597
727
  prompt += `**ID:** ${task.id}\n`;
@@ -642,49 +772,60 @@ class SubagentDispatcher extends EventEmitter {
642
772
  }
643
773
 
644
774
  /**
645
- * Execute prompt via Claude CLI
775
+ * Execute prompt via Claude CLI.
776
+ *
777
+ * Hardened: the prompt is delivered through stdin (never the command line)
778
+ * and the process is spawned by argv via cross-spawn — so the prompt can
779
+ * contain quotes, pipes, `;`, `$()` or any shell metacharacter without ever
780
+ * being interpreted as a command (shell-injection is structurally impossible).
781
+ * cross-spawn also resolves `claude.cmd` on Windows (native spawn → ENOENT).
782
+ *
646
783
  * @param {string} prompt - Prompt to execute
647
- * @returns {Promise<Object>} - Execution result
784
+ * @returns {Promise<Object>} - { success, output, filesModified }
648
785
  */
649
- executeClaude(prompt) {
650
- return new Promise((resolve, reject) => {
651
- const args = ['--print', '--dangerously-skip-permissions'];
652
- const escapedPrompt = prompt.replace(/'/g, "'\\''");
653
- const fullCommand = `echo '${escapedPrompt}' | claude ${args.join(' ')}`;
654
-
655
- const child = spawn('sh', ['-c', fullCommand], {
656
- cwd: this.rootPath,
657
- env: { ...process.env },
658
- stdio: ['pipe', 'pipe', 'pipe'],
659
- });
786
+ async executeClaude(prompt, agentId) {
787
+ if (!prompt || typeof prompt !== 'string') {
788
+ throw new Error('executeClaude requires a non-empty string prompt');
789
+ }
660
790
 
661
- let stdout = '';
662
- let stderr = '';
791
+ const args = ['--print', '--dangerously-skip-permissions'];
663
792
 
664
- child.stdout.on('data', (data) => {
665
- stdout += data.toString();
666
- });
793
+ const result = await runSafe('claude', args, {
794
+ cwd: this.rootPath,
795
+ env: { ...process.env, ...this._agentEnv(agentId) },
796
+ timeout: this.claudeTimeout,
797
+ input: prompt,
798
+ });
667
799
 
668
- child.stderr.on('data', (data) => {
669
- stderr += data.toString();
670
- });
800
+ const stdout = (result.stdout || '').trim();
801
+ const stderr = (result.stderr || '').trim();
671
802
 
672
- child.on('close', (code) => {
673
- if (code === 0) {
674
- resolve({
675
- success: true,
676
- output: stdout,
677
- filesModified: this.extractModifiedFiles(stdout),
678
- });
679
- } else {
680
- reject(new Error(`Claude CLI exited with code ${code}: ${stderr || stdout}`));
681
- }
682
- });
803
+ if (result.success) {
804
+ return {
805
+ success: true,
806
+ output: stdout,
807
+ filesModified: this.extractModifiedFiles(stdout),
808
+ };
809
+ }
683
810
 
684
- child.on('error', (error) => {
685
- reject(error);
686
- });
687
- });
811
+ if (result.signal) {
812
+ throw new Error(`Claude CLI killed by signal ${result.signal}: ${stderr || stdout}`);
813
+ }
814
+
815
+ // User-environment hooks (SessionEnd etc.) can fail AFTER the model already
816
+ // printed its full response, poisoning the exit code. In --print mode a
817
+ // non-empty stdout + hook-related stderr means the work was done — accept it
818
+ // instead of discarding paid output and retrying.
819
+ if (stdout.length > 0 && /hook/i.test(stderr)) {
820
+ this.log('exit_code_poisoned_by_hook', { code: result.code, stderr: stderr.slice(0, 200) });
821
+ return {
822
+ success: true,
823
+ output: stdout,
824
+ filesModified: this.extractModifiedFiles(stdout),
825
+ };
826
+ }
827
+
828
+ throw new Error(`Claude CLI exited with code ${result.code}: ${stderr || stdout}`);
688
829
  }
689
830
 
690
831
  /**
@@ -12,7 +12,10 @@ const _path = require('path');
12
12
  // Import dependencies with fallbacks
13
13
  let WaveAnalyzer;
14
14
  try {
15
- WaveAnalyzer = require('../../workflow-intelligence/engine/wave-analyzer');
15
+ // Named export — the module exports { WaveAnalyzer, ... }. Requiring the
16
+ // whole object and calling `new WaveAnalyzer()` (line ~37) would throw
17
+ // "not a constructor". Destructure the class.
18
+ ({ WaveAnalyzer } = require('../../workflow-intelligence/engine/wave-analyzer'));
16
19
  } catch {
17
20
  WaveAnalyzer = null;
18
21
  }
@@ -1,4 +1,4 @@
1
- # Grounding Hooks (Story 10.47)
1
+ # Grounding Hooks (Story 10.47 + GA-1.6)
2
2
 
3
3
  Hooks shipped by SINAPSE-AI that read user-supplied grounding sources and
4
4
  inject relevant context into agent prompts. Each hook is **opt-in**:
@@ -6,18 +6,78 @@ absence of `~/.claude/sinapse-ai-config.yaml` or `enabled: false` in the
6
6
  matching section means the hook is a complete no-op (no I/O, no warnings,
7
7
  no errors).
8
8
 
9
- | Hook | Reads | Behavior when disabled |
10
- |------|-------|------------------------|
11
- | `vault.cjs` | `~/.claude/sinapse-ai-config.yaml` `grounding.vault` | no-op |
12
- | `design-system.cjs` | `~/.claude/sinapse-ai-config.yaml` → `grounding.designSystem` | no-op |
13
- | `brand.cjs` | `~/.claude/sinapse-ai-config.yaml` `grounding.brand` | no-op |
9
+ ## Architecture: Two layers
10
+
11
+ ### Layer 1 — Executable hooks (`.sinapse-ai/hooks/`, story GA-1.6) ACTIVE
12
+
13
+ These are the hooks Claude Code actually runs via `UserPromptSubmit`. They
14
+ read real files and inject context into prompts. Registered automatically
15
+ during `npx sinapse-ai install` into `~/.claude/settings.json`.
16
+
17
+ | Executable hook | Reads | Injects |
18
+ |-----------------|-------|---------|
19
+ | `sinapse-vault-grounding.cjs` | `grounding.vault.path` → top-5 `.md` notes | `<vault-grounding>` |
20
+ | `sinapse-ds-grounding.cjs` | `grounding.designSystem.rootPath` → DS law file | `<ds-grounding>` |
21
+ | `sinapse-brand-grounding.cjs` | `grounding.brand.brandbookPath` → brandbook | `<brand-grounding>` |
22
+
23
+ **Caller:** `bin/lib/register-grounding-hooks.js` (invoked by `bin/commands/install.js`
24
+ phase 6b). Hook registration is idempotent and non-destructive.
25
+
26
+ ### Layer 2 — Library resolvers (`core/grounding/`, story 10.47) — DEFER
27
+
28
+ These library stubs (`vault.cjs`, `design-system.cjs`, `brand.cjs`) are
29
+ shallow wrappers around `config-loader.cjs`. They return a structured envelope
30
+ describing what is configured, but do **not** inject context into prompts.
31
+
32
+ **Why defer:** The executable hooks in Layer 1 (GA-1.6) already implement
33
+ the concrete content injection logic directly. The library resolvers were
34
+ designed as the "integration point" for future use cases where something
35
+ other than the Claude Code hook chain needs to consume grounding data
36
+ (e.g., a CLI command, a report generator, or a programmatic API). No such
37
+ consumer exists today.
38
+
39
+ **Trigger to activate library resolvers:** a concrete caller (e.g., a new
40
+ CLI sub-command `sinapse grounding status`, or a report generator) that
41
+ needs to query grounding config programmatically. At that point, extend
42
+ `vault.cjs` / `design-system.cjs` / `brand.cjs` to read actual files and
43
+ return rich structured data. Do **not** create a caller just to justify
44
+ activating these resolvers (Article XI — no orphans).
45
+
46
+ **Current callers of library resolvers:** `bin/lib/prompts.js` and
47
+ `bin/commands/update.js` import `grounding-config` (the wizard package, not
48
+ these library hooks). The library hooks themselves have no production caller
49
+ beyond their own test suite.
50
+
51
+ | Library resolver | Status | Defer trigger |
52
+ |-----------------|--------|---------------|
53
+ | `vault.cjs` | stub — returns config envelope | new CLI/API consumer |
54
+ | `design-system.cjs` | stub — returns config envelope | new CLI/API consumer |
55
+ | `brand.cjs` | stub — returns config envelope | new CLI/API consumer |
56
+ | `config-loader.cjs` | functional — reads YAML | used by all three stubs |
57
+
58
+ ---
59
+
60
+ ## Configuration
14
61
 
15
62
  Configure interactively via `npx sinapse-ai install` (Story 10.46+10.47
16
63
  wizard) or by editing the YAML file directly. See
17
64
  `docs/guides/grounding-setup.md` for the full guide.
18
65
 
19
- > Note: this story establishes the **shipping foundation** for grounding
20
- > hooks (config schema, no-op default, entry point). The concrete domain
21
- > integration logic (vault parser, DS lookup, brandbook reader) is
22
- > deliberately out-of-scope and will land in follow-up stories per
23
- > grounding type.
66
+ `~/.claude/sinapse-ai-config.yaml` schema:
67
+
68
+ ```yaml
69
+ version: '1'
70
+ grounding:
71
+ vault:
72
+ enabled: true
73
+ path: /abs/path/to/vault
74
+ domains: [sinapse, personal]
75
+ designSystem:
76
+ enabled: true
77
+ profileName: SINAPSE
78
+ rootPath: /abs/path/to/brandbook
79
+ brand:
80
+ enabled: true
81
+ profileName: SINAPSE
82
+ brandbookPath: /abs/path/to/brandbook/0.0-guidelines.md
83
+ ```
@@ -45,7 +45,7 @@ class FrameworkConfigCheck extends BaseCheck {
45
45
  severity: CheckSeverity.HIGH,
46
46
  timeout: 3000,
47
47
  cacheable: true,
48
- healingTier: 0, // Cannot auto-fix framework setup
48
+ healingTier: 1, // Can auto-create .sinapse local config dir
49
49
  tags: ['sinapse', 'config', 'framework'],
50
50
  });
51
51
  }
@@ -111,7 +111,9 @@ class FrameworkConfigCheck extends BaseCheck {
111
111
  if (missingRecommended.length > 0) {
112
112
  const missing = missingRecommended.map((c) => c.path).join(', ');
113
113
  return this.warning(`Missing recommended configuration: ${missing}`, {
114
- recommendation: 'Consider adding recommended configuration for full SINAPSE functionality',
114
+ recommendation: 'Run health check with --fix to create missing recommended configuration',
115
+ healable: true,
116
+ healingTier: 1,
115
117
  details: {
116
118
  missingRecommended: missingRecommended.map((c) => ({
117
119
  path: c.path,
@@ -126,6 +128,40 @@ class FrameworkConfigCheck extends BaseCheck {
126
128
  details: { found: found.map((c) => c.path) },
127
129
  });
128
130
  }
131
+
132
+ /**
133
+ * Get healer for this check
134
+ * @returns {Object} Healer configuration
135
+ */
136
+ getHealer() {
137
+ return {
138
+ name: 'create-sinapse-local-config',
139
+ action: 'create-config',
140
+ successMessage: 'Created .sinapse local configuration directory',
141
+ fix: async (_result) => {
142
+ const projectRoot = process.cwd();
143
+ const sinapseDir = path.join(projectRoot, '.sinapse');
144
+
145
+ await fs.mkdir(sinapseDir, { recursive: true });
146
+
147
+ const configPath = path.join(sinapseDir, 'config.yaml');
148
+ try {
149
+ await fs.access(configPath);
150
+ } catch {
151
+ // Create minimal config stub if it doesn't exist
152
+ const stub = [
153
+ '# SINAPSE local configuration',
154
+ '# Generated by SINAPSE Health Check auto-heal',
155
+ 'permissions:',
156
+ ' mode: ask',
157
+ ].join('\n') + '\n';
158
+ await fs.writeFile(configPath, stub, 'utf8');
159
+ }
160
+
161
+ return { success: true, message: 'Created .sinapse/config.yaml' };
162
+ },
163
+ };
164
+ }
129
165
  }
130
166
 
131
167
  module.exports = FrameworkConfigCheck;
@@ -28,7 +28,7 @@ class PackageJsonCheck extends BaseCheck {
28
28
  severity: CheckSeverity.CRITICAL,
29
29
  timeout: 2000,
30
30
  cacheable: true,
31
- healingTier: 0, // Cannot auto-fix missing/invalid package.json
31
+ healingTier: 1, // Can auto-patch cosmetic fields (name/version stubs) when JSON is valid
32
32
  tags: ['npm', 'config', 'required'],
33
33
  });
34
34
  }
@@ -70,8 +70,10 @@ class PackageJsonCheck extends BaseCheck {
70
70
 
71
71
  if (issues.length > 0) {
72
72
  return this.warning(`package.json has issues: ${issues.join(', ')}`, {
73
- recommendation: 'Fix the package.json fields to match npm requirements',
74
- details: { issues, path: packagePath },
73
+ recommendation: 'Run health check with --fix to patch missing fields',
74
+ healable: true,
75
+ healingTier: 1,
76
+ details: { issues, path: packagePath, packagePath },
75
77
  });
76
78
  }
77
79
 
@@ -100,6 +102,48 @@ class PackageJsonCheck extends BaseCheck {
100
102
  return this.error(`Failed to read package.json: ${error.message}`, error);
101
103
  }
102
104
  }
105
+
106
+ /**
107
+ * Get healer for this check
108
+ * Patches stub name/version fields when JSON is valid but incomplete
109
+ * @returns {Object} Healer configuration
110
+ */
111
+ getHealer() {
112
+ return {
113
+ name: 'patch-package-json-fields',
114
+ action: 'patch-fields',
115
+ successMessage: 'Patched missing package.json fields with safe defaults',
116
+ targetFile: 'package.json',
117
+ fix: async (_result) => {
118
+ const projectRoot = process.cwd();
119
+ const packagePath = path.join(projectRoot, 'package.json');
120
+
121
+ const content = await fs.readFile(packagePath, 'utf8');
122
+ const packageJson = JSON.parse(content);
123
+
124
+ let patched = false;
125
+
126
+ if (!packageJson.name) {
127
+ packageJson.name = path.basename(projectRoot).toLowerCase().replace(/[^a-z0-9-]/g, '-');
128
+ patched = true;
129
+ }
130
+
131
+ if (!packageJson.version) {
132
+ packageJson.version = '0.1.0';
133
+ patched = true;
134
+ }
135
+
136
+ if (patched) {
137
+ await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
138
+ }
139
+
140
+ return {
141
+ success: true,
142
+ message: patched ? 'Patched package.json with stub fields' : 'No patches needed',
143
+ };
144
+ },
145
+ };
146
+ }
103
147
  }
104
148
 
105
149
  module.exports = PackageJsonCheck;