infernoflow 0.37.1 → 0.37.4

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 (88) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,520 +1,13 @@
1
- /**
2
- * infernoflow switch
3
- *
4
- * Generates a handoff summary when switching between AI agents
5
- * (Copilot → Cursor → Claude → Windsurf → etc.).
6
- *
7
- * Captures the full session state — what was tried, what worked, what's
8
- * in progress, the design system, and the capability context — into a
9
- * single pasteable block so the next agent picks up exactly where you left off.
10
- *
11
- * Session boundary detection (same logic as `infernoflow recap`):
12
- * - All entries since the last `handoff` entry, or the last 24h whichever is more recent
13
- * - Use --since to override (e.g. --since 48h, --since 2026-04-20)
14
- * - Use --all to include every entry ever logged
15
- *
16
- * Also writes inferno/HANDOFF.md (auto-loaded by some IDEs).
17
- *
18
- * Usage:
19
- * infernoflow switch Generate handoff + write HANDOFF.md
20
- * infernoflow switch --to cursor Label the handoff for a specific agent
21
- * infernoflow switch --copy Copy to clipboard
22
- * infernoflow switch --show Print current HANDOFF.md
23
- * infernoflow switch --json Output as JSON
24
- * infernoflow switch --since 48h Include entries from the last 48 hours
25
- * infernoflow switch --all Include all entries ever logged
26
- */
27
-
28
- import * as fs from "node:fs";
29
- import * as path from "node:path";
30
- import * as os from "node:os";
31
- import { execSync } from "node:child_process";
32
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
33
-
34
- const INFERNO_DIR = "inferno";
35
- const HANDOFF_FILE = path.join(INFERNO_DIR, "HANDOFF.md");
36
- const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
37
- const STATE_FILE = path.join(INFERNO_DIR, "context-state.json");
38
- const CONTRACT_FILE = path.join(INFERNO_DIR, "contract.json");
39
- const THEME_FILE = path.join(INFERNO_DIR, "theme.json");
40
- const ADOPTION_FILE = path.join(INFERNO_DIR, "adoption_profile.json");
41
-
42
- function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
43
- function readFile(f) { try { return fs.readFileSync(f, "utf8"); } catch { return null; } }
44
-
45
- function fmtDate(iso) {
46
- if (!iso) return "unknown";
47
- return new Date(iso).toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
48
- }
49
-
50
- function fmtDuration(ms) {
51
- if (ms < 0) return "unknown";
52
- const h = Math.floor(ms / 3600000);
53
- const m = Math.floor((ms % 3600000) / 60000);
54
- if (h > 0) return `${h}h ${m}m`;
55
- return `${m}m`;
56
- }
57
-
58
- function getAllEntries() {
59
- if (!fs.existsSync(SESSIONS_FILE)) return [];
60
- return fs.readFileSync(SESSIONS_FILE, "utf8")
61
- .split("\n").filter(Boolean)
62
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
63
- .filter(Boolean);
64
- }
65
-
66
- /**
67
- * Find the start of the "current session":
68
- * - The timestamp of the last `handoff` entry, OR 24h ago — whichever is more recent
69
- * - --since overrides both; --all returns epoch (everything)
70
- */
71
- function findSessionStart(allEntries, sinceArg, allFlag) {
72
- if (allFlag) return new Date(0);
73
-
74
- if (sinceArg) {
75
- const hoursMatch = sinceArg.match(/^(\d+)h$/i);
76
- const daysMatch = sinceArg.match(/^(\d+)d$/i);
77
- if (hoursMatch) return new Date(Date.now() - parseInt(hoursMatch[1]) * 3600000);
78
- if (daysMatch) return new Date(Date.now() - parseInt(daysMatch[1]) * 86400000);
79
- const parsed = new Date(sinceArg);
80
- if (!isNaN(parsed)) return parsed;
81
- }
82
-
83
- // Find last handoff entry
84
- for (let i = allEntries.length - 1; i >= 0; i--) {
85
- if (allEntries[i].type === "handoff") {
86
- const ts = new Date(allEntries[i].ts || 0);
87
- const dayAgo = new Date(Date.now() - 86400000);
88
- return ts > dayAgo ? ts : dayAgo;
89
- }
90
- }
91
-
92
- return new Date(Date.now() - 86400000);
93
- }
94
-
95
- function copyToClipboard(text) {
96
- try {
97
- const p = process.platform;
98
- if (p === "win32") execSync("clip", { input: text });
99
- else if (p === "darwin") execSync("pbcopy", { input: text });
100
- else { try { execSync("xclip -selection clipboard", { input: text }); } catch { execSync("xsel --clipboard --input", { input: text }); } }
101
- return true;
102
- } catch { return false; }
103
- }
104
-
105
- /** Detect current IDE from environment */
106
- function detectIde() {
107
- if (process.env.CURSOR_SESSION) return "Cursor";
108
- if (process.env.COPILOT_SESSION) return "GitHub Copilot";
109
- if (process.env.CLAUDE_CODE_SESSION) return "Claude Code";
110
- if (process.env.WINDSURF_SESSION) return "Windsurf";
111
- if (process.env.TERM_PROGRAM === "vscode") return "VS Code";
112
- // Check adoption profile
113
- const profile = readJSON(ADOPTION_FILE);
114
- if (profile?.ide) return profile.ide;
115
- return null;
116
- }
117
-
118
- /** Get git diff stat since last commit */
119
- function getGitDiffStat() {
120
- try {
121
- const stat = execSync("git diff --stat HEAD 2>/dev/null || git diff --cached --stat 2>/dev/null", {
122
- encoding: "utf8",
123
- stdio: ["pipe", "pipe", "pipe"],
124
- }).trim();
125
- if (!stat) {
126
- // Try HEAD~1 if nothing staged/unstaged
127
- const logStat = execSync("git log --stat -1 --pretty= 2>/dev/null", {
128
- encoding: "utf8",
129
- stdio: ["pipe", "pipe", "pipe"],
130
- }).trim();
131
- return logStat || null;
132
- }
133
- return stat;
134
- } catch {
135
- return null;
136
- }
137
- }
138
-
139
- /** Get most-edited files from git this session */
140
- function getHotFiles(since) {
141
- try {
142
- const sinceArg = since && since.getTime() > 0
143
- ? `--after="${since.toISOString()}"`
144
- : "-10";
145
- const raw = execSync(
146
- `git log ${sinceArg} --name-only --pretty=format: 2>/dev/null`,
147
- { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
148
- ).trim();
149
- if (!raw) return [];
150
- const counts = {};
151
- for (const line of raw.split("\n")) {
152
- const f = line.trim();
153
- if (f) counts[f] = (counts[f] || 0) + 1;
154
- }
155
- return Object.entries(counts)
156
- .sort((a, b) => b[1] - a[1])
157
- .slice(0, 5)
158
- .map(([file, edits]) => ({ file, edits }));
159
- } catch {
160
- return [];
161
- }
162
- }
163
-
164
- /** Get recent git commits in this session */
165
- function getGitCommits(since) {
166
- try {
167
- const sinceArg = since ? `--after="${since.toISOString()}"` : "-5";
168
- const log = execSync(`git log ${sinceArg} --pretty=format:"%h %s" 2>/dev/null`, {
169
- encoding: "utf8",
170
- stdio: ["pipe", "pipe", "pipe"],
171
- }).trim();
172
- return log ? log.split("\n").filter(Boolean) : [];
173
- } catch {
174
- return [];
175
- }
176
- }
177
-
178
- /** Detect "open threads" — attempts without resolution + entries containing TODO/WIP markers */
179
- function findOpenThreads(sessions) {
180
- const open = [];
181
-
182
- // Failed/partial attempts that were never followed by a worked attempt
183
- const attempts = sessions.filter(e => e.type === "attempt" && (e.result === "failed" || e.result === "partial" || !e.result));
184
- for (const a of attempts) {
185
- const followedByWorked = sessions.find(e =>
186
- e.type === "attempt" &&
187
- e.result === "worked" &&
188
- new Date(e.ts) > new Date(a.ts) &&
189
- e.summary.toLowerCase().includes(a.summary.split(" ")[0].toLowerCase())
190
- );
191
- if (!followedByWorked) {
192
- open.push({ text: a.summary, ts: a.ts, kind: "unresolved-attempt" });
193
- }
194
- }
195
-
196
- // Any entry explicitly marked TODO/WIP in summary
197
- for (const e of sessions) {
198
- if (/\b(TODO|WIP|FIXME|BLOCKED|pending)\b/i.test(e.summary)) {
199
- if (!open.find(o => o.text === e.summary)) {
200
- open.push({ text: e.summary, ts: e.ts, kind: "flagged" });
201
- }
202
- }
203
- }
204
-
205
- return open.slice(0, 8); // cap at 8
206
- }
207
-
208
- function buildHandoff(toAgent, sinceArg, allFlag) {
209
- const state = readJSON(STATE_FILE) || {};
210
- const contract = readJSON(CONTRACT_FILE) || {};
211
- const theme = readJSON(THEME_FILE);
212
- const adoption = readJSON(ADOPTION_FILE);
213
- const allEntries = getAllEntries();
214
- const sessionStart = findSessionStart(allEntries, sinceArg, allFlag);
215
- const sessions = allEntries.filter(e => new Date(e.ts || 0) > sessionStart);
216
- const recentFallback = allEntries.slice(-5);
217
- const now = new Date();
218
- const nowStr = now.toLocaleString("en-GB", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" });
219
-
220
- const projectId = contract.policyId || path.basename(process.cwd());
221
- const version = contract.policyVersion || "?";
222
- const caps = (contract.capabilities || []).slice(0, 20);
223
- const ide = detectIde();
224
-
225
- // Session metadata
226
- const sessionDurationMs = sessionStart.getTime() > 0 ? now - sessionStart : -1;
227
- const sessionDuration = fmtDuration(sessionDurationMs);
228
- // Short session ID: hex of sessionStart ms
229
- const sessionId = sessionStart.getTime() > 0
230
- ? sessionStart.getTime().toString(16).slice(-6).toUpperCase()
231
- : "ALL";
232
-
233
- // Git data
234
- const commits = getGitCommits(sessionStart.getTime() > 0 ? sessionStart : null);
235
- const diffStat = getGitDiffStat();
236
- const hotFiles = getHotFiles(sessionStart.getTime() > 0 ? sessionStart : null);
237
-
238
- // Memory pool
239
- const pool = sessions.length > 0 ? sessions : recentFallback;
240
- const gotchas = pool.filter(e => e.type === "gotcha");
241
- const decisions = pool.filter(e => e.type === "decision");
242
- const attempts = pool.filter(e => e.type === "attempt").filter(e => e.result === "failed" || e.result === "partial");
243
- const prefs = pool.filter(e => e.type === "preference");
244
- const recent = pool.slice(-8);
245
- const openThreads = findOpenThreads(pool);
246
-
247
- const sinceStr = sessionStart.getTime() === 0
248
- ? "all time"
249
- : sessionStart.toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
250
-
251
- // ── Sources header ─────────────────────────────────────────────────────────
252
- const sourcesList = ["sessions.jsonl"];
253
- if (state.working || state.intent) sourcesList.push("context-state.json");
254
- if (theme) sourcesList.push("theme.json");
255
- if (contract.capabilities?.length) sourcesList.push("contract.json");
256
- if (adoption) sourcesList.push("adoption_profile.json");
257
- if (commits.length) sourcesList.push("git log");
258
-
259
- const lines = [
260
- `# 🔥 infernoflow Handoff — ${projectId}`,
261
- `> Generated: ${nowStr}${toAgent ? ` | Handing off to: **${toAgent}**` : ""}`,
262
- `> Session: **#${sessionId}** · ${sessionDuration} · **${sessions.length} entries** since ${sinceStr}`,
263
- `> Sources: ${sourcesList.join(" · ")}${ide ? ` · IDE: ${ide}` : ""}`,
264
- "",
265
- "---",
266
- "",
267
- "## Pick up here",
268
- "",
269
- ];
270
-
271
- // Current working state
272
- if (state.working || state.intent) {
273
- if (state.working) lines.push(`**Working on:** ${state.working} _(${fmtDate(state.workingUpdated)})_`);
274
- if (state.intent) lines.push(`**Intent:** ${state.intent} _(${fmtDate(state.intentUpdated)})_`);
275
- lines.push("");
276
- } else {
277
- lines.push("_No working state set. Run: `infernoflow context --working \"...\"` to set it._", "");
278
- }
279
-
280
- // ── Open threads — unresolved items ───────────────────────────────────────
281
- if (openThreads.length) {
282
- lines.push("## 🔓 Open threads — not yet resolved", "");
283
- for (const t of openThreads) {
284
- const badge = t.kind === "flagged" ? "[flagged]" : "[unresolved]";
285
- lines.push(`- ${badge} ${t.text} _(${fmtDate(t.ts)})_`);
286
- }
287
- lines.push("");
288
- }
289
-
290
- // ── Gotchas first — most critical for a new agent ─────────────────────────
291
- if (gotchas.length) {
292
- lines.push("## ⚠ Gotchas — read these first", "");
293
- for (const e of gotchas) {
294
- lines.push(`- ${e.summary} _(${fmtDate(e.ts)})_`);
295
- }
296
- lines.push("");
297
- }
298
-
299
- // ── Decisions ──────────────────────────────────────────────────────────────
300
- if (decisions.length) {
301
- lines.push("## Decisions made", "");
302
- for (const e of decisions) {
303
- const result = e.result ? ` → **${e.result}**` : "";
304
- lines.push(`- ${e.summary}${result} _(${fmtDate(e.ts)})_`);
305
- }
306
- lines.push("");
307
- }
308
-
309
- // ── Failed attempts — don't repeat them ───────────────────────────────────
310
- if (attempts.length) {
311
- lines.push("## ✗ Already tried — don't repeat", "");
312
- for (const e of attempts) {
313
- lines.push(`- ${e.summary} _(${fmtDate(e.ts)})_`);
314
- }
315
- lines.push("");
316
- }
317
-
318
- // ── Hot files — auto-detected from git ────────────────────────────────────
319
- if (hotFiles.length) {
320
- lines.push("## 📁 Hot Files This Session", "");
321
- for (const { file, edits } of hotFiles) {
322
- lines.push(`- \`${file}\` — ${edits} edit${edits !== 1 ? "s" : ""}`);
323
- }
324
- lines.push("");
325
- }
326
-
327
- // ── Preferences ───────────────────────────────────────────────────────────
328
- if (prefs.length) {
329
- lines.push("## Developer preferences", "");
330
- for (const e of prefs) {
331
- lines.push(`- ${e.summary}`);
332
- }
333
- lines.push("");
334
- }
335
-
336
- // ── Git activity this session ──────────────────────────────────────────────
337
- if (commits.length || diffStat) {
338
- lines.push("## Git activity this session", "");
339
- if (commits.length) {
340
- lines.push("**Commits:**");
341
- for (const c of commits) lines.push(`- \`${c}\``);
342
- lines.push("");
343
- }
344
- if (diffStat) {
345
- lines.push("**Uncommitted changes:**");
346
- lines.push("```");
347
- lines.push(diffStat.split("\n").slice(0, 15).join("\n")); // cap at 15 lines
348
- lines.push("```");
349
- lines.push("");
350
- }
351
- }
352
-
353
- // ── Design system ─────────────────────────────────────────────────────────
354
- if (theme) {
355
- lines.push("## Design system", "");
356
- if (theme.fonts?.primary) lines.push(`- **Font:** ${theme.fonts.primary}${theme.fonts.mono ? ` · mono: ${theme.fonts.mono}` : ""}`);
357
- if (theme.colors?.mode) lines.push(`- **Mode:** ${theme.colors.mode}`);
358
- if (theme.colors?.palette) {
359
- const pal = Object.entries(theme.colors.palette).map(([k,v]) => `${k}=${v}`).join(" ");
360
- lines.push(`- **Palette:** ${pal}`);
361
- }
362
- if (theme.cssVars && Object.keys(theme.cssVars).length) {
363
- const top = Object.entries(theme.cssVars).slice(0, 6).map(([k,v]) => `${k}: ${v}`).join(" | ");
364
- lines.push(`- **CSS vars:** ${top}`);
365
- }
366
- if (theme.framework) lines.push(`- **Framework:** ${theme.framework}`);
367
- lines.push("", "> ⚠ Always match these exactly. Do not introduce new colors or fonts.", "");
368
- }
369
-
370
- // ── Capabilities ──────────────────────────────────────────────────────────
371
- if (caps.length) {
372
- lines.push("## Capability contract", "");
373
- lines.push(`Project: **${projectId}** v${version}`);
374
- lines.push(`Capabilities: ${caps.join(", ")}`);
375
- lines.push("");
376
- }
377
-
378
- // ── Recent session log ────────────────────────────────────────────────────
379
- if (recent.length) {
380
- lines.push("## Recent session log", "");
381
- for (const e of recent) {
382
- const result = e.result ? ` [${e.result}]` : "";
383
- const src = e.source ? ` {${e.source}}` : "";
384
- lines.push(`- **${e.type}**${result}${src}: ${e.summary} _(${fmtDate(e.ts)})_`);
385
- }
386
- lines.push("");
387
- }
388
-
389
- lines.push("---");
390
- lines.push(`_Session #${sessionId} · ${sessionDuration} · Generated by infernoflow._`);
391
-
392
- return lines.join("\n");
393
- }
394
-
395
- export async function switchCommand(args) {
396
- const has = (f) => args.includes(f);
397
- const flag = (f) => { const i = args.indexOf(f); return i !== -1 && args[i+1] ? args[i+1] : null; };
398
- const showOnly = has("--show") || has("-s");
399
- const copyFlag = has("--copy") || has("-c");
400
- const jsonFlag = has("--json");
401
- const allFlag = has("--all");
402
- const sinceArg = flag("--since");
403
- const toAgent = flag("--to") || args.find(a => !a.startsWith("-") && !["switch"].includes(a)) || null;
404
-
405
- console.log("\n " + bold("🔥 infernoflow — switch"));
406
- console.log(" " + "─".repeat(50) + "\n");
407
-
408
- if (!fs.existsSync(INFERNO_DIR)) {
409
- console.error(red(" ✘ inferno/ not found — run: infernoflow init\n"));
410
- process.exit(1);
411
- }
412
-
413
- // ── Show existing handoff ─────────────────────────────────────────────────
414
- if (showOnly) {
415
- const existing = readFile(HANDOFF_FILE);
416
- if (!existing) {
417
- console.log(yellow(" ⚠ No HANDOFF.md yet — run: infernoflow switch\n"));
418
- return;
419
- }
420
- console.log(existing);
421
- return;
422
- }
423
-
424
- const handoff = buildHandoff(toAgent, sinceArg, allFlag);
425
-
426
- if (jsonFlag) {
427
- const state = readJSON(STATE_FILE) || {};
428
- const contract = readJSON(CONTRACT_FILE) || {};
429
- const theme = readJSON(THEME_FILE);
430
- const adoption = readJSON(ADOPTION_FILE);
431
- const allEntries = getAllEntries();
432
- const sessionStart = findSessionStart(allEntries, sinceArg, allFlag);
433
- const sessions = allEntries.filter(e => new Date(e.ts || 0) > sessionStart);
434
- const commits = getGitCommits(sessionStart.getTime() > 0 ? sessionStart : null);
435
- const ide = detectIde();
436
- console.log(JSON.stringify({
437
- state,
438
- contract: { policyId: contract.policyId, policyVersion: contract.policyVersion, capabilities: contract.capabilities },
439
- theme,
440
- adoption,
441
- sessions,
442
- commits,
443
- ide,
444
- sessionStart: sessionStart.toISOString(),
445
- sessionId: sessionStart.getTime() > 0 ? sessionStart.getTime().toString(16).slice(-6).toUpperCase() : "ALL",
446
- sessionDuration: fmtDuration(sessionStart.getTime() > 0 ? Date.now() - sessionStart.getTime() : -1),
447
- generatedAt: new Date().toISOString(),
448
- }, null, 2));
449
- return;
450
- }
451
-
452
- // Write HANDOFF.md
453
- fs.writeFileSync(HANDOFF_FILE, handoff, "utf8");
454
- console.log(green(" ✔ Written → inferno/HANDOFF.md\n"));
455
-
456
- // ── Rich summary printout — calculate BEFORE appending handoff entry ──────
457
- // (appending handoff first would make findSessionStart think the session just
458
- // started, showing 0 entries for the session that just ended)
459
- const allEntriesNow = getAllEntries();
460
- const sessionStartNow = findSessionStart(allEntriesNow, sinceArg, allFlag);
461
- const sessionEntries = allEntriesNow.filter(e => new Date(e.ts || 0) > sessionStartNow);
462
- const state = readJSON(STATE_FILE) || {};
463
- const theme = readJSON(THEME_FILE);
464
- const contract = readJSON(CONTRACT_FILE) || {};
465
- const commits = getGitCommits(sessionStartNow.getTime() > 0 ? sessionStartNow : null);
466
- const hotFilesNow = getHotFiles(sessionStartNow.getTime() > 0 ? sessionStartNow : null);
467
- const ide = detectIde();
468
- const pool = sessionEntries.length > 0 ? sessionEntries : allEntriesNow.slice(-5);
469
- const openThreads = findOpenThreads(pool);
470
- const sessionDuration = fmtDuration(sessionStartNow.getTime() > 0 ? Date.now() - sessionStartNow.getTime() : -1);
471
- const sessionId = sessionStartNow.getTime() > 0
472
- ? sessionStartNow.getTime().toString(16).slice(-6).toUpperCase()
473
- : "ALL";
474
-
475
- // Print rich summary
476
- console.log(" " + bold("Handoff ready"));
477
- console.log(" " + "─".repeat(50));
478
- console.log(" " + gray("Session #" + sessionId + " · " + sessionDuration));
479
- if (state.working) console.log(" Working on " + cyan(state.working));
480
- if (state.intent) console.log(" Intent " + cyan(state.intent));
481
- console.log(" Memory " + sessionEntries.length + " entries this session (total: " + allEntriesNow.length + ")");
482
- if (openThreads.length) console.log(" Open threads " + yellow(openThreads.length + " unresolved"));
483
- if (commits.length) console.log(" Git commits " + commits.length + " this session");
484
- if (hotFilesNow.length) {
485
- console.log(" Hot files " + hotFilesNow.map(f => cyan(f.file)).join(", "));
486
- }
487
- console.log(" Capabilities " + (contract.capabilities || []).length + " registered");
488
- if (theme?.fonts?.primary) console.log(" Font " + theme.fonts.primary);
489
- if (theme?.colors?.mode) console.log(" Color mode " + theme.colors.mode);
490
- if (ide) console.log(" IDE " + ide);
491
- if (toAgent) console.log(" Handing off → " + cyan(toAgent));
492
- console.log();
493
-
494
- if (copyFlag) {
495
- const ok = copyToClipboard(handoff);
496
- console.log(ok
497
- ? green(" ✔ Copied to clipboard — paste at the start of your next AI session")
498
- : yellow(" ⚠ Clipboard failed — open inferno/HANDOFF.md manually")
499
- );
500
- } else {
501
- console.log(" " + bold("Ready to use:"));
502
- console.log(" " + cyan("1.") + " Open " + cyan("inferno/HANDOFF.md"));
503
- console.log(" " + cyan("2.") + " Copy all");
504
- console.log(" " + cyan("3.") + " Paste at the start of your next AI session");
505
- console.log(" " + gray(" tip: use --copy to skip steps 1-2 automatically"));
506
- }
507
- console.log();
508
-
509
- // Append the handoff entry AFTER printing summary — so next run's session
510
- // boundary is set correctly (starts after this handoff)
511
- if (fs.existsSync(SESSIONS_FILE)) {
512
- const entry = {
513
- ts: new Date().toISOString(),
514
- agent: "infernoflow",
515
- type: "handoff",
516
- summary: toAgent ? `Handed off to ${toAgent}` : "Handoff generated",
517
- };
518
- fs.appendFileSync(SESSIONS_FILE, JSON.stringify(entry) + "\n", "utf8");
519
- }
520
- }
1
+ import*as m from"node:fs";import*as O from"node:path";import"node:os";import{execSync as y}from"node:child_process";import{bold as P,cyan as S,gray as et,green as ot,yellow as V,red as rt}from"../ui/output.mjs";const I="inferno",nt=O.join(I,"HANDOFF.md"),U=O.join(I,"sessions.jsonl"),J=O.join(I,"context-state.json"),K=O.join(I,"contract.json"),X=O.join(I,"theme.json"),q=O.join(I,"adoption_profile.json");function u(e){try{return JSON.parse(m.readFileSync(e,"utf8"))}catch{return null}}function ct(e){try{return m.readFileSync(e,"utf8")}catch{return null}}function F(e){return e?new Date(e).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}):"unknown"}function z(e){if(e<0)return"unknown";const i=Math.floor(e/36e5),l=Math.floor(e%36e5/6e4);return i>0?`${i}h ${l}m`:`${l}m`}function Q(){return m.existsSync(U)?m.readFileSync(U,"utf8").split(`
2
+ `).filter(Boolean).map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(Boolean):[]}function Y(e,i,l){if(l)return new Date(0);if(i){const n=i.match(/^(\d+)h$/i),c=i.match(/^(\d+)d$/i);if(n)return new Date(Date.now()-parseInt(n[1])*36e5);if(c)return new Date(Date.now()-parseInt(c[1])*864e5);const s=new Date(i);if(!isNaN(s))return s}for(let n=e.length-1;n>=0;n--)if(e[n].type==="handoff"){const c=new Date(e[n].ts||0),s=new Date(Date.now()-864e5);return c>s?c:s}return new Date(Date.now()-864e5)}function lt(e){try{const i=process.platform;if(i==="win32")y("clip",{input:e});else if(i==="darwin")y("pbcopy",{input:e});else try{y("xclip -selection clipboard",{input:e})}catch{y("xsel --clipboard --input",{input:e})}return!0}catch{return!1}}function Z(){if(process.env.CURSOR_SESSION)return"Cursor";if(process.env.COPILOT_SESSION)return"GitHub Copilot";if(process.env.CLAUDE_CODE_SESSION)return"Claude Code";if(process.env.WINDSURF_SESSION)return"Windsurf";if(process.env.TERM_PROGRAM==="vscode")return"VS Code";const e=u(q);return e?.ide?e.ide:null}function at(){try{const e=y("git diff --stat HEAD 2>/dev/null || git diff --cached --stat 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return e||y("git log --stat -1 --pretty= 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()||null}catch{return null}}function st(e){try{const i=e&&e.getTime()>0?`--after="${e.toISOString()}"`:"-10",l=y(`git log ${i} --name-only --pretty=format: 2>/dev/null`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();if(!l)return[];const n={};for(const c of l.split(`
3
+ `)){const s=c.trim();s&&(n[s]=(n[s]||0)+1)}return Object.entries(n).sort((c,s)=>s[1]-c[1]).slice(0,5).map(([c,s])=>({file:c,edits:s}))}catch{return[]}}function tt(e){try{const i=e?`--after="${e.toISOString()}"`:"-5",l=y(`git log ${i} --pretty=format:"%h %s" 2>/dev/null`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return l?l.split(`
4
+ `).filter(Boolean):[]}catch{return[]}}function it(e){const i=[],l=e.filter(n=>n.type==="attempt"&&(n.result==="failed"||n.result==="partial"||!n.result));for(const n of l)e.find(s=>s.type==="attempt"&&s.result==="worked"&&new Date(s.ts)>new Date(n.ts)&&s.summary.toLowerCase().includes(n.summary.split(" ")[0].toLowerCase()))||i.push({text:n.summary,ts:n.ts,kind:"unresolved-attempt"});for(const n of e)/\b(TODO|WIP|FIXME|BLOCKED|pending)\b/i.test(n.summary)&&(i.find(c=>c.text===n.summary)||i.push({text:n.summary,ts:n.ts,kind:"flagged"}));return i.slice(0,8)}function ft(e,i,l){const n=u(J)||{},c=u(K)||{},s=u(X),C=u(q),w=Q(),a=Y(w,i,l),_=w.filter(o=>new Date(o.ts||0)>a),T=w.slice(-5),f=new Date,N=f.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"}),$=c.policyId||O.basename(process.cwd()),j=c.policyVersion||"?",v=(c.capabilities||[]).slice(0,20),b=Z(),H=a.getTime()>0?f-a:-1,k=z(H),x=a.getTime()>0?a.getTime().toString(16).slice(-6).toUpperCase():"ALL",D=tt(a.getTime()>0?a:null),E=at(),G=st(a.getTime()>0?a:null),r=_.length>0?_:T,g=r.filter(o=>o.type==="gotcha"),R=r.filter(o=>o.type==="decision"),M=r.filter(o=>o.type==="attempt").filter(o=>o.result==="failed"||o.result==="partial"),A=r.filter(o=>o.type==="preference"),d=r.slice(-8),B=it(r),W=a.getTime()===0?"all time":a.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),h=["sessions.jsonl"];(n.working||n.intent)&&h.push("context-state.json"),s&&h.push("theme.json"),c.capabilities?.length&&h.push("contract.json"),C&&h.push("adoption_profile.json"),D.length&&h.push("git log");const t=[`# \u{1F525} infernoflow Handoff \u2014 ${$}`,`> Generated: ${N}${e?` | Handing off to: **${e}**`:""}`,`> Session: **#${x}** \xB7 ${k} \xB7 **${_.length} entries** since ${W}`,`> Sources: ${h.join(" \xB7 ")}${b?` \xB7 IDE: ${b}`:""}`,"","---","","## Pick up here",""];if(n.working||n.intent?(n.working&&t.push(`**Working on:** ${n.working} _(${F(n.workingUpdated)})_`),n.intent&&t.push(`**Intent:** ${n.intent} _(${F(n.intentUpdated)})_`),t.push("")):t.push('_No working state set. Run: `infernoflow context --working "..."` to set it._',""),B.length){t.push("## \u{1F513} Open threads \u2014 not yet resolved","");for(const o of B){const p=o.kind==="flagged"?"[flagged]":"[unresolved]";t.push(`- ${p} ${o.text} _(${F(o.ts)})_`)}t.push("")}if(g.length){t.push("## \u26A0 Gotchas \u2014 read these first","");for(const o of g)t.push(`- ${o.summary} _(${F(o.ts)})_`);t.push("")}if(R.length){t.push("## Decisions made","");for(const o of R){const p=o.result?` \u2192 **${o.result}**`:"";t.push(`- ${o.summary}${p} _(${F(o.ts)})_`)}t.push("")}if(M.length){t.push("## \u2717 Already tried \u2014 don't repeat","");for(const o of M)t.push(`- ${o.summary} _(${F(o.ts)})_`);t.push("")}if(G.length){t.push("## \u{1F4C1} Hot Files This Session","");for(const{file:o,edits:p}of G)t.push(`- \`${o}\` \u2014 ${p} edit${p!==1?"s":""}`);t.push("")}if(A.length){t.push("## Developer preferences","");for(const o of A)t.push(`- ${o.summary}`);t.push("")}if(D.length||E){if(t.push("## Git activity this session",""),D.length){t.push("**Commits:**");for(const o of D)t.push(`- \`${o}\``);t.push("")}E&&(t.push("**Uncommitted changes:**"),t.push("```"),t.push(E.split(`
5
+ `).slice(0,15).join(`
6
+ `)),t.push("```"),t.push(""))}if(s){if(t.push("## Design system",""),s.fonts?.primary&&t.push(`- **Font:** ${s.fonts.primary}${s.fonts.mono?` \xB7 mono: ${s.fonts.mono}`:""}`),s.colors?.mode&&t.push(`- **Mode:** ${s.colors.mode}`),s.colors?.palette){const o=Object.entries(s.colors.palette).map(([p,L])=>`${p}=${L}`).join(" ");t.push(`- **Palette:** ${o}`)}if(s.cssVars&&Object.keys(s.cssVars).length){const o=Object.entries(s.cssVars).slice(0,6).map(([p,L])=>`${p}: ${L}`).join(" | ");t.push(`- **CSS vars:** ${o}`)}s.framework&&t.push(`- **Framework:** ${s.framework}`),t.push("","> \u26A0 Always match these exactly. Do not introduce new colors or fonts.","")}if(v.length&&(t.push("## Capability contract",""),t.push(`Project: **${$}** v${j}`),t.push(`Capabilities: ${v.join(", ")}`),t.push("")),d.length){t.push("## Recent session log","");for(const o of d){const p=o.result?` [${o.result}]`:"",L=o.source?` {${o.source}}`:"";t.push(`- **${o.type}**${p}${L}: ${o.summary} _(${F(o.ts)})_`)}t.push("")}return t.push("---"),t.push(`_Session #${x} \xB7 ${k} \xB7 Generated by infernoflow._`),t.join(`
7
+ `)}async function gt(e){const i=r=>e.includes(r),l=r=>{const g=e.indexOf(r);return g!==-1&&e[g+1]?e[g+1]:null},n=i("--show")||i("-s"),c=i("--copy")||i("-c"),s=i("--json"),C=i("--all"),w=l("--since"),a=l("--to")||e.find(r=>!r.startsWith("-")&&!["switch"].includes(r))||null;if(console.log(`
8
+ `+P("\u{1F525} infernoflow \u2014 switch")),console.log(" "+"\u2500".repeat(50)+`
9
+ `),m.existsSync(I)||(console.error(rt(` \u2718 inferno/ not found \u2014 run: infernoflow init
10
+ `)),process.exit(1)),n){const r=ct(nt);if(!r){console.log(V(` \u26A0 No HANDOFF.md yet \u2014 run: infernoflow switch
11
+ `));return}console.log(r);return}const _=ft(a,w,C);if(s){const r=u(J)||{},g=u(K)||{},R=u(X),M=u(q),A=Q(),d=Y(A,w,C),B=A.filter(t=>new Date(t.ts||0)>d),W=tt(d.getTime()>0?d:null),h=Z();console.log(JSON.stringify({state:r,contract:{policyId:g.policyId,policyVersion:g.policyVersion,capabilities:g.capabilities},theme:R,adoption:M,sessions:B,commits:W,ide:h,sessionStart:d.toISOString(),sessionId:d.getTime()>0?d.getTime().toString(16).slice(-6).toUpperCase():"ALL",sessionDuration:z(d.getTime()>0?Date.now()-d.getTime():-1),generatedAt:new Date().toISOString()},null,2));return}m.writeFileSync(nt,_,"utf8"),console.log(ot(` \u2714 Written \u2192 inferno/HANDOFF.md
12
+ `));const T=Q(),f=Y(T,w,C),N=T.filter(r=>new Date(r.ts||0)>f),$=u(J)||{},j=u(X),v=u(K)||{},b=tt(f.getTime()>0?f:null),H=st(f.getTime()>0?f:null),k=Z(),x=N.length>0?N:T.slice(-5),D=it(x),E=z(f.getTime()>0?Date.now()-f.getTime():-1),G=f.getTime()>0?f.getTime().toString(16).slice(-6).toUpperCase():"ALL";if(console.log(" "+P("Handoff ready")),console.log(" "+"\u2500".repeat(50)),console.log(" "+et("Session #"+G+" \xB7 "+E)),$.working&&console.log(" Working on "+S($.working)),$.intent&&console.log(" Intent "+S($.intent)),console.log(" Memory "+N.length+" entries this session (total: "+T.length+")"),D.length&&console.log(" Open threads "+V(D.length+" unresolved")),b.length&&console.log(" Git commits "+b.length+" this session"),H.length&&console.log(" Hot files "+H.map(r=>S(r.file)).join(", ")),console.log(" Capabilities "+(v.capabilities||[]).length+" registered"),j?.fonts?.primary&&console.log(" Font "+j.fonts.primary),j?.colors?.mode&&console.log(" Color mode "+j.colors.mode),k&&console.log(" IDE "+k),a&&console.log(" Handing off \u2192 "+S(a)),console.log(),c){const r=lt(_);console.log(r?ot(" \u2714 Copied to clipboard \u2014 paste at the start of your next AI session"):V(" \u26A0 Clipboard failed \u2014 open inferno/HANDOFF.md manually"))}else console.log(" "+P("Ready to use:")),console.log(" "+S("1.")+" Open "+S("inferno/HANDOFF.md")),console.log(" "+S("2.")+" Copy all"),console.log(" "+S("3.")+" Paste at the start of your next AI session"),console.log(" "+et(" tip: use --copy to skip steps 1-2 automatically"));if(console.log(),m.existsSync(U)){const r={ts:new Date().toISOString(),agent:"infernoflow",type:"handoff",summary:a?`Handed off to ${a}`:"Handoff generated"};m.appendFileSync(U,JSON.stringify(r)+`
13
+ `,"utf8")}}export{gt as switchCommand};
@@ -1,96 +1 @@
1
- import { execFileSync } from "node:child_process";
2
- import * as path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { header, section, ok, warn, yellow, gray } from "../ui/output.mjs";
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
- const binPath = path.resolve(__dirname, "..", "..", "bin", "infernoflow.mjs");
9
-
10
- function runCliJson(args) {
11
- const out = execFileSync(process.execPath, [binPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
12
- return JSON.parse(out);
13
- }
14
-
15
- function tryRunCliJson(args) {
16
- try {
17
- return { ok: true, data: runCliJson(args) };
18
- } catch (err) {
19
- const stdout = err?.stdout?.toString?.() || "";
20
- try {
21
- return { ok: false, data: JSON.parse(stdout) };
22
- } catch {
23
- return { ok: false, data: { ok: false, errors: ["command_failed"] } };
24
- }
25
- }
26
- }
27
-
28
- export async function syncCommand(args = []) {
29
- const auto = args.includes("--auto");
30
- const asJson = args.includes("--json");
31
- const dryRun = args.includes("--dry-run");
32
-
33
- if (!auto) {
34
- const payload = { ok: false, error: "missing_required_flag", hint: "Use: infernoflow sync --auto" };
35
- if (asJson) {
36
- console.log(JSON.stringify(payload, null, 2));
37
- process.exit(1);
38
- }
39
- header("sync");
40
- warn("missing --auto flag");
41
- console.log(` ${yellow("→")} infernoflow sync --auto`);
42
- console.log();
43
- process.exit(1);
44
- }
45
-
46
- const impact = tryRunCliJson(["pr-impact", "--json"]);
47
- const needsSync = !impact.data?.ok;
48
- const confidence = impact.data?.confidence || "low";
49
- const policyDecision = confidence === "high" ? "auto" : confidence === "medium" ? "ask" : "block";
50
- const actions = needsSync
51
- ? ["Generate inferno update proposal (suggest)", "Review changes", "Validate with check --json"]
52
- : ["No inferno drift detected", "Validate with check --json"];
53
-
54
- const check = tryRunCliJson(["check", "--json"]);
55
- const payload = {
56
- ok: impact.ok && check.ok && !!check.data?.ok,
57
- mode: "auto-skeleton",
58
- dryRun,
59
- needsSync,
60
- didApply: false,
61
- confidence,
62
- policyDecision,
63
- actions,
64
- prImpact: impact.data,
65
- postCheck: check.data,
66
- reasonCodes: [
67
- ...(needsSync ? ["DRIFT_DETECTED"] : ["NO_DRIFT"]),
68
- `POLICY_${policyDecision.toUpperCase()}`,
69
- ...(policyDecision === "auto" ? ["AUTO_APPLY_DISABLED_IN_SKELETON"] : []),
70
- ],
71
- };
72
-
73
- if (asJson) {
74
- console.log(JSON.stringify(payload, null, 2));
75
- process.exit(payload.ok ? 0 : 1);
76
- }
77
-
78
- header("sync --auto");
79
- section("State");
80
- if (needsSync) warn("Inferno drift detected");
81
- else ok("No inferno drift detected");
82
- ok(`Confidence: ${gray(confidence)}`);
83
- ok(`Policy decision: ${gray(policyDecision)}`);
84
- ok(`Apply mode: ${gray("skeleton (no file writes)")}`);
85
- if (dryRun) ok("Dry run enabled");
86
-
87
- section("Plan");
88
- actions.forEach((a) => console.log(` ${yellow("→")} ${a}`));
89
-
90
- section("Validation");
91
- if (check.ok && check.data?.ok) ok("Post-check passed");
92
- else warn("Post-check failed; see infernoflow check --json");
93
- console.log();
94
- process.exit(payload.ok ? 0 : 1);
95
- }
96
-
1
+ import{execFileSync as w}from"node:child_process";import*as y from"node:path";import{fileURLToPath as P}from"node:url";import{header as h,section as d,ok as e,warn as f,yellow as g,gray as p}from"../ui/output.mjs";const S=P(import.meta.url),C=y.dirname(S),N=y.resolve(C,"..","..","bin","infernoflow.mjs");function D(o){const n=w(process.execPath,[N,...o],{encoding:"utf8",stdio:["ignore","pipe","pipe"]});return JSON.parse(n)}function _(o){try{return{ok:!0,data:D(o)}}catch(n){const s=n?.stdout?.toString?.()||"";try{return{ok:!1,data:JSON.parse(s)}}catch{return{ok:!1,data:{ok:!1,errors:["command_failed"]}}}}}async function E(o=[]){const n=o.includes("--auto"),s=o.includes("--json"),u=o.includes("--dry-run");n||(s&&(console.log(JSON.stringify({ok:!1,error:"missing_required_flag",hint:"Use: infernoflow sync --auto"},null,2)),process.exit(1)),h("sync"),f("missing --auto flag"),console.log(` ${g("\u2192")} infernoflow sync --auto`),console.log(),process.exit(1));const c=_(["pr-impact","--json"]),i=!c.data?.ok,a=c.data?.confidence||"low",r=a==="high"?"auto":a==="medium"?"ask":"block",k=i?["Generate inferno update proposal (suggest)","Review changes","Validate with check --json"]:["No inferno drift detected","Validate with check --json"],t=_(["check","--json"]),l={ok:c.ok&&t.ok&&!!t.data?.ok,mode:"auto-skeleton",dryRun:u,needsSync:i,didApply:!1,confidence:a,policyDecision:r,actions:k,prImpact:c.data,postCheck:t.data,reasonCodes:[...i?["DRIFT_DETECTED"]:["NO_DRIFT"],`POLICY_${r.toUpperCase()}`,...r==="auto"?["AUTO_APPLY_DISABLED_IN_SKELETON"]:[]]};s&&(console.log(JSON.stringify(l,null,2)),process.exit(l.ok?0:1)),h("sync --auto"),d("State"),i?f("Inferno drift detected"):e("No inferno drift detected"),e(`Confidence: ${p(a)}`),e(`Policy decision: ${p(r)}`),e(`Apply mode: ${p("skeleton (no file writes)")}`),u&&e("Dry run enabled"),d("Plan"),k.forEach(m=>console.log(` ${g("\u2192")} ${m}`)),d("Validation"),t.ok&&t.data?.ok?e("Post-check passed"):f("Post-check failed; see infernoflow check --json"),console.log(),process.exit(l.ok?0:1)}export{E as syncCommand};