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