infernoflow 0.33.0 → 0.34.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 (84) hide show
  1. package/README.md +208 -120
  2. package/dist/bin/infernoflow.mjs +271 -85
  3. package/dist/lib/adopters/angular.mjs +128 -1
  4. package/dist/lib/adopters/css.mjs +111 -1
  5. package/dist/lib/adopters/react.mjs +104 -1
  6. package/dist/lib/ai/ideDetection.mjs +31 -1
  7. package/dist/lib/ai/localProvider.mjs +88 -1
  8. package/dist/lib/ai/providerRouter.mjs +295 -2
  9. package/dist/lib/commands/adopt.mjs +869 -20
  10. package/dist/lib/commands/adoptWizard.mjs +320 -9
  11. package/dist/lib/commands/agent.mjs +191 -5
  12. package/dist/lib/commands/ai.mjs +407 -2
  13. package/dist/lib/commands/ask.mjs +299 -0
  14. package/dist/lib/commands/audit.mjs +300 -13
  15. package/dist/lib/commands/changelog.mjs +594 -26
  16. package/dist/lib/commands/check.mjs +184 -3
  17. package/dist/lib/commands/ci.mjs +208 -3
  18. package/dist/lib/commands/claudeMd.mjs +139 -28
  19. package/dist/lib/commands/cloud.mjs +521 -5
  20. package/dist/lib/commands/context.mjs +346 -34
  21. package/dist/lib/commands/coverage.mjs +282 -2
  22. package/dist/lib/commands/dashboard.mjs +635 -123
  23. package/dist/lib/commands/demo.mjs +465 -8
  24. package/dist/lib/commands/diff.mjs +274 -5
  25. package/dist/lib/commands/docGate.mjs +81 -2
  26. package/dist/lib/commands/doctor.mjs +321 -3
  27. package/dist/lib/commands/explain.mjs +438 -8
  28. package/dist/lib/commands/export.mjs +239 -10
  29. package/dist/lib/commands/generateSkills.mjs +163 -38
  30. package/dist/lib/commands/graph.mjs +378 -11
  31. package/dist/lib/commands/health.mjs +309 -2
  32. package/dist/lib/commands/impact.mjs +325 -2
  33. package/dist/lib/commands/implement.mjs +103 -7
  34. package/dist/lib/commands/init.mjs +545 -23
  35. package/dist/lib/commands/installCursorHooks.mjs +36 -1
  36. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
  37. package/dist/lib/commands/link.mjs +342 -2
  38. package/dist/lib/commands/log.mjs +164 -16
  39. package/dist/lib/commands/monorepo.mjs +428 -4
  40. package/dist/lib/commands/notify.mjs +258 -4
  41. package/dist/lib/commands/onboard.mjs +296 -4
  42. package/dist/lib/commands/prComment.mjs +361 -2
  43. package/dist/lib/commands/prImpact.mjs +157 -2
  44. package/dist/lib/commands/publish.mjs +316 -15
  45. package/dist/lib/commands/recap.mjs +359 -0
  46. package/dist/lib/commands/report.mjs +272 -28
  47. package/dist/lib/commands/review.mjs +223 -9
  48. package/dist/lib/commands/run.mjs +336 -8
  49. package/dist/lib/commands/scaffold.mjs +419 -54
  50. package/dist/lib/commands/scan.mjs +1118 -5
  51. package/dist/lib/commands/scout.mjs +291 -2
  52. package/dist/lib/commands/setup.mjs +310 -5
  53. package/dist/lib/commands/share.mjs +196 -13
  54. package/dist/lib/commands/snapshot.mjs +383 -3
  55. package/dist/lib/commands/stability.mjs +293 -2
  56. package/dist/lib/commands/stats.mjs +402 -0
  57. package/dist/lib/commands/status.mjs +172 -4
  58. package/dist/lib/commands/suggest.mjs +563 -21
  59. package/dist/lib/commands/switch.mjs +310 -0
  60. package/dist/lib/commands/syncAuto.mjs +96 -1
  61. package/dist/lib/commands/synthesize.mjs +228 -10
  62. package/dist/lib/commands/teamSync.mjs +388 -2
  63. package/dist/lib/commands/test.mjs +363 -6
  64. package/dist/lib/commands/theme.mjs +195 -18
  65. package/dist/lib/commands/upgrade.mjs +153 -0
  66. package/dist/lib/commands/version.mjs +282 -2
  67. package/dist/lib/commands/vibe.mjs +357 -7
  68. package/dist/lib/commands/watch.mjs +203 -4
  69. package/dist/lib/commands/why.mjs +358 -4
  70. package/dist/lib/cursorHooksInstall.mjs +60 -1
  71. package/dist/lib/draftToolingInstall.mjs +68 -7
  72. package/dist/lib/git/detect-drift.mjs +208 -4
  73. package/dist/lib/learning/adapt.mjs +101 -6
  74. package/dist/lib/learning/observe.mjs +119 -1
  75. package/dist/lib/learning/patternDetector.mjs +298 -1
  76. package/dist/lib/learning/profile.mjs +279 -2
  77. package/dist/lib/learning/skillSynthesizer.mjs +145 -24
  78. package/dist/lib/templates/index.mjs +131 -1
  79. package/dist/lib/theme/scanner.mjs +343 -4
  80. package/dist/lib/ui/errors.mjs +142 -1
  81. package/dist/lib/ui/output.mjs +72 -6
  82. package/dist/lib/ui/prompts.mjs +147 -6
  83. package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
  84. package/package.json +1 -1
@@ -0,0 +1,310 @@
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 { execSync } from "node:child_process";
31
+ import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
32
+
33
+ const INFERNO_DIR = "inferno";
34
+ const HANDOFF_FILE = path.join(INFERNO_DIR, "HANDOFF.md");
35
+ const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
36
+ const STATE_FILE = path.join(INFERNO_DIR, "context-state.json");
37
+ const CONTRACT_FILE = path.join(INFERNO_DIR, "contract.json");
38
+ const THEME_FILE = path.join(INFERNO_DIR, "theme.json");
39
+
40
+ function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
41
+ function readFile(f) { try { return fs.readFileSync(f, "utf8"); } catch { return null; } }
42
+
43
+ function fmtDate(iso) {
44
+ if (!iso) return "unknown";
45
+ return new Date(iso).toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
46
+ }
47
+
48
+ function getAllEntries() {
49
+ if (!fs.existsSync(SESSIONS_FILE)) return [];
50
+ return fs.readFileSync(SESSIONS_FILE, "utf8")
51
+ .split("\n").filter(Boolean)
52
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
53
+ .filter(Boolean);
54
+ }
55
+
56
+ /**
57
+ * Find the start of the "current session":
58
+ * - The timestamp of the last `handoff` entry, OR 24h ago — whichever is more recent
59
+ * - --since overrides both; --all returns epoch (everything)
60
+ */
61
+ function findSessionStart(allEntries, sinceArg, allFlag) {
62
+ if (allFlag) return new Date(0);
63
+
64
+ if (sinceArg) {
65
+ const hoursMatch = sinceArg.match(/^(\d+)h$/i);
66
+ const daysMatch = sinceArg.match(/^(\d+)d$/i);
67
+ if (hoursMatch) return new Date(Date.now() - parseInt(hoursMatch[1]) * 3600000);
68
+ if (daysMatch) return new Date(Date.now() - parseInt(daysMatch[1]) * 86400000);
69
+ const parsed = new Date(sinceArg);
70
+ if (!isNaN(parsed)) return parsed;
71
+ }
72
+
73
+ // Find last handoff entry
74
+ for (let i = allEntries.length - 1; i >= 0; i--) {
75
+ if (allEntries[i].type === "handoff") {
76
+ const ts = new Date(allEntries[i].ts || 0);
77
+ const dayAgo = new Date(Date.now() - 86400000);
78
+ return ts > dayAgo ? ts : dayAgo;
79
+ }
80
+ }
81
+
82
+ return new Date(Date.now() - 86400000);
83
+ }
84
+
85
+ function copyToClipboard(text) {
86
+ try {
87
+ const p = process.platform;
88
+ if (p === "win32") execSync("clip", { input: text });
89
+ else if (p === "darwin") execSync("pbcopy", { input: text });
90
+ else { try { execSync("xclip -selection clipboard", { input: text }); } catch { execSync("xsel --clipboard --input", { input: text }); } }
91
+ return true;
92
+ } catch { return false; }
93
+ }
94
+
95
+ function buildHandoff(toAgent, sinceArg, allFlag) {
96
+ const state = readJSON(STATE_FILE) || {};
97
+ const contract = readJSON(CONTRACT_FILE) || {};
98
+ const theme = readJSON(THEME_FILE);
99
+ const allEntries = getAllEntries();
100
+ const sessionStart = findSessionStart(allEntries, sinceArg, allFlag);
101
+ // Session entries: everything since boundary; also keep last 5 for "recent log"
102
+ const sessions = allEntries.filter(e => new Date(e.ts || 0) > sessionStart);
103
+ const recentFallback = allEntries.slice(-5); // fallback if session is empty
104
+ const now = new Date().toLocaleString("en-GB", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" });
105
+
106
+ const projectId = contract.policyId || path.basename(process.cwd());
107
+ const version = contract.policyVersion || "?";
108
+ const caps = (contract.capabilities || []).slice(0, 20);
109
+
110
+ // ── Session memory grouped by type ────────────────────────────────────────
111
+ const pool = sessions.length > 0 ? sessions : recentFallback;
112
+ const gotchas = pool.filter(e => e.type === "gotcha");
113
+ const decisions = pool.filter(e => e.type === "decision");
114
+ const attempts = pool.filter(e => e.type === "attempt").filter(e => e.result === "failed" || e.result === "partial");
115
+ const prefs = pool.filter(e => e.type === "preference");
116
+ const recent = pool.slice(-8); // last 8 regardless of type
117
+
118
+ const sinceStr = sessionStart.getTime() === 0
119
+ ? "all time"
120
+ : sessionStart.toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
121
+
122
+ const lines = [
123
+ `# 🔥 infernoflow Handoff — ${projectId}`,
124
+ `> Generated: ${now}${toAgent ? ` | Handing off to: **${toAgent}**` : ""}`,
125
+ `> Session memory: **${sessions.length} entries** since ${sinceStr}`,
126
+ "",
127
+ "---",
128
+ "",
129
+ "## Pick up here",
130
+ "",
131
+ ];
132
+
133
+ // Current working state
134
+ if (state.working || state.intent) {
135
+ if (state.working) lines.push(`**Working on:** ${state.working} _(${fmtDate(state.workingUpdated)})_`);
136
+ if (state.intent) lines.push(`**Intent:** ${state.intent} _(${fmtDate(state.intentUpdated)})_`);
137
+ lines.push("");
138
+ } else {
139
+ lines.push("_No working state set. Run: `infernoflow context --working \"...\"` to set it._", "");
140
+ }
141
+
142
+ // ── Gotchas first — most critical for a new agent ─────────────────────────
143
+ if (gotchas.length) {
144
+ lines.push("## ⚠ Gotchas — read these first", "");
145
+ for (const e of gotchas) {
146
+ lines.push(`- ${e.summary} _(${fmtDate(e.ts)})_`);
147
+ }
148
+ lines.push("");
149
+ }
150
+
151
+ // ── Decisions ──────────────────────────────────────────────────────────────
152
+ if (decisions.length) {
153
+ lines.push("## Decisions made", "");
154
+ for (const e of decisions) {
155
+ const result = e.result ? ` → **${e.result}**` : "";
156
+ lines.push(`- ${e.summary}${result} _(${fmtDate(e.ts)})_`);
157
+ }
158
+ lines.push("");
159
+ }
160
+
161
+ // ── Failed attempts — don't repeat them ───────────────────────────────────
162
+ if (attempts.length) {
163
+ lines.push("## ✗ Already tried — don't repeat", "");
164
+ for (const e of attempts) {
165
+ lines.push(`- ${e.summary} _(${fmtDate(e.ts)})_`);
166
+ }
167
+ lines.push("");
168
+ }
169
+
170
+ // ── Preferences ───────────────────────────────────────────────────────────
171
+ if (prefs.length) {
172
+ lines.push("## Developer preferences", "");
173
+ for (const e of prefs) {
174
+ lines.push(`- ${e.summary}`);
175
+ }
176
+ lines.push("");
177
+ }
178
+
179
+ // ── Design system ─────────────────────────────────────────────────────────
180
+ if (theme) {
181
+ lines.push("## Design system", "");
182
+ if (theme.fonts?.primary) lines.push(`- **Font:** ${theme.fonts.primary}${theme.fonts.mono ? ` · mono: ${theme.fonts.mono}` : ""}`);
183
+ if (theme.colors?.mode) lines.push(`- **Mode:** ${theme.colors.mode}`);
184
+ if (theme.colors?.palette) {
185
+ const pal = Object.entries(theme.colors.palette).map(([k,v]) => `${k}=${v}`).join(" ");
186
+ lines.push(`- **Palette:** ${pal}`);
187
+ }
188
+ if (theme.cssVars && Object.keys(theme.cssVars).length) {
189
+ const top = Object.entries(theme.cssVars).slice(0, 6).map(([k,v]) => `${k}: ${v}`).join(" | ");
190
+ lines.push(`- **CSS vars:** ${top}`);
191
+ }
192
+ if (theme.framework) lines.push(`- **Framework:** ${theme.framework}`);
193
+ lines.push("", "> ⚠ Always match these exactly. Do not introduce new colors or fonts.", "");
194
+ }
195
+
196
+ // ── Capabilities ──────────────────────────────────────────────────────────
197
+ if (caps.length) {
198
+ lines.push("## Capability contract", "");
199
+ lines.push(`Project: **${projectId}** v${version}`);
200
+ lines.push(`Capabilities: ${caps.join(", ")}`);
201
+ lines.push("");
202
+ }
203
+
204
+ // ── Recent session log ────────────────────────────────────────────────────
205
+ if (recent.length) {
206
+ lines.push("## Recent session log", "");
207
+ for (const e of recent) {
208
+ const result = e.result ? ` [${e.result}]` : "";
209
+ lines.push(`- **${e.type}**${result}: ${e.summary} _(${fmtDate(e.ts)})_`);
210
+ }
211
+ lines.push("");
212
+ }
213
+
214
+ lines.push("---");
215
+ lines.push("_Paste this at the start of your next AI session. Generated by infernoflow._");
216
+
217
+ return lines.join("\n");
218
+ }
219
+
220
+ export async function switchCommand(args) {
221
+ const has = (f) => args.includes(f);
222
+ const flag = (f) => { const i = args.indexOf(f); return i !== -1 && args[i+1] ? args[i+1] : null; };
223
+ const showOnly = has("--show") || has("-s");
224
+ const copyFlag = has("--copy") || has("-c");
225
+ const jsonFlag = has("--json");
226
+ const allFlag = has("--all");
227
+ const sinceArg = flag("--since");
228
+ const toAgent = flag("--to") || args.find(a => !a.startsWith("-") && !["switch"].includes(a)) || null;
229
+
230
+ console.log("\n " + bold("🔥 infernoflow — switch"));
231
+ console.log(" " + "─".repeat(50) + "\n");
232
+
233
+ if (!fs.existsSync(INFERNO_DIR)) {
234
+ console.error(red(" ✘ inferno/ not found — run: infernoflow init\n"));
235
+ process.exit(1);
236
+ }
237
+
238
+ // ── Show existing handoff ─────────────────────────────────────────────────
239
+ if (showOnly) {
240
+ const existing = readFile(HANDOFF_FILE);
241
+ if (!existing) {
242
+ console.log(yellow(" ⚠ No HANDOFF.md yet — run: infernoflow switch\n"));
243
+ return;
244
+ }
245
+ console.log(existing);
246
+ return;
247
+ }
248
+
249
+ const handoff = buildHandoff(toAgent, sinceArg, allFlag);
250
+
251
+ if (jsonFlag) {
252
+ const state = readJSON(STATE_FILE) || {};
253
+ const contract = readJSON(CONTRACT_FILE) || {};
254
+ const theme = readJSON(THEME_FILE);
255
+ const allEntries = getAllEntries();
256
+ const sessionStart = findSessionStart(allEntries, sinceArg, allFlag);
257
+ const sessions = allEntries.filter(e => new Date(e.ts || 0) > sessionStart);
258
+ console.log(JSON.stringify({ state, contract: { policyId: contract.policyId, policyVersion: contract.policyVersion, capabilities: contract.capabilities }, theme, sessions, sessionStart: sessionStart.toISOString(), generatedAt: new Date().toISOString() }, null, 2));
259
+ return;
260
+ }
261
+
262
+ // Write HANDOFF.md
263
+ fs.writeFileSync(HANDOFF_FILE, handoff, "utf8");
264
+ console.log(green(" ✔ Written → inferno/HANDOFF.md\n"));
265
+
266
+ // Also log the handoff itself to sessions.jsonl
267
+ if (fs.existsSync(SESSIONS_FILE)) {
268
+ const entry = {
269
+ ts: new Date().toISOString(),
270
+ agent: "infernoflow",
271
+ type: "handoff",
272
+ summary: toAgent ? `Handed off to ${toAgent}` : "Handoff generated",
273
+ };
274
+ fs.appendFileSync(SESSIONS_FILE, JSON.stringify(entry) + "\n", "utf8");
275
+ }
276
+
277
+ // Print preview
278
+ const allEntriesForCount = getAllEntries();
279
+ const sessionStartForCount = findSessionStart(allEntriesForCount, sinceArg, allFlag);
280
+ const sessionEntries = allEntriesForCount.filter(e => new Date(e.ts || 0) > sessionStartForCount);
281
+ const state = readJSON(STATE_FILE) || {};
282
+ const theme = readJSON(THEME_FILE);
283
+ const contract = readJSON(CONTRACT_FILE) || {};
284
+
285
+ console.log(" " + bold("Handoff summary"));
286
+ console.log(" " + "─".repeat(50));
287
+ if (state.working) console.log(" Working on " + cyan(state.working));
288
+ if (state.intent) console.log(" Intent " + cyan(state.intent));
289
+ console.log(" Session " + sessionEntries.length + " entries (total: " + allEntriesForCount.length + ")");
290
+ console.log(" Capabilities " + (contract.capabilities || []).length + " registered");
291
+ if (theme?.fonts?.primary) console.log(" Font " + theme.fonts.primary);
292
+ if (theme?.colors?.mode) console.log(" Color mode " + theme.colors.mode);
293
+ if (toAgent) console.log(" Handing off → " + cyan(toAgent));
294
+ console.log();
295
+
296
+ if (copyFlag) {
297
+ const ok = copyToClipboard(handoff);
298
+ console.log(ok
299
+ ? green(" ✔ Copied to clipboard — paste at the start of your next AI session")
300
+ : yellow(" ⚠ Clipboard failed — open inferno/HANDOFF.md manually")
301
+ );
302
+ } else {
303
+ console.log(" " + bold("Ready to use:"));
304
+ console.log(" " + cyan("1.") + " Open " + cyan("inferno/HANDOFF.md"));
305
+ console.log(" " + cyan("2.") + " Copy all");
306
+ console.log(" " + cyan("3.") + " Paste at the start of your next AI session");
307
+ console.log(" " + gray(" tip: use --copy to skip steps 1-2 automatically"));
308
+ }
309
+ console.log();
310
+ }
@@ -1 +1,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};
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,10 +1,228 @@
1
- import*as j from"node:fs";import*as x from"node:path";import*as q from"node:readline";import{header as N,ok as C,warn as $,info as g,done as P,bold as f,cyan as w,yellow as z,green as h,gray as y}from"../ui/output.mjs";import{readProfile as I,writeProfile as k}from"../learning/profile.mjs";import{detectPatterns as O,mergeCandidates as D,pendingCandidates as b}from"../learning/patternDetector.mjs";import{synthesizeCandidate as A}from"../learning/skillSynthesizer.mjs";import{observeCommandStart as L}from"../learning/observe.mjs";function J(t){const i=x.join(t,"inferno");return j.existsSync(i)?i:null}function M(t,i){return new Promise(o=>t.question(i,s=>o(s.trim().toLowerCase())))}function R(t,i){const o=t.type==="agent"?w("agent"):h("skill"),s=Math.round(t.confidence*100),u=s>=80?h(`${s}%`):s>=60?z(`${s}%`):y(`${s}%`),a=y(`seen ${t.frequency}\xD7`);console.log(`
2
- ${f(`[${i+1}]`)} ${o} ${f(t.name)} ${u} confidence ${a}`),console.log(` Pattern: ${w(t.trigger)}`),t.steps&&console.log(` Steps: ${t.steps.join(" \u2192 ")}`),t.examples?.length&&console.log(` Examples: ${t.examples.slice(0,2).map(n=>`"${n}"`).join(", ")}`)}async function W(t,i,o,s){if(s.length===0)return g("No new candidates to review."),{approved:0,rejected:0};const u=q.createInterface({input:process.stdin,output:process.stdout});let a=0,n=0;console.log(`
3
- ${f("Candidates found:")} ${s.length}
4
- `),console.log(y(` For each: [y] approve [n] reject [s] skip [q] quit
5
- `));for(let p=0;p<s.length;p++){const e=s[p];R(e,p);const c=await M(u,`
6
- Approve? [y/n/s/q]: `);if(c==="q")break;if(c==="s")continue;const l=[...o.agentCandidates||[],...o.skillCandidates||[]].find(r=>r.id===e.id);if(l)if(c==="y"){l.status="approved";try{const{filePaths:r}=A(t,i,e),d=e.type==="agent"?"approvedAgents":"approvedSkills";o[d]||(o[d]=[]),o[d].push({id:e.id,name:e.name,filePaths:r,approvedAt:new Date().toISOString()}),C(` ${e.type==="agent"?"Agent":"Skill"} created:`);for(const v of r)console.log(` ${h("\u2192")} ${v}`);a++}catch(r){$(` Could not write files: ${r.message}`)}}else l.status="rejected",n++}return u.close(),{approved:a,rejected:n}}function E(t,i,o,s,u=.8){let a=0;for(const n of s){if(n.confidence<u)continue;const e=[...o.agentCandidates||[],...o.skillCandidates||[]].find(c=>c.id===n.id);if(e){e.status="approved";try{const{filePaths:c}=A(t,i,n),m=n.type==="agent"?"approvedAgents":"approvedSkills";o[m]||(o[m]=[]),o[m].push({id:n.id,name:n.name,filePaths:c,approvedAt:new Date().toISOString()}),C(`Auto-approved ${n.type}: ${f(n.name)} (${Math.round(n.confidence*100)}% confidence)`);for(const l of c)console.log(` ${h("\u2192")} ${l}`);a++}catch(c){$(`Could not write ${n.name}: ${c.message}`)}}}return a}async function B(t){const i=process.cwd(),o=t.includes("--auto"),s=t.includes("--json"),u=t.includes("--watch"),a=(()=>{const e=t.indexOf("--threshold");return e!==-1&&Number(t[e+1])||3})(),n=J(i);n||(s?process.stdout.write(JSON.stringify({ok:!1,error:"inferno/ not found"})+`
7
- `):$("inferno/ not found \u2014 run infernoflow init first"),process.exit(1)),L(n,"synthesize");async function p(){const e=I(n),{agentCandidates:c,skillCandidates:m}=O(e,{minFreq:a});D(e,{agentCandidates:c,skillCandidates:m});const l=b(e);if(s){k(n,e),process.stdout.write(JSON.stringify({ok:!0,pendingCount:l.length,candidates:l,sessions:e.recentSessions?.length||0},null,2)+`
8
- `);return}if(N("infernoflow synthesize"),g(`Sessions analyzed: ${f(String(e.recentSessions?.length||0))}`),g(`Minimum frequency: ${f(String(a))}\xD7`),l.length===0){console.log(),(e.recentSessions?.length||0)<a?($(`Not enough session data yet \u2014 need at least ${a} sessions with similar commands.`),$("Keep using infernoflow and run synthesize again soon.")):C("No new patterns detected \u2014 your workflow is already well captured."),k(n,e);return}g(`New patterns detected: ${f(String(l.length))}`);let r;if(o)g("Auto-approving candidates with \u226580% confidence..."),r=E(i,n,e,l);else{const d=await W(i,n,e,l);r=d.approved,d.rejected>0&&g(`Rejected: ${d.rejected}`)}if(k(n,e),r>0){console.log(),P(`${r} skill${r!==1?"s/agents":"/agent"} synthesized`),console.log(`
9
- ${f("What was created:")}`);for(const d of[...e.approvedSkills||[],...e.approvedAgents||[]].slice(-r)){const v=e.approvedAgents?.find(S=>S.id===d.id)?"agent":"skill";console.log(` ${h("\u2714")} ${v}: ${f(d.name)}`);for(const S of d.filePaths||[])console.log(` ${y(S)}`)}console.log(`
10
- ${f("Next:")} Run ${w("infernoflow agent list")} to see your agents`),console.log(` or ${w("infernoflow agent run <name>")} to execute one`)}}if(u){g("Watching for new patterns \u2014 press Ctrl+C to stop");const e=6e4;await p(),setInterval(p,e),await new Promise(()=>{})}else await p()}export{B as synthesizeCommand};
1
+ /**
2
+ * infernoflow synthesize
3
+ *
4
+ * Analyzes your observed development sessions and auto-proposes reusable
5
+ * skills and agents based on repeated patterns.
6
+ *
7
+ * Usage:
8
+ * infernoflow synthesize interactive review of candidates
9
+ * infernoflow synthesize --auto — auto-approve high-confidence (≥0.8)
10
+ * infernoflow synthesize --json — print candidates as JSON, exit
11
+ * infernoflow synthesize --threshold 2 — surface patterns seen 2+ times
12
+ * infernoflow synthesize --watch — re-run every 60s, surface new candidates
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import * as readline from "node:readline";
18
+ import { header, ok, warn, info, done, bold, cyan, yellow, green, gray } from "../ui/output.mjs";
19
+ import { readProfile, writeProfile } from "../learning/profile.mjs";
20
+ import { detectPatterns, mergeCandidates, pendingCandidates } from "../learning/patternDetector.mjs";
21
+ import { synthesizeCandidate } from "../learning/skillSynthesizer.mjs";
22
+ import { observeCommandStart } from "../learning/observe.mjs";
23
+
24
+ function findInfernoDir(cwd) {
25
+ const infernoDir = path.join(cwd, "inferno");
26
+ if (!fs.existsSync(infernoDir)) return null;
27
+ return infernoDir;
28
+ }
29
+
30
+ function ask(rl, question) {
31
+ return new Promise(resolve => rl.question(question, a => resolve(a.trim().toLowerCase())));
32
+ }
33
+
34
+ function renderCandidate(c, index) {
35
+ const type = c.type === "agent" ? cyan("agent") : green("skill");
36
+ const conf = Math.round(c.confidence * 100);
37
+ const confStr = conf >= 80 ? green(`${conf}%`) : conf >= 60 ? yellow(`${conf}%`) : gray(`${conf}%`);
38
+ const freq = gray(`seen ${c.frequency}×`);
39
+
40
+ console.log(`\n ${bold(`[${index + 1}]`)} ${type} ${bold(c.name)} ${confStr} confidence ${freq}`);
41
+ console.log(` Pattern: ${cyan(c.trigger)}`);
42
+ if (c.steps) console.log(` Steps: ${c.steps.join(" → ")}`);
43
+ if (c.examples?.length) {
44
+ console.log(` Examples: ${c.examples.slice(0, 2).map(e => `"${e}"`).join(", ")}`);
45
+ }
46
+ }
47
+
48
+ async function reviewInteractive(cwd, infernoDir, profile, candidates) {
49
+ if (candidates.length === 0) {
50
+ info("No new candidates to review.");
51
+ return { approved: 0, rejected: 0 };
52
+ }
53
+
54
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
55
+ let approved = 0, rejected = 0;
56
+
57
+ console.log(`\n ${bold("Candidates found:")} ${candidates.length}\n`);
58
+ console.log(gray(" For each: [y] approve [n] reject [s] skip [q] quit\n"));
59
+
60
+ for (let i = 0; i < candidates.length; i++) {
61
+ const c = candidates[i];
62
+ renderCandidate(c, i);
63
+
64
+ const ans = await ask(rl, `\n Approve? [y/n/s/q]: `);
65
+
66
+ if (ans === "q") break;
67
+ if (ans === "s") continue;
68
+
69
+ const all = [...(profile.agentCandidates || []), ...(profile.skillCandidates || [])];
70
+ const entry = all.find(x => x.id === c.id);
71
+ if (!entry) continue;
72
+
73
+ if (ans === "y") {
74
+ entry.status = "approved";
75
+ try {
76
+ const { filePaths } = synthesizeCandidate(cwd, infernoDir, c);
77
+ const approvedList = c.type === "agent" ? "approvedAgents" : "approvedSkills";
78
+ if (!profile[approvedList]) profile[approvedList] = [];
79
+ profile[approvedList].push({
80
+ id: c.id,
81
+ name: c.name,
82
+ filePaths,
83
+ approvedAt: new Date().toISOString(),
84
+ });
85
+ ok(` ${c.type === "agent" ? "Agent" : "Skill"} created:`);
86
+ for (const fp of filePaths) console.log(` ${green("→")} ${fp}`);
87
+ approved++;
88
+ } catch (err) {
89
+ warn(` Could not write files: ${err.message}`);
90
+ }
91
+ } else {
92
+ entry.status = "rejected";
93
+ rejected++;
94
+ }
95
+ }
96
+
97
+ rl.close();
98
+ return { approved, rejected };
99
+ }
100
+
101
+ function autoApprove(cwd, infernoDir, profile, candidates, threshold = 0.8) {
102
+ let approved = 0;
103
+ for (const c of candidates) {
104
+ if (c.confidence < threshold) continue;
105
+
106
+ const all = [...(profile.agentCandidates || []), ...(profile.skillCandidates || [])];
107
+ const entry = all.find(x => x.id === c.id);
108
+ if (!entry) continue;
109
+
110
+ entry.status = "approved";
111
+ try {
112
+ const { filePaths } = synthesizeCandidate(cwd, infernoDir, c);
113
+ const approvedList = c.type === "agent" ? "approvedAgents" : "approvedSkills";
114
+ if (!profile[approvedList]) profile[approvedList] = [];
115
+ profile[approvedList].push({
116
+ id: c.id,
117
+ name: c.name,
118
+ filePaths,
119
+ approvedAt: new Date().toISOString(),
120
+ });
121
+ ok(`Auto-approved ${c.type}: ${bold(c.name)} (${Math.round(c.confidence * 100)}% confidence)`);
122
+ for (const fp of filePaths) console.log(` ${green("→")} ${fp}`);
123
+ approved++;
124
+ } catch (err) {
125
+ warn(`Could not write ${c.name}: ${err.message}`);
126
+ }
127
+ }
128
+ return approved;
129
+ }
130
+
131
+ export async function synthesizeCommand(args) {
132
+ const cwd = process.cwd();
133
+ const isAuto = args.includes("--auto");
134
+ const isJson = args.includes("--json");
135
+ const isWatch = args.includes("--watch");
136
+ const threshold = (() => {
137
+ const idx = args.indexOf("--threshold");
138
+ return idx !== -1 ? Number(args[idx + 1]) || 3 : 3;
139
+ })();
140
+
141
+ const infernoDir = findInfernoDir(cwd);
142
+ if (!infernoDir) {
143
+ if (isJson) {
144
+ process.stdout.write(JSON.stringify({ ok: false, error: "inferno/ not found" }) + "\n");
145
+ } else {
146
+ warn("inferno/ not found — run infernoflow init first");
147
+ }
148
+ process.exit(1);
149
+ }
150
+
151
+ observeCommandStart(infernoDir, "synthesize");
152
+
153
+ async function runOnce() {
154
+ const profile = readProfile(infernoDir);
155
+
156
+ // ── Detect patterns ──────────────────────────────────────────────────────
157
+ const { agentCandidates, skillCandidates } = detectPatterns(profile, { minFreq: threshold });
158
+ mergeCandidates(profile, { agentCandidates, skillCandidates });
159
+
160
+ const pending = pendingCandidates(profile);
161
+
162
+ // ── JSON mode ────────────────────────────────────────────────────────────
163
+ if (isJson) {
164
+ writeProfile(infernoDir, profile);
165
+ process.stdout.write(JSON.stringify({
166
+ ok: true,
167
+ pendingCount: pending.length,
168
+ candidates: pending,
169
+ sessions: profile.recentSessions?.length || 0,
170
+ }, null, 2) + "\n");
171
+ return;
172
+ }
173
+
174
+ header("infernoflow synthesize");
175
+ info(`Sessions analyzed: ${bold(String(profile.recentSessions?.length || 0))}`);
176
+ info(`Minimum frequency: ${bold(String(threshold))}×`);
177
+
178
+ if (pending.length === 0) {
179
+ console.log();
180
+ if ((profile.recentSessions?.length || 0) < threshold) {
181
+ warn(`Not enough session data yet — need at least ${threshold} sessions with similar commands.`);
182
+ warn(`Keep using infernoflow and run synthesize again soon.`);
183
+ } else {
184
+ ok("No new patterns detected — your workflow is already well captured.");
185
+ }
186
+ writeProfile(infernoDir, profile);
187
+ return;
188
+ }
189
+
190
+ info(`New patterns detected: ${bold(String(pending.length))}`);
191
+
192
+ // ── Auto mode ────────────────────────────────────────────────────────────
193
+ let approved;
194
+ if (isAuto) {
195
+ info(`Auto-approving candidates with ≥80% confidence...`);
196
+ approved = autoApprove(cwd, infernoDir, profile, pending);
197
+ } else {
198
+ const result = await reviewInteractive(cwd, infernoDir, profile, pending);
199
+ approved = result.approved;
200
+ if (result.rejected > 0) info(`Rejected: ${result.rejected}`);
201
+ }
202
+
203
+ writeProfile(infernoDir, profile);
204
+
205
+ if (approved > 0) {
206
+ console.log();
207
+ done(`${approved} skill${approved !== 1 ? "s/agents" : "/agent"} synthesized`);
208
+ console.log(`\n ${bold("What was created:")}`);
209
+ for (const item of [...(profile.approvedSkills || []), ...(profile.approvedAgents || [])].slice(-approved)) {
210
+ const type = profile.approvedAgents?.find(a => a.id === item.id) ? "agent" : "skill";
211
+ console.log(` ${green("✔")} ${type}: ${bold(item.name)}`);
212
+ for (const fp of item.filePaths || []) console.log(` ${gray(fp)}`);
213
+ }
214
+ console.log(`\n ${bold("Next:")} Run ${cyan("infernoflow agent list")} to see your agents`);
215
+ console.log(` or ${cyan("infernoflow agent run <name>")} to execute one`);
216
+ }
217
+ }
218
+
219
+ if (isWatch) {
220
+ info("Watching for new patterns — press Ctrl+C to stop");
221
+ const INTERVAL = 60_000;
222
+ await runOnce();
223
+ setInterval(runOnce, INTERVAL);
224
+ await new Promise(() => {}); // keep alive
225
+ } else {
226
+ await runOnce();
227
+ }
228
+ }