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
@@ -0,0 +1,384 @@
1
+ /**
2
+ * SINAPSE Git Hooks Installer (Stream B — Frente 4.2)
3
+ *
4
+ * Propagates the SINAPSE secret-scan guard into every project the framework
5
+ * creates or clones. Instead of relying on husky (which the target project may
6
+ * not have), it uses git's native `core.hooksPath` mechanism: a managed hooks
7
+ * directory under `.sinapse-ai/git-hooks/` with Node `.js` hook scripts.
8
+ *
9
+ * Cross-platform by construction:
10
+ * - Hooks are `.js` files with `#!/usr/bin/env node` shebang. Git on every
11
+ * platform invokes the file as an executable; the shebang routes it to node.
12
+ * We NEVER emit `.sh` or `.py` (they would not run on Caio's Windows).
13
+ * - Execute bit is set via `fs.chmodSync(path, 0o755)` on unix; on win32 the
14
+ * filesystem has no execute bit so chmod is a tolerated no-op.
15
+ * - `core.hooksPath` is set via `runSafe` (cross-spawn, argv-based, never
16
+ * `shell: true`) — no shell metacharacter interpolation, injection-proof.
17
+ *
18
+ * Idempotent: running install twice does not duplicate hooks or chaining. The
19
+ * managed hook is overwritten with the canonical content each run, and the
20
+ * husky chain is detected at runtime (not baked in), so it always reflects the
21
+ * current state of the target project.
22
+ *
23
+ * Husky / existing-hooks safety: the generated pre-commit detects an existing
24
+ * `.husky/pre-commit` (or a prior `core.hooksPath` hook backed up during
25
+ * takeover) and CHAINS to it — it never silently discards the project's own
26
+ * hooks.
27
+ *
28
+ * Callers (Article XI — no orphan):
29
+ * - installSinapseCore() in sinapse-ai-installer.js (the `sinapse init` flow)
30
+ * - GreenfieldHandler._ensureGitHooks() in greenfield-handler.js (Phase 0)
31
+ *
32
+ * @module installer/git-hooks-installer
33
+ */
34
+
35
+ 'use strict';
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+
40
+ // runSafe lives in the framework core. Resolve it relative to this file so the
41
+ // module works both inside the sinapse-ai repo and inside an installed package.
42
+ // packages/installer/src/installer -> repo root -> .sinapse-ai/core/utils
43
+ const { runSafe } = require('../../../../.sinapse-ai/core/utils/spawn-safe');
44
+
45
+ /**
46
+ * Name of the managed hooks directory, relative to the project root.
47
+ * Kept under `.sinapse-ai/` so it travels with the framework install and is
48
+ * obviously framework-owned.
49
+ * @constant {string}
50
+ */
51
+ const MANAGED_HOOKS_DIRNAME = path.join('.sinapse-ai', 'git-hooks');
52
+
53
+ /**
54
+ * Source scanner files copied into `<hooksDir>/lib/` so the generated hook can
55
+ * `require` them with a stable relative path regardless of where the project
56
+ * lives. `staged-secret-scan.js` is self-contained (only `child_process`).
57
+ * @constant {Array<{from: string, to: string}>}
58
+ */
59
+ const SCANNER_SOURCES = [
60
+ {
61
+ from: path.join(__dirname, '..', '..', '..', '..', 'bin', 'utils', 'staged-secret-scan.js'),
62
+ to: 'staged-secret-scan.js',
63
+ },
64
+ // staged-secret-scan.js requires secret-scanner-core.js via __dirname, so the
65
+ // core must travel with it into the lib/ bundle. Both files use only Node built-ins.
66
+ {
67
+ from: path.join(__dirname, '..', '..', '..', '..', 'bin', 'utils', 'secret-scanner-core.js'),
68
+ to: 'secret-scanner-core.js',
69
+ },
70
+ ];
71
+
72
+ /**
73
+ * Marker comment written into managed hooks so we can recognize (and safely
74
+ * overwrite) our own hooks without clobbering a user-authored one.
75
+ * @constant {string}
76
+ */
77
+ const MANAGED_MARKER = 'SINAPSE-MANAGED-GIT-HOOK';
78
+
79
+ /**
80
+ * Detect whether the target project already has a husky pre-commit hook.
81
+ *
82
+ * @param {string} projectDir - Absolute path to the project root.
83
+ * @returns {{ hasHusky: boolean, huskyPreCommit: string|null }}
84
+ */
85
+ function detectHusky(projectDir) {
86
+ const huskyPreCommit = path.join(projectDir, '.husky', 'pre-commit');
87
+ const hasHusky = fs.existsSync(huskyPreCommit);
88
+ return { hasHusky, huskyPreCommit: hasHusky ? huskyPreCommit : null };
89
+ }
90
+
91
+ /**
92
+ * Read the currently-configured `core.hooksPath` for a project (if any).
93
+ *
94
+ * @param {string} projectDir - Absolute path to the project root.
95
+ * @returns {Promise<string|null>} Configured value, or null if unset/error.
96
+ */
97
+ async function getCoreHooksPath(projectDir) {
98
+ try {
99
+ const result = await runSafe('git', ['-C', projectDir, 'config', '--get', 'core.hooksPath']);
100
+ if (result.success) {
101
+ const value = (result.stdout || '').trim();
102
+ return value || null;
103
+ }
104
+ } catch {
105
+ // git unavailable / not a repo — fall through to null.
106
+ }
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Set `core.hooksPath` for a project using argv-safe spawning (no shell).
112
+ *
113
+ * @param {string} projectDir - Absolute path to the project root.
114
+ * @param {string} hooksPathValue - Path to write into core.hooksPath (relative
115
+ * to projectDir is preferred so the repo stays portable).
116
+ * @returns {Promise<{ success: boolean, error: (string|null) }>}
117
+ */
118
+ async function setCoreHooksPath(projectDir, hooksPathValue) {
119
+ try {
120
+ const result = await runSafe('git', ['-C', projectDir, 'config', 'core.hooksPath', hooksPathValue]);
121
+ if (result.success) {
122
+ return { success: true, error: null };
123
+ }
124
+ return { success: false, error: (result.stderr || '').trim() || `git exited with code ${result.code}` };
125
+ } catch (error) {
126
+ return { success: false, error: error.message };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Write a managed hook file with the node shebang and set the execute bit.
132
+ *
133
+ * @param {string} hooksDir - Absolute path to the managed hooks directory.
134
+ * @param {string} hookName - Hook name (e.g. 'pre-commit').
135
+ * @param {string} scriptContent - Full file content (must start with shebang).
136
+ * @returns {string} Absolute path to the written hook.
137
+ */
138
+ function writeManagedHook(hooksDir, hookName, scriptContent) {
139
+ fs.mkdirSync(hooksDir, { recursive: true });
140
+ const hookPath = path.join(hooksDir, hookName);
141
+ fs.writeFileSync(hookPath, scriptContent, 'utf8');
142
+ try {
143
+ // 0o755 = rwxr-xr-x. No-op effect on win32 (no execute bit), tolerated.
144
+ fs.chmodSync(hookPath, 0o755);
145
+ } catch {
146
+ // chmod can fail on exotic filesystems; the shebang still routes to node
147
+ // and on Windows the bit is irrelevant. Non-fatal.
148
+ }
149
+ return hookPath;
150
+ }
151
+
152
+ /**
153
+ * Copy the scanner library files into `<hooksDir>/lib/` so the hook can require
154
+ * them with a stable relative path. Idempotent (overwrites).
155
+ *
156
+ * @param {string} hooksDir - Absolute path to the managed hooks directory.
157
+ * @returns {string[]} Relative names of the copied scanner files.
158
+ */
159
+ function copyScannerLib(hooksDir) {
160
+ const libDir = path.join(hooksDir, 'lib');
161
+ fs.mkdirSync(libDir, { recursive: true });
162
+
163
+ const copied = [];
164
+ for (const source of SCANNER_SOURCES) {
165
+ if (fs.existsSync(source.from)) {
166
+ fs.copyFileSync(source.from, path.join(libDir, source.to));
167
+ copied.push(source.to);
168
+ }
169
+ }
170
+ return copied;
171
+ }
172
+
173
+ /**
174
+ * Build the content of the managed `pre-commit` Node hook.
175
+ *
176
+ * The hook:
177
+ * 1. Runs the bundled staged-secret-scan against the staged index.
178
+ * 2. Blocks the commit (exit 1) on any finding, printing redacted reasons.
179
+ * 3. Fail-CLOSED: if the scanner cannot be loaded/run, the commit is blocked
180
+ * (a broken guard must never silently allow secrets through).
181
+ * 4. Chains to a pre-existing `.husky/pre-commit` (or a backed-up prior hook)
182
+ * so the project's own hooks still run.
183
+ *
184
+ * @param {Object} [options]
185
+ * @param {string|null} [options.chainHookRel] - Relative path (from project
186
+ * root) of a prior hook to chain to after the scan passes. When null, the
187
+ * hook auto-detects `.husky/pre-commit` at runtime.
188
+ * @returns {string} Hook file content.
189
+ */
190
+ function buildPreCommitHook(options = {}) {
191
+ const chainHookRel = options.chainHookRel || null;
192
+ const chainLiteral = chainHookRel ? JSON.stringify(chainHookRel) : 'null';
193
+
194
+ return `#!/usr/bin/env node
195
+ 'use strict';
196
+ /* ${MANAGED_MARKER} — Auto-generated by SINAPSE git-hooks-installer. Do not edit manually. */
197
+ /* Re-run \`sinapse init\` (or the greenfield bootstrap) to regenerate. */
198
+
199
+ const path = require('path');
200
+ const fs = require('fs');
201
+ const { spawnSync } = require('child_process');
202
+
203
+ const projectRoot = process.cwd();
204
+
205
+ // --- 1. SINAPSE staged secret scan (fail-CLOSED) ----------------------------
206
+ let scanStagedFiles;
207
+ let getStagedFiles;
208
+ try {
209
+ // The scanner is bundled next to this hook under ./lib so the require path is
210
+ // stable no matter where the project lives.
211
+ ({ scanStagedFiles, getStagedFiles } = require(path.join(__dirname, 'lib', 'staged-secret-scan.js')));
212
+ } catch (loadErr) {
213
+ process.stderr.write('SINAPSE Secret Scan: scanner could not be loaded — blocking commit (fail-closed).\\n');
214
+ process.stderr.write(' ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)) + '\\n');
215
+ process.exit(1);
216
+ }
217
+
218
+ try {
219
+ const files = getStagedFiles();
220
+ const findings = scanStagedFiles(files);
221
+ if (findings.length > 0) {
222
+ process.stderr.write('\\nSINAPSE Secret Scan: commit blocked.\\n\\n');
223
+ for (const f of findings) {
224
+ process.stderr.write(' - ' + f.filePath + ': ' + f.reason + '\\n');
225
+ }
226
+ process.stderr.write('\\nRemove the sensitive content before committing.\\n\\n');
227
+ process.exit(1);
228
+ }
229
+ } catch (scanErr) {
230
+ process.stderr.write('SINAPSE Secret Scan: scan failed — blocking commit (fail-closed).\\n');
231
+ process.stderr.write(' ' + (scanErr && scanErr.message ? scanErr.message : String(scanErr)) + '\\n');
232
+ process.exit(1);
233
+ }
234
+
235
+ // --- 2. Chain to a pre-existing hook (husky or backed-up prior hook) --------
236
+ const explicitChain = ${chainLiteral};
237
+ const candidates = [];
238
+ if (explicitChain) {
239
+ candidates.push(path.resolve(projectRoot, explicitChain));
240
+ }
241
+ candidates.push(path.join(projectRoot, '.husky', 'pre-commit'));
242
+
243
+ const selfPath = __filename;
244
+ for (const candidate of candidates) {
245
+ if (!candidate || !fs.existsSync(candidate)) continue;
246
+ if (path.resolve(candidate) === path.resolve(selfPath)) continue; // never recurse into self
247
+
248
+ const isJs = candidate.endsWith('.js') || candidate.endsWith('.cjs');
249
+ const cmd = isJs ? process.execPath : candidate;
250
+ const args = isJs ? [candidate] : [];
251
+ const res = spawnSync(cmd, args, { stdio: 'inherit', cwd: projectRoot });
252
+
253
+ if (res.error) {
254
+ // A shell-script hook (.husky uses sh) may not be directly executable via
255
+ // spawnSync on Windows. Try 'sh' as an interpreter before giving up.
256
+ if (!isJs) {
257
+ const shRes = spawnSync('sh', [candidate], { stdio: 'inherit', cwd: projectRoot });
258
+ if (!shRes.error) {
259
+ if (typeof shRes.status === 'number' && shRes.status !== 0) process.exit(shRes.status);
260
+ break;
261
+ }
262
+ }
263
+ process.stderr.write('SINAPSE pre-commit: failed to run chained hook ' + candidate + ': ' + res.error.message + '\\n');
264
+ process.exit(1);
265
+ }
266
+ if (typeof res.status === 'number' && res.status !== 0) {
267
+ process.exit(res.status);
268
+ }
269
+ break; // only chain to the first existing prior hook
270
+ }
271
+
272
+ process.exit(0);
273
+ `;
274
+ }
275
+
276
+ /**
277
+ * Install the SINAPSE git guard into a project.
278
+ *
279
+ * Steps:
280
+ * 1. Verify the target is a git repo (best-effort; we still write the hooks
281
+ * so a later `git init` picks them up via core.hooksPath).
282
+ * 2. If a non-SINAPSE `core.hooksPath` is already set, preserve it by chaining
283
+ * (we do not blow away a project's existing managed hooks dir).
284
+ * 3. Create `.sinapse-ai/git-hooks/`, bundle the scanner lib, write pre-commit.
285
+ * 4. Point `core.hooksPath` at the managed dir.
286
+ *
287
+ * Idempotent. Multiplatform. No shell.
288
+ *
289
+ * @param {Object} options
290
+ * @param {string} options.projectDir - Absolute path to the project root.
291
+ * @returns {Promise<{
292
+ * success: boolean,
293
+ * hooksDir: string,
294
+ * huskyDetected: boolean,
295
+ * chainedTo: (string|null),
296
+ * coreHooksPathSet: boolean,
297
+ * skipped: boolean,
298
+ * error: (string|null),
299
+ * }>}
300
+ */
301
+ async function installGitHooks(options = {}) {
302
+ const projectDir = options.projectDir;
303
+
304
+ const result = {
305
+ success: false,
306
+ hooksDir: null,
307
+ huskyDetected: false,
308
+ chainedTo: null,
309
+ coreHooksPathSet: false,
310
+ skipped: false,
311
+ error: null,
312
+ };
313
+
314
+ if (!projectDir || typeof projectDir !== 'string') {
315
+ result.error = 'installGitHooks requires options.projectDir (absolute string path)';
316
+ return result;
317
+ }
318
+
319
+ try {
320
+ const hooksDir = path.join(projectDir, MANAGED_HOOKS_DIRNAME);
321
+ result.hooksDir = hooksDir;
322
+
323
+ // Detect husky so we can chain to it (and report it).
324
+ const { hasHusky } = detectHusky(projectDir);
325
+ result.huskyDetected = hasHusky;
326
+ if (hasHusky) {
327
+ result.chainedTo = path.join('.husky', 'pre-commit');
328
+ }
329
+
330
+ // Detect a pre-existing non-SINAPSE core.hooksPath. If the project already
331
+ // routes hooks somewhere that ISN'T ours, chain to that dir's pre-commit so
332
+ // we never silently disable the project's existing guard.
333
+ const existingHooksPath = await getCoreHooksPath(projectDir);
334
+ let explicitChain = null;
335
+ if (existingHooksPath) {
336
+ const resolvedExisting = path.resolve(projectDir, existingHooksPath);
337
+ const resolvedOurs = path.resolve(hooksDir);
338
+ if (resolvedExisting !== resolvedOurs) {
339
+ const existingPreCommit = path.join(resolvedExisting, 'pre-commit');
340
+ if (fs.existsSync(existingPreCommit)) {
341
+ // Store as a project-relative path for portability.
342
+ explicitChain = path.relative(projectDir, existingPreCommit);
343
+ result.chainedTo = explicitChain;
344
+ }
345
+ }
346
+ }
347
+
348
+ // Bundle the scanner lib and write the managed pre-commit hook.
349
+ copyScannerLib(hooksDir);
350
+ writeManagedHook(hooksDir, 'pre-commit', buildPreCommitHook({ chainHookRel: explicitChain }));
351
+
352
+ // Point core.hooksPath at our managed dir (project-relative for portability).
353
+ const relHooksDir = MANAGED_HOOKS_DIRNAME.split(path.sep).join('/');
354
+ const set = await setCoreHooksPath(projectDir, relHooksDir);
355
+ result.coreHooksPathSet = set.success;
356
+
357
+ if (!set.success) {
358
+ // Hooks are on disk; only the git config wiring failed. Surface a soft
359
+ // error but keep the artifacts so a later retry / manual `git config`
360
+ // completes the wiring. Not fatal to the overall install.
361
+ result.error = set.error;
362
+ result.success = false;
363
+ return result;
364
+ }
365
+
366
+ result.success = true;
367
+ return result;
368
+ } catch (error) {
369
+ result.error = error.message;
370
+ return result;
371
+ }
372
+ }
373
+
374
+ module.exports = {
375
+ installGitHooks,
376
+ detectHusky,
377
+ getCoreHooksPath,
378
+ setCoreHooksPath,
379
+ writeManagedHook,
380
+ copyScannerLib,
381
+ buildPreCommitHook,
382
+ MANAGED_HOOKS_DIRNAME,
383
+ MANAGED_MARKER,
384
+ };
@@ -335,6 +335,22 @@ async function installSinapseCore(options = {}) {
335
335
  }
336
336
  }
337
337
 
338
+ // Stream B (Frente 4.2): propagate the SINAPSE secret-scan guard into the
339
+ // target project via git core.hooksPath + a managed Node pre-commit hook.
340
+ // Best-effort: a hooks-wiring failure must not abort a successful install.
341
+ spinner.text = 'Installing git secret-scan guard...';
342
+ result.gitHooksInstalled = false;
343
+ try {
344
+ const { installGitHooks } = require('./git-hooks-installer');
345
+ const hooksResult = await installGitHooks({ projectDir: targetDir });
346
+ result.gitHooksInstalled = hooksResult.success;
347
+ if (!hooksResult.success && hooksResult.error) {
348
+ result.errors.push(`Git hooks warning: ${hooksResult.error}`);
349
+ }
350
+ } catch (hooksError) {
351
+ result.errors.push(`Git hooks warning: ${hooksError.message}`);
352
+ }
353
+
338
354
  result.success = true;
339
355
  spinner.succeed(`SINAPSE core installed (${result.installedFiles.length} files)`);
340
356
 
@@ -560,8 +560,12 @@ async function copyClaudeHooksFolder(projectRoot) {
560
560
  'enforce-architecture-first.cjs',
561
561
  'enforce-delegation.cjs',
562
562
  'enforce-story-gate.cjs',
563
+ 'enforce-framework-boundary.cjs',
563
564
  'secret-scanning.cjs',
564
565
  'write-path-validation.cjs',
566
+ // Frente 4.3 — DORA + OTel telemetry hooks (STREAM A)
567
+ 'telemetry-post-tool.cjs',
568
+ 'telemetry-stop.cjs',
565
569
  'README.md',
566
570
  ];
567
571
 
@@ -620,6 +624,13 @@ const HOOK_EVENT_MAP = {
620
624
  matcher: 'Write|Edit',
621
625
  timeout: 5,
622
626
  },
627
+ // Boundary L1-L4 — protects untouchable framework core when
628
+ // boundary.frameworkProtection=true (read dynamically from core-config.yaml).
629
+ 'enforce-framework-boundary.cjs': {
630
+ event: 'PreToolUse',
631
+ matcher: 'Write|Edit',
632
+ timeout: 5,
633
+ },
623
634
  'write-path-validation.cjs': {
624
635
  event: 'PreToolUse',
625
636
  matcher: 'Write|Edit',
@@ -635,6 +646,18 @@ const HOOK_EVENT_MAP = {
635
646
  matcher: 'Write|Edit',
636
647
  timeout: 5,
637
648
  },
649
+ // Frente 4.3 — DORA + OTel observability hooks (STREAM A)
650
+ // FAIL-OPEN: these hooks swallow all errors and always exit 0.
651
+ 'telemetry-post-tool.cjs': {
652
+ event: 'PostToolUse',
653
+ matcher: null,
654
+ timeout: 3,
655
+ },
656
+ 'telemetry-stop.cjs': {
657
+ event: 'Stop',
658
+ matcher: null,
659
+ timeout: 5,
660
+ },
638
661
  };
639
662
 
640
663
  /** Default event config for unmapped hooks (backwards compatible). */
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const path = require('path');
12
+ const fs = require('fs');
12
13
 
13
14
  /**
14
15
  * Maximum input lengths to prevent buffer overflow
@@ -19,6 +20,32 @@ const INPUT_LIMITS = {
19
20
  generic: 500,
20
21
  };
21
22
 
23
+ /**
24
+ * Canonicalize the deepest EXISTING ancestor of a path (the target itself may
25
+ * not exist yet during install). Used to detect symlink escapes that
26
+ * path.relative() alone can't see (P3-002). Returns the realpath of the nearest
27
+ * existing ancestor, or the resolved path unchanged when nothing resolves.
28
+ * @param {string} resolved - An already path.resolve()'d absolute path
29
+ * @returns {string}
30
+ */
31
+ function realpathOfExistingAncestor(resolved) {
32
+ let current = resolved;
33
+ // Walk up until we hit an existing directory we can realpath.
34
+ for (let i = 0; i < 64; i++) {
35
+ try {
36
+ const real = fs.realpathSync(current);
37
+ // Re-attach the non-existent tail so containment checks stay meaningful.
38
+ const tail = path.relative(current, resolved);
39
+ return tail ? path.join(real, tail) : real;
40
+ } catch {
41
+ const parent = path.dirname(current);
42
+ if (parent === current) break; // reached the root
43
+ current = parent;
44
+ }
45
+ }
46
+ return resolved;
47
+ }
48
+
22
49
  /**
23
50
  * Allowed project types (whitelist)
24
51
  */
@@ -94,13 +121,23 @@ function validatePath(input, baseDir = process.cwd()) {
94
121
  // Use path.relative to detect traversal attempts
95
122
  // If resolved is within baseDir, relative path won't start with '..'
96
123
  const relativePath = path.relative(normalizedBaseDir, resolved);
97
-
124
+
98
125
  // Check for up-level traversal indicators
99
126
  // Empty string means paths are identical, which is valid
100
127
  if (relativePath && (relativePath.startsWith('..') || relativePath.includes('..'))) {
101
128
  return 'Path must be within project directory (path traversal detected)';
102
129
  }
103
130
 
131
+ // Symlink-escape guard (P3-002): a symlinked ancestor inside baseDir could
132
+ // point OUT of it — path.relative() on the lexical path wouldn't catch that.
133
+ // Compare canonical (realpath) forms of the deepest existing ancestors.
134
+ const realBase = realpathOfExistingAncestor(normalizedBaseDir);
135
+ const realResolved = realpathOfExistingAncestor(resolved);
136
+ const realRel = path.relative(realBase, realResolved);
137
+ if (realRel && (realRel.startsWith('..') || realRel.includes('..'))) {
138
+ return 'Path must be within project directory (symlink escape detected)';
139
+ }
140
+
104
141
  return true;
105
142
  }
106
143
 
@@ -195,15 +195,19 @@ describe('artifact-copy-pipeline (Story INS-4.3)', () => {
195
195
 
196
196
  test('covers all known hooks', () => {
197
197
  const keys = Object.keys(HOOK_EVENT_MAP);
198
- expect(keys).toHaveLength(8);
198
+ expect(keys).toHaveLength(11);
199
199
  expect(keys).toContain('synapse-engine.cjs');
200
200
  expect(keys).toContain('code-intel-pretool.cjs');
201
201
  expect(keys).toContain('precompact-session-digest.cjs');
202
202
  expect(keys).toContain('enforce-architecture-first.cjs');
203
203
  expect(keys).toContain('enforce-story-gate.cjs');
204
+ expect(keys).toContain('enforce-framework-boundary.cjs');
204
205
  expect(keys).toContain('write-path-validation.cjs');
205
206
  expect(keys).toContain('enforce-delegation.cjs');
206
207
  expect(keys).toContain('secret-scanning.cjs');
208
+ // Telemetry observers added in Onda 4.3 (PostToolUse + Stop, fail-open)
209
+ expect(keys).toContain('telemetry-post-tool.cjs');
210
+ expect(keys).toContain('telemetry-stop.cjs');
207
211
  });
208
212
 
209
213
  test('DEFAULT_HOOK_CONFIG falls back to UserPromptSubmit', () => {