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.
- package/.claude/CLAUDE.md +5 -11
- package/.claude/hooks/README.md +14 -1
- package/.claude/hooks/code-intel-pretool.cjs +115 -0
- package/.claude/hooks/enforce-delegation.cjs +31 -3
- package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
- package/.claude/hooks/enforce-permission-mode.cjs +249 -0
- package/.claude/hooks/secret-scanning.cjs +34 -43
- package/.claude/hooks/synapse-engine.cjs +23 -23
- package/.claude/hooks/telemetry-post-tool.cjs +128 -0
- package/.claude/hooks/telemetry-stop.cjs +132 -0
- package/.claude/hooks/verify-packages.cjs +9 -2
- package/.claude/rules/hook-governance.md +2 -0
- package/.sinapse-ai/cli/commands/health/index.js +24 -0
- package/.sinapse-ai/core/README.md +11 -0
- package/.sinapse-ai/core/config/config-loader.js +19 -0
- package/.sinapse-ai/core/execution/build-orchestrator.js +4 -1
- package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
- package/.sinapse-ai/core/execution/subagent-dispatcher.js +126 -28
- package/.sinapse-ai/core/execution/wave-executor.js +4 -1
- package/.sinapse-ai/core/grounding/README.md +71 -11
- package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
- package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
- package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
- package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
- package/.sinapse-ai/core/health-check/healers/index.js +40 -3
- package/.sinapse-ai/core/ideation/ideation-engine.js +170 -121
- package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
- package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
- package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
- package/.sinapse-ai/core/ids/index.js +30 -0
- package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
- package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
- package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
- package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
- package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
- package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
- package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
- package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
- package/.sinapse-ai/core/orchestration/master-orchestrator.js +105 -7
- package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
- package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
- package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
- package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
- package/.sinapse-ai/core/utils/output-formatter.js +8 -290
- package/.sinapse-ai/core-config.yaml +49 -1
- package/.sinapse-ai/data/entity-registry.yaml +15081 -13735
- package/.sinapse-ai/data/registry-update-log.jsonl +86 -0
- package/.sinapse-ai/development/agents/developer.md +2 -0
- package/.sinapse-ai/development/agents/devops.md +9 -0
- package/.sinapse-ai/development/external-executors/README.md +18 -0
- package/.sinapse-ai/development/external-executors/codex.md +56 -0
- package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
- package/.sinapse-ai/development/scripts/squad/squad-downloader.js +54 -11
- package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
- package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
- package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
- package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
- package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +4 -7
- package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +4 -7
- package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
- package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
- package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
- package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
- package/.sinapse-ai/install-manifest.yaml +158 -90
- package/.sinapse-ai/scripts/pm.sh +18 -6
- package/bin/cli.js +17 -0
- package/bin/commands/agents.js +96 -0
- package/bin/commands/doctor.js +15 -0
- package/bin/commands/ideate.js +129 -0
- package/bin/commands/uninstall.js +40 -0
- package/bin/postinstall.js +50 -4
- package/bin/sinapse.js +146 -2
- package/bin/utils/secret-scanner-core.js +253 -0
- package/bin/utils/staged-secret-scan.js +106 -40
- package/package.json +13 -3
- package/packages/installer/src/installer/git-hooks-installer.js +384 -0
- package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
- package/packages/installer/src/wizard/ide-config-generator.js +23 -0
- package/packages/installer/src/wizard/validators.js +38 -1
- package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
- package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
- package/scripts/eval-runner.js +422 -0
- package/scripts/generate-install-manifest.js +13 -9
- package/scripts/generate-synapse-runtime.js +51 -0
- package/scripts/validate-all.js +1 -0
- package/scripts/validate-evals.js +466 -0
- package/scripts/validate-schemas.js +539 -0
- package/scripts/validate-squad-orqx.js +9 -2
- package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
- package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
- package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
|
@@ -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(
|
|
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', () => {
|