@vibecheckai/cli 3.0.4 → 3.0.7

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 (108) hide show
  1. package/bin/dev/run-v2-torture.js +30 -0
  2. package/bin/runners/context/index.js +1 -1
  3. package/bin/runners/lib/analyzers.js +38 -0
  4. package/bin/runners/lib/assets/vibecheck-logo.png +0 -0
  5. package/bin/runners/lib/contracts/auth-contract.js +8 -0
  6. package/bin/runners/lib/contracts/env-contract.js +3 -0
  7. package/bin/runners/lib/contracts/external-contract.js +10 -2
  8. package/bin/runners/lib/contracts/route-contract.js +7 -0
  9. package/bin/runners/lib/contracts.js +804 -0
  10. package/bin/runners/lib/detectors-v2.js +703 -0
  11. package/bin/runners/lib/drift.js +425 -0
  12. package/bin/runners/lib/entitlements-v2.js +3 -1
  13. package/bin/runners/lib/entitlements.js +11 -3
  14. package/bin/runners/lib/env-resolver.js +417 -0
  15. package/bin/runners/lib/extractors/client-calls.js +990 -0
  16. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -0
  17. package/bin/runners/lib/extractors/fastify-routes.js +426 -0
  18. package/bin/runners/lib/extractors/index.js +363 -0
  19. package/bin/runners/lib/extractors/next-routes.js +524 -0
  20. package/bin/runners/lib/extractors/proof-graph.js +431 -0
  21. package/bin/runners/lib/extractors/route-matcher.js +451 -0
  22. package/bin/runners/lib/extractors/truthpack-v2.js +377 -0
  23. package/bin/runners/lib/extractors/ui-bindings.js +547 -0
  24. package/bin/runners/lib/findings-schema.js +281 -0
  25. package/bin/runners/lib/html-report.js +650 -0
  26. package/bin/runners/lib/missions/templates.js +45 -0
  27. package/bin/runners/lib/policy.js +295 -0
  28. package/bin/runners/lib/reality/correlation-detectors.js +359 -0
  29. package/bin/runners/lib/reality/index.js +318 -0
  30. package/bin/runners/lib/reality/request-hashing.js +416 -0
  31. package/bin/runners/lib/reality/request-mapper.js +453 -0
  32. package/bin/runners/lib/reality/safety-rails.js +463 -0
  33. package/bin/runners/lib/reality/semantic-snapshot.js +408 -0
  34. package/bin/runners/lib/reality/toast-detector.js +393 -0
  35. package/bin/runners/lib/report-html.js +5 -0
  36. package/bin/runners/lib/report-templates.js +5 -0
  37. package/bin/runners/lib/report.js +135 -0
  38. package/bin/runners/lib/route-truth.js +10 -10
  39. package/bin/runners/lib/schema-validator.js +350 -0
  40. package/bin/runners/lib/schemas/contracts.schema.json +160 -0
  41. package/bin/runners/lib/schemas/finding.schema.json +100 -0
  42. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -0
  43. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -0
  44. package/bin/runners/lib/schemas/reality-report.schema.json +162 -0
  45. package/bin/runners/lib/schemas/share-pack.schema.json +180 -0
  46. package/bin/runners/lib/schemas/ship-report.schema.json +117 -0
  47. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -0
  48. package/bin/runners/lib/schemas/validator.js +438 -0
  49. package/bin/runners/lib/ui.js +562 -0
  50. package/bin/runners/lib/verdict-engine.js +628 -0
  51. package/bin/runners/runAIAgent.js +228 -1
  52. package/bin/runners/runBadge.js +181 -1
  53. package/bin/runners/runCtx.js +7 -2
  54. package/bin/runners/runCtxDiff.js +301 -0
  55. package/bin/runners/runGuard.js +168 -0
  56. package/bin/runners/runInitGha.js +78 -15
  57. package/bin/runners/runLabs.js +341 -0
  58. package/bin/runners/runLaunch.js +180 -1
  59. package/bin/runners/runMdc.js +203 -1
  60. package/bin/runners/runProof.zip +0 -0
  61. package/bin/runners/runProve.js +23 -0
  62. package/bin/runners/runReplay.js +114 -84
  63. package/bin/runners/runScan.js +111 -32
  64. package/bin/runners/runShip.js +23 -2
  65. package/bin/runners/runTruthpack.js +9 -7
  66. package/bin/runners/runValidate.js +161 -1
  67. package/bin/vibecheck.js +416 -770
  68. package/mcp-server/.guardrail/audit/audit.log.jsonl +2 -0
  69. package/mcp-server/.specs/architecture.mdc +90 -0
  70. package/mcp-server/.specs/security.mdc +30 -0
  71. package/mcp-server/README.md +252 -0
  72. package/mcp-server/agent-checkpoint.js +364 -0
  73. package/mcp-server/architect-tools.js +707 -0
  74. package/mcp-server/audit-mcp.js +206 -0
  75. package/mcp-server/codebase-architect-tools.js +838 -0
  76. package/mcp-server/consolidated-tools.js +804 -0
  77. package/mcp-server/hygiene-tools.js +428 -0
  78. package/mcp-server/index-v1.js +698 -0
  79. package/mcp-server/index.js +2092 -0
  80. package/mcp-server/index.old.js +4137 -0
  81. package/mcp-server/intelligence-tools.js +664 -0
  82. package/mcp-server/intent-drift-tools.js +873 -0
  83. package/mcp-server/mdc-generator.js +298 -0
  84. package/mcp-server/package-lock.json +165 -0
  85. package/mcp-server/package.json +47 -0
  86. package/mcp-server/premium-tools.js +1275 -0
  87. package/mcp-server/test-mcp.js +108 -0
  88. package/mcp-server/test-tools.js +36 -0
  89. package/mcp-server/tier-auth.js +147 -0
  90. package/mcp-server/tools/index.js +72 -0
  91. package/mcp-server/tools-reorganized.ts +244 -0
  92. package/mcp-server/truth-context.js +581 -0
  93. package/mcp-server/truth-firewall-tools.js +1500 -0
  94. package/mcp-server/vibecheck-2.0-tools.js +748 -0
  95. package/mcp-server/vibecheck-tools.js +1075 -0
  96. package/package.json +10 -8
  97. package/bin/guardrail.js +0 -834
  98. package/bin/runners/runAudit.js +0 -2
  99. package/bin/runners/runAutopilot.js +0 -2
  100. package/bin/runners/runCertify.js +0 -2
  101. package/bin/runners/runDashboard.js +0 -10
  102. package/bin/runners/runEnhancedShip.js +0 -2
  103. package/bin/runners/runFixPacks.js +0 -2
  104. package/bin/runners/runNaturalLanguage.js +0 -3
  105. package/bin/runners/runProof.js +0 -2
  106. package/bin/runners/runRealitySniff.js +0 -2
  107. package/bin/runners/runUpgrade.js +0 -2
  108. package/bin/runners/runVerifyAgentOutput.js +0 -2
@@ -0,0 +1,1500 @@
1
+ /**
2
+ * Truth Firewall MCP Tools
3
+ *
4
+ * The "best-in-world" hallucination stopper toolkit.
5
+ * Agents cannot work without these tools - they enforce truth.
6
+ *
7
+ * Core Tools:
8
+ * get_truthpack() - Get the truth pack for this repo
9
+ * compile_context(task) - Get task-targeted context
10
+ * validate_claim(claim) - Verify a claim has evidence (CRITICAL)
11
+ * search_evidence(query) - Find evidence for a claim
12
+ * find_counterexamples() - Falsify a claim
13
+ * propose_patch() - Create proof-carrying patch
14
+ * verify_patch() - Verify a patch meets requirements
15
+ * check_invariants() - Check all invariants
16
+ */
17
+
18
+ import fs from 'fs/promises';
19
+ import path from 'path';
20
+ import crypto from 'crypto';
21
+ import { execSync } from 'child_process';
22
+
23
+ // =============================================================================
24
+ // TOOL DEFINITIONS
25
+ // =============================================================================
26
+
27
+ export const TRUTH_FIREWALL_TOOLS = [
28
+ {
29
+ name: "vibecheck.get_truthpack",
30
+ description: `📦 Get the Truth Pack — the verified ground truth about this codebase.
31
+
32
+ Returns evidence-backed facts about routes, auth, billing, env vars, and schema.
33
+ Every claim has file/line citations and confidence scores.
34
+
35
+ ⚠️ CRITICAL: Use this BEFORE making any assertions about the codebase.`,
36
+ inputSchema: {
37
+ type: "object",
38
+ properties: {
39
+ scope: {
40
+ type: "string",
41
+ enum: ["all", "routes", "auth", "billing", "env", "schema", "graph"],
42
+ description: "What to include (default: all)",
43
+ default: "all",
44
+ },
45
+ refresh: {
46
+ type: "boolean",
47
+ description: "Force recompile even if cached (default: false)",
48
+ default: false,
49
+ },
50
+ },
51
+ },
52
+ },
53
+
54
+ {
55
+ name: "vibecheck.validate_claim",
56
+ description: `🔍 TRUTH FIREWALL — Validate a claim before acting on it.
57
+
58
+ Returns: true | false | unknown
59
+ - If 'unknown': you MUST NOT proceed with actions that depend on this claim
60
+ - If 'false': the claim is disproven, do not proceed
61
+ - If 'true': proceed with evidence citations
62
+
63
+ Examples:
64
+ { "claim": "route_exists", "subject": { "method": "POST", "path": "/api/login" } }
65
+ { "claim": "auth_enforced", "subject": { "path": "/dashboard" } }
66
+ { "claim": "env_var_exists", "subject": { "name": "STRIPE_SECRET_KEY" } }`,
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ claim: {
71
+ type: "string",
72
+ enum: [
73
+ "route_exists",
74
+ "route_guarded",
75
+ "env_var_exists",
76
+ "env_var_used",
77
+ "middleware_applied",
78
+ "auth_enforced",
79
+ "billing_gate_exists",
80
+ "file_exists",
81
+ "function_exists",
82
+ "model_exists",
83
+ "component_exists",
84
+ ],
85
+ description: "Type of claim to verify",
86
+ },
87
+ subject: {
88
+ type: "object",
89
+ description: "What the claim is about",
90
+ properties: {
91
+ method: { type: "string", description: "HTTP method (for routes)" },
92
+ path: { type: "string", description: "Route path or file path" },
93
+ name: { type: "string", description: "Name of function/component/env var" },
94
+ },
95
+ },
96
+ expected: {
97
+ type: "boolean",
98
+ description: "Expected result (default: true)",
99
+ default: true,
100
+ },
101
+ },
102
+ required: ["claim", "subject"],
103
+ },
104
+ },
105
+
106
+ {
107
+ name: "vibecheck.compile_context",
108
+ description: `🎯 Get task-targeted context — minimal sufficient context for your task.
109
+
110
+ Big context = noise. Small context = missing facts.
111
+ This compiles exactly what you need for the task.
112
+
113
+ Returns relevant nodes, edges, evidence, and applicable invariants.`,
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ task: {
118
+ type: "string",
119
+ description: "What you're trying to do (e.g., 'fix dead login button', 'add Stripe checkout')",
120
+ },
121
+ files: {
122
+ type: "array",
123
+ items: { type: "string" },
124
+ description: "Specific files to include",
125
+ },
126
+ policy: {
127
+ type: "string",
128
+ enum: ["strict", "balanced", "permissive"],
129
+ description: "How strict to be about context (default: balanced)",
130
+ default: "balanced",
131
+ },
132
+ },
133
+ required: ["task"],
134
+ },
135
+ },
136
+
137
+ {
138
+ name: "vibecheck.search_evidence",
139
+ description: `📎 Search for evidence in the codebase.
140
+
141
+ Returns code snippets with file/line citations.
142
+ Use this to find proof for claims.`,
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ query: {
147
+ type: "string",
148
+ description: "What to search for (e.g., 'login handler', 'auth middleware', 'Stripe webhook')",
149
+ },
150
+ type: {
151
+ type: "string",
152
+ enum: ["route", "handler", "middleware", "component", "env_var", "model", "any"],
153
+ description: "Type of evidence to find (default: any)",
154
+ default: "any",
155
+ },
156
+ limit: {
157
+ type: "number",
158
+ description: "Max results (default: 10)",
159
+ default: 10,
160
+ },
161
+ },
162
+ required: ["query"],
163
+ },
164
+ },
165
+
166
+ {
167
+ name: "vibecheck.find_counterexamples",
168
+ description: `🔴 Find counterexamples that would disprove a claim.
169
+
170
+ This is the FALSIFICATION mechanism.
171
+ If counterexamples exist, the claim becomes false or low confidence.
172
+
173
+ Use this for high-stakes claims about auth, billing, security.`,
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ claim: {
178
+ type: "string",
179
+ enum: ["auth_enforced", "billing_gate_exists", "route_guarded", "no_bypass"],
180
+ description: "Claim to falsify",
181
+ },
182
+ subject: {
183
+ type: "object",
184
+ description: "What the claim is about",
185
+ properties: {
186
+ path: { type: "string" },
187
+ name: { type: "string" },
188
+ },
189
+ },
190
+ },
191
+ required: ["claim", "subject"],
192
+ },
193
+ },
194
+
195
+ {
196
+ name: "vibecheck.propose_patch",
197
+ description: `📝 Propose a proof-carrying patch.
198
+
199
+ When proposing changes, you MUST attach:
200
+ - Which findings it fixes
201
+ - Which claims it depends on (must be verified)
202
+ - Evidence references
203
+ - Verification commands
204
+
205
+ Patches without proof are NOT eligible for auto-apply.`,
206
+ inputSchema: {
207
+ type: "object",
208
+ properties: {
209
+ diff: {
210
+ type: "string",
211
+ description: "The diff content",
212
+ },
213
+ fixes: {
214
+ type: "array",
215
+ items: { type: "string" },
216
+ description: "Finding IDs this patch fixes",
217
+ },
218
+ claims: {
219
+ type: "array",
220
+ items: { type: "string" },
221
+ description: "Claim IDs this patch depends on (must all be verified)",
222
+ },
223
+ verification: {
224
+ type: "array",
225
+ items: { type: "string" },
226
+ description: "Commands to verify the patch (e.g., 'vibecheck ship', 'pnpm test')",
227
+ },
228
+ },
229
+ required: ["diff", "fixes"],
230
+ },
231
+ },
232
+
233
+ {
234
+ name: "vibecheck.check_invariants",
235
+ description: `⚖️ Check all invariants (product religion rules).
236
+
237
+ Returns:
238
+ - Ship killers: BLOCK deployment
239
+ - Warnings: Require acknowledgment
240
+
241
+ Invariants include:
242
+ - No paid feature without server-side enforcement
243
+ - No success UI without confirmed success
244
+ - No route reference without matching route map entry
245
+ - No silent catch in auth/billing flows`,
246
+ inputSchema: {
247
+ type: "object",
248
+ properties: {
249
+ category: {
250
+ type: "string",
251
+ enum: ["all", "auth", "billing", "security", "ux", "api"],
252
+ description: "Category to check (default: all)",
253
+ default: "all",
254
+ },
255
+ },
256
+ },
257
+ },
258
+
259
+ {
260
+ name: "vibecheck.add_assumption",
261
+ description: `⚠️ Log an assumption (with budget enforcement).
262
+
263
+ Assumptions stack up and cause failures. Track them explicitly.
264
+
265
+ Rules:
266
+ - Max 2 assumptions per mission
267
+ - Must provide verification steps
268
+ - If budget exceeded, you MUST use proof tooling instead`,
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {
272
+ description: {
273
+ type: "string",
274
+ description: "What you're assuming",
275
+ },
276
+ reason: {
277
+ type: "string",
278
+ description: "Why you need this assumption",
279
+ },
280
+ verificationSteps: {
281
+ type: "array",
282
+ items: { type: "string" },
283
+ description: "How to verify this assumption later",
284
+ },
285
+ },
286
+ required: ["description", "verificationSteps"],
287
+ },
288
+ },
289
+
290
+ {
291
+ name: "vibecheck.validate_plan",
292
+ description: `🛡️ HALLUCINATION STOPPER — Validate an AI plan against contracts.
293
+
294
+ Before executing a plan, you MUST validate it against contracts.
295
+ Rejects plans that:
296
+ - Reference routes not in routes.json
297
+ - Use env vars not in env.json
298
+ - Make auth assumptions that contradict auth.json
299
+ - Reference external services not in external.json
300
+
301
+ ⚠️ CRITICAL: If validation fails, you MUST NOT proceed with the plan.
302
+
303
+ Example:
304
+ { "plan": "Create POST /api/checkout endpoint using STRIPE_SECRET_KEY" }
305
+
306
+ Returns: { valid: boolean, violations: [], warnings: [], suggestions: [] }`,
307
+ inputSchema: {
308
+ type: "object",
309
+ properties: {
310
+ plan: {
311
+ type: "string",
312
+ description: "The plan text or JSON to validate",
313
+ },
314
+ strict: {
315
+ type: "boolean",
316
+ description: "If true, warnings also cause rejection (default: false)",
317
+ default: false,
318
+ },
319
+ },
320
+ required: ["plan"],
321
+ },
322
+ },
323
+
324
+ {
325
+ name: "vibecheck.check_drift",
326
+ description: `📊 Check for contract drift — detect when code has changed but contracts are stale.
327
+
328
+ Returns drift findings for:
329
+ - Routes added/removed but not in contract
330
+ - Env vars used but not declared
331
+ - Auth patterns changed
332
+ - External services added
333
+
334
+ Per spec: routes/env/auth drift → BLOCK (AI will lie about these)`,
335
+ inputSchema: {
336
+ type: "object",
337
+ properties: {
338
+ category: {
339
+ type: "string",
340
+ enum: ["all", "routes", "env", "auth", "external"],
341
+ description: "Category to check (default: all)",
342
+ default: "all",
343
+ },
344
+ },
345
+ },
346
+ },
347
+
348
+ {
349
+ name: "vibecheck.get_contracts",
350
+ description: `📜 Get the current contracts (routes/env/auth/external).
351
+
352
+ Contracts are the "you may not lie" rules for this repo.
353
+ AI output must satisfy these contracts.
354
+
355
+ Returns the contract files from .vibecheck/contracts/`,
356
+ inputSchema: {
357
+ type: "object",
358
+ properties: {
359
+ type: {
360
+ type: "string",
361
+ enum: ["all", "routes", "env", "auth", "external"],
362
+ description: "Which contract to get (default: all)",
363
+ default: "all",
364
+ },
365
+ },
366
+ },
367
+ },
368
+ ];
369
+
370
+ // =============================================================================
371
+ // TOOL HANDLERS
372
+ // =============================================================================
373
+
374
+ export async function handleTruthFirewallTool(toolName, args, projectPath = process.cwd()) {
375
+ switch (toolName) {
376
+ case "vibecheck.get_truthpack":
377
+ return await getTruthPack(projectPath, args);
378
+
379
+ case "vibecheck.validate_claim":
380
+ return await validateClaim(projectPath, args);
381
+
382
+ case "vibecheck.compile_context":
383
+ return await compileContext(projectPath, args);
384
+
385
+ case "vibecheck.search_evidence":
386
+ return await searchEvidence(projectPath, args);
387
+
388
+ case "vibecheck.find_counterexamples":
389
+ return await findCounterexamples(projectPath, args);
390
+
391
+ case "vibecheck.propose_patch":
392
+ return await proposePatch(projectPath, args);
393
+
394
+ case "vibecheck.check_invariants":
395
+ return await checkInvariants(projectPath, args);
396
+
397
+ case "vibecheck.add_assumption":
398
+ return await addAssumption(projectPath, args);
399
+
400
+ case "vibecheck.validate_plan":
401
+ return await validatePlanTool(projectPath, args);
402
+
403
+ case "vibecheck.check_drift":
404
+ return await checkDriftTool(projectPath, args);
405
+
406
+ case "vibecheck.get_contracts":
407
+ return await getContractsTool(projectPath, args);
408
+
409
+ default:
410
+ return { error: `Unknown tool: ${toolName}` };
411
+ }
412
+ }
413
+
414
+ // =============================================================================
415
+ // IMPLEMENTATION
416
+ // =============================================================================
417
+
418
+ // In-memory state
419
+ const state = {
420
+ truthPack: null,
421
+ assumptions: [],
422
+ verifiedClaims: new Map(),
423
+ maxAssumptions: 2,
424
+ };
425
+
426
+ async function getTruthPack(projectPath, args) {
427
+ const scope = args.scope || 'all';
428
+ const refresh = args.refresh || false;
429
+
430
+ if (state.truthPack && !refresh) {
431
+ return filterTruthPack(state.truthPack, scope);
432
+ }
433
+
434
+ // Build truth pack
435
+ const truthPack = {
436
+ version: '1.0.0',
437
+ generatedAt: new Date().toISOString(),
438
+ projectPath,
439
+ commitHash: getCommitHash(projectPath),
440
+ sections: {},
441
+ confidence: 0,
442
+ };
443
+
444
+ if (scope === 'all' || scope === 'routes') {
445
+ truthPack.sections.routes = await extractRoutes(projectPath);
446
+ }
447
+ if (scope === 'all' || scope === 'auth') {
448
+ truthPack.sections.auth = await extractAuth(projectPath);
449
+ }
450
+ if (scope === 'all' || scope === 'billing') {
451
+ truthPack.sections.billing = await extractBilling(projectPath);
452
+ }
453
+ if (scope === 'all' || scope === 'env') {
454
+ truthPack.sections.env = await extractEnv(projectPath);
455
+ }
456
+ if (scope === 'all' || scope === 'schema') {
457
+ truthPack.sections.schema = await extractSchema(projectPath);
458
+ }
459
+ if (scope === 'all' || scope === 'graph') {
460
+ truthPack.sections.graph = await extractGraph(projectPath);
461
+ }
462
+
463
+ // Calculate confidence
464
+ const sections = Object.values(truthPack.sections);
465
+ truthPack.confidence = sections.reduce((sum, s) => sum + (s.confidence || 0), 0) / sections.length;
466
+
467
+ state.truthPack = truthPack;
468
+ return truthPack;
469
+ }
470
+
471
+ async function validateClaim(projectPath, args) {
472
+ const { claim, subject, expected = true } = args;
473
+ const claimId = `claim_${crypto.createHash('sha256').update(JSON.stringify({ claim, subject })).digest('hex').slice(0, 12)}`;
474
+
475
+ // Check cache
476
+ if (state.verifiedClaims.has(claimId)) {
477
+ const cached = state.verifiedClaims.get(claimId);
478
+ if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
479
+ return { ...cached.result, cached: true };
480
+ }
481
+ }
482
+
483
+ let result = { result: 'unknown', confidence: 'low', evidence: [], nextSteps: [] };
484
+
485
+ try {
486
+ switch (claim) {
487
+ case 'route_exists':
488
+ result = await verifyRouteExists(projectPath, subject);
489
+ break;
490
+ case 'file_exists':
491
+ result = await verifyFileExists(projectPath, subject);
492
+ break;
493
+ case 'env_var_exists':
494
+ case 'env_var_used':
495
+ result = await verifyEnvVar(projectPath, subject, claim);
496
+ break;
497
+ case 'auth_enforced':
498
+ case 'route_guarded':
499
+ result = await verifyRouteGuarded(projectPath, subject);
500
+ break;
501
+ case 'function_exists':
502
+ case 'component_exists':
503
+ case 'model_exists':
504
+ result = await verifyEntityExists(projectPath, subject, claim);
505
+ break;
506
+ default:
507
+ result.nextSteps = [`Claim type "${claim}" not yet implemented. Use search_evidence instead.`];
508
+ }
509
+ } catch (error) {
510
+ result.nextSteps = [`Verification error: ${error.message}`];
511
+ }
512
+
513
+ // If unknown, add helpful next steps
514
+ if (result.result === 'unknown') {
515
+ result.nextSteps.push(
516
+ 'call vibecheck.search_evidence to find related code',
517
+ 'call vibecheck.get_truthpack to get full context',
518
+ );
519
+ result.warning = '⚠️ UNKNOWN claims BLOCK dependent actions. Verify before proceeding.';
520
+ }
521
+
522
+ // Cache result
523
+ state.verifiedClaims.set(claimId, { result, timestamp: Date.now() });
524
+
525
+ return {
526
+ claimId,
527
+ ...result,
528
+ timestamp: new Date().toISOString(),
529
+ };
530
+ }
531
+
532
+ async function compileContext(projectPath, args) {
533
+ const { task, files = [], policy = 'balanced' } = args;
534
+
535
+ // Analyze task to determine relevant domains
536
+ const domains = detectDomains(task);
537
+ const keywords = extractKeywords(task);
538
+
539
+ // Get relevant parts of truth pack
540
+ const truthPack = await getTruthPack(projectPath, { scope: 'all' });
541
+
542
+ // Filter to relevant content
543
+ const relevantRoutes = truthPack.sections.routes?.routes?.filter(r =>
544
+ keywords.some(k => r.path.includes(k) || r.file.includes(k))
545
+ ) || [];
546
+
547
+ const relevantAuth = domains.includes('auth') ? truthPack.sections.auth : null;
548
+ const relevantBilling = domains.includes('billing') ? truthPack.sections.billing : null;
549
+
550
+ // Get applicable invariants
551
+ const invariants = getInvariantsForDomains(domains);
552
+
553
+ // Estimate token count
554
+ const tokenCount = estimateTokens({ relevantRoutes, relevantAuth, relevantBilling });
555
+
556
+ return {
557
+ task,
558
+ policy,
559
+ domains,
560
+ context: {
561
+ routes: relevantRoutes.slice(0, policy === 'strict' ? 10 : 50),
562
+ auth: relevantAuth,
563
+ billing: relevantBilling,
564
+ },
565
+ invariants,
566
+ tokenCount,
567
+ warnings: generateContextWarnings(domains, policy, relevantRoutes.length),
568
+ };
569
+ }
570
+
571
+ async function searchEvidence(projectPath, args) {
572
+ const { query, type = 'any', limit = 10 } = args;
573
+ const results = [];
574
+
575
+ const files = await findSourceFiles(projectPath);
576
+ const pattern = new RegExp(query, 'gi');
577
+
578
+ for (const file of files.slice(0, 100)) {
579
+ try {
580
+ const content = await fs.readFile(file, 'utf8');
581
+ const lines = content.split('\n');
582
+ const relPath = path.relative(projectPath, file);
583
+
584
+ for (let i = 0; i < lines.length; i++) {
585
+ if (pattern.test(lines[i])) {
586
+ const snippet = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join('\n');
587
+ results.push({
588
+ file: relPath,
589
+ line: i + 1,
590
+ snippet: snippet.slice(0, 300),
591
+ hash: crypto.createHash('sha256').update(lines[i]).digest('hex').slice(0, 16),
592
+ });
593
+
594
+ if (results.length >= limit) break;
595
+ }
596
+ pattern.lastIndex = 0;
597
+ }
598
+
599
+ if (results.length >= limit) break;
600
+ } catch {}
601
+ }
602
+
603
+ return {
604
+ query,
605
+ count: results.length,
606
+ results,
607
+ };
608
+ }
609
+
610
+ async function findCounterexamples(projectPath, args) {
611
+ const { claim, subject } = args;
612
+ const counterexamples = [];
613
+
614
+ switch (claim) {
615
+ case 'auth_enforced':
616
+ // Check for client-only guards
617
+ const authResult = await verifyRouteGuarded(projectPath, subject);
618
+ if (authResult.result === 'true' && authResult.evidence) {
619
+ // Check if guard is client-only
620
+ for (const ev of authResult.evidence) {
621
+ const content = await fs.readFile(path.join(projectPath, ev.file), 'utf8');
622
+ if (content.includes('client') && !content.includes('middleware')) {
623
+ counterexamples.push({
624
+ type: 'bypass_possible',
625
+ description: 'Auth appears to be client-side only - can be bypassed',
626
+ evidence: ev,
627
+ severity: 'ship_killer',
628
+ });
629
+ }
630
+ }
631
+ }
632
+ break;
633
+
634
+ case 'billing_gate_exists':
635
+ // Check for client-only tier checks
636
+ const evidence = await searchEvidence(projectPath, { query: subject.name || 'tier', limit: 5 });
637
+ for (const ev of evidence.results) {
638
+ if (ev.snippet.includes('localStorage') || ev.snippet.includes('client')) {
639
+ counterexamples.push({
640
+ type: 'bypass_possible',
641
+ description: 'Billing check appears client-side only',
642
+ evidence: ev,
643
+ severity: 'ship_killer',
644
+ });
645
+ }
646
+ }
647
+ break;
648
+ }
649
+
650
+ return {
651
+ claim,
652
+ subject,
653
+ counterexamples,
654
+ claimDemoted: counterexamples.length > 0,
655
+ };
656
+ }
657
+
658
+ async function proposePatch(projectPath, args) {
659
+ const { diff, fixes, claims = [], verification = [] } = args;
660
+
661
+ // Validate all dependent claims
662
+ const claimValidation = [];
663
+ for (const claimId of claims) {
664
+ const cached = state.verifiedClaims.get(claimId);
665
+ if (!cached) {
666
+ claimValidation.push({ claimId, valid: false, error: 'Claim not verified' });
667
+ } else if (cached.result.result === 'unknown') {
668
+ claimValidation.push({ claimId, valid: false, error: 'Claim is unknown - cannot proceed' });
669
+ } else if (cached.result.result === 'false') {
670
+ claimValidation.push({ claimId, valid: false, error: 'Claim is false - invalid dependency' });
671
+ } else {
672
+ claimValidation.push({ claimId, valid: true });
673
+ }
674
+ }
675
+
676
+ const allClaimsValid = claimValidation.every(c => c.valid);
677
+
678
+ const patch = {
679
+ patchId: `patch_${crypto.randomUUID().slice(0, 12)}`,
680
+ diff: diff.slice(0, 5000), // Truncate for storage
681
+ fixes,
682
+ dependsOnClaims: claims,
683
+ verification: verification.length > 0 ? verification : ['vibecheck ship', 'pnpm test'],
684
+ createdAt: new Date().toISOString(),
685
+ eligible: allClaimsValid && fixes.length > 0,
686
+ claimValidation,
687
+ };
688
+
689
+ if (!patch.eligible) {
690
+ patch.blockers = claimValidation.filter(c => !c.valid);
691
+ patch.message = '⚠️ Patch NOT eligible for auto-apply. Fix blockers first.';
692
+ }
693
+
694
+ return patch;
695
+ }
696
+
697
+ async function checkInvariants(projectPath, args) {
698
+ const category = args.category || 'all';
699
+ const shipKillers = [];
700
+ const warnings = [];
701
+
702
+ // Check for silent catches in auth/billing
703
+ const silentCatches = await searchEvidence(projectPath, {
704
+ query: 'catch.*\\{\\s*\\}|catch.*\\{\\s*//|catch.*console\\.log',
705
+ limit: 20
706
+ });
707
+
708
+ for (const ev of silentCatches.results) {
709
+ if (ev.file.includes('auth') || ev.file.includes('billing') || ev.file.includes('middleware')) {
710
+ shipKillers.push({
711
+ invariant: 'security_no_silent_catch',
712
+ rule: 'No silent catch in auth/billing flows',
713
+ evidence: ev,
714
+ });
715
+ }
716
+ }
717
+
718
+ // Check for hardcoded secrets
719
+ const secrets = await searchEvidence(projectPath, {
720
+ query: 'sk_live_|sk_test_|apiKey.*=.*["\'][a-zA-Z0-9]{20,}',
721
+ limit: 10,
722
+ });
723
+
724
+ for (const ev of secrets.results) {
725
+ if (!ev.file.includes('.test.') && !ev.file.includes('.example')) {
726
+ shipKillers.push({
727
+ invariant: 'security_no_exposed_secrets',
728
+ rule: 'No hardcoded secrets or API keys',
729
+ evidence: ev,
730
+ });
731
+ }
732
+ }
733
+
734
+ return {
735
+ passed: shipKillers.length === 0,
736
+ shipKillers,
737
+ warnings,
738
+ summary: shipKillers.length === 0
739
+ ? '✅ All invariants pass'
740
+ : `❌ ${shipKillers.length} ship killers found - deployment blocked`,
741
+ };
742
+ }
743
+
744
+ async function addAssumption(projectPath, args) {
745
+ const { description, reason, verificationSteps } = args;
746
+
747
+ if (state.assumptions.length >= state.maxAssumptions) {
748
+ return {
749
+ error: `Assumption budget exceeded (${state.assumptions.length}/${state.maxAssumptions})`,
750
+ message: '⚠️ You MUST verify existing assumptions or use proof tooling instead of assuming.',
751
+ currentAssumptions: state.assumptions,
752
+ };
753
+ }
754
+
755
+ const assumption = {
756
+ id: `assumption_${Date.now()}`,
757
+ description,
758
+ reason,
759
+ verificationSteps,
760
+ madeAt: new Date().toISOString(),
761
+ verified: false,
762
+ };
763
+
764
+ state.assumptions.push(assumption);
765
+
766
+ return {
767
+ assumption,
768
+ budget: {
769
+ used: state.assumptions.length,
770
+ max: state.maxAssumptions,
771
+ remaining: state.maxAssumptions - state.assumptions.length,
772
+ },
773
+ warning: state.assumptions.length >= state.maxAssumptions - 1
774
+ ? '⚠️ Approaching assumption limit. Consider verifying claims instead.'
775
+ : null,
776
+ };
777
+ }
778
+
779
+ // =============================================================================
780
+ // PLAN VALIDATION & DRIFT DETECTION (Spec 10.3)
781
+ // =============================================================================
782
+
783
+ async function validatePlanTool(projectPath, args) {
784
+ const { plan, strict = false } = args;
785
+
786
+ // Load contracts
787
+ const contracts = await loadContractsFromDisk(projectPath);
788
+
789
+ if (!contracts || Object.keys(contracts).length === 0) {
790
+ return {
791
+ valid: true,
792
+ warning: 'No contracts found. Run "vibecheck ctx sync" to generate contracts.',
793
+ violations: [],
794
+ warnings: [],
795
+ suggestions: ['Generate contracts with: vibecheck ctx sync'],
796
+ };
797
+ }
798
+
799
+ // Parse plan to extract actions
800
+ const actions = parsePlanActions(plan);
801
+ const violations = [];
802
+ const warnings = [];
803
+ const suggestions = [];
804
+
805
+ // Validate route references
806
+ if (contracts.routes && actions.routes.length > 0) {
807
+ const contractRoutes = new Set(contracts.routes.routes?.map(r => r.path) || []);
808
+
809
+ for (const route of actions.routes) {
810
+ if (!contractRoutes.has(route.path)) {
811
+ // Check parameterized match
812
+ const match = contracts.routes.routes?.find(r => matchesParameterizedPath(r.path, route.path));
813
+ if (!match) {
814
+ violations.push({
815
+ type: 'invented_route',
816
+ severity: 'BLOCK',
817
+ route: route.path,
818
+ method: route.method,
819
+ message: `Plan references route ${route.method} ${route.path} which does not exist in contract`,
820
+ suggestion: `Available routes: ${contracts.routes.routes?.slice(0, 5).map(r => r.path).join(', ')}...`,
821
+ });
822
+ }
823
+ }
824
+ }
825
+ }
826
+
827
+ // Validate env var references
828
+ if (contracts.env && actions.envVars.length > 0) {
829
+ const contractVars = new Set(contracts.env.vars?.map(v => v.name) || []);
830
+
831
+ for (const varName of actions.envVars) {
832
+ if (!contractVars.has(varName)) {
833
+ warnings.push({
834
+ type: 'undeclared_env',
835
+ severity: 'WARN',
836
+ name: varName,
837
+ message: `Plan uses env var ${varName} which is not in contract`,
838
+ suggestion: 'Add to .vibecheck/contracts/env.json or .env.example',
839
+ });
840
+ }
841
+ }
842
+ }
843
+
844
+ // Validate auth assumptions
845
+ if (contracts.auth && actions.authAssumptions.length > 0) {
846
+ for (const assumption of actions.authAssumptions) {
847
+ if (assumption.type === 'no_auth') {
848
+ warnings.push({
849
+ type: 'auth_assumption',
850
+ severity: 'WARN',
851
+ message: 'Plan assumes some routes are public - verify against auth contract',
852
+ suggestion: `Protected patterns: ${contracts.auth.protectedPatterns?.slice(0, 3).join(', ')}...`,
853
+ });
854
+ }
855
+ }
856
+ }
857
+
858
+ // Validate external service usage
859
+ if (contracts.external && actions.externalCalls.length > 0) {
860
+ const contractServices = new Set(contracts.external.services?.map(s => s.name) || []);
861
+
862
+ for (const call of actions.externalCalls) {
863
+ if (!contractServices.has(call.service)) {
864
+ warnings.push({
865
+ type: 'undeclared_service',
866
+ severity: 'WARN',
867
+ service: call.service,
868
+ message: `Plan uses ${call.service} which is not declared in external contract`,
869
+ suggestion: 'Add to .vibecheck/contracts/external.json',
870
+ });
871
+ }
872
+ }
873
+ }
874
+
875
+ const valid = violations.length === 0 && (!strict || warnings.length === 0);
876
+
877
+ return {
878
+ valid,
879
+ violations,
880
+ warnings,
881
+ suggestions,
882
+ parsedActions: actions,
883
+ contractsLoaded: Object.keys(contracts),
884
+ message: valid
885
+ ? '✅ Plan validated against contracts'
886
+ : `❌ Plan validation failed: ${violations.length} violations, ${warnings.length} warnings`,
887
+ };
888
+ }
889
+
890
+ async function checkDriftTool(projectPath, args) {
891
+ const category = args.category || 'all';
892
+
893
+ // Load contracts
894
+ const contracts = await loadContractsFromDisk(projectPath);
895
+
896
+ if (!contracts || Object.keys(contracts).length === 0) {
897
+ return {
898
+ hasDrift: false,
899
+ message: 'No contracts found. Run "vibecheck ctx sync" to generate contracts.',
900
+ findings: [],
901
+ };
902
+ }
903
+
904
+ // Build current truthpack
905
+ const truthpack = await buildCurrentTruthpack(projectPath);
906
+
907
+ // Detect drift
908
+ const findings = [];
909
+
910
+ if (category === 'all' || category === 'routes') {
911
+ const routeDrift = detectRouteDrift(contracts.routes, truthpack);
912
+ findings.push(...routeDrift);
913
+ }
914
+
915
+ if (category === 'all' || category === 'env') {
916
+ const envDrift = detectEnvDrift(contracts.env, truthpack);
917
+ findings.push(...envDrift);
918
+ }
919
+
920
+ if (category === 'all' || category === 'auth') {
921
+ const authDrift = detectAuthDrift(contracts.auth, truthpack);
922
+ findings.push(...authDrift);
923
+ }
924
+
925
+ const blocks = findings.filter(f => f.severity === 'BLOCK');
926
+ const warns = findings.filter(f => f.severity === 'WARN');
927
+
928
+ return {
929
+ hasDrift: findings.length > 0,
930
+ verdict: blocks.length > 0 ? 'BLOCK' : warns.length > 0 ? 'WARN' : 'PASS',
931
+ summary: {
932
+ blocks: blocks.length,
933
+ warns: warns.length,
934
+ total: findings.length,
935
+ },
936
+ findings,
937
+ message: findings.length === 0
938
+ ? '✅ No drift detected - contracts match codebase'
939
+ : `⚠️ Drift detected: ${blocks.length} blocks, ${warns.length} warnings. Run 'vibecheck ctx sync' to update.`,
940
+ };
941
+ }
942
+
943
+ async function getContractsTool(projectPath, args) {
944
+ const type = args.type || 'all';
945
+ const contracts = await loadContractsFromDisk(projectPath);
946
+
947
+ if (!contracts || Object.keys(contracts).length === 0) {
948
+ return {
949
+ found: false,
950
+ message: 'No contracts found. Run "vibecheck ctx sync" to generate contracts.',
951
+ contracts: {},
952
+ };
953
+ }
954
+
955
+ if (type === 'all') {
956
+ return {
957
+ found: true,
958
+ contracts,
959
+ summary: {
960
+ routes: contracts.routes?.routes?.length || 0,
961
+ envVars: contracts.env?.vars?.length || 0,
962
+ authPatterns: contracts.auth?.protectedPatterns?.length || 0,
963
+ services: contracts.external?.services?.length || 0,
964
+ },
965
+ };
966
+ }
967
+
968
+ return {
969
+ found: !!contracts[type],
970
+ contracts: { [type]: contracts[type] },
971
+ };
972
+ }
973
+
974
+ // Helper: Load contracts from disk
975
+ async function loadContractsFromDisk(projectPath) {
976
+ const contractDir = path.join(projectPath, '.vibecheck', 'contracts');
977
+ const contracts = {};
978
+
979
+ const files = {
980
+ routes: 'routes.json',
981
+ env: 'env.json',
982
+ auth: 'auth.json',
983
+ external: 'external.json',
984
+ };
985
+
986
+ for (const [key, file] of Object.entries(files)) {
987
+ const filePath = path.join(contractDir, file);
988
+ try {
989
+ const content = await fs.readFile(filePath, 'utf8');
990
+ contracts[key] = JSON.parse(content);
991
+ } catch {}
992
+ }
993
+
994
+ return contracts;
995
+ }
996
+
997
+ // Helper: Parse plan to extract actions
998
+ function parsePlanActions(plan) {
999
+ const actions = {
1000
+ routes: [],
1001
+ envVars: [],
1002
+ files: [],
1003
+ authAssumptions: [],
1004
+ externalCalls: [],
1005
+ };
1006
+
1007
+ const planText = typeof plan === 'string' ? plan : JSON.stringify(plan);
1008
+
1009
+ // Extract route references
1010
+ const routePatterns = [
1011
+ /(?:fetch|axios|api\.?)\s*\(\s*['"`]([/][^'"`]+)['"`]/gi,
1012
+ /(?:GET|POST|PUT|PATCH|DELETE)\s+([/][^\s]+)/gi,
1013
+ /\/api\/[a-z0-9/_-]+/gi,
1014
+ ];
1015
+
1016
+ for (const pattern of routePatterns) {
1017
+ let match;
1018
+ while ((match = pattern.exec(planText)) !== null) {
1019
+ const p = match[1] || match[0];
1020
+ if (p.startsWith('/')) {
1021
+ actions.routes.push({
1022
+ path: p.replace(/['"`]/g, ''),
1023
+ method: inferMethodFromText(match[0]),
1024
+ });
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ // Extract env var references
1030
+ const envPatterns = [
1031
+ /process\.env\.([A-Z_][A-Z0-9_]*)/gi,
1032
+ /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/gi,
1033
+ ];
1034
+
1035
+ for (const pattern of envPatterns) {
1036
+ let match;
1037
+ while ((match = pattern.exec(planText)) !== null) {
1038
+ if (match[1] && /^[A-Z]/.test(match[1])) {
1039
+ actions.envVars.push(match[1]);
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ // Extract auth assumptions
1045
+ if (/(?:authenticated|logged in|auth required|protected)/i.test(planText)) {
1046
+ actions.authAssumptions.push({ type: 'requires_auth' });
1047
+ }
1048
+ if (/(?:public|no auth|unauthenticated)/i.test(planText)) {
1049
+ actions.authAssumptions.push({ type: 'no_auth' });
1050
+ }
1051
+
1052
+ // Extract external service references
1053
+ const servicePatterns = [
1054
+ { pattern: /stripe\./gi, service: 'stripe' },
1055
+ { pattern: /github\./gi, service: 'github' },
1056
+ { pattern: /sendgrid\./gi, service: 'sendgrid' },
1057
+ { pattern: /twilio\./gi, service: 'twilio' },
1058
+ { pattern: /supabase\./gi, service: 'supabase' },
1059
+ ];
1060
+
1061
+ for (const { pattern, service } of servicePatterns) {
1062
+ if (pattern.test(planText)) {
1063
+ actions.externalCalls.push({ service });
1064
+ }
1065
+ }
1066
+
1067
+ return actions;
1068
+ }
1069
+
1070
+ function inferMethodFromText(text) {
1071
+ const upper = text.toUpperCase();
1072
+ if (upper.includes('POST')) return 'POST';
1073
+ if (upper.includes('PUT')) return 'PUT';
1074
+ if (upper.includes('PATCH')) return 'PATCH';
1075
+ if (upper.includes('DELETE')) return 'DELETE';
1076
+ return 'GET';
1077
+ }
1078
+
1079
+ function matchesParameterizedPath(pattern, actual) {
1080
+ const patternParts = pattern.split('/').filter(Boolean);
1081
+ const actualParts = actual.split('/').filter(Boolean);
1082
+ if (patternParts.length !== actualParts.length) return false;
1083
+ for (let i = 0; i < patternParts.length; i++) {
1084
+ const p = patternParts[i];
1085
+ if (p.startsWith(':') || p.startsWith('*') || p.startsWith('[')) continue;
1086
+ if (p !== actualParts[i]) return false;
1087
+ }
1088
+ return true;
1089
+ }
1090
+
1091
+ // Helper: Build current truthpack (lightweight version)
1092
+ async function buildCurrentTruthpack(projectPath) {
1093
+ const routes = await extractRoutes(projectPath);
1094
+ const env = await extractEnv(projectPath);
1095
+ const auth = await extractAuth(projectPath);
1096
+
1097
+ return {
1098
+ routes: {
1099
+ server: routes.routes,
1100
+ clientRefs: [],
1101
+ },
1102
+ env: {
1103
+ vars: env.used,
1104
+ declared: env.declared.map(d => d.name),
1105
+ },
1106
+ auth: {
1107
+ nextMatcherPatterns: [], // Would need middleware parsing
1108
+ },
1109
+ };
1110
+ }
1111
+
1112
+ // Helper: Detect route drift
1113
+ function detectRouteDrift(routeContract, truthpack) {
1114
+ const findings = [];
1115
+ if (!routeContract?.routes) return findings;
1116
+
1117
+ const contractRoutes = new Map(routeContract.routes.map(r => [`${r.method}_${r.path}`, r]));
1118
+ const serverRoutes = truthpack?.routes?.server || [];
1119
+
1120
+ for (const route of serverRoutes) {
1121
+ const key = `${route.method}_${route.path}`;
1122
+ if (!contractRoutes.has(key)) {
1123
+ findings.push({
1124
+ type: 'new_route_not_in_contract',
1125
+ severity: 'BLOCK',
1126
+ title: `New route ${route.method} ${route.path} not in contract`,
1127
+ message: 'Route added to code but not synced to contracts.',
1128
+ });
1129
+ }
1130
+ }
1131
+
1132
+ return findings;
1133
+ }
1134
+
1135
+ // Helper: Detect env drift
1136
+ function detectEnvDrift(envContract, truthpack) {
1137
+ const findings = [];
1138
+ if (!envContract?.vars) return findings;
1139
+
1140
+ const contractVars = new Set(envContract.vars.map(v => v.name));
1141
+ const usedVars = truthpack?.env?.vars || [];
1142
+
1143
+ for (const v of usedVars) {
1144
+ if (!contractVars.has(v.name)) {
1145
+ findings.push({
1146
+ type: 'new_env_not_in_contract',
1147
+ severity: 'WARN',
1148
+ title: `Env var ${v.name} used but not in contract`,
1149
+ message: 'Env var used in code but not declared in contracts.',
1150
+ });
1151
+ }
1152
+ }
1153
+
1154
+ return findings;
1155
+ }
1156
+
1157
+ // Helper: Detect auth drift
1158
+ function detectAuthDrift(authContract, truthpack) {
1159
+ const findings = [];
1160
+ if (!authContract?.protectedPatterns) return findings;
1161
+
1162
+ const contractPatterns = new Set(authContract.protectedPatterns);
1163
+ const currentPatterns = new Set(truthpack?.auth?.nextMatcherPatterns || []);
1164
+
1165
+ for (const pattern of currentPatterns) {
1166
+ if (!contractPatterns.has(pattern)) {
1167
+ findings.push({
1168
+ type: 'new_auth_pattern',
1169
+ severity: 'BLOCK',
1170
+ title: `New auth pattern "${pattern}" not in contract`,
1171
+ message: 'Auth pattern added but not in contracts.',
1172
+ });
1173
+ }
1174
+ }
1175
+
1176
+ return findings;
1177
+ }
1178
+
1179
+ // =============================================================================
1180
+ // HELPERS
1181
+ // =============================================================================
1182
+
1183
+ function getCommitHash(projectPath) {
1184
+ try {
1185
+ return execSync('git rev-parse HEAD', { cwd: projectPath, encoding: 'utf8' }).trim();
1186
+ } catch {
1187
+ return 'unknown';
1188
+ }
1189
+ }
1190
+
1191
+ /**
1192
+ * Generate a project fingerprint for stale assumption detection (Spec 10.2)
1193
+ * Includes: commit hash, key file hashes, timestamp
1194
+ */
1195
+ export function getProjectFingerprint(projectPath) {
1196
+ const commitHash = getCommitHash(projectPath);
1197
+ const keyFiles = [
1198
+ 'package.json',
1199
+ 'prisma/schema.prisma',
1200
+ 'next.config.js',
1201
+ 'next.config.ts',
1202
+ '.vibecheck/contracts/routes.json',
1203
+ ];
1204
+
1205
+ const fileHashes = [];
1206
+ for (const file of keyFiles) {
1207
+ try {
1208
+ const content = require('fs').readFileSync(path.join(projectPath, file), 'utf8');
1209
+ const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 8);
1210
+ fileHashes.push(`${file}:${hash}`);
1211
+ } catch {}
1212
+ }
1213
+
1214
+ const fingerprintData = [
1215
+ commitHash,
1216
+ ...fileHashes,
1217
+ ].join('|');
1218
+
1219
+ return {
1220
+ hash: crypto.createHash('sha256').update(fingerprintData).digest('hex').slice(0, 16),
1221
+ commitHash,
1222
+ fileHashes,
1223
+ generatedAt: new Date().toISOString(),
1224
+ };
1225
+ }
1226
+
1227
+ /**
1228
+ * Wrap MCP response with standard metadata including fingerprint (Spec 10.2)
1229
+ */
1230
+ export function wrapMcpResponse(data, projectPath) {
1231
+ return {
1232
+ ok: true,
1233
+ version: '2.0.0',
1234
+ projectFingerprint: getProjectFingerprint(projectPath),
1235
+ data,
1236
+ };
1237
+ }
1238
+
1239
+ async function extractRoutes(projectPath) {
1240
+ const routes = [];
1241
+ const files = await findSourceFiles(projectPath);
1242
+ const routePatterns = [
1243
+ /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
1244
+ /router\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
1245
+ ];
1246
+
1247
+ for (const file of files.slice(0, 50)) {
1248
+ try {
1249
+ const content = await fs.readFile(file, 'utf8');
1250
+ const relPath = path.relative(projectPath, file);
1251
+
1252
+ for (const pattern of routePatterns) {
1253
+ let match;
1254
+ pattern.lastIndex = 0;
1255
+ while ((match = pattern.exec(content)) !== null) {
1256
+ const line = content.substring(0, match.index).split('\n').length;
1257
+ routes.push({
1258
+ method: match[1].toUpperCase(),
1259
+ path: match[2],
1260
+ file: relPath,
1261
+ line,
1262
+ });
1263
+ }
1264
+ }
1265
+ } catch {}
1266
+ }
1267
+
1268
+ return { count: routes.length, routes, confidence: routes.length > 0 ? 0.8 : 0.2 };
1269
+ }
1270
+
1271
+ async function extractAuth(projectPath) {
1272
+ const evidence = await searchEvidence(projectPath, {
1273
+ query: 'auth|authenticate|authorize|middleware|guard|jwt|session',
1274
+ limit: 30
1275
+ });
1276
+ return { count: evidence.count, indicators: evidence.results, confidence: evidence.count > 5 ? 0.8 : 0.4 };
1277
+ }
1278
+
1279
+ async function extractBilling(projectPath) {
1280
+ const evidence = await searchEvidence(projectPath, {
1281
+ query: 'stripe|billing|payment|subscription|checkout|tier|isPro',
1282
+ limit: 20
1283
+ });
1284
+ return { count: evidence.count, indicators: evidence.results, confidence: evidence.count > 3 ? 0.7 : 0.3 };
1285
+ }
1286
+
1287
+ async function extractEnv(projectPath) {
1288
+ const declared = [];
1289
+ const used = [];
1290
+
1291
+ // Check .env.example
1292
+ try {
1293
+ const content = await fs.readFile(path.join(projectPath, '.env.example'), 'utf8');
1294
+ const lines = content.split('\n');
1295
+ for (let i = 0; i < lines.length; i++) {
1296
+ const match = lines[i].match(/^([A-Z][A-Z0-9_]*)=/);
1297
+ if (match) declared.push({ name: match[1], line: i + 1 });
1298
+ }
1299
+ } catch {}
1300
+
1301
+ // Check process.env usage
1302
+ const evidence = await searchEvidence(projectPath, { query: 'process\\.env\\.([A-Z][A-Z0-9_]*)', limit: 50 });
1303
+ for (const ev of evidence.results) {
1304
+ const match = ev.snippet.match(/process\.env\.([A-Z][A-Z0-9_]*)/);
1305
+ if (match) used.push({ name: match[1], file: ev.file, line: ev.line });
1306
+ }
1307
+
1308
+ const declaredNames = new Set(declared.map(d => d.name));
1309
+ const usedNames = new Set(used.map(u => u.name));
1310
+
1311
+ return {
1312
+ declared,
1313
+ used,
1314
+ mismatches: {
1315
+ undeclared: Array.from(usedNames).filter(n => !declaredNames.has(n)),
1316
+ unused: Array.from(declaredNames).filter(n => !usedNames.has(n)),
1317
+ },
1318
+ confidence: 0.8,
1319
+ };
1320
+ }
1321
+
1322
+ async function extractSchema(projectPath) {
1323
+ const schemas = [];
1324
+
1325
+ // Check Prisma
1326
+ try {
1327
+ const content = await fs.readFile(path.join(projectPath, 'prisma', 'schema.prisma'), 'utf8');
1328
+ const models = content.matchAll(/model\s+(\w+)\s*\{/g);
1329
+ for (const match of models) {
1330
+ schemas.push({ type: 'prisma_model', name: match[1] });
1331
+ }
1332
+ } catch {}
1333
+
1334
+ return { count: schemas.length, schemas, confidence: schemas.length > 0 ? 0.9 : 0.3 };
1335
+ }
1336
+
1337
+ async function extractGraph(projectPath) {
1338
+ return { nodes: [], edges: [], message: 'Graph extraction requires full build. Use get_truthpack with refresh=true.' };
1339
+ }
1340
+
1341
+ async function verifyRouteExists(projectPath, subject) {
1342
+ const routes = await extractRoutes(projectPath);
1343
+ const match = routes.routes.find(r =>
1344
+ r.path === subject.path ||
1345
+ (subject.method && r.method === subject.method.toUpperCase() && r.path === subject.path)
1346
+ );
1347
+
1348
+ if (match) {
1349
+ return {
1350
+ result: 'true',
1351
+ confidence: 'high',
1352
+ evidence: [{ file: match.file, lines: `${match.line}`, hash: '' }],
1353
+ };
1354
+ }
1355
+
1356
+ return { result: 'false', confidence: 'high', evidence: [], nextSteps: ['Route not found in codebase'] };
1357
+ }
1358
+
1359
+ async function verifyFileExists(projectPath, subject) {
1360
+ const filePath = path.join(projectPath, subject.path || subject.name);
1361
+ try {
1362
+ await fs.access(filePath);
1363
+ return { result: 'true', confidence: 'high', evidence: [{ file: subject.path || subject.name, lines: '1', hash: '' }] };
1364
+ } catch {
1365
+ return { result: 'false', confidence: 'high', evidence: [] };
1366
+ }
1367
+ }
1368
+
1369
+ async function verifyEnvVar(projectPath, subject, claim) {
1370
+ const env = await extractEnv(projectPath);
1371
+ const name = subject.name;
1372
+
1373
+ const isDeclared = env.declared.some(d => d.name === name);
1374
+ const isUsed = env.used.some(u => u.name === name);
1375
+
1376
+ if (claim === 'env_var_exists') {
1377
+ return {
1378
+ result: isDeclared ? 'true' : 'unknown',
1379
+ confidence: isDeclared ? 'high' : 'low',
1380
+ evidence: isDeclared ? [{ file: '.env.example', lines: '1', hash: '' }] : [],
1381
+ };
1382
+ } else {
1383
+ return {
1384
+ result: isUsed ? 'true' : 'false',
1385
+ confidence: 'high',
1386
+ evidence: env.used.filter(u => u.name === name).map(u => ({ file: u.file, lines: `${u.line}`, hash: '' })),
1387
+ };
1388
+ }
1389
+ }
1390
+
1391
+ async function verifyRouteGuarded(projectPath, subject) {
1392
+ const routePath = subject.path;
1393
+ const evidence = await searchEvidence(projectPath, { query: `${routePath}.*auth|middleware.*${routePath}`, limit: 5 });
1394
+
1395
+ if (evidence.count > 0) {
1396
+ return {
1397
+ result: 'true',
1398
+ confidence: 'medium',
1399
+ evidence: evidence.results.map(e => ({ file: e.file, lines: `${e.line}`, hash: e.hash })),
1400
+ };
1401
+ }
1402
+
1403
+ return { result: 'unknown', confidence: 'low', evidence: [], nextSteps: ['Could not find auth guards for this route'] };
1404
+ }
1405
+
1406
+ async function verifyEntityExists(projectPath, subject, claim) {
1407
+ const name = subject.name;
1408
+ const evidence = await searchEvidence(projectPath, { query: name, limit: 5 });
1409
+
1410
+ if (evidence.count > 0) {
1411
+ return {
1412
+ result: 'true',
1413
+ confidence: 'medium',
1414
+ evidence: evidence.results.map(e => ({ file: e.file, lines: `${e.line}`, hash: e.hash })),
1415
+ };
1416
+ }
1417
+
1418
+ return { result: 'unknown', confidence: 'low', evidence: [] };
1419
+ }
1420
+
1421
+ function filterTruthPack(pack, scope) {
1422
+ if (scope === 'all') return pack;
1423
+ return {
1424
+ ...pack,
1425
+ sections: { [scope]: pack.sections[scope] },
1426
+ };
1427
+ }
1428
+
1429
+ function detectDomains(task) {
1430
+ const domains = [];
1431
+ if (/auth|login|logout|session|password/i.test(task)) domains.push('auth');
1432
+ if (/billing|payment|stripe|subscription/i.test(task)) domains.push('billing');
1433
+ if (/route|api|endpoint/i.test(task)) domains.push('api');
1434
+ if (/component|button|ui|form/i.test(task)) domains.push('ui');
1435
+ return domains.length > 0 ? domains : ['general'];
1436
+ }
1437
+
1438
+ function extractKeywords(task) {
1439
+ return task.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2);
1440
+ }
1441
+
1442
+ function getInvariantsForDomains(domains) {
1443
+ const invariants = [];
1444
+ if (domains.includes('auth')) {
1445
+ invariants.push('No protected route without server middleware');
1446
+ }
1447
+ if (domains.includes('billing')) {
1448
+ invariants.push('No paid feature without server-side enforcement');
1449
+ }
1450
+ invariants.push('No success UI without confirmed success');
1451
+ return invariants;
1452
+ }
1453
+
1454
+ function estimateTokens(context) {
1455
+ let tokens = 0;
1456
+ if (context.relevantRoutes) tokens += context.relevantRoutes.length * 50;
1457
+ if (context.relevantAuth) tokens += 200;
1458
+ if (context.relevantBilling) tokens += 200;
1459
+ return tokens;
1460
+ }
1461
+
1462
+ function generateContextWarnings(domains, policy, routeCount) {
1463
+ const warnings = [];
1464
+ if (domains.includes('auth') || domains.includes('billing')) {
1465
+ warnings.push('High-stakes domain - verify all claims before changes');
1466
+ }
1467
+ if (routeCount > 20 && policy === 'strict') {
1468
+ warnings.push('Large context - consider narrowing scope');
1469
+ }
1470
+ return warnings;
1471
+ }
1472
+
1473
+ async function findSourceFiles(projectPath) {
1474
+ const files = [];
1475
+ const ignoreDirs = ['node_modules', 'dist', 'build', '.git', '.next', 'coverage'];
1476
+
1477
+ async function walk(dir) {
1478
+ try {
1479
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1480
+ for (const entry of entries) {
1481
+ const fullPath = path.join(dir, entry.name);
1482
+ if (entry.isDirectory()) {
1483
+ if (!ignoreDirs.includes(entry.name) && !entry.name.startsWith('.')) {
1484
+ await walk(fullPath);
1485
+ }
1486
+ } else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
1487
+ files.push(fullPath);
1488
+ }
1489
+ }
1490
+ } catch {}
1491
+ }
1492
+
1493
+ await walk(projectPath);
1494
+ return files;
1495
+ }
1496
+
1497
+ export default {
1498
+ TRUTH_FIREWALL_TOOLS,
1499
+ handleTruthFirewallTool,
1500
+ };