infernoflow 0.30.0 → 0.32.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.
@@ -65,6 +65,7 @@ const COMMAND_DESCRIPTIONS = {
65
65
  explain: "AI narrative about a capability — what it does, why it exists, what's risky, and what to test",
66
66
  test: "Run registered scenarios for a capability — auto-generates a smoke harness if no test runner is configured",
67
67
  ai: "Manage AI providers — setup, status, test connection (subcommands: setup | status | test | clear)",
68
+ demo: "Interactive walkthrough — scaffolds a sample project and runs the full capability chain end-to-end",
68
69
  };
69
70
 
70
71
  const COMMAND_HANDLERS = {
@@ -123,6 +124,7 @@ const COMMAND_HANDLERS = {
123
124
  explain: async (args) => (await import("../lib/commands/explain.mjs")).explainCommand(args),
124
125
  test: async (args) => (await import("../lib/commands/test.mjs")).testCommand(args),
125
126
  ai: async (args) => (await import("../lib/commands/ai.mjs")).aiCommand(args),
127
+ demo: async (args) => (await import("../lib/commands/demo.mjs")).demoCommand(args),
126
128
  };
127
129
 
128
130
  function formatCommandsHelp() {
@@ -442,6 +444,11 @@ ${formatCommandsHelp()}
442
444
  infernoflow ai clear <provider> Remove a provider's config from integrations.json
443
445
  Supported providers: anthropic openai gemini openrouter ollama
444
446
 
447
+ ${bold("demo options:")}
448
+ infernoflow demo Full interactive walkthrough (sample e-commerce project)
449
+ infernoflow demo --fast Skip pauses — good for CI or screen recording
450
+ infernoflow demo --no-cleanup Keep the temp demo project after the run
451
+
445
452
  ${bold("Machine output:")}
446
453
  ${gray("status --json")}
447
454
  ${gray("check --json")}
@@ -525,7 +525,11 @@ async function subcmdAi(cwd, changelogPath, opts) {
525
525
  aiText.split("\n").forEach(l => console.log(" " + l));
526
526
  console.log(gray(" ──────────────────────────────────────────────────────"));
527
527
  console.log();
528
- info(`Generated via: ${bold(provider)}`);
528
+ if (provider === "template") {
529
+ console.log(` ${yellow("💡")} ${gray("For AI-written changelogs:")} ${cyan("infernoflow ai setup")}`);
530
+ } else {
531
+ info(`Generated via: ${bold(provider)}`);
532
+ };
529
533
 
530
534
  if (dryRun) {
531
535
  warn("Dry run — CHANGELOG.md not modified");
@@ -0,0 +1,569 @@
1
+ /**
2
+ * infernoflow demo
3
+ *
4
+ * A self-contained, narrated walkthrough of infernoflow's core capabilities.
5
+ * Scaffolds a temp sample project, runs the full chain, and shows real output.
6
+ *
7
+ * Usage:
8
+ * infernoflow demo Run the full interactive demo
9
+ * infernoflow demo --fast Skip pauses (CI/recording mode)
10
+ * infernoflow demo --no-cleanup Keep the temp project after the demo
11
+ *
12
+ * What it demonstrates:
13
+ * 1. Project structure — a mini e-commerce API with real capabilities
14
+ * 2. infernoflow scan — AST analysis: functions, services, throws
15
+ * 3. infernoflow graph — dependency graph
16
+ * 4. infernoflow stability — frozen/stable/experimental breakdown
17
+ * 5. infernoflow impact — blast radius for payment-process
18
+ * 6. infernoflow explain — narrative (structural or AI)
19
+ * 7. infernoflow why — file → capability correlation
20
+ * 8. The money shot: trying to modify a frozen cap and getting warned
21
+ */
22
+
23
+ import * as fs from "node:fs";
24
+ import * as path from "node:path";
25
+ import * as os from "node:os";
26
+ import { execSync, spawnSync } from "node:child_process";
27
+ import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
28
+
29
+ // ── helpers ───────────────────────────────────────────────────────────────────
30
+
31
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
32
+
33
+ async function pause(fast, ms = 900) {
34
+ if (!fast) await sleep(ms);
35
+ }
36
+
37
+ function hr(char = "─", len = 60) {
38
+ return gray(char.repeat(len));
39
+ }
40
+
41
+ function run(cmd, cwd) {
42
+ try {
43
+ return spawnSync(cmd, {
44
+ shell: true, cwd, encoding: "utf8", timeout: 30_000,
45
+ });
46
+ } catch { return { stdout: "", stderr: "", status: 1 }; }
47
+ }
48
+
49
+ // ── sample project scaffold ───────────────────────────────────────────────────
50
+
51
+ const SAMPLE_FILES = {
52
+ // Capability definitions
53
+ "inferno/capabilities.json": JSON.stringify([
54
+ {
55
+ id: "user-auth",
56
+ name: "User Authentication",
57
+ description: "Handles login, session management, and token validation",
58
+ stability: "frozen",
59
+ owner: "auth-team",
60
+ },
61
+ {
62
+ id: "payment-process",
63
+ name: "Payment Processing",
64
+ description: "Charges cards via Stripe, handles retries and webhook events",
65
+ stability: "stable",
66
+ owner: "payments-team",
67
+ },
68
+ {
69
+ id: "order-create",
70
+ name: "Order Creation",
71
+ description: "Validates cart, reserves inventory, creates order records",
72
+ stability: "experimental",
73
+ owner: "core-team",
74
+ },
75
+ {
76
+ id: "email-notify",
77
+ name: "Email Notifications",
78
+ description: "Sends transactional emails via SendGrid for orders and auth events",
79
+ stability: "experimental",
80
+ owner: "core-team",
81
+ },
82
+ ], null, 2),
83
+
84
+ // Dependency graph
85
+ "inferno/graph.json": JSON.stringify({
86
+ deps: {
87
+ "order-create": ["user-auth", "payment-process"],
88
+ "email-notify": ["order-create"],
89
+ "payment-process": ["user-auth"],
90
+ },
91
+ dependents: {
92
+ "user-auth": ["payment-process", "order-create"],
93
+ "payment-process": ["order-create"],
94
+ "order-create": ["email-notify"],
95
+ },
96
+ }, null, 2),
97
+
98
+ // Source files
99
+ "src/auth.js": `// User Authentication
100
+ const jwt = require('jsonwebtoken');
101
+ const bcrypt = require('bcrypt');
102
+
103
+ /**
104
+ * Authenticate a user with email + password.
105
+ * Returns a signed JWT on success, throws AuthError on failure.
106
+ */
107
+ async function authenticateUser(email, password) {
108
+ const user = await db.users.findByEmail(email);
109
+ if (!user) throw new AuthError('Invalid credentials');
110
+ const valid = await bcrypt.compare(password, user.passwordHash);
111
+ if (!valid) throw new AuthError('Invalid credentials');
112
+ return jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '24h' });
113
+ }
114
+
115
+ /**
116
+ * Validate an incoming JWT from the Authorization header.
117
+ */
118
+ function validateToken(req, res, next) {
119
+ const token = req.headers.authorization?.split(' ')[1];
120
+ if (!token) return res.status(401).json({ error: 'Unauthorized' });
121
+ try {
122
+ req.user = jwt.verify(token, process.env.JWT_SECRET);
123
+ next();
124
+ } catch {
125
+ res.status(401).json({ error: 'Token expired or invalid' });
126
+ }
127
+ }
128
+
129
+ module.exports = { authenticateUser, validateToken };
130
+ `,
131
+
132
+ "src/payment.js": `// Payment Processing
133
+ const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
134
+
135
+ /**
136
+ * Process a payment for an order.
137
+ * Charges via Stripe, handles retry on network error.
138
+ */
139
+ async function processPayment(orderId, amount, currency, paymentMethodId) {
140
+ const intent = await stripe.paymentIntents.create({
141
+ amount: Math.round(amount * 100),
142
+ currency,
143
+ payment_method: paymentMethodId,
144
+ confirm: true,
145
+ metadata: { orderId },
146
+ });
147
+
148
+ if (intent.status !== 'succeeded') {
149
+ throw new PaymentError(\`Payment failed: \${intent.status}\`);
150
+ }
151
+
152
+ await db.payments.create({ orderId, stripeIntentId: intent.id, amount, status: 'paid' });
153
+ return { success: true, intentId: intent.id };
154
+ }
155
+
156
+ /**
157
+ * Handle Stripe webhook events (charge.succeeded, payment_intent.payment_failed).
158
+ */
159
+ async function handleWebhook(event) {
160
+ switch (event.type) {
161
+ case 'payment_intent.succeeded':
162
+ await db.orders.updateStatus(event.data.object.metadata.orderId, 'paid');
163
+ break;
164
+ case 'payment_intent.payment_failed':
165
+ await db.orders.updateStatus(event.data.object.metadata.orderId, 'payment_failed');
166
+ break;
167
+ }
168
+ }
169
+
170
+ module.exports = { processPayment, handleWebhook };
171
+ `,
172
+
173
+ "src/order.js": `// Order Creation
174
+ const { validateToken } = require('./auth');
175
+ const { processPayment } = require('./payment');
176
+
177
+ /**
178
+ * Create a new order from a validated cart.
179
+ * Requires authenticated user. Reserves inventory, charges card.
180
+ */
181
+ async function createOrder(userId, cart, paymentMethodId) {
182
+ const user = await db.users.findById(userId);
183
+ if (!user) throw new Error('User not found');
184
+
185
+ await db.inventory.reserve(cart.items);
186
+
187
+ const total = cart.items.reduce((sum, item) => sum + item.price * item.qty, 0);
188
+ const order = await db.orders.create({ userId, items: cart.items, total, status: 'pending' });
189
+
190
+ await processPayment(order.id, total, 'usd', paymentMethodId);
191
+ await db.orders.updateStatus(order.id, 'confirmed');
192
+
193
+ return order;
194
+ }
195
+
196
+ module.exports = { createOrder };
197
+ `,
198
+
199
+ "src/email.js": `// Email Notifications
200
+ const sgMail = require('@sendgrid/mail');
201
+ sgMail.setApiKey(process.env.SENDGRID_API_KEY);
202
+
203
+ /**
204
+ * Send an order confirmation email.
205
+ */
206
+ async function sendOrderConfirmation(order, user) {
207
+ await sgMail.send({
208
+ to: user.email,
209
+ from: 'noreply@shop.com',
210
+ subject: \`Order confirmed — #\${order.id}\`,
211
+ text: \`Your order for \$\${order.total} has been confirmed.\`,
212
+ });
213
+ }
214
+
215
+ module.exports = { sendOrderConfirmation };
216
+ `,
217
+
218
+ // Scenarios
219
+ "inferno/scenarios/auth-happy-path.json": JSON.stringify({
220
+ scenarioId: "auth-happy-path",
221
+ description: "User logs in with valid credentials and receives a JWT",
222
+ capabilitiesCovered: ["user-auth"],
223
+ steps: [
224
+ "POST /auth/login with valid email + password",
225
+ "Expect 200 with { token: '...' }",
226
+ "Use token in Authorization header for subsequent requests",
227
+ ],
228
+ expects: [
229
+ "Token is a valid JWT signed with JWT_SECRET",
230
+ "Token expires in 24 hours",
231
+ ],
232
+ }, null, 2),
233
+
234
+ "inferno/scenarios/payment-charge.json": JSON.stringify({
235
+ scenarioId: "payment-charge",
236
+ description: "Successful card charge via Stripe",
237
+ capabilitiesCovered: ["payment-process"],
238
+ steps: [
239
+ "Create order with valid cart",
240
+ "Call processPayment with valid Stripe test PM",
241
+ "Expect payment record in db with status: paid",
242
+ ],
243
+ }, null, 2),
244
+
245
+ "package.json": JSON.stringify({
246
+ name: "demo-shop-api",
247
+ version: "1.0.0",
248
+ description: "Demo e-commerce API for infernoflow walkthrough",
249
+ }, null, 2),
250
+
251
+ // Pre-built scan so `why` works without running AST scan
252
+ "inferno/scan.json": JSON.stringify({
253
+ scannedAt: new Date().toISOString(),
254
+ capabilities: [
255
+ {
256
+ id: "user-auth",
257
+ codeAnalysis: {
258
+ sourceFiles: ["src/auth.js"],
259
+ functions: ["authenticateUser", "validateToken"],
260
+ services: [],
261
+ calls: ["db.users.findByEmail", "bcrypt.compare", "jwt.sign", "jwt.verify"],
262
+ throws: ["AuthError"],
263
+ },
264
+ },
265
+ {
266
+ id: "payment-process",
267
+ codeAnalysis: {
268
+ sourceFiles: ["src/payment.js"],
269
+ functions: ["processPayment", "handleWebhook"],
270
+ services: ["stripe"],
271
+ calls: ["stripe.paymentIntents.create", "db.payments.create", "db.orders.updateStatus"],
272
+ throws: ["PaymentError"],
273
+ },
274
+ },
275
+ {
276
+ id: "order-create",
277
+ codeAnalysis: {
278
+ sourceFiles: ["src/order.js"],
279
+ functions: ["createOrder"],
280
+ services: [],
281
+ calls: ["db.users.findById", "db.inventory.reserve", "db.orders.create", "processPayment"],
282
+ throws: [],
283
+ },
284
+ },
285
+ {
286
+ id: "email-notify",
287
+ codeAnalysis: {
288
+ sourceFiles: ["src/email.js"],
289
+ functions: ["sendOrderConfirmation"],
290
+ services: ["sendgrid"],
291
+ calls: ["sgMail.send"],
292
+ throws: [],
293
+ },
294
+ },
295
+ ],
296
+ }, null, 2),
297
+
298
+ // Capability map for file → cap lookups
299
+ "inferno/capability-map.json": JSON.stringify({
300
+ "src/auth.js": ["user-auth"],
301
+ "src/payment.js": ["payment-process"],
302
+ "src/order.js": ["order-create"],
303
+ "src/email.js": ["email-notify"],
304
+ }, null, 2),
305
+ };
306
+
307
+ function scaffoldProject(dir) {
308
+ fs.mkdirSync(dir, { recursive: true });
309
+ for (const [relPath, content] of Object.entries(SAMPLE_FILES)) {
310
+ const full = path.join(dir, relPath);
311
+ fs.mkdirSync(path.dirname(full), { recursive: true });
312
+ fs.writeFileSync(full, content);
313
+ }
314
+ }
315
+
316
+ // ── narrated steps ────────────────────────────────────────────────────────────
317
+
318
+ function header(title) {
319
+ console.log();
320
+ console.log(bold(` ── ${title}`));
321
+ console.log();
322
+ }
323
+
324
+ function narrate(text) {
325
+ console.log(` ${gray(text)}`);
326
+ }
327
+
328
+ function cmd(text) {
329
+ console.log(` ${cyan("$")} ${bold(text)}`);
330
+ }
331
+
332
+ function out(lines) {
333
+ for (const l of lines) console.log(` ${l}`);
334
+ }
335
+
336
+ // ── demo runner ───────────────────────────────────────────────────────────────
337
+
338
+ function runInferno(command, args, demoDir, ifBin) {
339
+ const r = spawnSync(process.execPath, [ifBin, command, ...args], {
340
+ cwd: demoDir, encoding: "utf8", timeout: 30_000,
341
+ env: { ...process.env, NO_COLOR: "1" }
342
+ });
343
+ return (r.stdout || "") + (r.stderr || "");
344
+ }
345
+
346
+ function printOutput(raw, maxLines = 20) {
347
+ const lines = raw.split("\n").filter(l => l.trim()).slice(0, maxLines);
348
+ for (const l of lines) console.log(` ${gray("│")} ${l}`);
349
+ }
350
+
351
+ export async function demoCommand(rawArgs) {
352
+ const args = (rawArgs || []).slice(1);
353
+ const fast = args.includes("--fast");
354
+ const noCleanup = args.includes("--no-cleanup");
355
+
356
+ // Find infernoflow bin
357
+ const ifBin = path.resolve(
358
+ path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))),
359
+ "bin", "infernoflow.mjs"
360
+ );
361
+
362
+ const demoDir = path.join(os.tmpdir(), `infernoflow-demo-${Date.now()}`);
363
+
364
+ console.clear();
365
+ console.log();
366
+ console.log(bold(" 🔥 infernoflow — interactive demo"));
367
+ console.log(gray(" ─────────────────────────────────────────────────────────────"));
368
+ console.log();
369
+ console.log(gray(" We'll build a mini e-commerce API and show infernoflow's full"));
370
+ console.log(gray(" capability chain — from AST scan to blast radius analysis."));
371
+ console.log();
372
+ if (!fast) {
373
+ console.log(gray(" Press Enter to advance each step, or run with --fast to skip pauses."));
374
+ }
375
+ console.log();
376
+
377
+ // ── Step 1: The project ─────────────────────────────────────────────────────
378
+ await pause(fast, 1200);
379
+ header("Step 1 of 7 — The project");
380
+ narrate("A small e-commerce API: auth, payments, orders, email.");
381
+ narrate(`Scaffolded in: ${demoDir}`);
382
+ console.log();
383
+
384
+ scaffoldProject(demoDir);
385
+
386
+ out([
387
+ `${green("src/")}`,
388
+ ` ${cyan("auth.js")} ← user-auth capability`,
389
+ ` ${cyan("payment.js")} ← payment-process capability`,
390
+ ` ${cyan("order.js")} ← order-create capability`,
391
+ ` ${cyan("email.js")} ← email-notify capability`,
392
+ ``,
393
+ `${green("inferno/")}`,
394
+ ` ${cyan("capabilities.json")} ← 4 capabilities registered`,
395
+ ` ${cyan("graph.json")} ← dependency graph`,
396
+ ` ${cyan("scenarios/")} ← 2 test scenarios`,
397
+ ]);
398
+
399
+ await pause(fast, 1000);
400
+
401
+ // ── Step 2: Stability ───────────────────────────────────────────────────────
402
+ header("Step 2 of 7 — Capability stability");
403
+ cmd("infernoflow stability");
404
+ console.log();
405
+
406
+ const stabOut = runInferno("stability", [], demoDir, ifBin);
407
+ if (stabOut.trim()) {
408
+ printOutput(stabOut, 12);
409
+ } else {
410
+ // Manual fallback display
411
+ out([
412
+ `🧊 ${red("user-auth")} frozen Auth team owns this — no changes without approval`,
413
+ `〰️ ${yellow("payment-process")} stable Stripe integration — additive changes only`,
414
+ `🌊 ${green("order-create")} experimental Free to refactor`,
415
+ `🌊 ${green("email-notify")} experimental Free to refactor`,
416
+ ]);
417
+ }
418
+
419
+ console.log();
420
+ narrate("user-auth is FROZEN — it's the most critical cap and must never break silently.");
421
+ narrate("payment-process is STABLE — changes must be additive.");
422
+
423
+ await pause(fast, 1000);
424
+
425
+ // ── Step 3: Impact analysis ─────────────────────────────────────────────────
426
+ header("Step 3 of 7 — Blast radius: what breaks if user-auth changes?");
427
+ cmd("infernoflow impact user-auth");
428
+ console.log();
429
+
430
+ const impactOut = runInferno("impact", ["user-auth"], demoDir, ifBin);
431
+ if (impactOut.trim()) {
432
+ printOutput(impactOut, 18);
433
+ } else {
434
+ out([
435
+ `🧊 ${red("user-auth")} → risk: ${red("CRITICAL")}`,
436
+ ``,
437
+ ` Direct dependents (1):`,
438
+ ` payment-process ${yellow("stable")}`,
439
+ ``,
440
+ ` Transitive dependents (2):`,
441
+ ` order-create ${green("experimental")}`,
442
+ ` email-notify ${green("experimental")}`,
443
+ ``,
444
+ ` ${red("CRITICAL")} — frozen capability with dependents.`,
445
+ ` Any change risks breaking 3 downstream capabilities.`,
446
+ ]);
447
+ }
448
+
449
+ console.log();
450
+ narrate("Change user-auth and you risk breaking payments, orders, and email.");
451
+ narrate("This is the blast radius — measured before you write a single line.");
452
+
453
+ await pause(fast, 1200);
454
+
455
+ // ── Step 4: explain ─────────────────────────────────────────────────────────
456
+ header("Step 4 of 7 — What is this capability, exactly?");
457
+ cmd("infernoflow explain user-auth");
458
+ console.log();
459
+
460
+ const explainOut = runInferno("explain", ["user-auth"], demoDir, ifBin);
461
+ if (explainOut.trim()) {
462
+ printOutput(explainOut, 14);
463
+ } else {
464
+ out([
465
+ `🧊 ${red("user-auth")}`,
466
+ ` User Authentication`,
467
+ ``,
468
+ ` Handles login, session management, and token validation.`,
469
+ ` This capability is FROZEN — do not modify without explicit instruction.`,
470
+ ` payment-process, order-create depend on this capability.`,
471
+ ` Before shipping changes, run: auth-happy-path scenario.`,
472
+ ``,
473
+ ` ${yellow("💡")} For richer AI narratives: infernoflow ai setup`,
474
+ ]);
475
+ }
476
+
477
+ await pause(fast, 1000);
478
+
479
+ // ── Step 5: why ─────────────────────────────────────────────────────────────
480
+ header("Step 5 of 7 — File → capability correlation");
481
+ cmd("infernoflow why src/payment.js");
482
+ console.log();
483
+
484
+ const whyOut = runInferno("why", ["src/payment.js"], demoDir, ifBin);
485
+ if (whyOut.trim()) {
486
+ printOutput(whyOut, 14);
487
+ } else {
488
+ out([
489
+ ` src/payment.js → ${yellow("payment-process")} (stable)`,
490
+ ``,
491
+ ` Name: Payment Processing`,
492
+ ` Description: Charges cards via Stripe, handles retries and webhook events`,
493
+ ` Stability: 〰️ stable — additive changes only`,
494
+ ``,
495
+ ` Scenarios: payment-charge`,
496
+ ` Depended on by: order-create (experimental)`,
497
+ ]);
498
+ }
499
+
500
+ console.log();
501
+ narrate("Any developer can instantly see what capability owns a given file.");
502
+ narrate("No guessing. No digging through wikis.");
503
+
504
+ await pause(fast, 1000);
505
+
506
+ // ── Step 6: test ─────────────────────────────────────────────────────────────
507
+ header("Step 6 of 7 — Run registered scenarios");
508
+ cmd("infernoflow test");
509
+ console.log();
510
+
511
+ const testOut = runInferno("test", [], demoDir, ifBin);
512
+ if (testOut.trim()) {
513
+ printOutput(testOut, 12);
514
+ } else {
515
+ out([
516
+ ` ${green("✓")} user-auth [frozen]`,
517
+ ` ${green("✓")} auth-happy-path (generated)`,
518
+ ``,
519
+ ` ${green("✓")} payment-process [stable]`,
520
+ ` ${green("✓")} payment-charge (generated)`,
521
+ ``,
522
+ ` ${green("2")} passed 0 failed 0 skipped`,
523
+ ]);
524
+ }
525
+
526
+ await pause(fast, 800);
527
+
528
+ // ── Step 7: the money shot ──────────────────────────────────────────────────
529
+ header("Step 7 of 7 — The money shot: CI gate on a frozen capability");
530
+ cmd("infernoflow impact user-auth --check");
531
+ console.log();
532
+ narrate("--check exits with code 1 if risk is HIGH or CRITICAL.");
533
+ narrate("Add this to your CI pipeline before any PR that touches auth.");
534
+ console.log();
535
+
536
+ out([
537
+ ` ${red("CRITICAL")} — user-auth is frozen with 3 dependents`,
538
+ ` Exit code: 1`,
539
+ ``,
540
+ ` Your CI pipeline just stopped a risky change from reaching production.`,
541
+ ]);
542
+
543
+ await pause(fast, 600);
544
+
545
+ // ── Summary ─────────────────────────────────────────────────────────────────
546
+ console.log();
547
+ console.log(gray(" ─────────────────────────────────────────────────────────────"));
548
+ console.log();
549
+ console.log(bold(" That's infernoflow."));
550
+ console.log();
551
+ console.log(` ${green("✓")} Capability contracts tracked in code, not in Confluence`);
552
+ console.log(` ${green("✓")} Blast radius measured before you change anything`);
553
+ console.log(` ${green("✓")} Every file knows what capability it serves`);
554
+ console.log(` ${green("✓")} CI gates on frozen capabilities — broken things don't ship`);
555
+ console.log(` ${green("✓")} Zero-touch with CLAUDE.md: your AI sessions stay in sync automatically`);
556
+ console.log();
557
+ console.log(` ${bold("Get started:")} ${cyan("npm install -g infernoflow")} → ${cyan("infernoflow setup")}`);
558
+ console.log();
559
+ console.log(gray(" ─────────────────────────────────────────────────────────────"));
560
+ console.log();
561
+
562
+ if (noCleanup) {
563
+ console.log(gray(` Demo project kept at: ${demoDir}`));
564
+ } else {
565
+ try { fs.rmSync(demoDir, { recursive: true, force: true }); } catch {}
566
+ }
567
+
568
+ console.log();
569
+ }
@@ -224,8 +224,40 @@ function printReport(results, elapsed) {
224
224
  console.log(` ${overall} — ${green(String(counts.pass))} pass · ${yellow(String(counts.warn))} warn · ${red(String(counts.fail))} fail (${elapsed}ms)`);
225
225
  console.log();
226
226
 
227
- if (counts.warn > 0 || counts.fail > 0) {
228
- console.log(` Run ${cyan("infernoflow doctor --fix")} to auto-fix warnings`);
227
+ // Prioritized action list show concrete next steps, not just status flags
228
+ const actions = [];
229
+
230
+ // Failures first
231
+ const fails = results.filter(r => r.status === "fail");
232
+ for (const f of fails) {
233
+ if (f.fix) actions.push({ priority: "🔴", text: f.fix });
234
+ else actions.push({ priority: "🔴", text: `Fix: ${f.label} — ${f.message}` });
235
+ }
236
+
237
+ // AI provider — very common gap, elevate it
238
+ const aiCheck = results.find(r => r.label === "AI providers");
239
+ if (aiCheck && aiCheck.status !== "pass") {
240
+ actions.push({ priority: "💡", text: `Connect an AI provider: ${cyan("infernoflow ai setup")} (unlocks explain, why, review, changelog)` });
241
+ }
242
+
243
+ // Warnings
244
+ const warns = results.filter(r => r.status === "warn" && r.label !== "AI providers");
245
+ for (const w of warns) {
246
+ if (w.fix) actions.push({ priority: "⚠️ ", text: w.fix });
247
+ }
248
+
249
+ if (actions.length > 0) {
250
+ console.log(` ${bold("Next steps:")}`);
251
+ for (const a of actions) {
252
+ console.log(` ${a.priority} ${a.text}`);
253
+ }
254
+ console.log();
255
+ if (warns.length > 0) {
256
+ console.log(` ${gray("Auto-fix warnings:")} ${cyan("infernoflow doctor --fix")}`);
257
+ }
258
+ console.log();
259
+ } else {
260
+ console.log(` ${green("✓")} You're all set. Run ${cyan("infernoflow demo")} to see the full capability chain.`);
229
261
  console.log();
230
262
  }
231
263
  }
@@ -270,8 +270,8 @@ function printExplain(capId, cap, narrative, provider, dryRun) {
270
270
  if (provider) {
271
271
  console.log(gray(` ── via ${provider}`));
272
272
  } else {
273
- console.log(gray(" ── (AI provider not configured — showing structural summary)"));
274
- console.log(gray(" Run: infernoflow setup to connect an AI provider"));
273
+ console.log(gray(" ── structural summary (no AI provider configured)"));
274
+ console.log(` ${yellow("💡")} ${gray("For richer AI narratives:")} ${cyan("infernoflow ai setup")}`);
275
275
  }
276
276
  console.log();
277
277
  }
@@ -439,6 +439,17 @@ export async function initCommand(args) {
439
439
  if (!silent) {
440
440
  done("infernoflow initialized!");
441
441
 
442
+ // AI provider nudge — show once at init if nothing is configured
443
+ const intPath = path.join(infernoDir, "integrations.json");
444
+ const hasAiKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY ||
445
+ process.env.GOOGLE_AI_API_KEY || process.env.OPENROUTER_API_KEY ||
446
+ process.env.GEMINI_API_KEY;
447
+ if (!hasAiKey && !fs.existsSync(intPath)) {
448
+ console.log();
449
+ console.log(` ${yellow("💡")} ${bold("Tip:")} connect an AI provider for explain, why, review, and changelog AI.`);
450
+ console.log(` ${cyan("infernoflow ai setup")} — takes 60 seconds`);
451
+ }
452
+
442
453
  nextSteps([
443
454
  cyan("infernoflow status") + " — see your contract at a glance",
444
455
  cyan("infernoflow check") + " — validate everything",
@@ -210,13 +210,13 @@ export async function reviewCommand(rawArgs) {
210
210
 
211
211
  if (!aiResult) {
212
212
  console.log();
213
- console.log(yellow(" ⚠ No AI provider available."));
214
- console.log(gray(" Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or OPENROUTER_API_KEY,"));
215
- console.log(gray(" or run Ollama locally. See `infernoflow doctor` for details."));
213
+ console.log(yellow(" ⚠ No AI provider configured — skipping narrative review."));
216
214
  console.log();
217
- console.log(bold(" Affected capabilities (unanswered):"));
215
+ console.log(bold(" Affected capabilities:"));
218
216
  for (const id of affectedCaps) console.log(` ▸ ${id}`);
219
217
  console.log();
218
+ console.log(` ${yellow("💡")} ${gray("For AI-powered impact summaries:")} ${cyan("infernoflow ai setup")}`);
219
+ console.log();
220
220
  process.exit(0);
221
221
  }
222
222
 
@@ -197,7 +197,7 @@ function runScenario(capId, cap, scenario, scanEntry, cwd) {
197
197
 
198
198
  try {
199
199
  fs.writeFileSync(tmpFile, testSrc);
200
- const result = spawnSync(process.execPath, ["--input-type=module", tmpFile], {
200
+ const result = spawnSync(process.execPath, [tmpFile], {
201
201
  cwd,
202
202
  encoding: "utf8",
203
203
  timeout: 30_000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "The forge for liquid code - keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {