@vibecheckai/cli 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +135 -63
  2. package/bin/_deprecations.js +447 -19
  3. package/bin/_router.js +1 -1
  4. package/bin/registry.js +347 -280
  5. package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
  6. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  7. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  8. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  9. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  10. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  11. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  12. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  13. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  14. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  15. package/bin/runners/lib/agent-firewall/index.js +200 -0
  16. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  17. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  18. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
  19. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  20. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  21. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  22. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  23. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  24. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  25. package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
  26. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  27. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  28. package/bin/runners/lib/artifact-envelope.js +540 -0
  29. package/bin/runners/lib/auth-shared.js +977 -0
  30. package/bin/runners/lib/checkpoint.js +941 -0
  31. package/bin/runners/lib/cleanup/engine.js +571 -0
  32. package/bin/runners/lib/cleanup/index.js +53 -0
  33. package/bin/runners/lib/cleanup/output.js +375 -0
  34. package/bin/runners/lib/cleanup/rules.js +1060 -0
  35. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  36. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  37. package/bin/runners/lib/doctor/fix-script.js +336 -0
  38. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  39. package/bin/runners/lib/doctor/modules/index.js +62 -3
  40. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  41. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  42. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  43. package/bin/runners/lib/engine/ast-cache.js +210 -210
  44. package/bin/runners/lib/engine/auth-extractor.js +211 -211
  45. package/bin/runners/lib/engine/billing-extractor.js +112 -112
  46. package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
  47. package/bin/runners/lib/engine/env-extractor.js +207 -207
  48. package/bin/runners/lib/engine/express-extractor.js +208 -208
  49. package/bin/runners/lib/engine/extractors.js +849 -849
  50. package/bin/runners/lib/engine/index.js +207 -207
  51. package/bin/runners/lib/engine/repo-index.js +514 -514
  52. package/bin/runners/lib/engine/types.js +124 -124
  53. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  54. package/bin/runners/lib/entitlements-v2.js +2 -2
  55. package/bin/runners/lib/missions/briefing.js +427 -0
  56. package/bin/runners/lib/missions/checkpoint.js +753 -0
  57. package/bin/runners/lib/missions/hardening.js +851 -0
  58. package/bin/runners/lib/missions/plan.js +421 -32
  59. package/bin/runners/lib/missions/safety-gates.js +645 -0
  60. package/bin/runners/lib/missions/schema.js +478 -0
  61. package/bin/runners/lib/packs/bundle.js +675 -0
  62. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  63. package/bin/runners/lib/packs/pack-factory.js +837 -0
  64. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  65. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  66. package/bin/runners/lib/safelist/index.js +96 -0
  67. package/bin/runners/lib/safelist/integration.js +334 -0
  68. package/bin/runners/lib/safelist/matcher.js +696 -0
  69. package/bin/runners/lib/safelist/schema.js +948 -0
  70. package/bin/runners/lib/safelist/store.js +438 -0
  71. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  72. package/bin/runners/lib/ship-gate.js +832 -0
  73. package/bin/runners/lib/ship-manifest.js +1153 -0
  74. package/bin/runners/lib/ship-output.js +1 -1
  75. package/bin/runners/lib/unified-cli-output.js +710 -383
  76. package/bin/runners/lib/upsell.js +3 -3
  77. package/bin/runners/lib/why-tree.js +650 -0
  78. package/bin/runners/runAllowlist.js +33 -4
  79. package/bin/runners/runApprove.js +240 -1122
  80. package/bin/runners/runAudit.js +692 -0
  81. package/bin/runners/runAuth.js +325 -29
  82. package/bin/runners/runCheckpoint.js +442 -494
  83. package/bin/runners/runCleanup.js +343 -0
  84. package/bin/runners/runDoctor.js +269 -19
  85. package/bin/runners/runFix.js +411 -32
  86. package/bin/runners/runForge.js +411 -0
  87. package/bin/runners/runIntent.js +906 -0
  88. package/bin/runners/runKickoff.js +878 -0
  89. package/bin/runners/runLaunch.js +2000 -0
  90. package/bin/runners/runLink.js +785 -0
  91. package/bin/runners/runMcp.js +1741 -837
  92. package/bin/runners/runPacks.js +2089 -0
  93. package/bin/runners/runPolish.js +41 -0
  94. package/bin/runners/runReality.js +178 -1
  95. package/bin/runners/runSafelist.js +1190 -0
  96. package/bin/runners/runScan.js +21 -9
  97. package/bin/runners/runShield.js +1282 -0
  98. package/bin/runners/runShip.js +395 -16
  99. package/bin/vibecheck.js +34 -6
  100. package/mcp-server/README.md +117 -158
  101. package/mcp-server/handlers/index.ts +2 -2
  102. package/mcp-server/handlers/tool-handler.ts +50 -11
  103. package/mcp-server/index.js +16 -0
  104. package/mcp-server/intent-firewall-interceptor.js +529 -0
  105. package/mcp-server/lib/executor.ts +5 -5
  106. package/mcp-server/lib/index.ts +14 -4
  107. package/mcp-server/lib/sandbox.test.ts +4 -4
  108. package/mcp-server/lib/sandbox.ts +2 -2
  109. package/mcp-server/manifest.json +473 -0
  110. package/mcp-server/package.json +1 -1
  111. package/mcp-server/registry/tool-registry.js +315 -523
  112. package/mcp-server/registry/tools.json +442 -428
  113. package/mcp-server/registry.test.ts +18 -12
  114. package/mcp-server/tier-auth.js +68 -11
  115. package/mcp-server/tools-v3.js +70 -16
  116. package/mcp-server/tsconfig.json +1 -0
  117. package/package.json +2 -1
  118. package/bin/runners/runProof.zip +0 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Upsell Copy Module - Central copy generator for tier upgrades
3
3
  *
4
- * Simple 2-tier model: FREE and PRO ($69/mo)
4
+ * Simple 2-tier model: FREE and PRO ($49/mo)
5
5
  *
6
6
  * Rules:
7
7
  * - Blunt, confident, minimal tone
@@ -48,7 +48,7 @@ const sym = {
48
48
  };
49
49
 
50
50
  // ═══════════════════════════════════════════════════════════════════════════════
51
- // SIMPLE 2-TIER CONFIG: FREE and PRO ($69/mo)
51
+ // SIMPLE 2-TIER CONFIG: FREE and PRO ($49/mo)
52
52
  // ═══════════════════════════════════════════════════════════════════════════════
53
53
  const TIER_LABELS = {
54
54
  free: "FREE",
@@ -61,7 +61,7 @@ const TIER_COLORS = {
61
61
  };
62
62
 
63
63
  const PRICING_URL = "https://vibecheckai.dev/pricing";
64
- const PRO_PRICE = "$69/mo";
64
+ const PRO_PRICE = "$49/mo";
65
65
 
66
66
  // ═══════════════════════════════════════════════════════════════════════════════
67
67
  // DENIAL COPY - Command-specific reasons and alternatives
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Why Tree - Evidence Chain Builder
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * CLEAR "WHY" FOR EVERY VERDICT
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ *
8
+ * Builds a human-readable evidence chain for verdicts:
9
+ * - Top 3 blockers with file:line evidence
10
+ * - Fix missions (actionable next steps)
11
+ * - Proof chain (how we know this is true)
12
+ *
13
+ * @module why-tree
14
+ * @version 1.1.0 - Hardened
15
+ */
16
+
17
+ "use strict";
18
+
19
+ const path = require("path");
20
+ const crypto = require("crypto");
21
+
22
+ // ═══════════════════════════════════════════════════════════════════════════════
23
+ // CONSTANTS
24
+ // ═══════════════════════════════════════════════════════════════════════════════
25
+
26
+ const MAX_STRING_LENGTH = 500;
27
+ const MAX_BLOCKERS = 10;
28
+ const MAX_WARNINGS = 20;
29
+ const MAX_FIX_MISSIONS = 10;
30
+ const MAX_EVIDENCE_ITEMS = 5;
31
+
32
+ // Characters to strip from output (prevent terminal injection)
33
+ const UNSAFE_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\u001B]/g;
34
+
35
+ // ═══════════════════════════════════════════════════════════════════════════════
36
+ // INPUT SANITIZATION
37
+ // ═══════════════════════════════════════════════════════════════════════════════
38
+
39
+ /**
40
+ * Sanitize a string for safe output (prevent injection)
41
+ * @param {*} str - Input to sanitize
42
+ * @param {number} maxLen - Maximum length
43
+ * @returns {string|null} Sanitized string or null
44
+ */
45
+ function sanitize(str, maxLen = MAX_STRING_LENGTH) {
46
+ if (str === null || str === undefined) {
47
+ return null;
48
+ }
49
+
50
+ const s = String(str)
51
+ .replace(UNSAFE_CHARS, "") // Remove unsafe characters
52
+ .trim();
53
+
54
+ if (!s) {
55
+ return null;
56
+ }
57
+
58
+ return s.length > maxLen ? s.slice(0, maxLen - 3) + "..." : s;
59
+ }
60
+
61
+ /**
62
+ * Validate and sanitize a finding object
63
+ * @param {*} finding - Finding to validate
64
+ * @returns {Object|null} Validated finding or null
65
+ */
66
+ function validateFinding(finding) {
67
+ if (!finding || typeof finding !== "object") {
68
+ return null;
69
+ }
70
+
71
+ // Must have at least a title or message
72
+ const title = sanitize(finding.title || finding.message);
73
+ if (!title) {
74
+ return null;
75
+ }
76
+
77
+ return {
78
+ id: sanitize(finding.id || finding.fingerprint, 64),
79
+ category: sanitize(finding.category, 50) || "Unknown",
80
+ severity: validateSeverity(finding.severity),
81
+ title,
82
+ why: sanitize(finding.why || finding.explanation),
83
+ confidence: validateConfidence(finding.confidence),
84
+ evidence: validateEvidence(finding.evidence),
85
+ fixHints: validateFixHints(finding.fixHints),
86
+ fingerprint: sanitize(finding.fingerprint, 64),
87
+ detectorId: sanitize(finding.detectorId, 50),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Validate severity
93
+ */
94
+ function validateSeverity(sev) {
95
+ const valid = ["BLOCK", "WARN", "INFO"];
96
+ const s = String(sev || "").toUpperCase();
97
+ return valid.includes(s) ? s : "INFO";
98
+ }
99
+
100
+ /**
101
+ * Validate confidence
102
+ */
103
+ function validateConfidence(conf) {
104
+ const valid = ["high", "medium", "low"];
105
+ const c = String(conf || "").toLowerCase();
106
+ return valid.includes(c) ? c : "medium";
107
+ }
108
+
109
+ /**
110
+ * Validate evidence array
111
+ */
112
+ function validateEvidence(evidence) {
113
+ if (!Array.isArray(evidence)) {
114
+ return [];
115
+ }
116
+
117
+ return evidence
118
+ .slice(0, MAX_EVIDENCE_ITEMS)
119
+ .map(e => {
120
+ if (!e || typeof e !== "object") return null;
121
+ return {
122
+ file: sanitize(e.file || e.path, 300),
123
+ lines: sanitize(e.lines, 20),
124
+ line: typeof e.line === "number" ? Math.max(0, Math.floor(e.line)) : null,
125
+ snippet: sanitize(e.snippet, 200),
126
+ kind: sanitize(e.kind, 20) || "file",
127
+ };
128
+ })
129
+ .filter(Boolean);
130
+ }
131
+
132
+ /**
133
+ * Validate fix hints
134
+ */
135
+ function validateFixHints(hints) {
136
+ if (!Array.isArray(hints)) {
137
+ return [];
138
+ }
139
+ return hints
140
+ .slice(0, 5)
141
+ .map(h => sanitize(h, MAX_STRING_LENGTH))
142
+ .filter(Boolean);
143
+ }
144
+
145
+ // ═══════════════════════════════════════════════════════════════════════════════
146
+ // WHY TREE BUILDER
147
+ // ═══════════════════════════════════════════════════════════════════════════════
148
+
149
+ /**
150
+ * Build a comprehensive why tree from findings.
151
+ *
152
+ * The why tree answers: "Why did we get this verdict?"
153
+ *
154
+ * @param {Array} findings - All findings
155
+ * @param {Object} options - Options
156
+ * @returns {Object} Why tree structure (always returns valid object)
157
+ */
158
+ function buildWhyTree(findings = [], options = {}) {
159
+ // Validate options
160
+ const opts = options || {};
161
+ const maxBlockers = Math.min(Math.max(1, Number(opts.maxBlockers) || 3), MAX_BLOCKERS);
162
+ const maxWarnings = Math.min(Math.max(1, Number(opts.maxWarnings) || 5), MAX_WARNINGS);
163
+ const maxFixMissions = Math.min(Math.max(1, Number(opts.maxFixMissions) || 5), MAX_FIX_MISSIONS);
164
+ const includeEvidence = opts.includeEvidence !== false;
165
+ const includeProofChain = opts.includeProofChain !== false;
166
+
167
+ // Validate and filter findings
168
+ const validFindings = (Array.isArray(findings) ? findings : [])
169
+ .map(validateFinding)
170
+ .filter(Boolean);
171
+
172
+ // Separate by severity
173
+ const blockers = validFindings
174
+ .filter(f => f.severity === "BLOCK")
175
+ .sort((a, b) => getConfidenceScore(b) - getConfidenceScore(a));
176
+
177
+ const warnings = validFindings
178
+ .filter(f => f.severity === "WARN")
179
+ .sort((a, b) => getConfidenceScore(b) - getConfidenceScore(a));
180
+
181
+ const info = validFindings.filter(f => f.severity === "INFO");
182
+
183
+ // Build tree structure with safe defaults
184
+ const tree = {
185
+ verdict: blockers.length > 0 ? "BLOCK" : warnings.length > 0 ? "WARN" : "SHIP",
186
+ summary: buildSummary(blockers, warnings),
187
+
188
+ // Top blockers with full evidence
189
+ topBlockers: blockers.slice(0, maxBlockers).map((b, i) => ({
190
+ rank: i + 1,
191
+ ...buildIssueNode(b, includeEvidence, includeProofChain),
192
+ })),
193
+
194
+ // Top warnings (if no blockers)
195
+ topWarnings: blockers.length === 0
196
+ ? warnings.slice(0, maxWarnings).map((w, i) => ({
197
+ rank: i + 1,
198
+ ...buildIssueNode(w, includeEvidence, false),
199
+ }))
200
+ : [],
201
+
202
+ // Fix missions (actionable next steps)
203
+ fixMissions: buildFixMissions(blockers, warnings, maxFixMissions),
204
+
205
+ // Stats
206
+ stats: {
207
+ totalFindings: validFindings.length,
208
+ blockers: blockers.length,
209
+ warnings: warnings.length,
210
+ info: info.length,
211
+ byCategory: groupByCategory(validFindings),
212
+ },
213
+ };
214
+
215
+ return tree;
216
+ }
217
+
218
+ /**
219
+ * Build summary text based on findings
220
+ */
221
+ function buildSummary(blockers, warnings) {
222
+ if (blockers.length === 0 && warnings.length === 0) {
223
+ return "All checks passed. Ready to ship.";
224
+ }
225
+
226
+ const parts = [];
227
+
228
+ if (blockers.length > 0) {
229
+ parts.push(`${blockers.length} blocker${blockers.length !== 1 ? "s" : ""} must be fixed`);
230
+ }
231
+
232
+ if (warnings.length > 0) {
233
+ parts.push(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""} to review`);
234
+ }
235
+
236
+ return parts.join(", ") + ".";
237
+ }
238
+
239
+ /**
240
+ * Build an issue node with evidence
241
+ */
242
+ function buildIssueNode(finding, includeEvidence, includeProofChain) {
243
+ const node = {
244
+ id: finding.fingerprint || finding.id || sha256Short(finding.title || ""),
245
+ category: finding.category,
246
+ severity: finding.severity,
247
+ title: finding.title || finding.message,
248
+ why: finding.why || finding.explanation || null,
249
+ confidence: finding.confidence || "medium",
250
+ };
251
+
252
+ // Add evidence
253
+ if (includeEvidence && finding.evidence?.length > 0) {
254
+ node.evidence = finding.evidence.slice(0, 3).map(formatEvidence);
255
+ node.primaryLocation = formatPrimaryLocation(finding);
256
+ }
257
+
258
+ // Add fix hints
259
+ if (finding.fixHints?.length > 0) {
260
+ node.fixHint = finding.fixHints[0];
261
+ node.allFixHints = finding.fixHints;
262
+ }
263
+
264
+ // Add proof chain (how we know this)
265
+ if (includeProofChain) {
266
+ node.proofChain = buildProofChain(finding);
267
+ }
268
+
269
+ return node;
270
+ }
271
+
272
+ /**
273
+ * Format evidence for display
274
+ */
275
+ function formatEvidence(ev) {
276
+ return {
277
+ file: ev.file || ev.path || null,
278
+ lines: ev.lines || (ev.line ? String(ev.line) : null),
279
+ snippet: ev.snippet ? truncate(ev.snippet, 100) : null,
280
+ kind: ev.kind || "file",
281
+ snippetHash: ev.snippetHash || null,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Format primary location as a clickable reference
287
+ */
288
+ function formatPrimaryLocation(finding) {
289
+ const ev = finding.evidence?.[0];
290
+ if (!ev) return null;
291
+
292
+ const file = ev.file || ev.path;
293
+ if (!file) return null;
294
+
295
+ const line = ev.lines?.split("-")[0] || ev.line;
296
+
297
+ return {
298
+ file,
299
+ line: line ? parseInt(line) : null,
300
+ display: line ? `${file}:${line}` : file,
301
+ vscodeUri: line ? `vscode://file/${file}:${line}` : `vscode://file/${file}`,
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Build proof chain (how we know this finding is true)
307
+ */
308
+ function buildProofChain(finding) {
309
+ const chain = [];
310
+
311
+ // Add detector info
312
+ if (finding.detectorId || finding.detector) {
313
+ chain.push({
314
+ step: "detected",
315
+ by: finding.detectorId || finding.detector,
316
+ type: finding.detectorType || "static",
317
+ });
318
+ }
319
+
320
+ // Add evidence verification
321
+ if (finding.evidence?.length > 0) {
322
+ const ev = finding.evidence[0];
323
+ chain.push({
324
+ step: "verified",
325
+ method: ev.kind === "runtime" ? "runtime" : "file-citation",
326
+ confidence: finding.confidence || "medium",
327
+ at: ev.file ? `${ev.file}:${ev.lines || ev.line || ""}` : "unknown",
328
+ });
329
+ }
330
+
331
+ // Add cross-reference if available
332
+ if (finding.crossRef) {
333
+ chain.push({
334
+ step: "cross-referenced",
335
+ with: finding.crossRef.type,
336
+ matches: finding.crossRef.matches,
337
+ });
338
+ }
339
+
340
+ return chain.length > 0 ? chain : null;
341
+ }
342
+
343
+ /**
344
+ * Build fix missions (prioritized action items)
345
+ */
346
+ function buildFixMissions(blockers, warnings, max) {
347
+ const missions = [];
348
+
349
+ // Priority 1: Blockers
350
+ for (const b of blockers.slice(0, max)) {
351
+ missions.push({
352
+ priority: "critical",
353
+ category: b.category,
354
+ target: b.evidence?.[0]?.file || "unknown",
355
+ action: b.fixHints?.[0] || `Fix ${b.category} issue: ${truncate(b.title, 50)}`,
356
+ findingId: b.fingerprint || b.id,
357
+ });
358
+ }
359
+
360
+ // Priority 2: Warnings (if room)
361
+ const remaining = max - missions.length;
362
+ if (remaining > 0) {
363
+ for (const w of warnings.slice(0, remaining)) {
364
+ missions.push({
365
+ priority: "recommended",
366
+ category: w.category,
367
+ target: w.evidence?.[0]?.file || "unknown",
368
+ action: w.fixHints?.[0] || `Address ${w.category} warning: ${truncate(w.title, 50)}`,
369
+ findingId: w.fingerprint || w.id,
370
+ });
371
+ }
372
+ }
373
+
374
+ return missions;
375
+ }
376
+
377
+ /**
378
+ * Group findings by category
379
+ */
380
+ function groupByCategory(findings) {
381
+ const groups = {};
382
+
383
+ for (const f of findings) {
384
+ const cat = f.category || "Other";
385
+ if (!groups[cat]) {
386
+ groups[cat] = { total: 0, blockers: 0, warnings: 0 };
387
+ }
388
+ groups[cat].total++;
389
+ if (f.severity === "BLOCK") groups[cat].blockers++;
390
+ if (f.severity === "WARN") groups[cat].warnings++;
391
+ }
392
+
393
+ return groups;
394
+ }
395
+
396
+ // ═══════════════════════════════════════════════════════════════════════════════
397
+ // RENDERING (Human-readable output)
398
+ // ═══════════════════════════════════════════════════════════════════════════════
399
+
400
+ /**
401
+ * Render why tree as terminal output (plain text with ANSI colors)
402
+ */
403
+ function renderWhyTreeTerminal(tree, options = {}) {
404
+ const { colorize = true, width = 72 } = options;
405
+ const lines = [];
406
+
407
+ // ANSI helpers
408
+ const c = colorize ? {
409
+ reset: "\x1b[0m",
410
+ bold: "\x1b[1m",
411
+ dim: "\x1b[2m",
412
+ red: "\x1b[38;2;255;100;100m",
413
+ yellow: "\x1b[38;2;255;200;100m",
414
+ green: "\x1b[38;2;100;255;150m",
415
+ cyan: "\x1b[38;2;100;200;255m",
416
+ gray: "\x1b[38;2;150;150;150m",
417
+ } : { reset: "", bold: "", dim: "", red: "", yellow: "", green: "", cyan: "", gray: "" };
418
+
419
+ // Header
420
+ const verdictColor = tree.verdict === "SHIP" ? c.green : tree.verdict === "WARN" ? c.yellow : c.red;
421
+ lines.push("");
422
+ lines.push(` ${c.bold}WHY: ${verdictColor}${tree.verdict}${c.reset}`);
423
+ lines.push(` ${c.dim}${tree.summary}${c.reset}`);
424
+ lines.push("");
425
+
426
+ // Top blockers
427
+ if (tree.topBlockers.length > 0) {
428
+ lines.push(` ${c.bold}Top Blockers:${c.reset}`);
429
+ lines.push("");
430
+
431
+ for (const blocker of tree.topBlockers) {
432
+ lines.push(` ${c.red}${blocker.rank}.${c.reset} ${c.bold}${blocker.title}${c.reset}`);
433
+
434
+ if (blocker.why) {
435
+ lines.push(` ${c.dim}Why: ${blocker.why}${c.reset}`);
436
+ }
437
+
438
+ if (blocker.primaryLocation) {
439
+ lines.push(` ${c.cyan}→ ${blocker.primaryLocation.display}${c.reset}`);
440
+ }
441
+
442
+ if (blocker.fixHint) {
443
+ lines.push(` ${c.green}Fix: ${blocker.fixHint}${c.reset}`);
444
+ }
445
+
446
+ lines.push("");
447
+ }
448
+ }
449
+
450
+ // Top warnings (if no blockers)
451
+ if (tree.topWarnings.length > 0) {
452
+ lines.push(` ${c.bold}Warnings:${c.reset}`);
453
+ lines.push("");
454
+
455
+ for (const warning of tree.topWarnings.slice(0, 3)) {
456
+ lines.push(` ${c.yellow}${warning.rank}.${c.reset} ${warning.title}`);
457
+
458
+ if (warning.primaryLocation) {
459
+ lines.push(` ${c.cyan}→ ${warning.primaryLocation.display}${c.reset}`);
460
+ }
461
+
462
+ lines.push("");
463
+ }
464
+ }
465
+
466
+ // Fix missions
467
+ if (tree.fixMissions.length > 0) {
468
+ lines.push(` ${c.bold}Fix Missions:${c.reset}`);
469
+ lines.push("");
470
+
471
+ for (const mission of tree.fixMissions.slice(0, 3)) {
472
+ const priorityColor = mission.priority === "critical" ? c.red : c.yellow;
473
+ lines.push(` ${priorityColor}[${mission.priority.toUpperCase()}]${c.reset} ${mission.action}`);
474
+ lines.push(` ${c.dim}Target: ${mission.target}${c.reset}`);
475
+ lines.push("");
476
+ }
477
+ }
478
+
479
+ // Stats summary
480
+ lines.push(` ${c.dim}${"─".repeat(width - 4)}${c.reset}`);
481
+ lines.push(` ${c.dim}Stats: ${tree.stats.blockers} blockers, ${tree.stats.warnings} warnings, ${tree.stats.info} info${c.reset}`);
482
+ lines.push("");
483
+
484
+ return lines.join("\n");
485
+ }
486
+
487
+ /**
488
+ * Render why tree as Markdown (for PR comments, reports)
489
+ */
490
+ function renderWhyTreeMarkdown(tree) {
491
+ const lines = [];
492
+
493
+ // Verdict header
494
+ const emoji = tree.verdict === "SHIP" ? "✅" : tree.verdict === "WARN" ? "⚠️" : "🚫";
495
+ lines.push(`## ${emoji} Verdict: ${tree.verdict}`);
496
+ lines.push("");
497
+ lines.push(tree.summary);
498
+ lines.push("");
499
+
500
+ // Top blockers
501
+ if (tree.topBlockers.length > 0) {
502
+ lines.push("### Top Blockers");
503
+ lines.push("");
504
+
505
+ for (const blocker of tree.topBlockers) {
506
+ lines.push(`**${blocker.rank}. ${blocker.title}**`);
507
+
508
+ if (blocker.why) {
509
+ lines.push(`> ${blocker.why}`);
510
+ }
511
+
512
+ if (blocker.primaryLocation) {
513
+ lines.push(`- 📁 \`${blocker.primaryLocation.display}\``);
514
+ }
515
+
516
+ if (blocker.fixHint) {
517
+ lines.push(`- 🔧 **Fix:** ${blocker.fixHint}`);
518
+ }
519
+
520
+ // Proof chain
521
+ if (blocker.proofChain?.length > 0) {
522
+ const chainStr = blocker.proofChain.map(c => c.by || c.method || c.with).join(" → ");
523
+ lines.push(`- 📋 Proof: ${chainStr}`);
524
+ }
525
+
526
+ lines.push("");
527
+ }
528
+ }
529
+
530
+ // Top warnings
531
+ if (tree.topWarnings.length > 0) {
532
+ lines.push("### Warnings");
533
+ lines.push("");
534
+
535
+ for (const warning of tree.topWarnings.slice(0, 5)) {
536
+ lines.push(`- ⚠️ **${warning.title}**`);
537
+ if (warning.primaryLocation) {
538
+ lines.push(` - \`${warning.primaryLocation.display}\``);
539
+ }
540
+ }
541
+ lines.push("");
542
+ }
543
+
544
+ // Fix missions
545
+ if (tree.fixMissions.length > 0) {
546
+ lines.push("### Fix Missions");
547
+ lines.push("");
548
+
549
+ for (const mission of tree.fixMissions.slice(0, 5)) {
550
+ const emoji = mission.priority === "critical" ? "🔴" : "🟡";
551
+ lines.push(`${emoji} **${mission.action}**`);
552
+ lines.push(` - Target: \`${mission.target}\``);
553
+ }
554
+ lines.push("");
555
+ }
556
+
557
+ // Stats
558
+ lines.push("---");
559
+ lines.push(`📊 **Stats:** ${tree.stats.blockers} blockers, ${tree.stats.warnings} warnings, ${tree.stats.totalFindings} total findings`);
560
+
561
+ return lines.join("\n");
562
+ }
563
+
564
+ /**
565
+ * Render why tree as JSON (for API/programmatic use)
566
+ */
567
+ function renderWhyTreeJSON(tree) {
568
+ return JSON.stringify(tree, null, 2);
569
+ }
570
+
571
+ // ═══════════════════════════════════════════════════════════════════════════════
572
+ // HELPERS
573
+ // ═══════════════════════════════════════════════════════════════════════════════
574
+
575
+ function getConfidenceScore(finding) {
576
+ if (!finding) return 0;
577
+ const scores = { high: 3, medium: 2, low: 1 };
578
+ return scores[finding.confidence] || 2;
579
+ }
580
+
581
+ function truncate(str, len) {
582
+ if (!str) return "";
583
+ const s = String(str);
584
+ if (s.length <= len) return s;
585
+ return s.slice(0, Math.max(0, len - 3)) + "...";
586
+ }
587
+
588
+ function sha256Short(input) {
589
+ try {
590
+ return crypto.createHash("sha256").update(String(input || "")).digest("hex").slice(0, 8);
591
+ } catch {
592
+ return "00000000";
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Escape special characters for safe terminal output
598
+ */
599
+ function escapeTerminal(str) {
600
+ if (!str) return "";
601
+ return String(str).replace(UNSAFE_CHARS, "");
602
+ }
603
+
604
+ /**
605
+ * Escape special characters for safe markdown output
606
+ */
607
+ function escapeMarkdown(str) {
608
+ if (!str) return "";
609
+ return String(str)
610
+ .replace(UNSAFE_CHARS, "")
611
+ .replace(/[<>&]/g, c => ({
612
+ "<": "&lt;",
613
+ ">": "&gt;",
614
+ "&": "&amp;",
615
+ }[c]));
616
+ }
617
+
618
+ // ═══════════════════════════════════════════════════════════════════════════════
619
+ // EXPORTS
620
+ // ═══════════════════════════════════════════════════════════════════════════════
621
+
622
+ module.exports = {
623
+ // Main builder
624
+ buildWhyTree,
625
+
626
+ // Renderers
627
+ renderWhyTreeTerminal,
628
+ renderWhyTreeMarkdown,
629
+ renderWhyTreeJSON,
630
+
631
+ // Individual builders (for custom use)
632
+ buildIssueNode,
633
+ buildFixMissions,
634
+ buildProofChain,
635
+ formatPrimaryLocation,
636
+
637
+ // Validation (for external use)
638
+ validateFinding,
639
+ sanitize,
640
+
641
+ // Utilities
642
+ escapeTerminal,
643
+ escapeMarkdown,
644
+
645
+ // Constants
646
+ MAX_STRING_LENGTH,
647
+ MAX_BLOCKERS,
648
+ MAX_WARNINGS,
649
+ MAX_FIX_MISSIONS,
650
+ };