@vibecheckai/cli 3.0.9 → 3.0.10

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.
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * vibecheck prove - One Command Reality Proof
3
3
  *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * ENTERPRISE EDITION - World-Class Terminal Experience
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ *
4
8
  * Orchestrates the complete "make it real" loop:
5
9
  * 1. ctx - Refresh truthpack
6
10
  * 2. reality --verify-auth - Runtime proof
@@ -33,7 +37,677 @@ try {
33
37
  runFixCore = null;
34
38
  }
35
39
 
36
- const { c, sym, Spinner, printHeader, printSection, printDivider, verdictBadge, formatDuration, box } = require("./lib/ui");
40
+ // ═══════════════════════════════════════════════════════════════════════════════
41
+ // ADVANCED TERMINAL - ANSI CODES & UTILITIES
42
+ // ═══════════════════════════════════════════════════════════════════════════════
43
+
44
+ const c = {
45
+ reset: '\x1b[0m',
46
+ bold: '\x1b[1m',
47
+ dim: '\x1b[2m',
48
+ italic: '\x1b[3m',
49
+ underline: '\x1b[4m',
50
+ blink: '\x1b[5m',
51
+ inverse: '\x1b[7m',
52
+ hidden: '\x1b[8m',
53
+ strike: '\x1b[9m',
54
+ // Colors
55
+ black: '\x1b[30m',
56
+ red: '\x1b[31m',
57
+ green: '\x1b[32m',
58
+ yellow: '\x1b[33m',
59
+ blue: '\x1b[34m',
60
+ magenta: '\x1b[35m',
61
+ cyan: '\x1b[36m',
62
+ white: '\x1b[37m',
63
+ // Bright colors
64
+ gray: '\x1b[90m',
65
+ brightRed: '\x1b[91m',
66
+ brightGreen: '\x1b[92m',
67
+ brightYellow: '\x1b[93m',
68
+ brightBlue: '\x1b[94m',
69
+ brightMagenta: '\x1b[95m',
70
+ brightCyan: '\x1b[96m',
71
+ brightWhite: '\x1b[97m',
72
+ // Background
73
+ bgBlack: '\x1b[40m',
74
+ bgRed: '\x1b[41m',
75
+ bgGreen: '\x1b[42m',
76
+ bgYellow: '\x1b[43m',
77
+ bgBlue: '\x1b[44m',
78
+ bgMagenta: '\x1b[45m',
79
+ bgCyan: '\x1b[46m',
80
+ bgWhite: '\x1b[47m',
81
+ bgBrightBlack: '\x1b[100m',
82
+ // Cursor control
83
+ cursorUp: (n = 1) => `\x1b[${n}A`,
84
+ cursorDown: (n = 1) => `\x1b[${n}B`,
85
+ cursorRight: (n = 1) => `\x1b[${n}C`,
86
+ cursorLeft: (n = 1) => `\x1b[${n}D`,
87
+ clearLine: '\x1b[2K',
88
+ clearScreen: '\x1b[2J',
89
+ saveCursor: '\x1b[s',
90
+ restoreCursor: '\x1b[u',
91
+ hideCursor: '\x1b[?25l',
92
+ showCursor: '\x1b[?25h',
93
+ };
94
+
95
+ // True color support
96
+ const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
97
+ const bgRgb = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
98
+
99
+ // Premium color palette
100
+ const colors = {
101
+ // Gradient for banner (purple/magenta theme for "prove")
102
+ gradient1: rgb(180, 100, 255), // Light purple
103
+ gradient2: rgb(150, 80, 255), // Purple
104
+ gradient3: rgb(120, 60, 255), // Deep purple
105
+ gradient4: rgb(100, 50, 220), // Blue-purple
106
+ gradient5: rgb(80, 40, 200), // Dark purple
107
+ gradient6: rgb(60, 30, 180), // Deepest
108
+
109
+ // Accent colors
110
+ science: rgb(180, 100, 255), // Scientific purple
111
+ reality: rgb(255, 150, 100), // Reality orange
112
+ truth: rgb(100, 200, 255), // Truth blue
113
+ fix: rgb(100, 255, 200), // Fix green
114
+
115
+ // Verdict colors
116
+ shipGreen: rgb(0, 255, 150),
117
+ warnAmber: rgb(255, 200, 0),
118
+ blockRed: rgb(255, 80, 80),
119
+
120
+ // UI colors
121
+ accent: rgb(180, 130, 255),
122
+ muted: rgb(120, 100, 140),
123
+ subtle: rgb(80, 70, 100),
124
+ highlight: rgb(255, 255, 255),
125
+
126
+ // Step colors
127
+ step1: rgb(100, 200, 255), // Context - blue
128
+ step2: rgb(255, 150, 100), // Reality - orange
129
+ step3: rgb(0, 255, 150), // Ship - green
130
+ step4: rgb(255, 200, 100), // Fix - yellow
131
+ step5: rgb(180, 100, 255), // Verify - purple
132
+ };
133
+
134
+ // ═══════════════════════════════════════════════════════════════════════════════
135
+ // PREMIUM BANNER
136
+ // ═══════════════════════════════════════════════════════════════════════════════
137
+
138
+ const PROVE_BANNER = `
139
+ ${rgb(200, 120, 255)} ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗${c.reset}
140
+ ${rgb(180, 100, 255)} ██╔══██╗██╔══██╗██╔═══██╗██║ ██║██╔════╝${c.reset}
141
+ ${rgb(160, 80, 255)} ██████╔╝██████╔╝██║ ██║██║ ██║█████╗ ${c.reset}
142
+ ${rgb(140, 60, 255)} ██╔═══╝ ██╔══██╗██║ ██║╚██╗ ██╔╝██╔══╝ ${c.reset}
143
+ ${rgb(120, 40, 255)} ██║ ██║ ██║╚██████╔╝ ╚████╔╝ ███████╗${c.reset}
144
+ ${rgb(100, 20, 255)} ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝${c.reset}
145
+ `;
146
+
147
+ const BANNER_FULL = `
148
+ ${rgb(200, 120, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${c.reset}
149
+ ${rgb(180, 100, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${c.reset}
150
+ ${rgb(160, 80, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${c.reset}
151
+ ${rgb(140, 60, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${c.reset}
152
+ ${rgb(120, 40, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${c.reset}
153
+ ${rgb(100, 20, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${c.reset}
154
+
155
+ ${c.dim} ┌─────────────────────────────────────────────────────────────────────┐${c.reset}
156
+ ${c.dim} │${c.reset} ${rgb(180, 100, 255)}🔬${c.reset} ${c.bold}PROVE${c.reset} ${c.dim}•${c.reset} ${rgb(200, 200, 200)}One Command${c.reset} ${c.dim}•${c.reset} ${rgb(150, 150, 150)}Reality Proof${c.reset} ${c.dim}•${c.reset} ${rgb(100, 100, 100)}Make It Real${c.reset} ${c.dim}│${c.reset}
157
+ ${c.dim} └─────────────────────────────────────────────────────────────────────┘${c.reset}
158
+ `;
159
+
160
+ // ═══════════════════════════════════════════════════════════════════════════════
161
+ // ICONS & SYMBOLS
162
+ // ═══════════════════════════════════════════════════════════════════════════════
163
+
164
+ const ICONS = {
165
+ // Steps
166
+ context: '📋',
167
+ reality: '🎭',
168
+ ship: '🚀',
169
+ fix: '🔧',
170
+ verify: '✅',
171
+ prove: '🔬',
172
+
173
+ // Status
174
+ check: '✓',
175
+ cross: '✗',
176
+ warning: '⚠',
177
+ info: 'ℹ',
178
+ arrow: '→',
179
+ bullet: '•',
180
+
181
+ // Actions
182
+ running: '▶',
183
+ complete: '●',
184
+ pending: '○',
185
+ skip: '◌',
186
+
187
+ // Objects
188
+ route: '🛤️',
189
+ env: '🌍',
190
+ auth: '🔒',
191
+ clock: '⏱',
192
+ target: '🎯',
193
+ lightning: '⚡',
194
+ loop: '🔄',
195
+ doc: '📄',
196
+ folder: '📁',
197
+ graph: '📊',
198
+
199
+ // Misc
200
+ sparkle: '✨',
201
+ fire: '🔥',
202
+ star: '★',
203
+ microscope: '🔬',
204
+ beaker: '🧪',
205
+ dna: '🧬',
206
+ };
207
+
208
+ // ═══════════════════════════════════════════════════════════════════════════════
209
+ // BOX DRAWING
210
+ // ═══════════════════════════════════════════════════════════════════════════════
211
+
212
+ const BOX = {
213
+ topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯',
214
+ horizontal: '─', vertical: '│',
215
+ teeRight: '├', teeLeft: '┤', teeDown: '┬', teeUp: '┴',
216
+ cross: '┼',
217
+ // Double line
218
+ dTopLeft: '╔', dTopRight: '╗', dBottomLeft: '╚', dBottomRight: '╝',
219
+ dHorizontal: '═', dVertical: '║',
220
+ // Heavy
221
+ hTopLeft: '┏', hTopRight: '┓', hBottomLeft: '┗', hBottomRight: '┛',
222
+ hHorizontal: '━', hVertical: '┃',
223
+ };
224
+
225
+ // ═══════════════════════════════════════════════════════════════════════════════
226
+ // SPINNER & PROGRESS
227
+ // ═══════════════════════════════════════════════════════════════════════════════
228
+
229
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
230
+ const SPINNER_DOTS = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
231
+ const SPINNER_DNA = ['🧬', '🔬', '🧪', '⚗️', '🧫', '🔭'];
232
+
233
+ let spinnerIndex = 0;
234
+ let spinnerInterval = null;
235
+ let spinnerStartTime = null;
236
+
237
+ function formatDuration(ms) {
238
+ if (ms < 1000) return `${ms}ms`;
239
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
240
+ const mins = Math.floor(ms / 60000);
241
+ const secs = Math.floor((ms % 60000) / 1000);
242
+ return `${mins}m ${secs}s`;
243
+ }
244
+
245
+ function formatNumber(num) {
246
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
247
+ }
248
+
249
+ function truncate(str, len) {
250
+ if (!str) return '';
251
+ if (str.length <= len) return str;
252
+ return str.slice(0, len - 3) + '...';
253
+ }
254
+
255
+ function padCenter(str, width) {
256
+ const padding = Math.max(0, width - str.length);
257
+ const left = Math.floor(padding / 2);
258
+ const right = padding - left;
259
+ return ' '.repeat(left) + str + ' '.repeat(right);
260
+ }
261
+
262
+ function progressBar(percent, width = 30, opts = {}) {
263
+ const filled = Math.round((percent / 100) * width);
264
+ const empty = width - filled;
265
+
266
+ let filledColor;
267
+ if (opts.color) {
268
+ filledColor = opts.color;
269
+ } else if (percent >= 80) {
270
+ filledColor = colors.shipGreen;
271
+ } else if (percent >= 50) {
272
+ filledColor = colors.warnAmber;
273
+ } else {
274
+ filledColor = colors.blockRed;
275
+ }
276
+
277
+ const filledChar = opts.filled || '█';
278
+ const emptyChar = opts.empty || '░';
279
+
280
+ return `${filledColor}${filledChar.repeat(filled)}${c.dim}${emptyChar.repeat(empty)}${c.reset}`;
281
+ }
282
+
283
+ function startSpinner(message, color = colors.science) {
284
+ spinnerStartTime = Date.now();
285
+ process.stdout.write(c.hideCursor);
286
+
287
+ spinnerInterval = setInterval(() => {
288
+ const elapsed = formatDuration(Date.now() - spinnerStartTime);
289
+ process.stdout.write(`\r${c.clearLine} ${color}${SPINNER_DOTS[spinnerIndex]}${c.reset} ${message} ${c.dim}${elapsed}${c.reset}`);
290
+ spinnerIndex = (spinnerIndex + 1) % SPINNER_DOTS.length;
291
+ }, 80);
292
+ }
293
+
294
+ function stopSpinner(message, success = true) {
295
+ if (spinnerInterval) {
296
+ clearInterval(spinnerInterval);
297
+ spinnerInterval = null;
298
+ }
299
+ const elapsed = spinnerStartTime ? formatDuration(Date.now() - spinnerStartTime) : '';
300
+ const icon = success ? `${colors.shipGreen}${ICONS.check}${c.reset}` : `${colors.blockRed}${ICONS.cross}${c.reset}`;
301
+ process.stdout.write(`\r${c.clearLine} ${icon} ${message} ${c.dim}${elapsed}${c.reset}\n`);
302
+ process.stdout.write(c.showCursor);
303
+ spinnerStartTime = null;
304
+ }
305
+
306
+ function updateSpinner(message) {
307
+ if (spinnerInterval) {
308
+ // Just update the message, spinner keeps running
309
+ }
310
+ }
311
+
312
+ // ═══════════════════════════════════════════════════════════════════════════════
313
+ // SECTION HEADERS
314
+ // ═══════════════════════════════════════════════════════════════════════════════
315
+
316
+ function printBanner() {
317
+ console.log(BANNER_FULL);
318
+ }
319
+
320
+ function printCompactBanner() {
321
+ console.log(PROVE_BANNER);
322
+ }
323
+
324
+ function printDivider(char = '─', width = 69, color = c.dim) {
325
+ console.log(`${color} ${char.repeat(width)}${c.reset}`);
326
+ }
327
+
328
+ function printSection(title, icon = '◆') {
329
+ console.log();
330
+ console.log(` ${colors.accent}${icon}${c.reset} ${c.bold}${title}${c.reset}`);
331
+ printDivider();
332
+ }
333
+
334
+ // ═══════════════════════════════════════════════════════════════════════════════
335
+ // STEP DISPLAY - THE PROVE PIPELINE VISUALIZATION
336
+ // ═══════════════════════════════════════════════════════════════════════════════
337
+
338
+ function getStepConfig(stepNum) {
339
+ const configs = {
340
+ 1: { icon: ICONS.context, name: 'CONTEXT', color: colors.step1, desc: 'Refresh truthpack' },
341
+ 2: { icon: ICONS.reality, name: 'REALITY', color: colors.step2, desc: 'Runtime UI proof' },
342
+ 3: { icon: ICONS.ship, name: 'SHIP', color: colors.step3, desc: 'Static verdict' },
343
+ 4: { icon: ICONS.fix, name: 'FIX', color: colors.step4, desc: 'Auto-fix loop' },
344
+ 5: { icon: ICONS.verify, name: 'VERIFY', color: colors.step5, desc: 'Final verification' },
345
+ };
346
+ return configs[stepNum] || { icon: ICONS.bullet, name: `STEP ${stepNum}`, color: colors.accent, desc: '' };
347
+ }
348
+
349
+ function printStepHeader(stepNum, subtitle = null) {
350
+ const config = getStepConfig(stepNum);
351
+
352
+ console.log();
353
+ console.log(` ${config.color}${BOX.hTopLeft}${BOX.hHorizontal.repeat(3)}${c.reset} ${config.icon} ${c.bold}Step ${stepNum}: ${config.name}${c.reset}`);
354
+
355
+ if (subtitle) {
356
+ console.log(` ${config.color}${BOX.hVertical}${c.reset} ${c.dim}${subtitle}${c.reset}`);
357
+ } else {
358
+ console.log(` ${config.color}${BOX.hVertical}${c.reset} ${c.dim}${config.desc}${c.reset}`);
359
+ }
360
+
361
+ console.log(` ${config.color}${BOX.hBottomLeft}${BOX.hHorizontal.repeat(66)}${c.reset}`);
362
+ }
363
+
364
+ function printStepResult(stepNum, status, details = {}) {
365
+ const config = getStepConfig(stepNum);
366
+
367
+ let statusIcon, statusColor, statusText;
368
+
369
+ switch (status) {
370
+ case 'ok':
371
+ case 'SHIP':
372
+ case 'CLEAN':
373
+ statusIcon = ICONS.check;
374
+ statusColor = colors.shipGreen;
375
+ statusText = status === 'SHIP' ? 'SHIP' : status === 'CLEAN' ? 'CLEAN' : 'Complete';
376
+ break;
377
+ case 'WARN':
378
+ statusIcon = ICONS.warning;
379
+ statusColor = colors.warnAmber;
380
+ statusText = 'WARN';
381
+ break;
382
+ case 'BLOCK':
383
+ case 'error':
384
+ statusIcon = ICONS.cross;
385
+ statusColor = colors.blockRed;
386
+ statusText = status === 'BLOCK' ? 'BLOCK' : 'Failed';
387
+ break;
388
+ case 'skipped':
389
+ statusIcon = ICONS.skip;
390
+ statusColor = colors.muted;
391
+ statusText = 'Skipped';
392
+ break;
393
+ default:
394
+ statusIcon = ICONS.info;
395
+ statusColor = colors.accent;
396
+ statusText = status;
397
+ }
398
+
399
+ let resultLine = ` ${statusColor}${statusIcon}${c.reset} ${c.bold}${statusText}${c.reset}`;
400
+
401
+ // Add details
402
+ const detailParts = [];
403
+ if (details.routes !== undefined) detailParts.push(`${details.routes} routes`);
404
+ if (details.envVars !== undefined) detailParts.push(`${details.envVars} env vars`);
405
+ if (details.findings !== undefined) {
406
+ if (typeof details.findings === 'object') {
407
+ detailParts.push(`${details.findings.BLOCK || 0} blockers, ${details.findings.WARN || 0} warnings`);
408
+ } else {
409
+ detailParts.push(`${details.findings} findings`);
410
+ }
411
+ }
412
+ if (details.coverage !== undefined && details.coverage !== null) {
413
+ detailParts.push(`${details.coverage}% coverage`);
414
+ }
415
+ if (details.pagesVisited !== undefined) detailParts.push(`${details.pagesVisited} pages`);
416
+ if (details.reason) detailParts.push(details.reason);
417
+ if (details.error) detailParts.push(`error: ${truncate(details.error, 40)}`);
418
+
419
+ if (detailParts.length > 0) {
420
+ resultLine += ` ${c.dim}(${detailParts.join(', ')})${c.reset}`;
421
+ }
422
+
423
+ console.log(resultLine);
424
+ }
425
+
426
+ // ═══════════════════════════════════════════════════════════════════════════════
427
+ // PIPELINE PROGRESS VISUALIZATION
428
+ // ═══════════════════════════════════════════════════════════════════════════════
429
+
430
+ function printPipelineOverview(steps = []) {
431
+ console.log();
432
+ console.log(` ${c.dim}Pipeline:${c.reset}`);
433
+
434
+ let pipeline = ' ';
435
+ const stepConfigs = [1, 2, 3, 4, 5].map(n => getStepConfig(n));
436
+
437
+ for (let i = 0; i < stepConfigs.length; i++) {
438
+ const step = stepConfigs[i];
439
+ const stepStatus = steps[i];
440
+
441
+ let color = colors.muted;
442
+ let icon = ICONS.pending;
443
+
444
+ if (stepStatus === 'complete') {
445
+ color = colors.shipGreen;
446
+ icon = ICONS.complete;
447
+ } else if (stepStatus === 'running') {
448
+ color = step.color;
449
+ icon = ICONS.running;
450
+ } else if (stepStatus === 'error') {
451
+ color = colors.blockRed;
452
+ icon = ICONS.cross;
453
+ } else if (stepStatus === 'skipped') {
454
+ color = colors.muted;
455
+ icon = ICONS.skip;
456
+ }
457
+
458
+ pipeline += `${color}${icon}${c.reset}`;
459
+
460
+ if (i < stepConfigs.length - 1) {
461
+ const connectorColor = stepStatus === 'complete' ? colors.shipGreen : colors.muted;
462
+ pipeline += `${connectorColor}───${c.reset}`;
463
+ }
464
+ }
465
+
466
+ console.log(pipeline);
467
+
468
+ // Labels
469
+ let labels = ' ';
470
+ for (let i = 0; i < stepConfigs.length; i++) {
471
+ const step = stepConfigs[i];
472
+ labels += `${c.dim}${step.name.slice(0, 3)}${c.reset}`;
473
+ if (i < stepConfigs.length - 1) {
474
+ labels += ' ';
475
+ }
476
+ }
477
+ console.log(labels);
478
+ }
479
+
480
+ // ═══════════════════════════════════════════════════════════════════════════════
481
+ // FIX LOOP VISUALIZATION
482
+ // ═══════════════════════════════════════════════════════════════════════════════
483
+
484
+ function printFixLoopHeader(round, maxRounds) {
485
+ const progress = Math.round((round / maxRounds) * 100);
486
+
487
+ console.log();
488
+ console.log(` ${colors.step4}${BOX.topLeft}${BOX.horizontal.repeat(3)}${c.reset} ${ICONS.loop} ${c.bold}Fix Round ${round}/${maxRounds}${c.reset}`);
489
+ console.log(` ${colors.step4}${BOX.vertical}${c.reset} ${progressBar(progress, 20, { color: colors.step4 })} ${c.dim}${progress}%${c.reset}`);
490
+ console.log(` ${colors.step4}${BOX.bottomLeft}${BOX.horizontal.repeat(50)}${c.reset}`);
491
+ }
492
+
493
+ function printMissionStatus(mission, status, detail = null) {
494
+ let icon, color;
495
+
496
+ switch (status) {
497
+ case 'planning':
498
+ icon = ICONS.target;
499
+ color = colors.accent;
500
+ break;
501
+ case 'applying':
502
+ icon = ICONS.lightning;
503
+ color = colors.step4;
504
+ break;
505
+ case 'success':
506
+ icon = ICONS.check;
507
+ color = colors.shipGreen;
508
+ break;
509
+ case 'failed':
510
+ icon = ICONS.cross;
511
+ color = colors.blockRed;
512
+ break;
513
+ case 'skipped':
514
+ icon = ICONS.skip;
515
+ color = colors.muted;
516
+ break;
517
+ default:
518
+ icon = ICONS.bullet;
519
+ color = colors.muted;
520
+ }
521
+
522
+ let line = ` ${color}${icon}${c.reset} ${truncate(mission.title || mission, 50)}`;
523
+ if (detail) {
524
+ line += ` ${c.dim}${detail}${c.reset}`;
525
+ }
526
+ console.log(line);
527
+ }
528
+
529
+ // ═══════════════════════════════════════════════════════════════════════════════
530
+ // FINAL VERDICT DISPLAY
531
+ // ═══════════════════════════════════════════════════════════════════════════════
532
+
533
+ function getVerdictConfig(verdict) {
534
+ if (verdict === 'SHIP') {
535
+ return {
536
+ icon: '🚀',
537
+ headline: 'PROVED REAL',
538
+ tagline: 'Your app is verified and ready to ship!',
539
+ color: colors.shipGreen,
540
+ bgColor: bgRgb(0, 80, 50),
541
+ borderColor: rgb(0, 200, 120),
542
+ };
543
+ }
544
+
545
+ if (verdict === 'WARN') {
546
+ return {
547
+ icon: '⚠️',
548
+ headline: 'PARTIALLY PROVED',
549
+ tagline: 'Some warnings remain - review before shipping',
550
+ color: colors.warnAmber,
551
+ bgColor: bgRgb(80, 60, 0),
552
+ borderColor: rgb(200, 160, 0),
553
+ };
554
+ }
555
+
556
+ return {
557
+ icon: '🛑',
558
+ headline: 'NOT PROVED',
559
+ tagline: 'Blockers remain - cannot verify readiness',
560
+ color: colors.blockRed,
561
+ bgColor: bgRgb(80, 20, 20),
562
+ borderColor: rgb(200, 60, 60),
563
+ };
564
+ }
565
+
566
+ function printFinalVerdict(verdict, duration, fixRounds, findings) {
567
+ const config = getVerdictConfig(verdict);
568
+ const w = 68;
569
+
570
+ console.log();
571
+ console.log();
572
+
573
+ // Top border
574
+ console.log(` ${config.borderColor}${BOX.dTopLeft}${BOX.dHorizontal.repeat(w)}${BOX.dTopRight}${c.reset}`);
575
+
576
+ // Empty line
577
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
578
+
579
+ // Icon and headline
580
+ const headlineText = `${config.icon} ${config.headline}`;
581
+ const headlinePadded = padCenter(headlineText, w);
582
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${config.color}${c.bold}${headlinePadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
583
+
584
+ // Tagline
585
+ const taglinePadded = padCenter(config.tagline, w);
586
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${c.dim}${taglinePadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
587
+
588
+ // Empty line
589
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
590
+
591
+ // Stats row
592
+ const stats = `Duration: ${c.bold}${duration}${c.reset} ${c.dim}•${c.reset} Fix Rounds: ${c.bold}${fixRounds}${c.reset} ${c.dim}•${c.reset} Findings: ${c.bold}${findings}${c.reset}`;
593
+ const statsPadded = padCenter(stats.replace(/\x1b\[[0-9;]*m/g, '').length < w ? stats : stats, w);
594
+ // We need to calculate visible length for padding
595
+ const visibleStats = `Duration: ${duration} • Fix Rounds: ${fixRounds} • Findings: ${findings}`;
596
+ const leftPad = Math.floor((w - visibleStats.length) / 2);
597
+ const rightPad = w - visibleStats.length - leftPad;
598
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(leftPad)}Duration: ${c.bold}${duration}${c.reset} ${c.dim}•${c.reset} Fix Rounds: ${c.bold}${fixRounds}${c.reset} ${c.dim}•${c.reset} Findings: ${c.bold}${findings}${c.reset}${' '.repeat(rightPad)}${config.borderColor}${BOX.dVertical}${c.reset}`);
599
+
600
+ // Empty line
601
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
602
+
603
+ // Bottom border
604
+ console.log(` ${config.borderColor}${BOX.dBottomLeft}${BOX.dHorizontal.repeat(w)}${BOX.dBottomRight}${c.reset}`);
605
+
606
+ console.log();
607
+ }
608
+
609
+ // ═══════════════════════════════════════════════════════════════════════════════
610
+ // TIMELINE SUMMARY
611
+ // ═══════════════════════════════════════════════════════════════════════════════
612
+
613
+ function printTimelineSummary(timeline) {
614
+ printSection('PROOF TIMELINE', ICONS.graph);
615
+ console.log();
616
+
617
+ for (const entry of timeline) {
618
+ const stepNum = typeof entry.step === 'number' ? entry.step : parseInt(entry.step) || 0;
619
+ const config = getStepConfig(stepNum);
620
+
621
+ let statusIcon, statusColor;
622
+
623
+ if (entry.status === 'ok' || entry.status === 'SHIP' || entry.status === 'CLEAN') {
624
+ statusIcon = ICONS.check;
625
+ statusColor = colors.shipGreen;
626
+ } else if (entry.status === 'WARN') {
627
+ statusIcon = ICONS.warning;
628
+ statusColor = colors.warnAmber;
629
+ } else if (entry.status === 'BLOCK' || entry.status === 'error') {
630
+ statusIcon = ICONS.cross;
631
+ statusColor = colors.blockRed;
632
+ } else if (entry.status === 'skipped') {
633
+ statusIcon = ICONS.skip;
634
+ statusColor = colors.muted;
635
+ } else {
636
+ statusIcon = ICONS.bullet;
637
+ statusColor = colors.accent;
638
+ }
639
+
640
+ let line = ` ${statusColor}${statusIcon}${c.reset} ${c.bold}${entry.action.toUpperCase().padEnd(10)}${c.reset}`;
641
+
642
+ // Add relevant details
643
+ const details = [];
644
+ if (entry.routes !== undefined) details.push(`${entry.routes} routes`);
645
+ if (entry.verdict) details.push(entry.verdict);
646
+ if (entry.findingsCount !== undefined) details.push(`${entry.findingsCount} findings`);
647
+ if (entry.coverage !== undefined && entry.coverage !== null) details.push(`${entry.coverage}% coverage`);
648
+ if (entry.reason) details.push(entry.reason);
649
+
650
+ if (details.length > 0) {
651
+ line += ` ${c.dim}${details.join(' · ')}${c.reset}`;
652
+ }
653
+
654
+ console.log(line);
655
+ }
656
+ }
657
+
658
+ // ═══════════════════════════════════════════════════════════════════════════════
659
+ // HELP DISPLAY
660
+ // ═══════════════════════════════════════════════════════════════════════════════
661
+
662
+ function printHelp() {
663
+ console.log(BANNER_FULL);
664
+ console.log(`
665
+ ${c.bold}Usage:${c.reset} vibecheck prove [options]
666
+
667
+ ${c.bold}One Command Reality Proof${c.reset} — Make it real or keep fixing until it is.
668
+
669
+ ${c.bold}Pipeline:${c.reset}
670
+ ${colors.step1}1. CTX${c.reset} Refresh truthpack (ground truth)
671
+ ${colors.step2}2. REALITY${c.reset} Runtime UI proof ${c.dim}(if --url provided)${c.reset}
672
+ ${colors.step3}3. SHIP${c.reset} Static + runtime verdict
673
+ ${colors.step4}4. FIX${c.reset} Auto-fix if BLOCK ${c.dim}(up to N rounds)${c.reset}
674
+ ${colors.step5}5. VERIFY${c.reset} Re-run to confirm SHIP
675
+
676
+ ${c.bold}Options:${c.reset}
677
+ ${colors.accent}--url, -u <url>${c.reset} Base URL for runtime testing
678
+ ${colors.accent}--auth <email:pass>${c.reset} Login credentials for auth verification
679
+ ${colors.accent}--storage-state <path>${c.reset} Playwright session state file
680
+ ${colors.accent}--fastify-entry <path>${c.reset} Fastify entry file for route extraction
681
+ ${colors.accent}--max-fix-rounds <n>${c.reset} Max auto-fix attempts ${c.dim}(default: 3)${c.reset}
682
+ ${colors.accent}--skip-reality${c.reset} Skip runtime crawling (static only)
683
+ ${colors.accent}--skip-fix${c.reset} Don't auto-fix, just diagnose
684
+ ${colors.accent}--headed${c.reset} Run browser in headed mode
685
+ ${colors.accent}--danger${c.reset} Allow clicking destructive elements
686
+ ${colors.accent}--help, -h${c.reset} Show this help
687
+
688
+ ${c.bold}Exit Codes:${c.reset}
689
+ ${colors.shipGreen}0${c.reset} SHIP — Proved real, ready to deploy
690
+ ${colors.warnAmber}1${c.reset} WARN — Warnings found, review recommended
691
+ ${colors.blockRed}2${c.reset} BLOCK — Blockers found, not proved
692
+
693
+ ${c.bold}Examples:${c.reset}
694
+ ${c.dim}# Full proof with runtime testing${c.reset}
695
+ vibecheck prove --url http://localhost:3000
696
+
697
+ ${c.dim}# With authentication${c.reset}
698
+ vibecheck prove --url http://localhost:3000 --auth user@test.com:pass
699
+
700
+ ${c.dim}# Static only (no runtime)${c.reset}
701
+ vibecheck prove --skip-reality
702
+
703
+ ${c.dim}# More fix attempts${c.reset}
704
+ vibecheck prove --url http://localhost:3000 --max-fix-rounds 5
705
+ `);
706
+ }
707
+
708
+ // ═══════════════════════════════════════════════════════════════════════════════
709
+ // UTILITIES
710
+ // ═══════════════════════════════════════════════════════════════════════════════
37
711
 
38
712
  function ensureDir(p) {
39
713
  fs.mkdirSync(p, { recursive: true });
@@ -45,44 +719,9 @@ function stamp() {
45
719
  return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}_${z(d.getHours())}${z(d.getMinutes())}${z(d.getSeconds())}`;
46
720
  }
47
721
 
48
- function printHelp() {
49
- console.log(`
50
- ${c.cyan}${c.bold}vibecheck prove${c.reset} - One Command Reality Proof
51
-
52
- ${c.bold}USAGE${c.reset}
53
- vibecheck prove [options]
54
-
55
- ${c.bold}OPTIONS${c.reset}
56
- --url, -u <url> Base URL for runtime testing
57
- --auth <email:pass> Login credentials for auth verification
58
- --storage-state <path> Playwright session state file
59
- --fastify-entry <path> Fastify entry file for route extraction
60
- --max-fix-rounds <n> Max auto-fix attempts (default: 3)
61
- --skip-reality Skip runtime crawling (static only)
62
- --skip-fix Don't auto-fix, just diagnose
63
- --headed Run browser in headed mode
64
- --danger Allow clicking destructive elements
65
- --help, -h Show this help
66
-
67
- ${c.bold}WHAT IT DOES${c.reset}
68
- 1. ${c.cyan}ctx${c.reset} - Refresh truthpack (ground truth)
69
- 2. ${c.cyan}reality${c.reset} - Runtime UI proof (if --url provided)
70
- 3. ${c.cyan}ship${c.reset} - Static + runtime verdict
71
- 4. ${c.cyan}fix${c.reset} - Auto-fix if BLOCK (up to N rounds)
72
- 5. ${c.cyan}verify${c.reset} - Re-run to confirm SHIP
73
-
74
- ${c.bold}EXIT CODES${c.reset}
75
- 0 = SHIP (ready to deploy)
76
- 1 = WARN (warnings found)
77
- 2 = BLOCK (blockers found)
78
-
79
- ${c.bold}EXAMPLES${c.reset}
80
- vibecheck prove --url http://localhost:3000
81
- vibecheck prove --url http://localhost:3000 --auth user@test.com:pass
82
- vibecheck prove --skip-reality
83
- vibecheck prove --url http://localhost:3000 --max-fix-rounds 5
84
- `);
85
- }
722
+ // ═══════════════════════════════════════════════════════════════════════════════
723
+ // MAIN PROVE FUNCTION
724
+ // ═══════════════════════════════════════════════════════════════════════════════
86
725
 
87
726
  async function runProve(argsOrOpts = {}) {
88
727
  // Handle array args from CLI
@@ -130,14 +769,22 @@ async function runProve(argsOrOpts = {}) {
130
769
  } = argsOrOpts;
131
770
 
132
771
  const root = repoRoot || process.cwd();
772
+ const projectName = path.basename(root);
133
773
  const startTime = Date.now();
134
774
 
135
- console.log(`
136
- ${c.cyan}${c.bold}╔════════════════════════════════════════════════════════════╗
137
- 🔬 vibecheck prove ║
138
- One command to make it real ║
139
- ╚════════════════════════════════════════════════════════════╝${c.reset}
140
- `);
775
+ // Print banner
776
+ printBanner();
777
+
778
+ console.log(` ${c.dim}Project:${c.reset} ${c.bold}${projectName}${c.reset}`);
779
+ console.log(` ${c.dim}Path:${c.reset} ${root}`);
780
+ if (url) {
781
+ console.log(` ${c.dim}URL:${c.reset} ${colors.accent}${url}${c.reset}`);
782
+ }
783
+ console.log(` ${c.dim}Fix Rounds:${c.reset} ${maxFixRounds} max`);
784
+
785
+ // Show pipeline overview
786
+ const pipelineStatus = ['pending', 'pending', 'pending', 'pending', 'pending'];
787
+ printPipelineOverview(pipelineStatus);
141
788
 
142
789
  const outDir = path.join(root, ".vibecheck", "prove", stamp());
143
790
  ensureDir(outDir);
@@ -148,13 +795,18 @@ ${c.cyan}${c.bold}╔═══════════════════
148
795
  // ═══════════════════════════════════════════════════════════════════════════
149
796
  // STEP 1: CTX - Refresh truthpack
150
797
  // ═══════════════════════════════════════════════════════════════════════════
151
- console.log(`${c.cyan}▶ Step 1: Refreshing context (truthpack)...${c.reset}`);
798
+ printStepHeader(1);
799
+ pipelineStatus[0] = 'running';
800
+
801
+ startSpinner('Refreshing truthpack...', colors.step1);
152
802
 
153
803
  try {
154
804
  const fastEntry = fastifyEntry || (await detectFastifyEntry(root));
155
805
  const truthpack = await buildTruthpack({ repoRoot: root, fastifyEntry: fastEntry });
156
806
  writeTruthpack(root, truthpack);
157
807
 
808
+ stopSpinner('Truthpack refreshed', true);
809
+
158
810
  timeline.push({
159
811
  step: 1,
160
812
  action: "ctx",
@@ -163,7 +815,10 @@ ${c.cyan}${c.bold}╔═══════════════════
163
815
  envVars: truthpack.env?.vars?.length || 0
164
816
  });
165
817
 
166
- console.log(` ${c.green}✓${c.reset} Truthpack refreshed (${truthpack.routes?.server?.length || 0} routes, ${truthpack.env?.vars?.length || 0} env vars)`);
818
+ printStepResult(1, 'ok', {
819
+ routes: truthpack.routes?.server?.length || 0,
820
+ envVars: truthpack.env?.vars?.length || 0
821
+ });
167
822
 
168
823
  // Check for contract drift after truthpack refresh
169
824
  if (hasContracts(root)) {
@@ -172,7 +827,7 @@ ${c.cyan}${c.bold}╔═══════════════════
172
827
  const driftSummary = getDriftSummary(driftFindings);
173
828
 
174
829
  if (driftSummary.hasDrift) {
175
- console.log(` ${driftSummary.blocks > 0 ? c.yellow + '⚠️' : c.dim + 'ℹ️'} Contract drift: ${driftSummary.blocks} blocks, ${driftSummary.warns} warnings${c.reset}`);
830
+ console.log(` ${driftSummary.blocks > 0 ? colors.warnAmber + ICONS.warning : colors.muted + ICONS.info}${c.reset} Contract drift: ${driftSummary.blocks} blocks, ${driftSummary.warns} warnings`);
176
831
  timeline.push({
177
832
  step: "1.5",
178
833
  action: "drift_check",
@@ -182,20 +837,26 @@ ${c.cyan}${c.bold}╔═══════════════════
182
837
  });
183
838
 
184
839
  if (driftSummary.blocks > 0) {
185
- console.log(` ${c.dim}Run 'vibecheck ctx sync' to update contracts${c.reset}`);
840
+ console.log(` ${c.dim}Run 'vibecheck ctx sync' to update contracts${c.reset}`);
186
841
  }
187
842
  }
188
843
  }
844
+
845
+ pipelineStatus[0] = 'complete';
189
846
  } catch (err) {
847
+ stopSpinner(`Context refresh failed: ${err.message}`, false);
190
848
  timeline.push({ step: 1, action: "ctx", status: "error", error: err.message });
191
- console.log(` ${c.yellow}⚠️ Context refresh failed: ${err.message}${c.reset}`);
849
+ pipelineStatus[0] = 'error';
192
850
  }
193
851
 
194
852
  // ═══════════════════════════════════════════════════════════════════════════
195
853
  // STEP 2: REALITY - Runtime UI proof (if URL provided)
196
854
  // ═══════════════════════════════════════════════════════════════════════════
197
855
  if (url && !skipReality && runReality) {
198
- console.log(`\n${c.cyan}▶ Step 2: Running reality check (${url})...${c.reset}`);
856
+ printStepHeader(2, url);
857
+ pipelineStatus[1] = 'running';
858
+
859
+ startSpinner('Running reality check...', colors.step2);
199
860
 
200
861
  try {
201
862
  await runReality({
@@ -218,6 +879,8 @@ ${c.cyan}${c.bold}╔═══════════════════
218
879
  const blocks = (reality.findings || []).filter(f => f.severity === "BLOCK").length;
219
880
  const warns = (reality.findings || []).filter(f => f.severity === "WARN").length;
220
881
 
882
+ stopSpinner('Reality check complete', true);
883
+
221
884
  timeline.push({
222
885
  step: 2,
223
886
  action: "reality",
@@ -227,30 +890,39 @@ ${c.cyan}${c.bold}╔═══════════════════
227
890
  coverage: reality.coverage?.percent || null
228
891
  });
229
892
 
230
- console.log(` ${c.green}✓${c.reset} Reality check complete (${blocks} BLOCK, ${warns} WARN)`);
231
- if (reality.coverage) {
232
- console.log(` ${c.dim}Coverage: ${reality.coverage.percent}% of UI paths${c.reset}`);
233
- }
893
+ printStepResult(2, blocks ? 'BLOCK' : warns ? 'WARN' : 'CLEAN', {
894
+ findings: { BLOCK: blocks, WARN: warns },
895
+ pagesVisited: reality.passes?.anon?.pagesVisited?.length || 0,
896
+ coverage: reality.coverage?.percent
897
+ });
898
+
899
+ pipelineStatus[1] = blocks ? 'error' : 'complete';
234
900
  }
235
901
  } catch (err) {
902
+ stopSpinner(`Reality check failed: ${err.message}`, false);
236
903
  timeline.push({ step: 2, action: "reality", status: "error", error: err.message });
237
- console.log(` ${c.yellow}⚠️ Reality check failed: ${err.message}${c.reset}`);
904
+ pipelineStatus[1] = 'error';
238
905
  }
239
- } else if (!url) {
240
- console.log(`\n${c.dim}▶ Step 2: Skipping reality (no --url provided)${c.reset}`);
241
- timeline.push({ step: 2, action: "reality", status: "skipped", reason: "no URL" });
242
- } else if (skipReality) {
243
- console.log(`\n${c.dim}▶ Step 2: Skipping reality (--skip-reality)${c.reset}`);
244
- timeline.push({ step: 2, action: "reality", status: "skipped", reason: "--skip-reality" });
906
+ } else {
907
+ const reason = !url ? 'no --url provided' : '--skip-reality flag';
908
+ console.log();
909
+ console.log(` ${colors.muted}${ICONS.skip}${c.reset} ${c.dim}Step 2: REALITY skipped (${reason})${c.reset}`);
910
+ timeline.push({ step: 2, action: "reality", status: "skipped", reason });
911
+ pipelineStatus[1] = 'skipped';
245
912
  }
246
913
 
247
914
  // ═══════════════════════════════════════════════════════════════════════════
248
915
  // STEP 3: SHIP - Get initial verdict
249
916
  // ═══════════════════════════════════════════════════════════════════════════
250
- console.log(`\n${c.cyan}▶ Step 3: Running ship check...${c.reset}`);
917
+ printStepHeader(3);
918
+ pipelineStatus[2] = 'running';
919
+
920
+ startSpinner('Running ship check...', colors.step3);
251
921
 
252
922
  let shipResult = await shipCore({ repoRoot: root, fastifyEntry, noWrite: false });
253
923
 
924
+ stopSpinner('Ship check complete', true);
925
+
254
926
  timeline.push({
255
927
  step: 3,
256
928
  action: "ship",
@@ -258,8 +930,11 @@ ${c.cyan}${c.bold}╔═══════════════════
258
930
  findingsCount: shipResult.report?.findings?.length || 0
259
931
  });
260
932
 
261
- const verdictColor = shipResult.verdict === "SHIP" ? c.green : shipResult.verdict === "WARN" ? c.yellow : c.red;
262
- console.log(` ${verdictColor}${shipResult.verdict}${c.reset} (${shipResult.report?.findings?.length || 0} findings)`);
933
+ printStepResult(3, shipResult.verdict, {
934
+ findings: shipResult.report?.findings?.length || 0
935
+ });
936
+
937
+ pipelineStatus[2] = shipResult.verdict === 'SHIP' ? 'complete' : shipResult.verdict === 'WARN' ? 'complete' : 'error';
263
938
 
264
939
  // ═══════════════════════════════════════════════════════════════════════════
265
940
  // STEP 4: FIX LOOP - If BLOCK, attempt autopilot fix
@@ -268,7 +943,13 @@ ${c.cyan}${c.bold}╔═══════════════════
268
943
 
269
944
  while (shipResult.verdict === "BLOCK" && !skipFix && fixRound < maxFixRounds) {
270
945
  fixRound++;
271
- console.log(`\n${c.cyan}▶ Step 4.${fixRound}: Fix round ${fixRound}/${maxFixRounds}...${c.reset}`);
946
+
947
+ if (fixRound === 1) {
948
+ printStepHeader(4, `Up to ${maxFixRounds} rounds`);
949
+ pipelineStatus[3] = 'running';
950
+ }
951
+
952
+ printFixLoopHeader(fixRound, maxFixRounds);
272
953
 
273
954
  try {
274
955
  // Import fix dependencies lazily
@@ -285,16 +966,18 @@ ${c.cyan}${c.bold}╔═══════════════════
285
966
  const missions = planMissions(findings, { maxMissions, blocksOnlyFirst: true });
286
967
 
287
968
  if (!missions.length) {
288
- console.log(` ${c.yellow}No fixable missions found.${c.reset}`);
969
+ console.log(` ${colors.warnAmber}${ICONS.warning}${c.reset} No fixable missions found`);
289
970
  timeline.push({ step: `4.${fixRound}`, action: "fix", status: "no_missions" });
290
971
  break;
291
972
  }
292
973
 
293
- console.log(` Planning ${missions.length} missions...`);
974
+ console.log(` ${c.dim}Planning ${missions.length} missions...${c.reset}`);
294
975
 
295
976
  let fixedAny = false;
296
977
 
297
978
  for (const mission of missions.slice(0, 3)) {
979
+ printMissionStatus(mission, 'planning');
980
+
298
981
  const targetFindings = findings.filter(f => mission.targetFindingIds.includes(f.id));
299
982
  const template = templateForMissionType(mission.type);
300
983
 
@@ -318,13 +1001,13 @@ ${c.cyan}${c.bold}╔═══════════════════
318
1001
  allowedFiles: expanded.allowedFiles
319
1002
  });
320
1003
 
321
- console.log(` ${c.dim}Mission: ${mission.title}${c.reset}`);
1004
+ printMissionStatus(mission, 'applying');
322
1005
 
323
1006
  try {
324
1007
  const patchResponse = await generatePatchJson(prompt);
325
1008
 
326
1009
  if (!patchResponse || patchResponse.status !== "ok" || !patchResponse.edits?.length) {
327
- console.log(` ${c.yellow}⚠️ No patch generated${c.reset}`);
1010
+ printMissionStatus(mission, 'skipped', 'no patch generated');
328
1011
  continue;
329
1012
  }
330
1013
 
@@ -334,7 +1017,7 @@ ${c.cyan}${c.bold}╔═══════════════════
334
1017
  });
335
1018
 
336
1019
  if (!validation.valid) {
337
- console.log(` ${c.yellow}⚠️ Patch validation failed: ${validation.reason}${c.reset}`);
1020
+ printMissionStatus(mission, 'failed', `validation: ${validation.reason}`);
338
1021
  continue;
339
1022
  }
340
1023
 
@@ -351,17 +1034,19 @@ ${c.cyan}${c.bold}╔═══════════════════
351
1034
  try {
352
1035
  await applyUnifiedDiff(root, edit.diff);
353
1036
  appliedAny = true;
354
- console.log(` ${c.green}✓${c.reset} Applied: ${edit.path}`);
355
1037
  } catch (e) {
356
- console.log(` ${c.red}✗${c.reset} Failed: ${edit.path} - ${e.message}`);
1038
+ // Patch failed
357
1039
  }
358
1040
  }
359
1041
 
360
1042
  if (appliedAny) {
361
1043
  fixedAny = true;
1044
+ printMissionStatus(mission, 'success', `${patchResponse.edits.length} edits`);
1045
+ } else {
1046
+ printMissionStatus(mission, 'failed', 'patch apply failed');
362
1047
  }
363
1048
  } catch (e) {
364
- console.log(` ${c.yellow}⚠️ LLM error: ${e.message}${c.reset}`);
1049
+ printMissionStatus(mission, 'failed', `LLM error: ${truncate(e.message, 30)}`);
365
1050
  }
366
1051
  }
367
1052
 
@@ -373,29 +1058,42 @@ ${c.cyan}${c.bold}╔═══════════════════
373
1058
  });
374
1059
 
375
1060
  if (!fixedAny) {
376
- console.log(` ${c.yellow}No fixes applied this round.${c.reset}`);
1061
+ console.log(` ${colors.warnAmber}${ICONS.warning}${c.reset} No fixes applied this round`);
377
1062
  break;
378
1063
  }
379
1064
 
380
1065
  // Re-run ship to check progress
381
- console.log(` Re-checking ship verdict...`);
1066
+ startSpinner('Re-checking ship verdict...', colors.step3);
382
1067
  shipResult = await shipCore({ repoRoot: root, fastifyEntry, noWrite: false });
1068
+ stopSpinner('Ship check complete', true);
383
1069
 
384
- const newVerdictColor = shipResult.verdict === "SHIP" ? c.green : shipResult.verdict === "WARN" ? c.yellow : c.red;
385
- console.log(` ${newVerdictColor}${shipResult.verdict}${c.reset} (${shipResult.report?.findings?.length || 0} findings)`);
1070
+ printStepResult(3, shipResult.verdict, {
1071
+ findings: shipResult.report?.findings?.length || 0
1072
+ });
386
1073
 
387
1074
  } catch (err) {
388
- console.log(` ${c.red}Fix error: ${err.message}${c.reset}`);
1075
+ console.log(` ${colors.blockRed}${ICONS.cross}${c.reset} Fix error: ${err.message}`);
389
1076
  timeline.push({ step: `4.${fixRound}`, action: "fix", status: "error", error: err.message });
390
1077
  break;
391
1078
  }
392
1079
  }
1080
+
1081
+ if (fixRound > 0) {
1082
+ pipelineStatus[3] = shipResult.verdict === 'SHIP' ? 'complete' : 'error';
1083
+ } else if (skipFix) {
1084
+ pipelineStatus[3] = 'skipped';
1085
+ } else if (shipResult.verdict !== 'BLOCK') {
1086
+ pipelineStatus[3] = 'skipped';
1087
+ }
393
1088
 
394
1089
  // ═══════════════════════════════════════════════════════════════════════════
395
1090
  // STEP 5: FINAL VERIFICATION - Re-run reality + ship
396
1091
  // ═══════════════════════════════════════════════════════════════════════════
397
1092
  if (url && !skipReality && runReality && fixRound > 0) {
398
- console.log(`\n${c.cyan}▶ Step 5: Final verification...${c.reset}`);
1093
+ printStepHeader(5);
1094
+ pipelineStatus[4] = 'running';
1095
+
1096
+ startSpinner('Running final verification...', colors.step5);
399
1097
 
400
1098
  try {
401
1099
  await runReality({
@@ -413,6 +1111,8 @@ ${c.cyan}${c.bold}╔═══════════════════
413
1111
 
414
1112
  shipResult = await shipCore({ repoRoot: root, fastifyEntry, noWrite: false });
415
1113
 
1114
+ stopSpinner('Final verification complete', true);
1115
+
416
1116
  timeline.push({
417
1117
  step: 5,
418
1118
  action: "verify",
@@ -420,11 +1120,17 @@ ${c.cyan}${c.bold}╔═══════════════════
420
1120
  findingsCount: shipResult.report?.findings?.length || 0
421
1121
  });
422
1122
 
423
- const finalColor = shipResult.verdict === "SHIP" ? c.green : shipResult.verdict === "WARN" ? c.yellow : c.red;
424
- console.log(` Final verdict: ${finalColor}${shipResult.verdict}${c.reset}`);
1123
+ printStepResult(5, shipResult.verdict, {
1124
+ findings: shipResult.report?.findings?.length || 0
1125
+ });
1126
+
1127
+ pipelineStatus[4] = shipResult.verdict === 'SHIP' ? 'complete' : 'error';
425
1128
  } catch (err) {
426
- console.log(` ${c.yellow}⚠️ Final verification failed: ${err.message}${c.reset}`);
1129
+ stopSpinner(`Final verification failed: ${err.message}`, false);
1130
+ pipelineStatus[4] = 'error';
427
1131
  }
1132
+ } else {
1133
+ pipelineStatus[4] = 'skipped';
428
1134
  }
429
1135
 
430
1136
  finalVerdict = shipResult.verdict;
@@ -432,13 +1138,14 @@ ${c.cyan}${c.bold}╔═══════════════════
432
1138
  // ═══════════════════════════════════════════════════════════════════════════
433
1139
  // SUMMARY
434
1140
  // ═══════════════════════════════════════════════════════════════════════════
435
- const duration = Math.round((Date.now() - startTime) / 1000);
1141
+ const duration = Date.now() - startTime;
1142
+ const durationStr = formatDuration(duration);
436
1143
 
437
1144
  const report = {
438
1145
  meta: {
439
1146
  startedAt: new Date(startTime).toISOString(),
440
1147
  finishedAt: new Date().toISOString(),
441
- durationSeconds: duration,
1148
+ durationMs: duration,
442
1149
  url: url || null,
443
1150
  fixRounds: fixRound
444
1151
  },
@@ -454,20 +1161,34 @@ ${c.cyan}${c.bold}╔═══════════════════
454
1161
  ensureDir(path.dirname(latestPath));
455
1162
  fs.writeFileSync(latestPath, JSON.stringify(report, null, 2), "utf8");
456
1163
 
457
- console.log(`
458
- ${c.cyan}${c.bold}════════════════════════════════════════════════════════════${c.reset}
459
- ${c.bold}PROVE COMPLETE${c.reset}
460
-
461
- Duration: ${duration}s
462
- Fix rounds: ${fixRound}
463
- Final verdict: ${finalVerdict === "SHIP" ? c.green : finalVerdict === "WARN" ? c.yellow : c.red}${finalVerdict}${c.reset}
464
- Findings: ${shipResult.report?.findings?.length || 0}
1164
+ // Final verdict display
1165
+ printFinalVerdict(finalVerdict, durationStr, fixRound, shipResult.report?.findings?.length || 0);
1166
+
1167
+ // Timeline summary
1168
+ printTimelineSummary(timeline);
465
1169
 
466
- Report: .vibecheck/prove/${path.basename(outDir)}/prove_report.json
467
- ${c.cyan}════════════════════════════════════════════════════════════${c.reset}
468
- `);
1170
+ // Report links
1171
+ printSection('REPORTS', ICONS.doc);
1172
+ console.log();
1173
+ console.log(` ${colors.accent}${outDir}/prove_report.json${c.reset}`);
1174
+ console.log(` ${c.dim}${path.join(root, '.vibecheck', 'prove', 'last_prove.json')}${c.reset}`);
1175
+ console.log();
1176
+
1177
+ // Next steps if not proved
1178
+ if (finalVerdict !== 'SHIP') {
1179
+ printSection('NEXT STEPS', ICONS.lightning);
1180
+ console.log();
1181
+ if (skipFix) {
1182
+ console.log(` ${colors.accent}vibecheck prove --url ${url || '<url>'}${c.reset} ${c.dim}Enable auto-fix${c.reset}`);
1183
+ } else if (fixRound >= maxFixRounds) {
1184
+ console.log(` ${colors.accent}vibecheck prove --max-fix-rounds ${maxFixRounds + 2}${c.reset} ${c.dim}Try more fix rounds${c.reset}`);
1185
+ }
1186
+ console.log(` ${colors.accent}vibecheck ship --fix${c.reset} ${c.dim}Manual fix mode${c.reset}`);
1187
+ console.log();
1188
+ }
469
1189
 
470
1190
  process.exitCode = finalVerdict === "SHIP" ? 0 : finalVerdict === "WARN" ? 1 : 2;
1191
+ return process.exitCode;
471
1192
  }
472
1193
 
473
- module.exports = { runProve };
1194
+ module.exports = { runProve };