@vibecheckai/cli 3.6.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +135 -63
  2. package/bin/_deprecations.js +447 -19
  3. package/bin/_router.js +1 -1
  4. package/bin/registry.js +347 -280
  5. package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
  6. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  7. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  8. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  9. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  10. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  11. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  12. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  13. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  14. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  15. package/bin/runners/lib/agent-firewall/index.js +200 -0
  16. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  17. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  18. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
  19. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  20. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  21. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  22. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  23. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  24. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  25. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +31 -38
  26. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +68 -3
  27. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +4 -2
  28. package/bin/runners/lib/agent-firewall/risk/thresholds.js +5 -4
  29. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  30. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  31. package/bin/runners/lib/artifact-envelope.js +540 -0
  32. package/bin/runners/lib/auth-shared.js +977 -0
  33. package/bin/runners/lib/checkpoint.js +941 -0
  34. package/bin/runners/lib/cleanup/engine.js +571 -0
  35. package/bin/runners/lib/cleanup/index.js +53 -0
  36. package/bin/runners/lib/cleanup/output.js +375 -0
  37. package/bin/runners/lib/cleanup/rules.js +1060 -0
  38. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  39. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  40. package/bin/runners/lib/doctor/fix-script.js +336 -0
  41. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  42. package/bin/runners/lib/doctor/modules/index.js +62 -3
  43. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  44. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  45. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  46. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  47. package/bin/runners/lib/entitlements-v2.js +2 -2
  48. package/bin/runners/lib/error-messages.js +1 -1
  49. package/bin/runners/lib/missions/briefing.js +427 -0
  50. package/bin/runners/lib/missions/checkpoint.js +753 -0
  51. package/bin/runners/lib/missions/hardening.js +851 -0
  52. package/bin/runners/lib/missions/plan.js +421 -32
  53. package/bin/runners/lib/missions/safety-gates.js +645 -0
  54. package/bin/runners/lib/missions/schema.js +478 -0
  55. package/bin/runners/lib/packs/bundle.js +675 -0
  56. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  57. package/bin/runners/lib/packs/pack-factory.js +837 -0
  58. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  59. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  60. package/bin/runners/lib/report-output.js +6 -6
  61. package/bin/runners/lib/safelist/index.js +96 -0
  62. package/bin/runners/lib/safelist/integration.js +334 -0
  63. package/bin/runners/lib/safelist/matcher.js +696 -0
  64. package/bin/runners/lib/safelist/schema.js +948 -0
  65. package/bin/runners/lib/safelist/store.js +438 -0
  66. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  67. package/bin/runners/lib/ship-gate.js +832 -0
  68. package/bin/runners/lib/ship-manifest.js +1153 -0
  69. package/bin/runners/lib/ship-output.js +1 -1
  70. package/bin/runners/lib/unified-cli-output.js +710 -383
  71. package/bin/runners/lib/upsell.js +3 -3
  72. package/bin/runners/lib/why-tree.js +650 -0
  73. package/bin/runners/runAllowlist.js +33 -4
  74. package/bin/runners/runApprove.js +240 -1122
  75. package/bin/runners/runAudit.js +692 -0
  76. package/bin/runners/runAuth.js +325 -29
  77. package/bin/runners/runCheckpoint.js +442 -494
  78. package/bin/runners/runCleanup.js +343 -0
  79. package/bin/runners/runDoctor.js +269 -19
  80. package/bin/runners/runFix.js +411 -32
  81. package/bin/runners/runForge.js +411 -0
  82. package/bin/runners/runIntent.js +906 -0
  83. package/bin/runners/runKickoff.js +878 -0
  84. package/bin/runners/runLaunch.js +2000 -0
  85. package/bin/runners/runLink.js +785 -0
  86. package/bin/runners/runMcp.js +1741 -837
  87. package/bin/runners/runPacks.js +2089 -0
  88. package/bin/runners/runPolish.js +41 -0
  89. package/bin/runners/runSafelist.js +1190 -0
  90. package/bin/runners/runScan.js +21 -9
  91. package/bin/runners/runShield.js +1282 -0
  92. package/bin/runners/runShip.js +395 -16
  93. package/bin/vibecheck.js +34 -6
  94. package/mcp-server/README.md +117 -158
  95. package/mcp-server/handlers/tool-handler.ts +3 -3
  96. package/mcp-server/index.js +16 -0
  97. package/mcp-server/intent-firewall-interceptor.js +529 -0
  98. package/mcp-server/manifest.json +473 -0
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/registry/tool-registry.js +315 -523
  101. package/mcp-server/registry/tools.json +442 -428
  102. package/mcp-server/tier-auth.js +164 -16
  103. package/mcp-server/tools-v3.js +70 -16
  104. package/package.json +1 -1
  105. package/bin/runners/runProof.zip +0 -0
@@ -1,1202 +1,320 @@
1
1
  /**
2
- * vibecheck approve - Authority Approval Command
2
+ * vibecheck approve - Session Approval CLI
3
3
  *
4
- * Execute authorities to get structured verdicts with proofs.
5
- * Requires STARTER tier for advisory verdicts.
6
- * Requires PRO tier for enforcement.
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * SEAMLESS INTENT - SESSION APPROVAL COMMAND
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
7
  *
8
- * Usage:
9
- * vibecheck approve <authority-id>
10
- * vibecheck approve safe-consolidation
11
- * vibecheck approve --list
8
+ * Review and approve changes made during a vibe session.
9
+ * Creates intent retroactively from approved changes.
12
10
  *
13
- * Part of the Authority System - "The AI That Says No"
11
+ * Usage:
12
+ * vibecheck approve # Interactive review
13
+ * vibecheck approve -y # Auto-approve with generated intent
14
+ * vibecheck approve -m "..." # Approve with custom message
15
+ * vibecheck approve --reject # Reject pending changes
14
16
  *
15
- * Production Features:
16
- * - Rate limiting to prevent abuse
17
- * - Input validation for all parameters
18
- * - Timeout enforcement for long-running analyses
19
- * - Audit trail for all executions
20
- * - Signed verdicts (PRO tier)
17
+ * @module runApprove
18
+ * @version 1.0.0
21
19
  */
22
20
 
21
+ "use strict";
22
+
23
23
  const path = require("path");
24
- const fs = require("fs");
25
- const { withErrorHandling, createUserError } = require("./lib/error-handler");
26
- const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
27
- const { EXIT } = require("./lib/exit-codes");
28
24
 
29
- // ═══════════════════════════════════════════════════════════════════════════════
30
- // TERMINAL UI
31
- // ═══════════════════════════════════════════════════════════════════════════════
25
+ // Lazy imports
26
+ let _globalFlags = null;
27
+ let _exitCodes = null;
28
+ let _cliOutput = null;
29
+ let _sessionModule = null;
32
30
 
33
- const {
34
- ansi,
35
- colors,
36
- Spinner,
37
- } = require("./lib/terminal-ui");
31
+ function getGlobalFlags() {
32
+ if (!_globalFlags) {
33
+ _globalFlags = require("./lib/global-flags");
34
+ }
35
+ return _globalFlags;
36
+ }
38
37
 
39
- const BANNER = `
40
- ${ansi.rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${ansi.reset}
41
- ${ansi.rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${ansi.reset}
42
- ${ansi.rgb(60, 160, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${ansi.reset}
43
- ${ansi.rgb(90, 140, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${ansi.reset}
44
- ${ansi.rgb(120, 120, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${ansi.reset}
45
- ${ansi.rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${ansi.reset}
38
+ function getExitCodes() {
39
+ if (!_exitCodes) {
40
+ _exitCodes = require("./lib/exit-codes");
41
+ }
42
+ return _exitCodes;
43
+ }
46
44
 
47
- ${ansi.dim} ┌─────────────────────────────────────────────────────────────────────┐${ansi.reset}
48
- ${ansi.dim} │${ansi.reset} ${ansi.rgb(255, 255, 255)}${ansi.bold}Authority System${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(200, 200, 200)}Approve${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(150, 150, 150)}Verdicts with Proofs${ansi.reset} ${ansi.dim}│${ansi.reset}
49
- ${ansi.dim} └─────────────────────────────────────────────────────────────────────┘${ansi.reset}
50
- `;
45
+ function getCliOutput() {
46
+ if (!_cliOutput) {
47
+ _cliOutput = require("./lib/unified-cli-output");
48
+ }
49
+ return _cliOutput;
50
+ }
51
51
 
52
- function printBanner() {
53
- console.log(BANNER);
52
+ function getSessionModule() {
53
+ if (!_sessionModule) {
54
+ _sessionModule = require("./lib/agent-firewall/session");
55
+ }
56
+ return _sessionModule;
54
57
  }
55
58
 
56
59
  // ═══════════════════════════════════════════════════════════════════════════════
57
- // ARGS PARSER
60
+ // HELP
58
61
  // ═══════════════════════════════════════════════════════════════════════════════
59
62
 
60
- function parseArgs(args) {
61
- const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
62
-
63
- const opts = {
64
- path: globalFlags.path || process.cwd(),
65
- json: globalFlags.json || false,
66
- verbose: globalFlags.verbose || false,
67
- help: globalFlags.help || false,
68
- noBanner: globalFlags.noBanner || false,
69
- ci: globalFlags.ci || false,
70
- quiet: globalFlags.quiet || false,
71
- // Authority options
72
- authority: null, // Authority ID to execute
73
- list: false, // List available authorities
74
- // Output options
75
- output: null, // Output file path
76
- badge: false, // Generate badge for PROCEED
77
- // Dry run
78
- dryRun: false, // Don't save results, just analyze
79
- };
80
-
81
- for (let i = 0; i < cleanArgs.length; i++) {
82
- const arg = cleanArgs[i];
83
-
84
- if (arg === '--list' || arg === '-l') opts.list = true;
85
- else if (arg === '--output' || arg === '-o') opts.output = cleanArgs[++i];
86
- else if (arg === '--badge' || arg === '-b') opts.badge = true;
87
- else if (arg === '--dry-run') opts.dryRun = true;
88
- else if (arg === '--path' || arg === '-p') opts.path = cleanArgs[++i] || process.cwd();
89
- else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
90
- else if (!arg.startsWith('-') && !opts.authority) opts.authority = arg;
91
- }
63
+ function printHelp() {
64
+ const { ansi } = getCliOutput();
92
65
 
93
- return opts;
94
- }
95
-
96
- function printHelp(showBanner = true) {
97
- if (showBanner && shouldShowBanner({})) {
98
- printBanner();
99
- }
100
66
  console.log(`
101
- ${ansi.bold}USAGE${ansi.reset}
102
- ${colors.accent}vibecheck approve${ansi.reset} <authority> [options]
103
- ${colors.accent}vibecheck approve${ansi.reset} --list
67
+ ${ansi.bold}${ansi.cyan}╔═══════════════════════════════════════════════════════════════════════════════╗
68
+ ║ ║
69
+ ${ansi.reset}${ansi.bold}SESSION APPROVAL${ansi.cyan}
70
+ ║ ${ansi.reset}${ansi.dim}Review and approve vibe session changes${ansi.cyan} ║
71
+ ║ ║
72
+ ╚═══════════════════════════════════════════════════════════════════════════════╝${ansi.reset}
104
73
 
105
- ${ansi.dim}Requires: STARTER tier (advisory) or PRO tier (enforcement)${ansi.reset}
106
-
107
- Execute an authority to get a structured verdict with proofs.
108
- Authorities are policy engines that analyze your code and produce
109
- PROCEED/STOP/DEFER verdicts with evidence.
74
+ ${ansi.bold}USAGE${ansi.reset}
75
+ ${ansi.cyan}vibecheck approve${ansi.reset} [options]
110
76
 
111
- ${ansi.bold}AUTHORITIES${ansi.reset}
112
- ${colors.accent}safe-consolidation${ansi.reset} Zero-behavior-change code cleanup
113
- ${colors.accent}inventory${ansi.reset} Read-only duplication/legacy maps ${ansi.dim}(use 'classify')${ansi.reset}
77
+ ${ansi.dim}Review AI changes made during a vibe session.
78
+ Approved changes create intent retroactively.${ansi.reset}
114
79
 
115
80
  ${ansi.bold}OPTIONS${ansi.reset}
116
- ${colors.accent}--list, -l${ansi.reset} List all available authorities
117
- ${colors.accent}--badge, -b${ansi.reset} Generate badge for PROCEED verdicts
118
- ${colors.accent}--dry-run${ansi.reset} Analyze without saving results
119
- ${colors.accent}--output, -o <file>${ansi.reset} Save verdict to file
81
+ ${ansi.cyan}-y, --yes${ansi.reset} Auto-approve with generated intent
82
+ ${ansi.cyan}-m, --message <text>${ansi.reset} Approve with custom intent message
83
+ ${ansi.cyan}--reject${ansi.reset} Reject all pending changes
84
+ ${ansi.cyan}--clear${ansi.reset} Clear session without creating intent
120
85
 
121
- ${ansi.bold}GLOBAL OPTIONS${ansi.reset}
122
- ${colors.accent}--json${ansi.reset} Output verdict as JSON
123
- ${colors.accent}--path, -p <dir>${ansi.reset} Run in specified directory
124
- ${colors.accent}--verbose, -v${ansi.reset} Show detailed analysis
125
- ${colors.accent}--quiet, -q${ansi.reset} Suppress non-essential output
126
- ${colors.accent}--ci${ansi.reset} CI mode (quiet + exit codes)
127
- ${colors.accent}--help, -h${ansi.reset} Show this help
86
+ ${ansi.bold}EXAMPLES${ansi.reset}
87
+ ${ansi.dim}# Interactive review${ansi.reset}
88
+ vibecheck approve
128
89
 
129
- ${ansi.bold}💡 EXAMPLES${ansi.reset}
90
+ ${ansi.dim}# Auto-approve${ansi.reset}
91
+ vibecheck approve -y
130
92
 
131
- ${ansi.dim}# Run safe-consolidation authority${ansi.reset}
132
- vibecheck approve safe-consolidation
93
+ ${ansi.dim}# Custom intent${ansi.reset}
94
+ vibecheck approve -m "Added user profile feature"
133
95
 
134
- ${ansi.dim}# JSON output for CI integration${ansi.reset}
135
- vibecheck approve safe-consolidation --json --ci
96
+ ${ansi.dim}# Reject changes${ansi.reset}
97
+ vibecheck approve --reject
136
98
 
137
- ${ansi.dim}# Generate badge for PROCEED verdicts${ansi.reset}
138
- vibecheck approve safe-consolidation --badge
139
-
140
- ${ansi.dim}# List available authorities${ansi.reset}
141
- vibecheck approve --list
142
-
143
- ${ansi.bold}📊 VERDICT ACTIONS${ansi.reset}
144
- ${colors.success}PROCEED${ansi.reset} Safe to continue, all proofs satisfied (exit 0)
145
- ${colors.warning}DEFER${ansi.reset} Manual review required (exit 1)
146
- ${colors.error}STOP${ansi.reset} Unsafe, hard stops triggered (exit 2)
147
-
148
- ${ansi.bold}🔗 RELATED COMMANDS${ansi.reset}
149
- ${colors.accent}vibecheck classify${ansi.reset} Read-only inventory ${ansi.dim}(FREE)${ansi.reset}
150
- ${colors.accent}vibecheck scan${ansi.reset} Full code analysis
151
- ${colors.accent}vibecheck ship${ansi.reset} SHIP/WARN/BLOCK verdict
152
-
153
- ${ansi.dim}─────────────────────────────────────────────────────────────${ansi.reset}
99
+ ${ansi.dim}────────────────────────────────────────────────────────────────────${ansi.reset}
154
100
  ${ansi.dim}Documentation: https://docs.vibecheckai.dev/cli/approve${ansi.reset}
155
- `);
156
- }
157
-
158
- // ═══════════════════════════════════════════════════════════════════════════════
159
- // AUTHORITY DEFINITIONS (Inline for now - will use packages/core in production)
160
- // ═══════════════════════════════════════════════════════════════════════════════
161
-
162
- const AUTHORITIES = {
163
- 'safe-consolidation': {
164
- id: 'safe-consolidation',
165
- version: '1.0.0',
166
- description: 'Zero-behavior-change cleanup of duplicated and legacy code',
167
- tier: 'starter',
168
- scope: {
169
- allowedActions: ['analyze', 'propose_diff'],
170
- disallowedActions: ['delete', 'rename_public_exports', 'force_publish', 'modify_env', 'modify_migrations'],
171
- allowedRisk: ['LOW'],
172
- excludedPaths: [
173
- 'packages/cli/**',
174
- '**/migrations/**',
175
- '**/prisma/migrations/**',
176
- '**/*.env*',
177
- '**/node_modules/**',
178
- '**/dist/**',
179
- '**/build/**',
180
- '**/.git/**',
181
- '**/auth/**',
182
- '**/secrets/**',
183
- '**/billing/**',
184
- ],
185
- },
186
- permissions: {
187
- readRepo: true,
188
- writeProposals: true,
189
- applyChanges: false,
190
- enforceCI: true,
191
- },
192
- requiredProofs: {
193
- reachability: 'Static + dynamic analysis proving no runtime imports',
194
- compatibility: 'Import path preservation via aliases/re-exports',
195
- behaviorLock: 'Observable behavior unchanged (errors, timing, side effects)',
196
- rollback: 'Single git revert restores previous state',
197
- },
198
- hardStops: {
199
- dynamicImportDetected: true,
200
- migrationFlagDetected: true,
201
- behaviorChangeUncertain: true,
202
- securitySensitive: true,
203
- },
204
- },
205
- 'security-remediation': {
206
- id: 'security-remediation',
207
- version: '1.0.0',
208
- description: 'Verified security fixes with proof of safety',
209
- tier: 'pro',
210
- scope: {
211
- allowedActions: ['analyze', 'propose_diff'],
212
- disallowedActions: [
213
- 'delete_auth_code',
214
- 'modify_crypto_primitives',
215
- 'alter_permission_checks',
216
- 'remove_rate_limits',
217
- 'expose_internal_apis',
218
- ],
219
- allowedRisk: ['LOW', 'MEDIUM'],
220
- excludedPaths: [
221
- '**/node_modules/**',
222
- '**/dist/**',
223
- '**/build/**',
224
- '**/.git/**',
225
- '**/migrations/**',
226
- '**/*.env*',
227
- ],
228
- },
229
- permissions: {
230
- readRepo: true,
231
- writeProposals: true,
232
- applyChanges: false,
233
- enforceCI: true,
234
- },
235
- requiredProofs: {
236
- reachability: 'Proof that vulnerable code path is reachable and fix addresses it',
237
- compatibility: 'Proof that fix does not break existing functionality',
238
- behaviorLock: 'Proof that security behavior is strengthened, not weakened',
239
- rollback: 'Proof that fix can be reverted without leaving system vulnerable',
240
- },
241
- hardStops: {
242
- dynamicImportDetected: true,
243
- migrationFlagDetected: false,
244
- behaviorChangeUncertain: true,
245
- securitySensitive: false,
246
- },
247
- },
248
- 'inventory': {
249
- id: 'inventory',
250
- version: '1.0.0',
251
- description: 'Read-only inventory of duplication and legacy code',
252
- tier: 'free',
253
- note: 'Use "vibecheck classify" for this authority',
254
- },
255
- };
256
-
257
- // ═══════════════════════════════════════════════════════════════════════════════
258
- // TIER CHECKING
259
- // ═══════════════════════════════════════════════════════════════════════════════
260
-
261
- async function checkTierAccess(authorityTier) {
262
- try {
263
- const entitlementsV2 = require("./lib/entitlements-v2");
264
- const access = await entitlementsV2.enforce(`authority.${authorityTier}`, {
265
- silent: true,
266
- });
267
- return access;
268
- } catch (err) {
269
- // Fallback to free tier if entitlements fail
270
- return {
271
- allowed: authorityTier === 'free',
272
- tier: 'free',
273
- reason: authorityTier === 'free' ? 'Free tier access' : 'Upgrade required',
274
- };
275
- }
101
+ `);
276
102
  }
277
103
 
278
104
  // ═══════════════════════════════════════════════════════════════════════════════
279
- // SAFE CONSOLIDATION EXECUTOR
105
+ // MAIN ENTRY
280
106
  // ═══════════════════════════════════════════════════════════════════════════════
281
107
 
282
108
  /**
283
- * Check for unsafe patterns in file content
109
+ * Main approve command handler
110
+ * @param {string[]} args - Command arguments
111
+ * @param {Object} context - Execution context
112
+ * @returns {Promise<number>} Exit code
284
113
  */
285
- function checkUnsafePatterns(content) {
286
- const patterns = [
287
- { pattern: /import\s*\(/, reason: 'Dynamic import detected' },
288
- { pattern: /require\s*\((?!['"])/, reason: 'Dynamic require detected' },
289
- { pattern: /process\.env\./, reason: 'Environment-dependent behavior' },
290
- { pattern: /FEATURE_FLAG/i, reason: 'Feature flag detected' },
291
- { pattern: /feature\s*toggle/i, reason: 'Feature toggle detected' },
292
- { pattern: /eval\s*\(/, reason: 'Eval detected' },
293
- { pattern: /Function\s*\(/, reason: 'Dynamic function construction' },
294
- ];
295
-
296
- const triggered = [];
297
- for (const { pattern, reason } of patterns) {
298
- if (pattern.test(content)) {
299
- triggered.push(reason);
300
- }
301
- }
114
+ async function runApprove(args = [], context = {}) {
115
+ const { parseGlobalFlags, shouldSuppressOutput, isJsonMode } = getGlobalFlags();
116
+ const { EXIT } = getExitCodes();
117
+ const { ansi, renderSuccess, renderError, renderWarning } = getCliOutput();
302
118
 
303
- return triggered;
304
- }
305
-
306
- /**
307
- * Check if file is in excluded paths
308
- */
309
- function isExcludedPath(filePath, excludedPaths) {
310
- const minimatch = require('minimatch');
119
+ const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
120
+ const quiet = shouldSuppressOutput(globalFlags);
121
+ const json = isJsonMode(globalFlags) || args.includes("--json");
122
+ const projectRoot = context.repoRoot || globalFlags.path || process.cwd();
311
123
 
312
- for (const pattern of excludedPaths) {
313
- if (minimatch(filePath, pattern, { matchBase: true })) {
314
- return true;
315
- }
124
+ // Handle help
125
+ if (globalFlags.help || args.includes("--help") || args.includes("-h")) {
126
+ printHelp();
127
+ return EXIT.SUCCESS;
316
128
  }
317
129
 
318
- return false;
319
- }
320
-
321
- /**
322
- * Analyze files for safe consolidation
323
- */
324
- async function analyzeSafeConsolidation(projectPath, authority, opts, spinner) {
325
- const startTime = Date.now();
326
- const findings = {
327
- safeToConsolidate: [],
328
- needsReview: [],
329
- blocked: [],
330
- };
331
- const hardStopsTriggered = [];
332
- const filesTouched = [];
333
-
334
- // Get inventory first
335
- const { runClassify } = require("./runClassify");
336
-
337
- spinner.update('Running inventory analysis...');
338
-
339
- // Run classify in quiet mode to get data
340
- const inventoryResult = await runInventoryForApprove(projectPath, opts);
341
-
342
- spinner.update('Analyzing consolidation safety...');
343
-
344
- // Analyze each duplicate group
345
- for (const dupGroup of inventoryResult.duplicationMap) {
346
- const primary = dupGroup.primary;
347
- filesTouched.push(primary);
348
-
349
- // Skip if in excluded paths
350
- if (isExcludedPath(primary, authority.scope.excludedPaths)) {
351
- findings.blocked.push({
352
- file: primary,
353
- reason: 'In excluded path',
354
- recommendation: 'No action needed - file is intentionally excluded',
355
- });
356
- continue;
357
- }
358
-
359
- // Read primary file content
360
- const primaryPath = path.join(projectPath, primary);
361
- let content = '';
362
- try {
363
- content = await fs.promises.readFile(primaryPath, 'utf-8');
364
- } catch (err) {
365
- findings.blocked.push({
366
- file: primary,
367
- reason: 'Unable to read file',
368
- recommendation: 'Check file permissions',
369
- });
370
- continue;
371
- }
372
-
373
- // Check for unsafe patterns
374
- const unsafeReasons = checkUnsafePatterns(content);
375
- if (unsafeReasons.length > 0) {
376
- findings.blocked.push({
377
- file: primary,
378
- reason: unsafeReasons.join(', '),
379
- recommendation: 'Manual review required - contains patterns that prevent safe automated consolidation',
380
- });
381
-
382
- if (authority.hardStops.dynamicImportDetected && unsafeReasons.some(r => r.includes('import'))) {
383
- hardStopsTriggered.push(`Dynamic import in ${primary}`);
384
- }
385
- continue;
386
- }
387
-
388
- // Check similarity level
389
- if (dupGroup.similarity >= 0.95) {
390
- findings.safeToConsolidate.push({
391
- primary: primary,
392
- duplicates: dupGroup.duplicates,
393
- similarity: dupGroup.similarity,
394
- type: dupGroup.type,
395
- action: 'Create re-export from duplicates to primary',
396
- linesSaved: dupGroup.lineCount * dupGroup.duplicates.length,
397
- });
398
- } else {
399
- findings.needsReview.push({
400
- file: primary,
401
- duplicates: dupGroup.duplicates,
402
- similarity: dupGroup.similarity,
403
- reason: `Similarity ${Math.round(dupGroup.similarity * 100)}% below threshold for auto-consolidation`,
404
- recommendation: 'Review differences manually before consolidating',
405
- });
406
- }
407
- }
130
+ // Parse options
131
+ const autoApprove = args.includes("-y") || args.includes("--yes");
132
+ const reject = args.includes("--reject");
133
+ const clear = args.includes("--clear");
134
+ const customMessage = getArgValue(args, "-m") || getArgValue(args, "--message");
408
135
 
409
- // Analyze legacy code
410
- for (const legacyEntry of inventoryResult.legacyMap) {
411
- filesTouched.push(legacyEntry.file);
412
-
413
- if (isExcludedPath(legacyEntry.file, authority.scope.excludedPaths)) {
414
- continue;
415
- }
416
-
417
- if (legacyEntry.type === 'backup' && legacyEntry.confidence >= 0.9) {
418
- findings.safeToConsolidate.push({
419
- primary: legacyEntry.file,
420
- duplicates: [],
421
- type: 'legacy-backup',
422
- action: 'Archive or remove backup file',
423
- evidence: legacyEntry.evidence,
424
- });
136
+ // Load session module
137
+ let SessionCollector;
138
+ try {
139
+ SessionCollector = getSessionModule().SessionCollector;
140
+ } catch (e) {
141
+ if (json) {
142
+ console.log(JSON.stringify({ success: false, error: "Session module not available" }));
425
143
  } else {
426
- findings.needsReview.push({
427
- file: legacyEntry.file,
428
- type: legacyEntry.type,
429
- confidence: legacyEntry.confidence,
430
- reason: 'Legacy code - requires manual review',
431
- recommendation: 'Verify no active references before removal',
432
- });
144
+ renderError("Session module not available");
433
145
  }
146
+ return EXIT.INTERNAL_ERROR;
434
147
  }
435
148
 
436
- // Generate proofs
437
- const proofs = {
438
- reachability: hardStopsTriggered.length > 0
439
- ? `FAILED: ${hardStopsTriggered.join('; ')}`
440
- : 'PASSED: No dynamic imports detected in safe files',
441
- compatibility: findings.safeToConsolidate.length > 0
442
- ? 'PASSED: Re-exports preserve import paths'
443
- : 'N/A: No consolidations proposed',
444
- rollback: 'PASSED: Single git revert restores state',
445
- };
446
-
447
- // Determine verdict
448
- let action = 'PROCEED';
449
- let confidence = 0.95;
450
-
451
- if (hardStopsTriggered.length > 0) {
452
- action = 'STOP';
453
- confidence = 1.0;
454
- } else if (findings.needsReview.length > findings.safeToConsolidate.length) {
455
- action = 'DEFER';
456
- confidence = 0.6;
457
- } else if (findings.blocked.length > 0) {
458
- action = 'DEFER';
459
- confidence = 0.7;
460
- }
461
-
462
- return {
463
- authority: authority.id,
464
- version: authority.version,
465
- timestamp: new Date().toISOString(),
466
- action,
467
- riskLevel: action === 'STOP' ? 'HIGH' : action === 'DEFER' ? 'MEDIUM' : 'LOW',
468
- exitCode: action === 'PROCEED' ? 0 : action === 'DEFER' ? 1 : 2,
469
- filesTouched,
470
- proofs,
471
- behaviorChange: action === 'STOP',
472
- confidence,
473
- hardStopsTriggered,
474
- notes: generateNotes(action, findings, hardStopsTriggered),
475
- analysis: {
476
- safeToConsolidate: findings.safeToConsolidate,
477
- needsReview: findings.needsReview,
478
- blocked: findings.blocked,
479
- summary: {
480
- safeCount: findings.safeToConsolidate.length,
481
- reviewCount: findings.needsReview.length,
482
- blockedCount: findings.blocked.length,
483
- totalLinesSavings: findings.safeToConsolidate.reduce((sum, f) => sum + (f.linesSaved || 0), 0),
484
- },
485
- },
486
- analysisTimeMs: Date.now() - startTime,
487
- };
488
- }
489
-
490
- /**
491
- * Run inventory analysis for approve command
492
- */
493
- async function runInventoryForApprove(projectPath, opts) {
494
- const { runClassify } = require("./runClassify");
495
-
496
- // Create a minimal spinner that captures output
497
- const inventoryOpts = {
498
- path: projectPath,
499
- json: true,
500
- quiet: true,
501
- includeNear: true,
502
- includeSemantic: false,
503
- threshold: 0.8,
504
- maxFiles: 5000,
505
- };
506
-
507
- // Directly run the inventory analysis logic
508
- const crypto = require("crypto");
509
-
510
- const EXCLUDED_DIRS = new Set([
511
- 'node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.vibecheck',
512
- ]);
149
+ const collector = new SessionCollector(projectRoot);
513
150
 
514
- async function findFiles(rootPath, maxFiles) {
515
- const files = [];
516
- const extensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
517
-
518
- async function walk(dir, depth = 0) {
519
- if (depth > 20 || files.length >= maxFiles) return;
520
- try {
521
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
522
- for (const entry of entries) {
523
- if (files.length >= maxFiles) break;
524
- const fullPath = path.join(dir, entry.name);
525
- const relativePath = path.relative(rootPath, fullPath);
526
- if (entry.isDirectory()) {
527
- if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
528
- await walk(fullPath, depth + 1);
529
- }
530
- } else if (entry.isFile()) {
531
- const ext = path.extname(entry.name).toLowerCase();
532
- if (extensions.has(ext)) {
533
- files.push({ path: fullPath, relativePath, name: entry.name, ext });
534
- }
535
- }
536
- }
537
- } catch (err) { /* skip */ }
151
+ // Check for pending changes
152
+ if (!collector.hasPending()) {
153
+ if (json) {
154
+ console.log(JSON.stringify({ success: true, message: "No pending changes", changes: 0 }));
155
+ } else if (!quiet) {
156
+ renderWarning("No pending changes to review");
157
+ console.log(`\n ${ansi.dim}Changes are logged when AI modifies files in OBSERVE mode.${ansi.reset}\n`);
538
158
  }
539
- await walk(rootPath);
540
- return files;
159
+ return EXIT.SUCCESS;
541
160
  }
542
161
 
543
- const files = await findFiles(projectPath, 5000);
544
- const fileContents = new Map();
545
- const fileHashes = new Map();
546
-
547
- for (const file of files) {
548
- try {
549
- const content = await fs.promises.readFile(file.path, 'utf-8');
550
- const lines = content.split('\n').length;
551
- fileContents.set(file.relativePath, { content, lines });
552
- fileHashes.set(file.relativePath, crypto.createHash('sha256').update(content).digest('hex').slice(0, 16));
553
- } catch (err) { /* skip */ }
554
- }
162
+ const session = collector.getPending();
163
+ const summary = collector.getSummary();
555
164
 
556
- // Find duplicates
557
- const hashGroups = new Map();
558
- for (const [filePath, hash] of fileHashes.entries()) {
559
- if (!hashGroups.has(hash)) hashGroups.set(hash, []);
560
- hashGroups.get(hash).push(filePath);
165
+ // Handle reject
166
+ if (reject) {
167
+ collector.reject();
168
+ if (json) {
169
+ console.log(JSON.stringify({ success: true, action: "rejected", changes: summary.total_changes }));
170
+ } else if (!quiet) {
171
+ renderSuccess("Session changes rejected");
172
+ console.log(`\n ${ansi.dim}${summary.total_changes} change(s) discarded.${ansi.reset}\n`);
173
+ }
174
+ return EXIT.SUCCESS;
561
175
  }
562
176
 
563
- const duplicationMap = [];
564
- for (const [hash, files] of hashGroups.entries()) {
565
- if (files.length > 1) {
566
- const { lines } = fileContents.get(files[0]) || { lines: 0 };
567
- duplicationMap.push({
568
- primary: files[0],
569
- duplicates: files.slice(1),
570
- similarity: 1.0,
571
- type: 'exact',
572
- lineCount: lines,
573
- });
177
+ // Handle clear (no intent created)
178
+ if (clear) {
179
+ collector.clear();
180
+ if (json) {
181
+ console.log(JSON.stringify({ success: true, action: "cleared", changes: summary.total_changes }));
182
+ } else if (!quiet) {
183
+ renderSuccess("Session cleared");
184
+ console.log(`\n ${ansi.dim}Session reset. No intent created.${ansi.reset}\n`);
574
185
  }
186
+ return EXIT.SUCCESS;
575
187
  }
576
188
 
577
- // Find legacy code
578
- const LEGACY_PATTERNS = [
579
- { pattern: /\.old\.(js|ts|tsx|jsx)$/i, type: 'backup', confidence: 0.9 },
580
- { pattern: /\.bak\.(js|ts|tsx|jsx)$/i, type: 'backup', confidence: 0.95 },
581
- { pattern: /\.backup\.(js|ts|tsx|jsx)$/i, type: 'backup', confidence: 0.95 },
582
- { pattern: /\.deprecated\.(js|ts|tsx|jsx)$/i, type: 'deprecated', confidence: 0.9 },
583
- ];
584
-
585
- const legacyMap = [];
586
- for (const [filePath, { content }] of fileContents.entries()) {
587
- for (const { pattern, type, confidence } of LEGACY_PATTERNS) {
588
- if (pattern.test(filePath)) {
589
- legacyMap.push({
590
- file: filePath,
591
- type,
592
- evidence: [`File name matches pattern: ${pattern}`],
593
- confidence,
594
- });
595
- break;
189
+ // Show changes summary
190
+ if (!json && !quiet) {
191
+ console.log(`
192
+ ${ansi.cyan}${ansi.bold}╔═══════════════════════════════════════════════════════════════════╗
193
+ ║ ║
194
+ ║ SESSION REVIEW ║
195
+ ║ ║
196
+ ╚═══════════════════════════════════════════════════════════════════╝${ansi.reset}
197
+
198
+ ${ansi.bold}Session Info${ansi.reset}
199
+ ID: ${session.id}
200
+ Started: ${new Date(session.started_at).toLocaleString()}
201
+ Duration: ${summary.duration_minutes} minutes
202
+
203
+ ${ansi.bold}Changes (${summary.total_changes})${ansi.reset}`);
204
+
205
+ // Group by domain
206
+ const byDomain = {};
207
+ for (const change of session.changes) {
208
+ const domain = change.domain || "general";
209
+ if (!byDomain[domain]) byDomain[domain] = [];
210
+ byDomain[domain].push(change);
211
+ }
212
+
213
+ for (const [domain, changes] of Object.entries(byDomain)) {
214
+ console.log(`\n ${ansi.bold}${domain}${ansi.reset} (${changes.length})`);
215
+ for (const change of changes.slice(0, 5)) {
216
+ const icon = change.type === "file_create" ? "+" : change.type === "file_delete" ? "-" : "~";
217
+ const color = change.type === "file_create" ? ansi.green : change.type === "file_delete" ? ansi.red : ansi.yellow;
218
+ console.log(` ${color}${icon}${ansi.reset} ${change.path}`);
219
+ }
220
+ if (changes.length > 5) {
221
+ console.log(` ${ansi.dim}... and ${changes.length - 5} more${ansi.reset}`);
596
222
  }
597
223
  }
598
- // Check content for @deprecated
599
- if (/@deprecated/i.test(content)) {
600
- const existing = legacyMap.find(l => l.file === filePath);
601
- if (!existing) {
602
- legacyMap.push({
603
- file: filePath,
604
- type: 'deprecated',
605
- evidence: ['Contains @deprecated annotation'],
606
- confidence: 0.85,
607
- });
224
+
225
+ // Show auto-generated intent
226
+ const generatedIntent = collector.generateIntentSummary();
227
+ console.log(`
228
+ ${ansi.bold}Auto-Generated Intent${ansi.reset}
229
+ "${generatedIntent}"
230
+ `);
231
+ }
232
+
233
+ // Handle auto-approve
234
+ if (autoApprove || customMessage) {
235
+ const intentMessage = customMessage || collector.generateIntentSummary();
236
+ const result = collector.approve(intentMessage);
237
+
238
+ if (json) {
239
+ console.log(JSON.stringify({
240
+ success: result.success,
241
+ action: "approved",
242
+ intent: result.intent ? {
243
+ hash: result.intent.hash,
244
+ summary: result.intent.summary,
245
+ } : null,
246
+ changes: summary.total_changes,
247
+ error: result.error,
248
+ }, null, 2));
249
+ } else if (!quiet) {
250
+ if (result.success) {
251
+ console.log(`
252
+ ${ansi.green}${ansi.bold}╔═══════════════════════════════════════════════════════════════════╗
253
+ ║ ║
254
+ ║ ✓ SESSION APPROVED ║
255
+ ║ ║
256
+ ╚═══════════════════════════════════════════════════════════════════╝${ansi.reset}
257
+
258
+ ${ansi.bold}Intent Created${ansi.reset}
259
+ "${result.intent.summary}"
260
+
261
+ ${ansi.bold}Changes Approved${ansi.reset}
262
+ ${result.changes_approved} change(s)
263
+
264
+ ${ansi.dim}Hash: ${result.intent.hash.slice(0, 16)}...${ansi.reset}
265
+
266
+ ${ansi.bold}The Agent Firewall is now in ENFORCE mode.${ansi.reset}
267
+ ${ansi.dim}Future changes must align with this intent.${ansi.reset}
268
+ `);
269
+ } else {
270
+ renderError(result.error || "Failed to approve session");
608
271
  }
609
272
  }
273
+
274
+ return result.success ? EXIT.SUCCESS : EXIT.INTERNAL_ERROR;
610
275
  }
611
276
 
612
- return {
613
- duplicationMap,
614
- legacyMap,
615
- summary: {
616
- totalFiles: files.length,
617
- duplicatedFiles: duplicationMap.reduce((sum, d) => sum + 1 + d.duplicates.length, 0),
618
- legacyFiles: legacyMap.length,
619
- },
620
- };
621
- }
622
-
623
- /**
624
- * Generate human-readable notes for verdict
625
- */
626
- function generateNotes(action, findings, hardStopsTriggered) {
627
- if (action === 'STOP') {
628
- return `BLOCKED: Hard stops triggered. ${hardStopsTriggered.join('. ')}. Manual intervention required.`;
277
+ // Interactive mode - show options
278
+ if (!json && !quiet) {
279
+ console.log(` ${ansi.bold}Options${ansi.reset}
280
+ ${ansi.cyan}vibecheck approve -y${ansi.reset} Approve with auto-generated intent
281
+ ${ansi.cyan}vibecheck approve -m "..."${ansi.reset} Approve with custom message
282
+ ${ansi.cyan}vibecheck approve --reject${ansi.reset} Reject all changes
283
+ `);
629
284
  }
630
285
 
631
- if (action === 'DEFER') {
632
- const reviewCount = findings.needsReview.length;
633
- const blockedCount = findings.blocked.length;
634
- return `REVIEW NEEDED: ${reviewCount} items need manual review, ${blockedCount} items are blocked. Safe consolidations: ${findings.safeToConsolidate.length}.`;
286
+ if (json) {
287
+ console.log(JSON.stringify({
288
+ pending: true,
289
+ session_id: session.id,
290
+ changes: summary.total_changes,
291
+ files_touched: summary.files_touched,
292
+ domains: summary.domains,
293
+ generated_intent: collector.generateIntentSummary(),
294
+ }, null, 2));
635
295
  }
636
296
 
637
- const linesSaved = findings.safeToConsolidate.reduce((sum, f) => sum + (f.linesSaved || 0), 0);
638
- return `SAFE TO PROCEED: ${findings.safeToConsolidate.length} consolidations identified. Estimated ${linesSaved} lines can be reduced.`;
297
+ return EXIT.SUCCESS;
639
298
  }
640
299
 
641
300
  // ═══════════════════════════════════════════════════════════════════════════════
642
- // OUTPUT FORMATTERS
301
+ // UTILITIES
643
302
  // ═══════════════════════════════════════════════════════════════════════════════
644
303
 
645
- function formatVerdictOutput(verdict, projectPath) {
646
- const lines = [];
647
-
648
- const actionSymbol = verdict.action === 'PROCEED' ? '✓' : verdict.action === 'DEFER' ? '⚠' : '✗';
649
- const actionColor = verdict.action === 'PROCEED' ? colors.success : verdict.action === 'DEFER' ? colors.warning : colors.error;
650
-
651
- lines.push('');
652
- lines.push('┌────────────────────────────────────────────────────────────────────┐');
653
- lines.push(`│ ${actionColor}${actionSymbol}${ansi.reset} Authority Verdict: ${ansi.bold}${verdict.action}${ansi.reset} │`);
654
- lines.push('├────────────────────────────────────────────────────────────────────┤');
655
- lines.push(`│ Authority: ${verdict.authority} v${verdict.version}`.padEnd(68) + '│');
656
- lines.push(`│ Risk Level: ${verdict.riskLevel}`.padEnd(68) + '│');
657
- lines.push(`│ Confidence: ${Math.round(verdict.confidence * 100)}%`.padEnd(68) + '│');
658
- lines.push(`│ Exit Code: ${verdict.exitCode}`.padEnd(68) + '│');
659
- lines.push('└────────────────────────────────────────────────────────────────────┘');
660
- lines.push('');
661
-
662
- // Hard stops
663
- if (verdict.hardStopsTriggered.length > 0) {
664
- lines.push(`${ansi.bold}${colors.error}Hard Stops Triggered:${ansi.reset}`);
665
- for (const stop of verdict.hardStopsTriggered) {
666
- lines.push(` ${colors.error}✗${ansi.reset} ${stop}`);
304
+ function getArgValue(args, ...flags) {
305
+ for (const flag of flags) {
306
+ const index = args.indexOf(flag);
307
+ if (index !== -1 && args[index + 1] && !args[index + 1].startsWith("-")) {
308
+ return args[index + 1];
667
309
  }
668
- lines.push('');
669
- }
670
-
671
- // Proofs
672
- lines.push(`${ansi.bold}Proofs:${ansi.reset}`);
673
- for (const [key, value] of Object.entries(verdict.proofs)) {
674
- const icon = value.startsWith('PASSED') ? colors.success + '✓' : value.startsWith('FAILED') ? colors.error + '✗' : colors.warning + '○';
675
- lines.push(` ${icon}${ansi.reset} ${key}: ${ansi.dim}${value}${ansi.reset}`);
676
- }
677
- lines.push('');
678
-
679
- // Analysis summary
680
- if (verdict.analysis) {
681
- const { summary } = verdict.analysis;
682
- lines.push(`${ansi.bold}Analysis Summary:${ansi.reset}`);
683
- lines.push(` ${colors.success}✓${ansi.reset} Safe to consolidate: ${summary.safeCount}`);
684
- lines.push(` ${colors.warning}⚠${ansi.reset} Needs review: ${summary.reviewCount}`);
685
- lines.push(` ${colors.error}✗${ansi.reset} Blocked: ${summary.blockedCount}`);
686
- lines.push(` ${ansi.dim}Estimated lines saved: ${summary.totalLinesSavings}${ansi.reset}`);
687
- lines.push('');
688
- }
689
-
690
- // Notes
691
- lines.push(`${ansi.bold}Notes:${ansi.reset}`);
692
- lines.push(` ${verdict.notes}`);
693
- lines.push('');
694
-
695
- // Timestamp
696
- lines.push(`${ansi.dim}${verdict.timestamp}${ansi.reset}`);
697
- lines.push('');
698
-
699
- return lines.join('\n');
700
- }
701
-
702
- /**
703
- * Generate authority approved badge SVG
704
- */
705
- function generateBadge(verdict) {
706
- const color = verdict.action === 'PROCEED' ? '#22C55E' : verdict.action === 'DEFER' ? '#F59E0B' : '#EF4444';
707
- const text = verdict.action;
708
- const conf = Math.round(verdict.confidence * 100);
709
-
710
- return `<svg xmlns="http://www.w3.org/2000/svg" width="240" height="20">
711
- <linearGradient id="b" x2="0" y2="100%">
712
- <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
713
- <stop offset="1" stop-opacity=".1"/>
714
- </linearGradient>
715
- <clipPath id="a">
716
- <rect width="240" height="20" rx="3" fill="#fff"/>
717
- </clipPath>
718
- <g clip-path="url(#a)">
719
- <path fill="#555" d="M0 0h150v20H0z"/>
720
- <path fill="${color}" d="M150 0h90v20H150z"/>
721
- <path fill="url(#b)" d="M0 0h240v20H0z"/>
722
- </g>
723
- <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
724
- <text x="75" y="15" fill="#010101" fill-opacity=".3">Authority Approved</text>
725
- <text x="75" y="14">Authority Approved</text>
726
- <text x="195" y="15" fill="#010101" fill-opacity=".3">${text} ${conf}%</text>
727
- <text x="195" y="14">${text} ${conf}%</text>
728
- </g>
729
- </svg>`;
730
- }
731
-
732
- // ═══════════════════════════════════════════════════════════════════════════════
733
- // HARDENING CONSTANTS
734
- // ═══════════════════════════════════════════════════════════════════════════════
735
-
736
- /** Maximum execution time for an authority (ms) */
737
- const MAX_EXECUTION_TIME_MS = 5 * 60 * 1000; // 5 minutes
738
-
739
- /** Rate limit tracking */
740
- const rateLimitStore = new Map();
741
-
742
- /**
743
- * Check rate limit for user
744
- */
745
- function checkRateLimit(userId, windowMs, maxRequests) {
746
- const key = `${userId}:${windowMs}`;
747
- const now = Date.now();
748
-
749
- let entry = rateLimitStore.get(key);
750
-
751
- if (!entry || entry.resetAt < now) {
752
- entry = { count: 0, resetAt: now + windowMs };
753
- rateLimitStore.set(key, entry);
754
- }
755
-
756
- const allowed = entry.count < maxRequests;
757
- if (allowed) {
758
- entry.count++;
759
- }
760
-
761
- return {
762
- allowed,
763
- remaining: Math.max(0, maxRequests - entry.count),
764
- resetAt: new Date(entry.resetAt),
765
- };
766
- }
767
-
768
- /**
769
- * Validate authority ID format
770
- */
771
- function validateAuthorityId(id) {
772
- const pattern = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/;
773
-
774
- if (!id || typeof id !== 'string') {
775
- return { valid: false, error: 'Authority ID must be a non-empty string' };
776
- }
777
-
778
- if (id.length < 2 || id.length > 64) {
779
- return { valid: false, error: 'Authority ID must be 2-64 characters' };
780
- }
781
-
782
- if (!pattern.test(id)) {
783
- return { valid: false, error: 'Authority ID must be lowercase alphanumeric with hyphens' };
784
- }
785
-
786
- return { valid: true };
787
- }
788
-
789
- /**
790
- * Validate project path for security
791
- */
792
- function validateProjectPath(projectPath) {
793
- if (!projectPath || typeof projectPath !== 'string') {
794
- return { valid: false, error: 'Project path must be a non-empty string' };
795
- }
796
-
797
- // Check for path traversal attempts
798
- if (projectPath.includes('..') || projectPath.includes('\0')) {
799
- return { valid: false, error: 'Invalid characters in project path' };
800
- }
801
-
802
- // Must be absolute path
803
- const isAbsolute = projectPath.startsWith('/') || /^[A-Za-z]:[\\/]/.test(projectPath);
804
- if (!isAbsolute) {
805
- return { valid: false, error: 'Project path must be absolute' };
806
- }
807
-
808
- return { valid: true };
809
- }
810
-
811
- /**
812
- * Execute with timeout
813
- */
814
- async function withTimeout(fn, timeoutMs, timeoutError = 'Operation timed out') {
815
- let timeoutId;
816
-
817
- const timeoutPromise = new Promise((_, reject) => {
818
- timeoutId = setTimeout(() => {
819
- reject(new Error(timeoutError));
820
- }, timeoutMs);
821
- });
822
-
823
- try {
824
- const result = await Promise.race([fn(), timeoutPromise]);
825
- clearTimeout(timeoutId);
826
- return result;
827
- } catch (error) {
828
- clearTimeout(timeoutId);
829
- throw error;
830
- }
831
- }
832
-
833
- /**
834
- * Sanitize error for safe logging
835
- */
836
- function sanitizeError(error) {
837
- if (error instanceof Error) {
838
- let message = error.message;
839
- // Remove file paths
840
- message = message.replace(/[A-Za-z]:[\\\/][^\s]+/g, '[PATH]');
841
- message = message.replace(/\/[^\s]+/g, '[PATH]');
842
- // Remove potential secrets
843
- message = message.replace(/['"][^'"]{20,}['"]/g, '[REDACTED]');
844
-
845
- return {
846
- message: message.slice(0, 200),
847
- code: error.name || 'Error',
848
- };
849
310
  }
850
-
851
- return {
852
- message: 'An unexpected error occurred',
853
- code: 'UNKNOWN_ERROR',
854
- };
311
+ return null;
855
312
  }
856
313
 
857
314
  // ═══════════════════════════════════════════════════════════════════════════════
858
- // MAIN COMMAND
315
+ // EXPORTS
859
316
  // ═══════════════════════════════════════════════════════════════════════════════
860
317
 
861
- async function runApprove(args) {
862
- const opts = parseArgs(args);
863
- const executionId = `exec_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
864
- const startTime = Date.now();
865
-
866
- // Show help
867
- if (opts.help) {
868
- printHelp(shouldShowBanner(opts));
869
- return 0;
870
- }
871
-
872
- // Print banner
873
- if (shouldShowBanner(opts)) {
874
- printBanner();
875
- }
876
-
877
- // List authorities
878
- if (opts.list) {
879
- console.log(`\n ${ansi.bold}Available Authorities:${ansi.reset}\n`);
880
-
881
- for (const [id, auth] of Object.entries(AUTHORITIES)) {
882
- const tierLabel = auth.tier === 'free' ? colors.success + '[FREE]' :
883
- auth.tier === 'starter' ? colors.accent + '[STARTER]' :
884
- auth.tier === 'pro' ? colors.warning + '[PRO]' :
885
- colors.error + '[ENTERPRISE]';
886
- console.log(` ${colors.accent}${id}${ansi.reset} ${tierLabel}${ansi.reset}`);
887
- console.log(` ${ansi.dim}${auth.description}${ansi.reset}`);
888
- if (auth.note) {
889
- console.log(` ${colors.warning}Note: ${auth.note}${ansi.reset}`);
890
- }
891
- console.log();
892
- }
893
-
894
- console.log(` ${ansi.dim}Usage: vibecheck approve <authority-id>${ansi.reset}\n`);
895
- return 0;
896
- }
897
-
898
- // Require authority argument
899
- if (!opts.authority) {
900
- console.error(`\n ${colors.error}✗${ansi.reset} Authority ID required\n`);
901
- console.log(` ${ansi.dim}Usage: vibecheck approve <authority-id>${ansi.reset}`);
902
- console.log(` ${ansi.dim}List: vibecheck approve --list${ansi.reset}\n`);
903
- return EXIT.USER_ERROR;
904
- }
905
-
906
- // HARDENING: Validate authority ID format
907
- const idValidation = validateAuthorityId(opts.authority);
908
- if (!idValidation.valid) {
909
- console.error(`\n ${colors.error}✗${ansi.reset} Invalid authority ID: ${idValidation.error}\n`);
910
- return EXIT.USER_ERROR;
911
- }
912
-
913
- // Check authority exists
914
- const authority = AUTHORITIES[opts.authority];
915
- if (!authority) {
916
- console.error(`\n ${colors.error}✗${ansi.reset} Unknown authority: ${opts.authority}\n`);
917
- console.log(` ${ansi.dim}Available: ${Object.keys(AUTHORITIES).join(', ')}${ansi.reset}\n`);
918
- return EXIT.USER_ERROR;
919
- }
920
-
921
- // Special case for inventory - redirect to classify
922
- if (opts.authority === 'inventory') {
923
- console.log(`\n ${colors.warning}⚠${ansi.reset} Use 'vibecheck classify' for the inventory authority\n`);
924
- return EXIT.USER_ERROR;
925
- }
926
-
927
- // HARDENING: Rate limiting (10 requests per minute)
928
- const userId = process.env.VIBECHECK_USER_ID || 'anonymous';
929
- const rateCheck = checkRateLimit(userId, 60 * 1000, 10);
930
- if (!rateCheck.allowed) {
931
- console.error(`\n ${colors.error}✗${ansi.reset} Rate limit exceeded`);
932
- console.log(` ${ansi.dim}Try again at: ${rateCheck.resetAt.toISOString()}${ansi.reset}\n`);
933
- return EXIT.RATE_LIMITED || 429;
934
- }
935
-
936
- // Check tier access
937
- const access = await checkTierAccess(authority.tier);
938
- if (!access.allowed) {
939
- console.log(`\n ${colors.error}✗${ansi.reset} ${ansi.bold}${authority.tier.toUpperCase()} tier required${ansi.reset}`);
940
- console.log(` ${ansi.dim}Current tier: ${access.tier || 'FREE'}${ansi.reset}`);
941
- console.log(` ${ansi.dim}Upgrade at: https://vibecheckai.dev/pricing${ansi.reset}\n`);
942
- return EXIT.TIER_REQUIRED;
943
- }
944
-
945
- const projectPath = path.resolve(opts.path);
946
-
947
- // HARDENING: Validate project path
948
- const pathValidation = validateProjectPath(projectPath);
949
- if (!pathValidation.valid) {
950
- throw createUserError(pathValidation.error, "SecurityError");
951
- }
952
-
953
- // Validate project path exists
954
- if (!fs.existsSync(projectPath)) {
955
- throw createUserError(`Project path does not exist: ${projectPath}`, "ValidationError");
956
- }
957
-
958
- if (!opts.quiet) {
959
- console.log(` ${ansi.dim}Execution:${ansi.reset} ${ansi.dim}${executionId}${ansi.reset}`);
960
- console.log(` ${ansi.dim}Project:${ansi.reset} ${ansi.bold}${path.basename(projectPath)}${ansi.reset}`);
961
- console.log(` ${ansi.dim}Authority:${ansi.reset} ${colors.accent}${authority.id}${ansi.reset} v${authority.version}`);
962
- console.log(` ${ansi.dim}Tier:${ansi.reset} ${authority.tier.toUpperCase()}`);
963
- console.log();
964
- }
965
-
966
- // Run analysis with timeout
967
- const spinner = new Spinner({ color: colors.primary });
968
- spinner.start('Executing authority...');
969
-
970
- try {
971
- let verdict;
972
-
973
- // HARDENING: Execute with timeout
974
- verdict = await withTimeout(
975
- async () => {
976
- if (opts.authority === 'safe-consolidation') {
977
- return await analyzeSafeConsolidation(projectPath, authority, opts, spinner);
978
- } else if (opts.authority === 'security-remediation') {
979
- // Forward to security analysis
980
- return await analyzeSecurityRemediation(projectPath, authority, opts, spinner);
981
- } else {
982
- throw createUserError(`Authority executor not implemented: ${opts.authority}`, "NotImplemented");
983
- }
984
- },
985
- MAX_EXECUTION_TIME_MS,
986
- `Authority execution timed out after ${MAX_EXECUTION_TIME_MS / 1000}s`
987
- );
988
-
989
- // Add execution metadata
990
- verdict.executionId = executionId;
991
- verdict.analysisTimeMs = Date.now() - startTime;
992
-
993
- spinner.succeed(`Analysis complete (${verdict.analysisTimeMs}ms)`);
994
-
995
- // Output
996
- if (opts.json) {
997
- console.log(JSON.stringify(verdict, null, 2));
998
- } else {
999
- // Use Mission Control format
1000
- const { formatApproveOutput } = require('./lib/approve-output');
1001
- console.log(formatApproveOutput(verdict, { projectPath, version: 'v3.5.5' }));
1002
- }
1003
-
1004
- // Save to file if requested
1005
- if (opts.output && !opts.dryRun) {
1006
- const outputPath = path.resolve(opts.output);
1007
- await fs.promises.writeFile(outputPath, JSON.stringify(verdict, null, 2));
1008
-
1009
- if (!opts.quiet && !opts.json) {
1010
- console.log(` ${colors.success}✓${ansi.reset} Verdict saved to: ${outputPath}`);
1011
- }
1012
- }
1013
-
1014
- // Generate badge if requested
1015
- if (opts.badge && verdict.action === 'PROCEED' && !opts.dryRun) {
1016
- const badgePath = path.join(projectPath, '.vibecheck', 'badges', `${authority.id}-badge.svg`);
1017
- const badgeDir = path.dirname(badgePath);
1018
-
1019
- if (!fs.existsSync(badgeDir)) {
1020
- fs.mkdirSync(badgeDir, { recursive: true });
1021
- }
1022
-
1023
- await fs.promises.writeFile(badgePath, generateBadge(verdict));
1024
-
1025
- if (!opts.quiet && !opts.json) {
1026
- console.log(` ${colors.success}✓${ansi.reset} Badge saved to: ${badgePath}`);
1027
- }
1028
- }
1029
-
1030
- // Log execution summary (for audit)
1031
- if (opts.verbose) {
1032
- console.log(`\n ${ansi.dim}Execution Summary:${ansi.reset}`);
1033
- console.log(` ${ansi.dim}├─ ID: ${executionId}${ansi.reset}`);
1034
- console.log(` ${ansi.dim}├─ Duration: ${verdict.analysisTimeMs}ms${ansi.reset}`);
1035
- console.log(` ${ansi.dim}├─ Files Analyzed: ${verdict.filesTouched.length}${ansi.reset}`);
1036
- console.log(` ${ansi.dim}└─ Verdict: ${verdict.action}${ansi.reset}\n`);
1037
- }
1038
-
1039
- return verdict.exitCode;
1040
-
1041
- } catch (error) {
1042
- const sanitized = sanitizeError(error);
1043
- spinner.fail(`Authority execution failed: ${sanitized.message}`);
1044
-
1045
- // Log sanitized error details
1046
- if (opts.verbose) {
1047
- console.error(` ${ansi.dim}Error Code: ${sanitized.code}${ansi.reset}`);
1048
- console.error(` ${ansi.dim}Execution ID: ${executionId}${ansi.reset}`);
1049
- }
1050
-
1051
- throw error;
1052
- }
1053
- }
1054
-
1055
- /**
1056
- * Security remediation authority executor
1057
- */
1058
- async function analyzeSecurityRemediation(projectPath, authority, opts, spinner) {
1059
- const startTime = Date.now();
1060
- spinner.update('Scanning for security vulnerabilities...');
1061
-
1062
- // Security patterns to detect
1063
- const SECURITY_PATTERNS = {
1064
- COMMAND_INJECTION: {
1065
- pattern: /execSync\s*\(\s*[`$]/,
1066
- severity: 'CRITICAL',
1067
- description: 'Command injection via shell interpolation',
1068
- },
1069
- SQL_INJECTION: {
1070
- pattern: /query\s*\(\s*[`$]/,
1071
- severity: 'CRITICAL',
1072
- description: 'SQL injection via string interpolation',
1073
- },
1074
- EVAL_USAGE: {
1075
- pattern: /eval\s*\(/,
1076
- severity: 'HIGH',
1077
- description: 'Unsafe eval usage',
1078
- },
1079
- HARDCODED_SECRET: {
1080
- pattern: /process\.env\.\w+\s*\|\|\s*['"][^'"]{8,}['"]/,
1081
- severity: 'HIGH',
1082
- description: 'Hardcoded fallback secret',
1083
- },
1084
- PATH_TRAVERSAL: {
1085
- pattern: /path\.join\s*\([^)]*req\./,
1086
- severity: 'HIGH',
1087
- description: 'Path traversal via user input',
1088
- },
1089
- };
1090
-
1091
- const findings = [];
1092
- const filesTouched = [];
1093
-
1094
- // Scan files
1095
- const extensions = new Set(['.ts', '.tsx', '.js', '.jsx']);
1096
- const excludedDirs = new Set(['node_modules', '.git', 'dist', 'build']);
1097
-
1098
- async function walkDir(dir, depth = 0) {
1099
- if (depth > 15) return;
1100
-
1101
- try {
1102
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
1103
-
1104
- for (const entry of entries) {
1105
- const fullPath = path.join(dir, entry.name);
1106
- const relativePath = path.relative(projectPath, fullPath);
1107
-
1108
- if (entry.isDirectory()) {
1109
- if (!excludedDirs.has(entry.name) && !entry.name.startsWith('.')) {
1110
- await walkDir(fullPath, depth + 1);
1111
- }
1112
- } else if (entry.isFile()) {
1113
- const ext = path.extname(entry.name).toLowerCase();
1114
- if (extensions.has(ext)) {
1115
- try {
1116
- const content = await fs.promises.readFile(fullPath, 'utf-8');
1117
- filesTouched.push(relativePath);
1118
-
1119
- for (const [name, { pattern, severity, description }] of Object.entries(SECURITY_PATTERNS)) {
1120
- if (pattern.test(content)) {
1121
- const match = content.match(pattern);
1122
- const beforeMatch = content.slice(0, match.index);
1123
- const line = (beforeMatch.match(/\n/g) || []).length + 1;
1124
-
1125
- findings.push({
1126
- file: relativePath,
1127
- line,
1128
- type: name,
1129
- severity,
1130
- description,
1131
- evidence: match[0].slice(0, 50),
1132
- });
1133
- }
1134
- }
1135
- } catch (err) {
1136
- // Skip unreadable files
1137
- }
1138
- }
1139
- }
1140
- }
1141
- } catch (err) {
1142
- // Skip inaccessible directories
1143
- }
1144
- }
1145
-
1146
- await walkDir(projectPath);
1147
-
1148
- // Determine verdict
1149
- const criticalCount = findings.filter(f => f.severity === 'CRITICAL').length;
1150
- const highCount = findings.filter(f => f.severity === 'HIGH').length;
1151
-
1152
- let action = 'PROCEED';
1153
- let riskLevel = 'LOW';
1154
- let confidence = 0.9;
1155
-
1156
- if (criticalCount > 0) {
1157
- action = 'STOP';
1158
- riskLevel = 'CRITICAL';
1159
- confidence = 1.0;
1160
- } else if (highCount > 0) {
1161
- action = 'DEFER';
1162
- riskLevel = 'HIGH';
1163
- confidence = 0.8;
1164
- }
1165
-
1166
- return {
1167
- authority: authority.id,
1168
- version: authority.version,
1169
- timestamp: new Date().toISOString(),
1170
- action,
1171
- riskLevel,
1172
- exitCode: action === 'PROCEED' ? 0 : action === 'DEFER' ? 1 : 2,
1173
- filesTouched,
1174
- proofs: {
1175
- reachability: 'Static analysis - pattern matching',
1176
- compatibility: 'N/A - read-only scan',
1177
- rollback: 'N/A - no changes made',
1178
- },
1179
- behaviorChange: false,
1180
- confidence,
1181
- hardStopsTriggered: criticalCount > 0 ? [`${criticalCount} CRITICAL vulnerabilities`] : [],
1182
- notes: criticalCount > 0
1183
- ? `BLOCKED: ${criticalCount} critical vulnerabilities found. Fix before deployment.`
1184
- : highCount > 0
1185
- ? `REVIEW: ${highCount} high severity issues found. Manual review recommended.`
1186
- : `PASSED: No critical security issues detected.`,
1187
- analysis: {
1188
- findings,
1189
- summary: {
1190
- critical: criticalCount,
1191
- high: highCount,
1192
- total: findings.length,
1193
- filesScanned: filesTouched.length,
1194
- },
1195
- },
1196
- analysisTimeMs: Date.now() - startTime,
1197
- };
1198
- }
1199
-
1200
318
  module.exports = {
1201
- runApprove: withErrorHandling(runApprove, "Approve failed"),
319
+ runApprove,
1202
320
  };