@vibecheckai/cli 3.1.8 → 3.2.1

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 (84) hide show
  1. package/bin/registry.js +106 -116
  2. package/bin/runners/context/generators/mcp.js +18 -0
  3. package/bin/runners/context/index.js +72 -4
  4. package/bin/runners/context/proof-context.js +293 -1
  5. package/bin/runners/context/security-scanner.js +311 -73
  6. package/bin/runners/lib/agent-firewall/change-packet/builder.js +214 -0
  7. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  8. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  9. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  10. package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
  11. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  12. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  13. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  14. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
  15. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  16. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
  17. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  18. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  19. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  20. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  21. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  22. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  23. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  24. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  25. package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
  26. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  27. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  28. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  29. package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
  30. package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
  31. package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
  32. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  33. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  34. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
  35. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
  36. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
  37. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  38. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  39. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  40. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  41. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  42. package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
  43. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  44. package/bin/runners/lib/analysis-core.js +198 -180
  45. package/bin/runners/lib/analyzers.js +1394 -224
  46. package/bin/runners/lib/detectors-v2.js +560 -641
  47. package/bin/runners/lib/entitlements-v2.js +48 -1
  48. package/bin/runners/lib/evidence-pack.js +678 -0
  49. package/bin/runners/lib/fingerprint.js +377 -0
  50. package/bin/runners/lib/html-proof-report.js +913 -0
  51. package/bin/runners/lib/missions/plan.js +231 -41
  52. package/bin/runners/lib/missions/templates.js +125 -0
  53. package/bin/runners/lib/route-truth.js +1167 -322
  54. package/bin/runners/lib/scan-output.js +558 -235
  55. package/bin/runners/lib/ship-output.js +901 -641
  56. package/bin/runners/lib/truth.js +1004 -321
  57. package/bin/runners/runAgent.js +161 -0
  58. package/bin/runners/runCheckpoint.js +44 -3
  59. package/bin/runners/runContext.d.ts +4 -0
  60. package/bin/runners/runDoctor.js +10 -2
  61. package/bin/runners/runFirewall.js +134 -0
  62. package/bin/runners/runFirewallHook.js +56 -0
  63. package/bin/runners/runFix.js +51 -341
  64. package/bin/runners/runInit.js +11 -0
  65. package/bin/runners/runPolish.d.ts +4 -0
  66. package/bin/runners/runPolish.js +608 -29
  67. package/bin/runners/runProve.js +210 -25
  68. package/bin/runners/runReality.js +846 -101
  69. package/bin/runners/runScan.js +351 -14
  70. package/bin/runners/runShip.js +19 -3
  71. package/bin/runners/runTruth.js +89 -0
  72. package/bin/runners/runWatch.js +14 -1
  73. package/bin/vibecheck.js +32 -2
  74. package/mcp-server/agent-firewall-interceptor.js +164 -0
  75. package/mcp-server/consolidated-tools.js +408 -42
  76. package/mcp-server/index.js +498 -327
  77. package/mcp-server/proof-tools.js +571 -0
  78. package/mcp-server/tier-auth.js +22 -19
  79. package/mcp-server/tools-v3.js +744 -0
  80. package/mcp-server/truth-context.js +131 -90
  81. package/mcp-server/truth-firewall-tools.js +1494 -941
  82. package/package.json +3 -1
  83. package/bin/runners/runInstall.js +0 -281
  84. package/bin/runners/runLabs.js +0 -341
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Base Interceptor
3
+ *
4
+ * Common logic for all IDE interceptors.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const path = require("path");
10
+ const fs = require("fs");
11
+ const { extractClaims } = require("../claims/extractor");
12
+ const { resolveEvidence } = require("../evidence/resolver");
13
+ const { evaluatePolicy } = require("../policy/engine");
14
+ const { buildChangePacket, buildMultiFileChangePacket } = require("../change-packet/builder");
15
+ const { storePacket } = require("../change-packet/store");
16
+ const { generateUnblockPlan } = require("../unblock/planner");
17
+ const { loadPolicy } = require("../policy/loader");
18
+
19
+ /**
20
+ * Intercept a file write attempt
21
+ * @param {object} params
22
+ * @param {string} params.projectRoot - Project root directory
23
+ * @param {string} params.agentId - Agent identifier
24
+ * @param {string} params.intent - Agent intent message
25
+ * @param {string} params.filePath - File path (relative to project root)
26
+ * @param {string} params.content - New file content
27
+ * @param {string} params.oldContent - Old file content (optional)
28
+ * @returns {object} Interception result
29
+ */
30
+ async function interceptFileWrite({
31
+ projectRoot,
32
+ agentId,
33
+ intent,
34
+ filePath,
35
+ content,
36
+ oldContent = null
37
+ }) {
38
+ // Load policy
39
+ const policy = loadPolicy(projectRoot);
40
+
41
+ // Generate diff
42
+ const diff = generateDiff(oldContent, content);
43
+
44
+ // Extract claims - write content to temp file if file doesn't exist
45
+ const fileAbs = path.join(projectRoot, filePath);
46
+ let tempFile = null;
47
+
48
+ if (!fs.existsSync(fileAbs) && content) {
49
+ // Write content to temp file for extraction
50
+ const tempDir = path.join(projectRoot, ".vibecheck", "temp");
51
+ if (!fs.existsSync(tempDir)) {
52
+ fs.mkdirSync(tempDir, { recursive: true });
53
+ }
54
+ tempFile = path.join(tempDir, path.basename(filePath));
55
+ fs.writeFileSync(tempFile, content, "utf8");
56
+ }
57
+
58
+ const fileToExtract = fs.existsSync(fileAbs) ? fileAbs : (tempFile || fileAbs);
59
+ const { claims } = extractClaims({
60
+ repoRoot: projectRoot,
61
+ changedFilesAbs: [fileToExtract]
62
+ });
63
+
64
+ // Clean up temp file
65
+ if (tempFile && fs.existsSync(tempFile)) {
66
+ fs.unlinkSync(tempFile);
67
+ }
68
+
69
+ // Resolve evidence
70
+ const evidence = resolveEvidence(projectRoot, claims);
71
+
72
+ // Build file info
73
+ const files = [{
74
+ path: filePath,
75
+ linesChanged: calculateLinesChanged(diff),
76
+ domain: classifyFileDomain(filePath)
77
+ }];
78
+
79
+ // Evaluate policy
80
+ const verdict = evaluatePolicy({
81
+ policy,
82
+ claims,
83
+ evidence,
84
+ files,
85
+ intent
86
+ });
87
+
88
+ // Generate unblock plan if blocked
89
+ let unblockPlan = null;
90
+ if (verdict.decision === "BLOCK") {
91
+ unblockPlan = generateUnblockPlan({
92
+ violations: verdict.violations,
93
+ claims,
94
+ projectRoot
95
+ });
96
+ }
97
+
98
+ // Build change packet
99
+ const packet = buildChangePacket({
100
+ agentId,
101
+ intent,
102
+ diff,
103
+ filePath,
104
+ claims,
105
+ evidence,
106
+ verdict,
107
+ unblockPlan,
108
+ policy
109
+ });
110
+
111
+ // Store packet if policy requires it
112
+ if (policy.output?.write_change_packets !== false) {
113
+ storePacket(projectRoot, packet);
114
+ }
115
+
116
+ // Return interception result
117
+ return {
118
+ allowed: verdict.decision === "ALLOW",
119
+ verdict: verdict.decision,
120
+ violations: verdict.violations,
121
+ unblockPlan,
122
+ packetId: packet.id,
123
+ message: verdict.message
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Intercept multiple file writes
129
+ */
130
+ async function interceptMultiFileWrite({
131
+ projectRoot,
132
+ agentId,
133
+ intent,
134
+ changes
135
+ }) {
136
+ // Load policy
137
+ const policy = loadPolicy(projectRoot);
138
+
139
+ // Extract claims from all files
140
+ const allClaims = [];
141
+ const allFiles = [];
142
+
143
+ for (const change of changes) {
144
+ const fileAbs = path.join(projectRoot, change.filePath);
145
+ const { claims } = extractClaims({
146
+ repoRoot: projectRoot,
147
+ changedFilesAbs: [fileAbs]
148
+ });
149
+
150
+ allClaims.push(...claims.map(c => ({ ...c, file: change.filePath })));
151
+ allFiles.push({
152
+ path: change.filePath,
153
+ linesChanged: calculateLinesChanged(change.diff),
154
+ domain: classifyFileDomain(change.filePath)
155
+ });
156
+ }
157
+
158
+ // Resolve evidence
159
+ const evidence = resolveEvidence(projectRoot, allClaims);
160
+
161
+ // Evaluate policy
162
+ const verdict = evaluatePolicy({
163
+ policy,
164
+ claims: allClaims,
165
+ evidence,
166
+ files: allFiles,
167
+ intent
168
+ });
169
+
170
+ // Generate unblock plan if blocked
171
+ let unblockPlan = null;
172
+ if (verdict.decision === "BLOCK") {
173
+ unblockPlan = generateUnblockPlan({
174
+ violations: verdict.violations,
175
+ claims: allClaims,
176
+ projectRoot
177
+ });
178
+ }
179
+
180
+ // Build change packet
181
+ const packet = buildMultiFileChangePacket({
182
+ agentId,
183
+ intent,
184
+ changes: changes.map(c => ({
185
+ filePath: c.filePath,
186
+ diff: c.diff,
187
+ claims: allClaims.filter(cl => cl.file === c.filePath)
188
+ })),
189
+ evidence,
190
+ verdict,
191
+ unblockPlan,
192
+ policy
193
+ });
194
+
195
+ // Store packet if policy requires it
196
+ if (policy.output?.write_change_packets !== false) {
197
+ storePacket(projectRoot, packet);
198
+ }
199
+
200
+ // Return interception result
201
+ return {
202
+ allowed: verdict.decision === "ALLOW",
203
+ verdict: verdict.decision,
204
+ violations: verdict.violations,
205
+ unblockPlan,
206
+ packetId: packet.id,
207
+ message: verdict.message
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Generate unified diff
213
+ */
214
+ function generateDiff(oldContent, newContent) {
215
+ if (!oldContent) {
216
+ return {
217
+ before: "",
218
+ after: newContent,
219
+ unified: generateUnifiedDiff("", newContent)
220
+ };
221
+ }
222
+
223
+ return {
224
+ before: oldContent,
225
+ after: newContent,
226
+ unified: generateUnifiedDiff(oldContent, newContent)
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Generate unified diff format
232
+ */
233
+ function generateUnifiedDiff(oldContent, newContent) {
234
+ const oldLines = oldContent.split(/\r?\n/);
235
+ const newLines = newContent.split(/\r?\n/);
236
+
237
+ const diff = [];
238
+ let i = 0, j = 0;
239
+
240
+ while (i < oldLines.length || j < newLines.length) {
241
+ if (i >= oldLines.length) {
242
+ diff.push(`+${newLines[j]}`);
243
+ j++;
244
+ } else if (j >= newLines.length) {
245
+ diff.push(`-${oldLines[i]}`);
246
+ i++;
247
+ } else if (oldLines[i] === newLines[j]) {
248
+ diff.push(` ${oldLines[i]}`);
249
+ i++;
250
+ j++;
251
+ } else {
252
+ // Try to find matching line ahead
253
+ let found = false;
254
+ for (let k = j + 1; k < Math.min(j + 10, newLines.length); k++) {
255
+ if (oldLines[i] === newLines[k]) {
256
+ // Add new lines
257
+ for (let l = j; l < k; l++) {
258
+ diff.push(`+${newLines[l]}`);
259
+ }
260
+ j = k;
261
+ found = true;
262
+ break;
263
+ }
264
+ }
265
+
266
+ if (!found) {
267
+ diff.push(`-${oldLines[i]}`);
268
+ diff.push(`+${newLines[j]}`);
269
+ i++;
270
+ j++;
271
+ }
272
+ }
273
+ }
274
+
275
+ return diff.join("\n");
276
+ }
277
+
278
+ /**
279
+ * Calculate lines changed from diff
280
+ */
281
+ function calculateLinesChanged(diff) {
282
+ if (!diff || !diff.unified) return 0;
283
+ return diff.unified.split('\n').filter(line =>
284
+ line.startsWith('+') || line.startsWith('-')
285
+ ).length;
286
+ }
287
+
288
+ /**
289
+ * Classify file domain
290
+ */
291
+ function classifyFileDomain(filePath) {
292
+ const s = filePath.toLowerCase();
293
+ if (s.includes("auth")) return "auth";
294
+ if (s.includes("stripe") || s.includes("payment")) return "payments";
295
+ if (s.includes("routes") || s.includes("router") || s.includes("api")) return "routes";
296
+ if (s.includes("schema") || s.includes("contract") || s.includes("openapi")) return "contracts";
297
+ if (s.includes("ui") || s.includes("components") || s.includes("pages")) return "ui";
298
+ return "general";
299
+ }
300
+
301
+ module.exports = {
302
+ interceptFileWrite,
303
+ interceptMultiFileWrite
304
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Cursor IDE Interceptor
3
+ *
4
+ * Cursor-specific integration for agent firewall.
5
+ * Hooks into Cursor's file write events.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const { interceptFileWrite } = require("./base");
11
+
12
+ /**
13
+ * Intercept file write in Cursor
14
+ * This would be called by Cursor's extension/plugin system
15
+ */
16
+ async function interceptCursorWrite({
17
+ projectRoot,
18
+ filePath,
19
+ content,
20
+ oldContent,
21
+ agentId = "cursor"
22
+ }) {
23
+ return await interceptFileWrite({
24
+ projectRoot,
25
+ agentId,
26
+ intent: "Cursor AI edit",
27
+ filePath,
28
+ content,
29
+ oldContent
30
+ });
31
+ }
32
+
33
+ module.exports = {
34
+ interceptCursorWrite
35
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * VS Code LSP Interceptor
3
+ *
4
+ * VS Code LSP integration for agent firewall.
5
+ * Generic LSP hook for file writes.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const { interceptFileWrite } = require("./base");
11
+
12
+ /**
13
+ * Intercept file write via LSP
14
+ * This would be called by VS Code's LSP server
15
+ */
16
+ async function interceptVSCodeWrite({
17
+ projectRoot,
18
+ filePath,
19
+ content,
20
+ oldContent,
21
+ agentId = "vscode"
22
+ }) {
23
+ return await interceptFileWrite({
24
+ projectRoot,
25
+ agentId,
26
+ intent: "VS Code AI edit",
27
+ filePath,
28
+ content,
29
+ oldContent
30
+ });
31
+ }
32
+
33
+ module.exports = {
34
+ interceptVSCodeWrite
35
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Windsurf IDE Interceptor
3
+ *
4
+ * Windsurf-specific integration for agent firewall.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const { interceptFileWrite } = require("./base");
10
+
11
+ /**
12
+ * Intercept file write in Windsurf
13
+ * This would be called by Windsurf's extension/plugin system
14
+ */
15
+ async function interceptWindsurfWrite({
16
+ projectRoot,
17
+ filePath,
18
+ content,
19
+ oldContent,
20
+ agentId = "windsurf"
21
+ }) {
22
+ return await interceptFileWrite({
23
+ projectRoot,
24
+ agentId,
25
+ intent: "Windsurf AI edit",
26
+ filePath,
27
+ content,
28
+ oldContent
29
+ });
30
+ }
31
+
32
+ module.exports = {
33
+ interceptWindsurfWrite
34
+ };
@@ -0,0 +1,84 @@
1
+ {
2
+ "version": "1.0",
3
+ "mode": "enforce",
4
+ "profile": "repo-lock",
5
+ "scope": {
6
+ "max_files_touched": 10,
7
+ "max_lines_changed": 600,
8
+ "blocked_paths": [
9
+ "**/node_modules/**",
10
+ "**/dist/**",
11
+ "**/.next/**",
12
+ "**/.vibecheck/packets/**"
13
+ ],
14
+ "allowed_paths": [
15
+ "apps/**",
16
+ "packages/**",
17
+ "src/**"
18
+ ],
19
+ "require_intent_for_expand_scope": true
20
+ },
21
+ "hard_domains": {
22
+ "routes": true,
23
+ "env": true,
24
+ "auth": true,
25
+ "contracts": true,
26
+ "payments": true,
27
+ "side_effects": true
28
+ },
29
+ "rules": {
30
+ "ghost_route": {
31
+ "severity": "block",
32
+ "enabled": true
33
+ },
34
+ "ghost_env": {
35
+ "severity": "block",
36
+ "enabled": true
37
+ },
38
+ "auth_drift": {
39
+ "severity": "block",
40
+ "enabled": true
41
+ },
42
+ "contract_drift": {
43
+ "severity": "block",
44
+ "enabled": true
45
+ },
46
+ "fake_success_ui": {
47
+ "severity": "warn",
48
+ "enabled": true,
49
+ "block_if_domain": ["payments", "auth", "side_effects"]
50
+ },
51
+ "scope_explosion": {
52
+ "severity": "block",
53
+ "enabled": true
54
+ },
55
+ "unsafe_side_effect": {
56
+ "severity": "block",
57
+ "enabled": true
58
+ }
59
+ },
60
+ "evidence": {
61
+ "require_pointers": true,
62
+ "acceptable_sources": [
63
+ "truthpack.routes",
64
+ "truthpack.env",
65
+ "truthpack.auth",
66
+ "truthpack.contracts",
67
+ "repo.search"
68
+ ],
69
+ "pointer_format": "file:lineStart-lineEnd"
70
+ },
71
+ "verification": {
72
+ "require_for_domains": ["auth", "payments", "side_effects"],
73
+ "accepted": ["tests", "reality"],
74
+ "reality": {
75
+ "enabled": true,
76
+ "block_on": ["fake_success", "no_mutation", "network_error"]
77
+ }
78
+ },
79
+ "output": {
80
+ "write_change_packets": true,
81
+ "packet_dir": ".vibecheck/packets",
82
+ "report_formats": ["md", "html"]
83
+ }
84
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Policy Engine
3
+ *
4
+ * Main policy engine that evaluates all rules deterministically.
5
+ * No LLM opinions - pure rule-based evaluation.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const ghostRoute = require("./rules/ghost-route");
11
+ const ghostEnv = require("./rules/ghost-env");
12
+ const authDrift = require("./rules/auth-drift");
13
+ const contractDrift = require("./rules/contract-drift");
14
+ const fakeSuccess = require("./rules/fake-success");
15
+ const scope = require("./rules/scope");
16
+ const unsafeSideEffect = require("./rules/unsafe-side-effect");
17
+ const { generateVerdict } = require("./verdict");
18
+
19
+ /**
20
+ * Evaluate policy against change packet
21
+ * @param {object} params
22
+ * @param {object} params.policy - Policy configuration
23
+ * @param {array} params.claims - Extracted claims
24
+ * @param {array} params.evidence - Evidence resolution results
25
+ * @param {array} params.files - Changed files
26
+ * @param {string} params.intent - Agent intent message
27
+ * @returns {object} Verdict object
28
+ */
29
+ function evaluatePolicy({ policy, claims, evidence, files, intent }) {
30
+ const violations = [];
31
+
32
+ // Evaluate all rules
33
+ const ruleEvaluators = [
34
+ ghostRoute,
35
+ ghostEnv,
36
+ authDrift,
37
+ contractDrift,
38
+ fakeSuccess,
39
+ unsafeSideEffect
40
+ ];
41
+
42
+ // Evaluate claim-based rules
43
+ for (const evaluator of ruleEvaluators) {
44
+ try {
45
+ const violation = evaluator.evaluate({ claims, evidence, policy });
46
+ if (violation) {
47
+ violations.push(violation);
48
+ }
49
+ } catch (error) {
50
+ console.warn(`Rule evaluation error: ${error.message}`);
51
+ }
52
+ }
53
+
54
+ // Evaluate scope rule (file-based)
55
+ try {
56
+ const violation = scope.evaluate({ files, intent, policy });
57
+ if (violation) {
58
+ violations.push(violation);
59
+ }
60
+ } catch (error) {
61
+ console.warn(`Scope rule evaluation error: ${error.message}`);
62
+ }
63
+
64
+ // Generate final verdict
65
+ const verdict = generateVerdict(violations);
66
+
67
+ return verdict;
68
+ }
69
+
70
+ module.exports = {
71
+ evaluatePolicy
72
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Policy Loader
3
+ *
4
+ * Loads and validates agent firewall policy from .vibecheck/policy.json
5
+ * Falls back to default policy if not found.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const Ajv = require("ajv").default || require("ajv");
13
+
14
+ // Load schema
15
+ const schemaPath = path.join(__dirname, "schema.json");
16
+ const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
17
+
18
+ // Load default policy
19
+ const defaultPolicyPath = path.join(__dirname, "default-policy.json");
20
+ const defaultPolicy = JSON.parse(fs.readFileSync(defaultPolicyPath, "utf8"));
21
+
22
+ /**
23
+ * Load policy from project root
24
+ * @param {string} projectRoot - Project root directory
25
+ * @param {object} overrides - Optional policy overrides
26
+ * @returns {object} Loaded and validated policy
27
+ */
28
+ function loadPolicy(projectRoot, overrides = {}) {
29
+ const policyPath = path.join(projectRoot, ".vibecheck", "policy.json");
30
+
31
+ let policy;
32
+
33
+ // Try to load project policy
34
+ if (fs.existsSync(policyPath)) {
35
+ try {
36
+ policy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
37
+ } catch (error) {
38
+ throw new Error(`Failed to parse policy file: ${error.message}`);
39
+ }
40
+ } else {
41
+ // Use default policy
42
+ policy = JSON.parse(JSON.stringify(defaultPolicy));
43
+ }
44
+
45
+ // Apply overrides (deep merge)
46
+ if (Object.keys(overrides).length > 0) {
47
+ policy = deepMerge(policy, overrides);
48
+ }
49
+
50
+ // Validate against schema
51
+ const ajv = new Ajv({ allErrors: true });
52
+ const validate = ajv.compile(schema);
53
+ const valid = validate(policy);
54
+
55
+ if (!valid) {
56
+ const errors = validate.errors.map(e =>
57
+ `${e.instancePath || 'root'} ${e.message}`
58
+ ).join(', ');
59
+ throw new Error(`Policy validation failed: ${errors}`);
60
+ }
61
+
62
+ return policy;
63
+ }
64
+
65
+ /**
66
+ * Deep merge two objects
67
+ * @param {object} target - Target object
68
+ * @param {object} source - Source object to merge
69
+ * @returns {object} Merged object
70
+ */
71
+ function deepMerge(target, source) {
72
+ const output = Object.assign({}, target);
73
+
74
+ if (isObject(target) && isObject(source)) {
75
+ Object.keys(source).forEach(key => {
76
+ if (isObject(source[key])) {
77
+ if (!(key in target)) {
78
+ Object.assign(output, { [key]: source[key] });
79
+ } else {
80
+ output[key] = deepMerge(target[key], source[key]);
81
+ }
82
+ } else {
83
+ Object.assign(output, { [key]: source[key] });
84
+ }
85
+ });
86
+ }
87
+
88
+ return output;
89
+ }
90
+
91
+ /**
92
+ * Check if value is a plain object
93
+ * @param {*} item - Value to check
94
+ * @returns {boolean}
95
+ */
96
+ function isObject(item) {
97
+ return item && typeof item === 'object' && !Array.isArray(item);
98
+ }
99
+
100
+ /**
101
+ * Get default policy
102
+ * @returns {object} Default policy
103
+ */
104
+ function getDefaultPolicy() {
105
+ return JSON.parse(JSON.stringify(defaultPolicy));
106
+ }
107
+
108
+ /**
109
+ * Save policy to project root
110
+ * @param {string} projectRoot - Project root directory
111
+ * @param {object} policy - Policy to save
112
+ */
113
+ function savePolicy(projectRoot, policy) {
114
+ const vibecheckDir = path.join(projectRoot, ".vibecheck");
115
+ const policyPath = path.join(vibecheckDir, "policy.json");
116
+
117
+ // Ensure .vibecheck directory exists
118
+ if (!fs.existsSync(vibecheckDir)) {
119
+ fs.mkdirSync(vibecheckDir, { recursive: true });
120
+ }
121
+
122
+ // Validate before saving
123
+ const ajv = new Ajv({ allErrors: true });
124
+ const validate = ajv.compile(schema);
125
+ const valid = validate(policy);
126
+
127
+ if (!valid) {
128
+ const errors = validate.errors.map(e =>
129
+ `${e.instancePath || 'root'} ${e.message}`
130
+ ).join(', ');
131
+ throw new Error(`Policy validation failed: ${errors}`);
132
+ }
133
+
134
+ fs.writeFileSync(policyPath, JSON.stringify(policy, null, 2));
135
+ }
136
+
137
+ module.exports = {
138
+ loadPolicy,
139
+ getDefaultPolicy,
140
+ savePolicy,
141
+ schema,
142
+ defaultPolicy
143
+ };