@vibecheckai/cli 3.6.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +135 -63
  2. package/bin/_deprecations.js +447 -19
  3. package/bin/_router.js +1 -1
  4. package/bin/registry.js +347 -280
  5. package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
  6. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  7. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  8. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  9. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  10. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  11. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  12. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  13. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  14. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  15. package/bin/runners/lib/agent-firewall/index.js +200 -0
  16. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  17. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  18. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
  19. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  20. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  21. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  22. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  23. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  24. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  25. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +31 -38
  26. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +68 -3
  27. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +4 -2
  28. package/bin/runners/lib/agent-firewall/risk/thresholds.js +5 -4
  29. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  30. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  31. package/bin/runners/lib/artifact-envelope.js +540 -0
  32. package/bin/runners/lib/auth-shared.js +977 -0
  33. package/bin/runners/lib/checkpoint.js +941 -0
  34. package/bin/runners/lib/cleanup/engine.js +571 -0
  35. package/bin/runners/lib/cleanup/index.js +53 -0
  36. package/bin/runners/lib/cleanup/output.js +375 -0
  37. package/bin/runners/lib/cleanup/rules.js +1060 -0
  38. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  39. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  40. package/bin/runners/lib/doctor/fix-script.js +336 -0
  41. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  42. package/bin/runners/lib/doctor/modules/index.js +62 -3
  43. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  44. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  45. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  46. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  47. package/bin/runners/lib/entitlements-v2.js +2 -2
  48. package/bin/runners/lib/error-messages.js +1 -1
  49. package/bin/runners/lib/missions/briefing.js +427 -0
  50. package/bin/runners/lib/missions/checkpoint.js +753 -0
  51. package/bin/runners/lib/missions/hardening.js +851 -0
  52. package/bin/runners/lib/missions/plan.js +421 -32
  53. package/bin/runners/lib/missions/safety-gates.js +645 -0
  54. package/bin/runners/lib/missions/schema.js +478 -0
  55. package/bin/runners/lib/packs/bundle.js +675 -0
  56. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  57. package/bin/runners/lib/packs/pack-factory.js +837 -0
  58. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  59. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  60. package/bin/runners/lib/report-output.js +6 -6
  61. package/bin/runners/lib/safelist/index.js +96 -0
  62. package/bin/runners/lib/safelist/integration.js +334 -0
  63. package/bin/runners/lib/safelist/matcher.js +696 -0
  64. package/bin/runners/lib/safelist/schema.js +948 -0
  65. package/bin/runners/lib/safelist/store.js +438 -0
  66. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  67. package/bin/runners/lib/ship-gate.js +832 -0
  68. package/bin/runners/lib/ship-manifest.js +1153 -0
  69. package/bin/runners/lib/ship-output.js +1 -1
  70. package/bin/runners/lib/unified-cli-output.js +710 -383
  71. package/bin/runners/lib/upsell.js +3 -3
  72. package/bin/runners/lib/why-tree.js +650 -0
  73. package/bin/runners/runAllowlist.js +33 -4
  74. package/bin/runners/runApprove.js +240 -1122
  75. package/bin/runners/runAudit.js +692 -0
  76. package/bin/runners/runAuth.js +325 -29
  77. package/bin/runners/runCheckpoint.js +442 -494
  78. package/bin/runners/runCleanup.js +343 -0
  79. package/bin/runners/runDoctor.js +269 -19
  80. package/bin/runners/runFix.js +411 -32
  81. package/bin/runners/runForge.js +411 -0
  82. package/bin/runners/runIntent.js +906 -0
  83. package/bin/runners/runKickoff.js +878 -0
  84. package/bin/runners/runLaunch.js +2000 -0
  85. package/bin/runners/runLink.js +785 -0
  86. package/bin/runners/runMcp.js +1741 -837
  87. package/bin/runners/runPacks.js +2089 -0
  88. package/bin/runners/runPolish.js +41 -0
  89. package/bin/runners/runSafelist.js +1190 -0
  90. package/bin/runners/runScan.js +21 -9
  91. package/bin/runners/runShield.js +1282 -0
  92. package/bin/runners/runShip.js +395 -16
  93. package/bin/vibecheck.js +34 -6
  94. package/mcp-server/README.md +117 -158
  95. package/mcp-server/handlers/tool-handler.ts +3 -3
  96. package/mcp-server/index.js +16 -0
  97. package/mcp-server/intent-firewall-interceptor.js +529 -0
  98. package/mcp-server/manifest.json +473 -0
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/registry/tool-registry.js +315 -523
  101. package/mcp-server/registry/tools.json +442 -428
  102. package/mcp-server/tier-auth.js +164 -16
  103. package/mcp-server/tools-v3.js +70 -16
  104. package/package.json +1 -1
  105. package/bin/runners/runProof.zip +0 -0
@@ -0,0 +1,878 @@
1
+ /**
2
+ * vibecheck kickoff - First Run Dopamine
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * 60 SECONDS TO VALUE. KNOWS WHAT TO DO NEXT.
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ *
8
+ * Goal: User sees value and knows exactly what to do next in under 60 seconds.
9
+ *
10
+ * Flow: link → forge → audit → (optional) ship
11
+ *
12
+ * Magic:
13
+ * • Auto-detects intent (frontend? API? full-stack?)
14
+ * • Adapts checks to project type
15
+ * • Produces shareable summary
16
+ * • Clear next steps based on results
17
+ *
18
+ * @version 2.0.0
19
+ */
20
+
21
+ "use strict";
22
+
23
+ const fs = require("fs");
24
+ const path = require("path");
25
+ const crypto = require("crypto");
26
+ const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
27
+ const { EXIT } = require("./lib/exit-codes");
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════════
30
+ // CONSTANTS
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+
33
+ const VERSION = "2.0.0";
34
+ const SIXTY_SECONDS = 60000;
35
+
36
+ // Project intent types
37
+ const INTENT = {
38
+ FRONTEND: "frontend",
39
+ API: "api",
40
+ FULLSTACK: "fullstack",
41
+ STATIC: "static",
42
+ LIBRARY: "library",
43
+ UNKNOWN: "unknown",
44
+ };
45
+
46
+ // Intent detection markers
47
+ const INTENT_MARKERS = {
48
+ frontend: [
49
+ "next", "react", "vue", "@vue/cli", "svelte", "@sveltejs/kit",
50
+ "nuxt", "@remix-run/react", "gatsby", "vite", "create-react-app",
51
+ "angular", "@angular/core", "solid-js", "qwik",
52
+ ],
53
+ api: [
54
+ "express", "fastify", "@nestjs/core", "hono", "koa", "@hapi/hapi",
55
+ "restify", "polka", "micro", "@trpc/server", "graphql-yoga",
56
+ ],
57
+ library: [
58
+ "tsup", "rollup", "microbundle", "unbuild", "tsdx",
59
+ ],
60
+ };
61
+
62
+ // ═══════════════════════════════════════════════════════════════════════════════
63
+ // UNIFIED CLI OUTPUT
64
+ // ═══════════════════════════════════════════════════════════════════════════════
65
+
66
+ let _cliOutput = null;
67
+
68
+ function getCliOutput() {
69
+ if (!_cliOutput) _cliOutput = require("./lib/unified-cli-output");
70
+ return _cliOutput;
71
+ }
72
+
73
+ // Get styling from unified module (with fallback)
74
+ function getStyles() {
75
+ try {
76
+ const cli = getCliOutput();
77
+ return { c: cli.ansi, sym: cli.sym };
78
+ } catch {
79
+ // Fallback if unified module not available
80
+ const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
81
+ return {
82
+ c: SUPPORTS_COLOR ? {
83
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
84
+ red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m",
85
+ cyan: "\x1b[36m", gray: "\x1b[90m", magenta: "\x1b[35m",
86
+ } : Object.fromEntries(["reset", "bold", "dim", "red", "green", "yellow", "cyan", "gray", "magenta"].map(k => [k, ""])),
87
+ sym: {
88
+ check: "✓", cross: "✗", warning: "⚠", arrow: "→",
89
+ rocket: "🚀", brain: "🧠", search: "🔍", link: "🔗",
90
+ party: "🎉", target: "🎯", sparkle: "✨", lightning: "⚡",
91
+ boxTL: "╔", boxTR: "╗", boxBL: "╚", boxBR: "╝", boxH: "═", boxV: "║",
92
+ },
93
+ };
94
+ }
95
+ }
96
+
97
+ const { c, sym } = getStyles();
98
+
99
+ // ═══════════════════════════════════════════════════════════════════════════════
100
+ // BANNER (uses unified CLI output)
101
+ // ═══════════════════════════════════════════════════════════════════════════════
102
+
103
+ function printWelcomeBanner(projectName, intent, projectRoot) {
104
+ const intentLabel = {
105
+ [INTENT.FRONTEND]: "Frontend App",
106
+ [INTENT.API]: "API Service",
107
+ [INTENT.FULLSTACK]: "Full-Stack App",
108
+ [INTENT.STATIC]: "Static Site",
109
+ [INTENT.LIBRARY]: "Library/Package",
110
+ [INTENT.UNKNOWN]: "Node Project",
111
+ }[intent] || "Project";
112
+
113
+ try {
114
+ const cli = getCliOutput();
115
+ console.log(cli.renderFullHeader("kickoff", {
116
+ version: VERSION,
117
+ tier: "FREE",
118
+ integrity: intentLabel.toUpperCase(),
119
+ target: projectRoot || process.cwd(),
120
+ sessionId: cli.generateSessionId(),
121
+ }));
122
+ } catch {
123
+ // Fallback to simple banner
124
+ console.log();
125
+ console.log(` ${c.cyan}╭${"─".repeat(65)}╮${c.reset}`);
126
+ console.log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`);
127
+ console.log(` ${c.cyan}│${c.reset} ${sym.rocket} ${c.bold}VIBECHECK KICKOFF${c.reset} ${c.cyan}│${c.reset}`);
128
+ console.log(` ${c.cyan}│${c.reset} ${c.dim}60 seconds to value${c.reset} ${c.cyan}│${c.reset}`);
129
+ console.log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`);
130
+ console.log(` ${c.cyan}│${c.reset} ${c.bold}Project:${c.reset} ${projectName.substring(0, 40).padEnd(40)} ${c.cyan}│${c.reset}`);
131
+ console.log(` ${c.cyan}│${c.reset} ${c.bold}Type:${c.reset} ${intentLabel.padEnd(43)} ${c.cyan}│${c.reset}`);
132
+ console.log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`);
133
+ console.log(` ${c.cyan}╰${"─".repeat(65)}╯${c.reset}`);
134
+ console.log();
135
+ }
136
+ }
137
+
138
+ // ═══════════════════════════════════════════════════════════════════════════════
139
+ // ARGS PARSING
140
+ // ═══════════════════════════════════════════════════════════════════════════════
141
+
142
+ function parseArgs(args) {
143
+ const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
144
+
145
+ const opts = {
146
+ path: globalFlags.path || process.cwd(),
147
+ json: globalFlags.json || false,
148
+ ci: globalFlags.ci || false,
149
+ quiet: globalFlags.quiet || false,
150
+ verbose: globalFlags.verbose || false,
151
+ noBanner: globalFlags.noBanner || false,
152
+ help: globalFlags.help || false,
153
+ // Kickoff-specific
154
+ fast: false, // Skip optional steps
155
+ noShip: false, // Skip ship step
156
+ noOpen: false, // Don't open browser
157
+ tier: "standard", // Forge tier
158
+ };
159
+
160
+ for (let i = 0; i < cleanArgs.length; i++) {
161
+ const arg = cleanArgs[i];
162
+ if (arg === "--fast" || arg === "-f") opts.fast = true;
163
+ else if (arg === "--no-ship") opts.noShip = true;
164
+ else if (arg === "--no-open") opts.noOpen = true;
165
+ else if (arg === "--tier" || arg === "-t") opts.tier = cleanArgs[++i] || "standard";
166
+ else if (arg.startsWith("--tier=")) opts.tier = arg.split("=")[1];
167
+ else if (arg === "--path" || arg === "-p") opts.path = cleanArgs[++i] || process.cwd();
168
+ else if (arg.startsWith("--path=")) opts.path = arg.split("=")[1];
169
+ }
170
+
171
+ opts.path = path.resolve(opts.path);
172
+ return opts;
173
+ }
174
+
175
+ function printHelp() {
176
+ console.log(`
177
+ ${c.bold}${c.cyan}VIBECHECK KICKOFF${c.reset} - First Run Dopamine (v${VERSION})
178
+
179
+ ${c.bold}USAGE${c.reset}
180
+ ${c.cyan}vibecheck kickoff${c.reset} [options]
181
+
182
+ ${c.bold}DESCRIPTION${c.reset}
183
+ Get value in 60 seconds. Auto-detects your project type and runs the
184
+ optimal validation pipeline. Perfect for first-time setup.
185
+
186
+ ${c.dim}Flow: link → forge → audit → (optional) ship${c.reset}
187
+
188
+ ${c.bold}OPTIONS${c.reset}
189
+ ${c.cyan}--fast, -f${c.reset} Skip optional checks for speed
190
+ ${c.cyan}--no-ship${c.reset} Skip the ship verdict step
191
+ ${c.cyan}--no-open${c.reset} Don't open summary in browser
192
+ ${c.cyan}--tier <tier>${c.reset} Forge tier: minimal, standard, extended
193
+ ${c.cyan}--path, -p <dir>${c.reset} Project path ${c.dim}(default: current directory)${c.reset}
194
+ ${c.cyan}--json${c.reset} Output as JSON
195
+ ${c.cyan}--ci${c.reset} CI mode (non-interactive)
196
+ ${c.cyan}--quiet, -q${c.reset} Suppress non-essential output
197
+ ${c.cyan}--help, -h${c.reset} Show this help
198
+
199
+ ${c.bold}PROJECT TYPES${c.reset}
200
+ ${c.cyan}Frontend${c.reset} React, Vue, Svelte, Next.js, etc.
201
+ ${c.cyan}API${c.reset} Express, Fastify, NestJS, Hono, etc.
202
+ ${c.cyan}Full-Stack${c.reset} Both frontend and backend detected
203
+ ${c.cyan}Library${c.reset} npm packages, shared modules
204
+
205
+ ${c.bold}EXAMPLES${c.reset}
206
+ ${c.dim}# First run${c.reset}
207
+ vibecheck kickoff
208
+
209
+ ${c.dim}# Fast mode (30s)${c.reset}
210
+ vibecheck kickoff --fast
211
+
212
+ ${c.dim}# Minimal rules tier${c.reset}
213
+ vibecheck kickoff --tier minimal
214
+
215
+ ${c.bold}EXIT CODES${c.reset}
216
+ ${c.green}0${c.reset} Success - All clear
217
+ ${c.yellow}1${c.reset} Warnings - Non-blocking issues found
218
+ ${c.red}2${c.reset} Blockers - Issues need attention
219
+
220
+ ${c.dim}Documentation: https://docs.vibecheckai.dev/commands/kickoff${c.reset}
221
+ `);
222
+ }
223
+
224
+ // ═══════════════════════════════════════════════════════════════════════════════
225
+ // INTENT DETECTION
226
+ // ═══════════════════════════════════════════════════════════════════════════════
227
+
228
+ function detectIntent(projectRoot) {
229
+ const pkgPath = path.join(projectRoot, "package.json");
230
+
231
+ if (!fs.existsSync(pkgPath)) {
232
+ // Check for static site markers
233
+ if (fs.existsSync(path.join(projectRoot, "index.html"))) {
234
+ return { intent: INTENT.STATIC, confidence: "high", evidence: ["index.html present"] };
235
+ }
236
+ return { intent: INTENT.UNKNOWN, confidence: "low", evidence: ["No package.json"] };
237
+ }
238
+
239
+ let pkg;
240
+ try {
241
+ pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
242
+ } catch {
243
+ return { intent: INTENT.UNKNOWN, confidence: "low", evidence: ["Invalid package.json"] };
244
+ }
245
+
246
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
247
+ const depNames = Object.keys(deps);
248
+ const evidence = [];
249
+
250
+ // Check for frontend markers
251
+ const hasFrontend = INTENT_MARKERS.frontend.some(marker => {
252
+ if (depNames.includes(marker)) {
253
+ evidence.push(`${marker} dependency`);
254
+ return true;
255
+ }
256
+ return false;
257
+ });
258
+
259
+ // Check for API markers
260
+ const hasAPI = INTENT_MARKERS.api.some(marker => {
261
+ if (depNames.includes(marker)) {
262
+ evidence.push(`${marker} dependency`);
263
+ return true;
264
+ }
265
+ return false;
266
+ });
267
+
268
+ // Check for library markers
269
+ const isLibrary = INTENT_MARKERS.library.some(marker => {
270
+ if (depNames.includes(marker)) {
271
+ evidence.push(`${marker} (library tooling)`);
272
+ return true;
273
+ }
274
+ return false;
275
+ }) || pkg.main || pkg.module || pkg.exports;
276
+
277
+ // Determine intent
278
+ if (hasFrontend && hasAPI) {
279
+ return { intent: INTENT.FULLSTACK, confidence: "high", evidence };
280
+ }
281
+ if (hasFrontend) {
282
+ return { intent: INTENT.FRONTEND, confidence: "high", evidence };
283
+ }
284
+ if (hasAPI) {
285
+ return { intent: INTENT.API, confidence: "high", evidence };
286
+ }
287
+ if (isLibrary) {
288
+ evidence.push("Library structure detected");
289
+ return { intent: INTENT.LIBRARY, confidence: "medium", evidence };
290
+ }
291
+
292
+ // Fallback checks
293
+ if (fs.existsSync(path.join(projectRoot, "src", "pages")) ||
294
+ fs.existsSync(path.join(projectRoot, "pages")) ||
295
+ fs.existsSync(path.join(projectRoot, "app"))) {
296
+ evidence.push("pages/app directory structure");
297
+ return { intent: INTENT.FRONTEND, confidence: "medium", evidence };
298
+ }
299
+
300
+ if (fs.existsSync(path.join(projectRoot, "src", "routes")) ||
301
+ fs.existsSync(path.join(projectRoot, "routes"))) {
302
+ evidence.push("routes directory structure");
303
+ return { intent: INTENT.API, confidence: "medium", evidence };
304
+ }
305
+
306
+ return { intent: INTENT.UNKNOWN, confidence: "low", evidence: ["Generic Node.js project"] };
307
+ }
308
+
309
+ // ═══════════════════════════════════════════════════════════════════════════════
310
+ // STEP RUNNERS
311
+ // ═══════════════════════════════════════════════════════════════════════════════
312
+
313
+ async function runLinkStep(projectRoot, opts) {
314
+ const start = Date.now();
315
+
316
+ try {
317
+ const { runLink } = require("./runLink");
318
+ const result = await runLink(["--path", projectRoot, "--quiet"]);
319
+
320
+ // Load the generated receipt
321
+ const receiptPath = path.join(projectRoot, ".vibecheck", "project.json");
322
+ let receipt = null;
323
+ if (fs.existsSync(receiptPath)) {
324
+ try {
325
+ receipt = JSON.parse(fs.readFileSync(receiptPath, "utf-8"));
326
+ } catch {}
327
+ }
328
+
329
+ return {
330
+ success: result === 0 || result === 1 || result === 12, // 12 = already linked
331
+ duration: Date.now() - start,
332
+ alreadyLinked: result === 12,
333
+ receipt,
334
+ };
335
+ } catch (err) {
336
+ return {
337
+ success: false,
338
+ duration: Date.now() - start,
339
+ error: err.message,
340
+ };
341
+ }
342
+ }
343
+
344
+ async function runForgeStep(projectRoot, opts, intent) {
345
+ const start = Date.now();
346
+
347
+ try {
348
+ const { runForge } = require("./runForge");
349
+
350
+ // Adjust tier based on intent
351
+ let tier = opts.tier;
352
+ if (opts.fast) {
353
+ tier = "minimal";
354
+ } else if (intent === INTENT.API || intent === INTENT.FULLSTACK) {
355
+ tier = tier === "minimal" ? "standard" : tier;
356
+ }
357
+
358
+ const args = ["--path", projectRoot, "--tier", tier];
359
+ if (opts.quiet || opts.ci) args.push("--quiet");
360
+
361
+ const result = await runForge(args);
362
+
363
+ // Count generated files
364
+ const generated = [];
365
+ const cursorRules = path.join(projectRoot, ".cursorrules");
366
+ if (fs.existsSync(cursorRules)) generated.push(".cursorrules");
367
+
368
+ const cursorDir = path.join(projectRoot, ".cursor", "rules");
369
+ if (fs.existsSync(cursorDir)) {
370
+ const files = fs.readdirSync(cursorDir).filter(f => f.endsWith(".mdc"));
371
+ generated.push(...files.map(f => `.cursor/rules/${f}`));
372
+ }
373
+
374
+ return {
375
+ success: result === 0,
376
+ duration: Date.now() - start,
377
+ tier,
378
+ generated,
379
+ rulesCount: generated.length,
380
+ };
381
+ } catch (err) {
382
+ return {
383
+ success: false,
384
+ duration: Date.now() - start,
385
+ error: err.message,
386
+ };
387
+ }
388
+ }
389
+
390
+ async function runAuditStep(projectRoot, opts, intent) {
391
+ const start = Date.now();
392
+
393
+ try {
394
+ // Try new runAudit first, fall back to runScan
395
+ let runAuditFn;
396
+ try {
397
+ runAuditFn = require("./runAudit").runAudit;
398
+ } catch {
399
+ runAuditFn = require("./runScan").runScan;
400
+ }
401
+
402
+ const args = ["--path", projectRoot];
403
+ if (opts.fast) args.push("--fast");
404
+ if (opts.quiet || opts.ci) args.push("--quiet");
405
+
406
+ // Focus on relevant checks for intent
407
+ if (intent === INTENT.FRONTEND) {
408
+ args.push("--skip", "auth,integrations");
409
+ } else if (intent === INTENT.API) {
410
+ args.push("--skip", "components,ui");
411
+ }
412
+
413
+ const result = await runAuditFn(args);
414
+
415
+ // Load results
416
+ let auditData = null;
417
+ const latestPath = path.join(projectRoot, ".vibecheck", "results", "latest.json");
418
+ const auditPath = path.join(projectRoot, ".vibecheck", "audit", "latest.json");
419
+
420
+ if (fs.existsSync(auditPath)) {
421
+ try { auditData = JSON.parse(fs.readFileSync(auditPath, "utf-8")); } catch {}
422
+ } else if (fs.existsSync(latestPath)) {
423
+ try { auditData = JSON.parse(fs.readFileSync(latestPath, "utf-8")); } catch {}
424
+ }
425
+
426
+ const findings = auditData?.result?.findings || auditData?.findings || [];
427
+ const blockers = findings.filter(f =>
428
+ f.severity === "BLOCK" || f.severity === "critical" || f.severity === "high"
429
+ );
430
+ const warnings = findings.filter(f =>
431
+ f.severity === "WARN" || f.severity === "warning" || f.severity === "medium"
432
+ );
433
+
434
+ return {
435
+ success: true,
436
+ duration: Date.now() - start,
437
+ exitCode: result,
438
+ findings: findings.length,
439
+ blockers: blockers.length,
440
+ warnings: warnings.length,
441
+ topFindings: findings.slice(0, 5).map(f => ({
442
+ type: f.type || f.rule || "issue",
443
+ severity: f.severity,
444
+ message: f.message || f.description,
445
+ file: f.file || f.location?.file,
446
+ })),
447
+ };
448
+ } catch (err) {
449
+ return {
450
+ success: false,
451
+ duration: Date.now() - start,
452
+ error: err.message,
453
+ };
454
+ }
455
+ }
456
+
457
+ async function runShipStep(projectRoot, opts) {
458
+ const start = Date.now();
459
+
460
+ try {
461
+ const { runShip } = require("./runShip");
462
+ const args = ["--path", projectRoot, "--no-banner", "--quiet"];
463
+
464
+ const result = await runShip(args);
465
+
466
+ // Load ship results
467
+ const shipPath = path.join(projectRoot, ".vibecheck", "last_ship.json");
468
+ let shipData = null;
469
+ if (fs.existsSync(shipPath)) {
470
+ try { shipData = JSON.parse(fs.readFileSync(shipPath, "utf-8")); } catch {}
471
+ }
472
+
473
+ const verdict = shipData?.meta?.verdict ||
474
+ (result === 0 ? "SHIP" : result === 1 ? "WARN" : "BLOCK");
475
+ const score = shipData?.proofGraph?.summary?.overallScore ||
476
+ (100 - (shipData?.proofGraph?.summary?.riskScore || 30));
477
+
478
+ return {
479
+ success: true,
480
+ duration: Date.now() - start,
481
+ exitCode: result,
482
+ verdict,
483
+ score: Math.round(score),
484
+ };
485
+ } catch (err) {
486
+ return {
487
+ success: false,
488
+ duration: Date.now() - start,
489
+ error: err.message,
490
+ };
491
+ }
492
+ }
493
+
494
+ // ═══════════════════════════════════════════════════════════════════════════════
495
+ // OUTPUT RENDERING
496
+ // ═══════════════════════════════════════════════════════════════════════════════
497
+
498
+ function printStep(num, total, icon, name, description) {
499
+ const progress = `[${num}/${total}]`;
500
+ process.stdout.write(` ${c.cyan}${progress}${c.reset} ${icon} ${name}... ${c.dim}${description}${c.reset}`);
501
+ }
502
+
503
+ function printStepResult(result, detail = "") {
504
+ const detailStr = detail ? ` ${c.dim}${detail}${c.reset}` : "";
505
+ if (result.success) {
506
+ console.log(` ${c.green}${sym.check}${c.reset}${detailStr}`);
507
+ } else if (result.skipped) {
508
+ console.log(` ${c.yellow}→${c.reset} ${c.dim}skipped${c.reset}`);
509
+ } else {
510
+ console.log(` ${c.red}${sym.cross}${c.reset}${detailStr}`);
511
+ }
512
+ }
513
+
514
+ function printSummaryBox(results, intent, totalDuration) {
515
+ const { link, forge, audit, ship } = results;
516
+
517
+ // Determine overall verdict
518
+ let verdict, verdictIcon, verdictColor;
519
+ if (ship) {
520
+ verdict = ship.verdict;
521
+ verdictIcon = ship.verdict === "SHIP" ? sym.rocket : ship.verdict === "WARN" ? sym.warning : sym.cross;
522
+ verdictColor = ship.verdict === "SHIP" ? c.green : ship.verdict === "WARN" ? c.yellow : c.red;
523
+ } else if (audit) {
524
+ if (audit.blockers > 0) {
525
+ verdict = "NEEDS ATTENTION";
526
+ verdictIcon = sym.target;
527
+ verdictColor = c.yellow;
528
+ } else {
529
+ verdict = "LOOKING GOOD";
530
+ verdictIcon = sym.heart;
531
+ verdictColor = c.green;
532
+ }
533
+ } else {
534
+ verdict = "SETUP COMPLETE";
535
+ verdictIcon = sym.check;
536
+ verdictColor = c.green;
537
+ }
538
+
539
+ console.log();
540
+ console.log(` ${c.cyan}╭${"─".repeat(65)}╮${c.reset}`);
541
+ console.log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`);
542
+ console.log(` ${c.cyan}│${c.reset} ${verdictIcon} ${c.bold}${verdict}${c.reset}${" ".repeat(Math.max(0, 55 - verdict.length))}${c.cyan}│${c.reset}`);
543
+ console.log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`);
544
+ console.log(` ${c.cyan}├${"─".repeat(65)}┤${c.reset}`);
545
+
546
+ // Stats row
547
+ const stats = [];
548
+ if (forge?.rulesCount) stats.push(`${forge.rulesCount} rules`);
549
+ if (audit?.findings !== undefined) stats.push(`${audit.findings} findings`);
550
+ if (audit?.blockers) stats.push(`${c.red}${audit.blockers} blockers${c.reset}`);
551
+ if (ship?.score) stats.push(`${ship.score}/100 score`);
552
+
553
+ const statsLine = stats.join(" ${c.dim}•${c.reset} ");
554
+ console.log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`);
555
+ console.log(` ${c.cyan}│${c.reset} ${statsLine}${" ".repeat(Math.max(0, 62 - statsLine.replace(/\x1b\[[0-9;]*m/g, "").length))}${c.cyan}│${c.reset}`);
556
+ console.log(` ${c.cyan}│${c.reset} ${c.dim}Completed in ${(totalDuration / 1000).toFixed(1)}s${c.reset} ${c.cyan}│${c.reset}`);
557
+ console.log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`);
558
+ console.log(` ${c.cyan}╰${"─".repeat(65)}╯${c.reset}`);
559
+ }
560
+
561
+ function printTopFindings(findings) {
562
+ if (!findings || findings.length === 0) return;
563
+
564
+ console.log();
565
+ console.log(` ${c.bold}Top Findings:${c.reset}`);
566
+ console.log();
567
+
568
+ findings.slice(0, 3).forEach((f, i) => {
569
+ const icon = f.severity === "BLOCK" || f.severity === "critical" ? c.red + sym.cross : c.yellow + sym.warning;
570
+ const msg = (f.message || f.type).substring(0, 55);
571
+ console.log(` ${icon}${c.reset} ${msg}`);
572
+ if (f.file) {
573
+ const file = path.basename(f.file);
574
+ console.log(` ${c.dim}${file}${c.reset}`);
575
+ }
576
+ });
577
+ }
578
+
579
+ function printNextSteps(results, intent, opts) {
580
+ const { audit, ship, forge } = results;
581
+
582
+ console.log();
583
+ console.log(` ${c.bold}${sym.arrow} Next Steps:${c.reset}`);
584
+ console.log();
585
+
586
+ // Determine what to suggest based on results
587
+ if (audit?.blockers > 0) {
588
+ console.log(` ${c.cyan}1.${c.reset} Fix ${audit.blockers} blocker(s):`);
589
+ console.log(` ${c.dim}vibecheck audit --verbose${c.reset} ${c.dim}# See full details${c.reset}`);
590
+ console.log(` ${c.dim}vibecheck fix${c.reset} ${c.dim}# Auto-fix suggestions${c.reset}`);
591
+ console.log();
592
+ console.log(` ${c.cyan}2.${c.reset} Then verify:`);
593
+ console.log(` ${c.dim}vibecheck ship${c.reset} ${c.dim}# Get ship verdict${c.reset}`);
594
+ } else if (!ship && !opts.noShip) {
595
+ console.log(` ${c.cyan}1.${c.reset} Get your ship verdict:`);
596
+ console.log(` ${c.dim}vibecheck ship${c.reset} ${c.dim}# Production readiness check${c.reset}`);
597
+ console.log();
598
+ console.log(` ${c.cyan}2.${c.reset} Before deploying:`);
599
+ console.log(` ${c.dim}vibecheck launch${c.reset} ${c.dim}# Full pre-release wizard${c.reset}`);
600
+ } else if (ship?.verdict === "SHIP") {
601
+ console.log(` ${c.green}${sym.party} Ready to deploy!${c.reset}`);
602
+ console.log();
603
+ console.log(` ${c.cyan}1.${c.reset} Before release:`);
604
+ console.log(` ${c.dim}vibecheck launch${c.reset} ${c.dim}# Full pre-release validation${c.reset}`);
605
+ console.log();
606
+ console.log(` ${c.cyan}2.${c.reset} Ongoing protection:`);
607
+ console.log(` ${c.dim}vibecheck shield${c.reset} ${c.dim}# Enable agent firewall${c.reset}`);
608
+ } else if (ship?.verdict === "WARN") {
609
+ console.log(` ${c.cyan}1.${c.reset} Review warnings:`);
610
+ console.log(` ${c.dim}vibecheck audit --verbose${c.reset} ${c.dim}# See all findings${c.reset}`);
611
+ console.log();
612
+ console.log(` ${c.cyan}2.${c.reset} When ready:`);
613
+ console.log(` ${c.dim}vibecheck launch${c.reset} ${c.dim}# Pre-release validation${c.reset}`);
614
+ } else {
615
+ console.log(` ${c.cyan}1.${c.reset} Fix blockers first:`);
616
+ console.log(` ${c.dim}vibecheck audit --verbose${c.reset} ${c.dim}# See details${c.reset}`);
617
+ console.log(` ${c.dim}vibecheck fix${c.reset} ${c.dim}# Get fix suggestions${c.reset}`);
618
+ }
619
+
620
+ // Add forge tip if rules were generated
621
+ if (forge?.rulesCount > 0) {
622
+ console.log();
623
+ console.log(` ${c.dim}${sym.brain} ${forge.rulesCount} AI rules generated in .cursorrules${c.reset}`);
624
+ console.log(` ${c.dim} Open your IDE to see them in action!${c.reset}`);
625
+ }
626
+
627
+ console.log();
628
+ }
629
+
630
+ // ═══════════════════════════════════════════════════════════════════════════════
631
+ // SHAREABLE SUMMARY
632
+ // ═══════════════════════════════════════════════════════════════════════════════
633
+
634
+ function generateShareableSummary(results, intent, projectName, duration) {
635
+ const { link, forge, audit, ship } = results;
636
+ const timestamp = new Date().toISOString();
637
+ const runId = `kickoff-${crypto.randomBytes(4).toString("hex")}`;
638
+
639
+ // Determine overall status
640
+ let status;
641
+ if (ship?.verdict === "SHIP") status = "ready";
642
+ else if (ship?.verdict === "WARN") status = "warnings";
643
+ else if (audit?.blockers > 0) status = "blockers";
644
+ else status = "setup";
645
+
646
+ const summary = {
647
+ version: VERSION,
648
+ runId,
649
+ timestamp,
650
+ duration: {
651
+ ms: duration,
652
+ formatted: `${(duration / 1000).toFixed(1)}s`,
653
+ },
654
+ project: {
655
+ name: projectName,
656
+ intent: intent.intent,
657
+ confidence: intent.confidence,
658
+ },
659
+ status,
660
+ steps: {
661
+ link: link ? { success: link.success, duration: link.duration, alreadyLinked: link.alreadyLinked } : null,
662
+ forge: forge ? { success: forge.success, duration: forge.duration, tier: forge.tier, rulesCount: forge.rulesCount } : null,
663
+ audit: audit ? { success: audit.success, duration: audit.duration, findings: audit.findings, blockers: audit.blockers, warnings: audit.warnings } : null,
664
+ ship: ship ? { success: ship.success, duration: ship.duration, verdict: ship.verdict, score: ship.score } : null,
665
+ },
666
+ summary: {
667
+ verdict: ship?.verdict || (audit?.blockers > 0 ? "NEEDS_ATTENTION" : "SETUP_COMPLETE"),
668
+ score: ship?.score || null,
669
+ findings: audit?.findings || 0,
670
+ blockers: audit?.blockers || 0,
671
+ warnings: audit?.warnings || 0,
672
+ rulesGenerated: forge?.rulesCount || 0,
673
+ },
674
+ };
675
+
676
+ return summary;
677
+ }
678
+
679
+ function saveSummary(summary, projectRoot) {
680
+ const outputDir = path.join(projectRoot, ".vibecheck", "kickoff");
681
+ fs.mkdirSync(outputDir, { recursive: true });
682
+
683
+ // JSON summary
684
+ const jsonPath = path.join(outputDir, "summary.json");
685
+ fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2));
686
+
687
+ // Markdown summary
688
+ const mdPath = path.join(outputDir, "summary.md");
689
+ const md = generateMarkdownSummary(summary);
690
+ fs.writeFileSync(mdPath, md);
691
+
692
+ return { jsonPath, mdPath };
693
+ }
694
+
695
+ function generateMarkdownSummary(summary) {
696
+ const { project, steps, summary: s } = summary;
697
+
698
+ let md = `# Kickoff Summary\n\n`;
699
+ md += `**Project:** ${project.name}\n`;
700
+ md += `**Type:** ${project.intent}\n`;
701
+ md += `**Date:** ${new Date(summary.timestamp).toLocaleString()}\n`;
702
+ md += `**Duration:** ${summary.duration.formatted}\n\n`;
703
+
704
+ md += `## Results\n\n`;
705
+ md += `| Metric | Value |\n`;
706
+ md += `|--------|-------|\n`;
707
+ md += `| Verdict | ${s.verdict} |\n`;
708
+ if (s.score) md += `| Score | ${s.score}/100 |\n`;
709
+ md += `| Findings | ${s.findings} |\n`;
710
+ md += `| Blockers | ${s.blockers} |\n`;
711
+ md += `| Warnings | ${s.warnings} |\n`;
712
+ md += `| Rules Generated | ${s.rulesGenerated} |\n`;
713
+ md += `\n`;
714
+
715
+ md += `## Steps\n\n`;
716
+ if (steps.link) md += `- [${steps.link.success ? "✓" : "✗"}] Link: ${steps.link.alreadyLinked ? "Already linked" : "Project bound"}\n`;
717
+ if (steps.forge) md += `- [${steps.forge.success ? "✓" : "✗"}] Forge: ${steps.forge.rulesCount} rules (${steps.forge.tier} tier)\n`;
718
+ if (steps.audit) md += `- [${steps.audit.success ? "✓" : "✗"}] Audit: ${steps.audit.findings} findings\n`;
719
+ if (steps.ship) md += `- [${steps.ship.success ? "✓" : "✗"}] Ship: ${steps.ship.verdict} (${steps.ship.score}/100)\n`;
720
+ md += `\n`;
721
+
722
+ md += `---\n`;
723
+ md += `*Generated by vibecheck kickoff v${summary.version}*\n`;
724
+
725
+ return md;
726
+ }
727
+
728
+ // ═══════════════════════════════════════════════════════════════════════════════
729
+ // MAIN
730
+ // ═══════════════════════════════════════════════════════════════════════════════
731
+
732
+ async function runKickoff(args, context = {}) {
733
+ const startTime = Date.now();
734
+ const opts = parseArgs(args);
735
+
736
+ // Help
737
+ if (opts.help) {
738
+ printHelp();
739
+ return EXIT.SUCCESS;
740
+ }
741
+
742
+ const projectRoot = opts.path;
743
+ const projectName = path.basename(projectRoot);
744
+
745
+ // Validate path
746
+ if (!fs.existsSync(projectRoot)) {
747
+ if (!opts.quiet) {
748
+ console.error(`${c.red}${sym.cross}${c.reset} Project path does not exist: ${projectRoot}`);
749
+ }
750
+ return EXIT.USER_ERROR;
751
+ }
752
+
753
+ // Detect intent
754
+ const intent = detectIntent(projectRoot);
755
+
756
+ // Show banner
757
+ if (!opts.quiet && !opts.ci && !opts.noBanner) {
758
+ printWelcomeBanner(projectName, intent.intent, projectRoot);
759
+
760
+ if (opts.verbose && intent.evidence.length > 0) {
761
+ console.log(` ${c.dim}Detected: ${intent.evidence.slice(0, 2).join(", ")}${c.reset}`);
762
+ console.log();
763
+ }
764
+ }
765
+
766
+ // Build step list based on options
767
+ const steps = [
768
+ { id: "link", name: "Link", icon: sym.link, desc: "binding project", runner: runLinkStep },
769
+ { id: "forge", name: "Forge", icon: sym.brain, desc: "generating AI rules", runner: runForgeStep },
770
+ { id: "audit", name: "Audit", icon: sym.search, desc: "scanning for issues", runner: runAuditStep },
771
+ ];
772
+
773
+ // Add ship step unless skipped or has blockers
774
+ if (!opts.noShip && !opts.fast) {
775
+ steps.push({ id: "ship", name: "Ship", icon: sym.rocket, desc: "computing verdict", runner: runShipStep });
776
+ }
777
+
778
+ const results = {};
779
+ let exitCode = EXIT.SUCCESS;
780
+
781
+ // Run steps
782
+ for (let i = 0; i < steps.length; i++) {
783
+ const step = steps[i];
784
+
785
+ if (!opts.quiet && !opts.ci) {
786
+ printStep(i + 1, steps.length, step.icon, step.name, step.desc);
787
+ }
788
+
789
+ const result = await step.runner(projectRoot, opts, intent.intent);
790
+ results[step.id] = result;
791
+
792
+ // Format detail for output
793
+ let detail = "";
794
+ if (step.id === "link" && result.alreadyLinked) detail = "(already linked)";
795
+ if (step.id === "forge" && result.rulesCount) detail = `(${result.rulesCount} rules)`;
796
+ if (step.id === "audit" && result.findings !== undefined) {
797
+ detail = result.blockers > 0
798
+ ? `(${result.blockers} blockers)`
799
+ : `(${result.findings} findings)`;
800
+ }
801
+ if (step.id === "ship" && result.verdict) detail = `(${result.verdict})`;
802
+
803
+ if (!opts.quiet && !opts.ci) {
804
+ printStepResult(result, detail);
805
+ }
806
+
807
+ // Check for blockers after audit - skip ship if blockers found
808
+ if (step.id === "audit" && result.blockers > 0 && steps.find(s => s.id === "ship")) {
809
+ // Remove ship step
810
+ const shipIndex = steps.findIndex(s => s.id === "ship");
811
+ if (shipIndex > i) {
812
+ steps.splice(shipIndex, 1);
813
+ if (!opts.quiet && !opts.ci) {
814
+ console.log(` ${c.dim}${sym.arrow} Skipping ship (blockers found)${c.reset}`);
815
+ }
816
+ }
817
+ }
818
+
819
+ // Update exit code
820
+ if (!result.success) {
821
+ exitCode = Math.max(exitCode, EXIT.WARNINGS);
822
+ }
823
+ }
824
+
825
+ const totalDuration = Date.now() - startTime;
826
+
827
+ // Generate summary
828
+ const summary = generateShareableSummary(results, intent, projectName, totalDuration);
829
+ const saved = saveSummary(summary, projectRoot);
830
+
831
+ // Output
832
+ if (opts.json) {
833
+ console.log(JSON.stringify(summary, null, 2));
834
+ } else if (opts.ci) {
835
+ console.log(`VERDICT=${summary.summary.verdict}`);
836
+ console.log(`SCORE=${summary.summary.score || 0}`);
837
+ console.log(`FINDINGS=${summary.summary.findings}`);
838
+ console.log(`BLOCKERS=${summary.summary.blockers}`);
839
+ console.log(`RULES=${summary.summary.rulesGenerated}`);
840
+ console.log(`DURATION_MS=${totalDuration}`);
841
+ } else if (!opts.quiet) {
842
+ printSummaryBox(results, intent.intent, totalDuration);
843
+
844
+ if (results.audit?.topFindings?.length > 0 && opts.verbose) {
845
+ printTopFindings(results.audit.topFindings);
846
+ }
847
+
848
+ printNextSteps(results, intent.intent, opts);
849
+
850
+ console.log(` ${c.dim}📄 Summary saved: ${path.relative(projectRoot, saved.mdPath)}${c.reset}`);
851
+ console.log();
852
+ }
853
+
854
+ // Determine final exit code
855
+ if (results.audit?.blockers > 0) {
856
+ exitCode = EXIT.BLOCKING;
857
+ } else if (results.audit?.warnings > 0 || results.ship?.verdict === "WARN") {
858
+ exitCode = EXIT.WARNINGS;
859
+ }
860
+
861
+ return exitCode;
862
+ }
863
+
864
+ // ═══════════════════════════════════════════════════════════════════════════════
865
+ // EXPORTS
866
+ // ═══════════════════════════════════════════════════════════════════════════════
867
+
868
+ module.exports = { runKickoff };
869
+
870
+ // Direct execution
871
+ if (require.main === module) {
872
+ runKickoff(process.argv.slice(2))
873
+ .then(code => process.exit(code))
874
+ .catch(err => {
875
+ console.error(err);
876
+ process.exit(10);
877
+ });
878
+ }