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
@@ -1,34 +1,346 @@
1
- import c from"node:fs";import y from"node:path";import{execSync as C}from"node:child_process";import{bold as I,gray as g,cyan as p,red as Z,green as d,yellow as _}from"../ui/output.mjs";import{buildCursorImplementPrompt as Ct,buildGenericImplementPrompt as $t}from"../ui/prompts.mjs";import{detectDrift as bt}from"../git/detect-drift.mjs";function jt(n){try{const i=process.platform;if(i==="win32")C("clip",{input:n});else if(i==="darwin")C("pbcopy",{input:n});else try{C("xclip -selection clipboard",{input:n})}catch{C("xsel --clipboard --input",{input:n})}return!0}catch{return!1}}const h="inferno",D=y.join(h,"CONTEXT.md"),tt=y.join(h,"context-state.json");function et(n){try{return JSON.parse(c.readFileSync(n,"utf8"))}catch{return null}}function E(n){try{return c.readFileSync(n,"utf8")}catch{return null}}function nt(){const n=E(tt);if(!n)return{};try{return JSON.parse(n)}catch{return{}}}function ot(n){c.writeFileSync(tt,JSON.stringify(n,null,2),"utf8")}function A(n){return n?new Date(n).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}):"unknown"}function Ft(n,i){if(!n)return[];const s=[];let l=null;for(const a of n.split(`
2
- `))if(a.startsWith("## ")){if(l&&s.length<i&&s.push(l),s.length>=i)break;l={title:a.replace("## ","").trim(),items:[]}}else l&&a.startsWith("- ")&&l.items.push(a.replace("- ","").trim());return l&&s.length<i&&s.push(l),s.filter(a=>a.items.length>0)}async function It(n){const i=t=>n.includes(t),s=t=>{const o=n.indexOf(t);return o!==-1&&n[o+1]?n[o+1]:null},l=s("--intent")||s("-i"),a=s("--working")||s("-w"),x=s("--decision")||s("-d"),it=i("--show")||i("-s"),B=i("--copy")||i("-c"),st=i("--cursor"),rt=i("--copilot"),ct=i("--reset"),lt=i("--watch"),P=i("--auto-commit")||i("--auto-push"),L=i("--auto-push"),R=parseInt(s("--interval")||"30",10)*1e3;console.log(`
3
- `+I("\uFFFD\uFFFD\uFFFD infernoflow \u2014 context")),console.log(" "+"\u2500".repeat(50)+`
4
- `),c.existsSync(h)||(console.error(Z(" \u2718 inferno/ not found")),console.error(g(` \u2192 Run: infernoflow init
5
- `)),process.exit(1));const S=et(y.join(h,"contract.json")),T=et(y.join(h,"capabilities.json")),at=E(y.join(h,"CHANGELOG.md"));(!S||!T)&&(console.error(Z(` \u2718 Missing contract.json or capabilities.json
6
- `)),process.exit(1));let e=nt();ct&&(e={},console.log(_(` \u26A0 State reset
7
- `))),l&&(e.intent=l,e.intentUpdated=new Date().toISOString(),console.log(d(' \u2714 Intent saved: "'+l+'"'))),a&&(e.working=a,e.workingUpdated=new Date().toISOString(),console.log(d(' \u2714 Working on: "'+a+'"'))),x&&(e.decisions||(e.decisions=[]),e.decisions.push({text:x,date:new Date().toISOString()}),console.log(d(' \u2714 Decision recorded: "'+x+'"'))),(l||a||x)&&ot(e);const O=T.capabilities||[],V=O.length===(S.capabilities||[]).length,J=Ft(at,3),W=String(S.policyVersion).replace(/^v/i,""),pt=new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}),U=V?"\u2713 validated":"\u26A0 out of sync",G=e.intent||"describe the exact task to implement",X={task:G,contract:S,caps:T,scenarios:[],state:e},dt=Ct(X),ut=$t(X),gt=O.map(t=>"- **"+t.id+"** \u2014 "+t.title).join(`
8
- `),mt=J.length>0?J.map(t=>"### "+t.title+`
9
- `+t.items.map(o=>" - "+o).join(`
10
- `)).join(`
11
-
12
- `):"_No recent changes_",ft=e.intent?e.intent+" _("+A(e.intentUpdated)+")_":'_Not set \u2014 run: infernoflow context --intent "..."_',ht=e.working?e.working+" _("+A(e.workingUpdated)+")_":'_Not set \u2014 run: infernoflow context --working "..."_',wt=e.decisions&&e.decisions.length>0?e.decisions.slice(-5).map(t=>"- "+t.text+" _("+A(t.date)+")_").join(`
13
- `):"_No decisions recorded_",M=y.join(h,"sessions.jsonl");let H='_No session history yet \u2014 use: infernoflow log "<what happened>"_';if(c.existsSync(M)){const t=c.readFileSync(M,"utf8").split(`
14
- `).filter(Boolean).map(o=>{try{return JSON.parse(o)}catch{return null}}).filter(Boolean).slice(-10);t.length&&(H=t.map(o=>{const f=new Date(o.ts).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),u=o.result?` [${o.result}]`:"";return`- **${o.type||"note"}**${u} ${o.summary} _(${f})_`}).join(`
15
- `))}const K=y.join(h,"theme.json");let q="_Not scanned yet \u2014 run: infernoflow theme_";if(c.existsSync(K))try{const t=JSON.parse(c.readFileSync(K,"utf8")),o=[];if(t.fonts?.primary&&o.push(`- **Font (primary):** ${t.fonts.primary}`),t.fonts?.mono&&o.push(`- **Font (mono):** ${t.fonts.mono}`),t.colors?.mode&&o.push(`- **Color mode:** ${t.colors.mode}`),t.colors?.palette){const f=Object.entries(t.colors.palette).map(([u,k])=>`${u}=${k}`).join(" ");o.push(`- **Palette:** ${f}`)}if(t.cssVars&&Object.keys(t.cssVars).length){const f=Object.entries(t.cssVars).slice(0,8).map(([u,k])=>`${u}: ${k}`).join(" | ");o.push(`- **CSS vars:** ${f}`)}t.framework&&o.push(`- **Framework:** ${t.framework}`),o.push("","> \u26A0 Always use these exact values. Do not introduce new colors or fonts."),q=o.join(`
16
- `)}catch{}const N=["# Project Context \u2014 "+S.policyId+" v"+W,"> Generated by infernoflow | "+pt+" | "+U,"","---","","## Session Memory \u2014 what AI can't infer from code","_Decisions made, failed attempts, gotchas, preferences_","",H,"","---","","## Design System","_Fonts, colors, CSS vars \u2014 always match these exactly_","",q,"","---","","## What this system does","",gt,"","---","","## Recent changes","",mt,"","---","","## Current state","","- **Capabilities:** "+O.length,"- **Version:** v"+W,"- **Sync:** "+U,"","---","","## What I am working on right now","",ht,"","---","","## Intent \u2014 what I want to build next","",ft,"","---","","## Decisions & notes","",wt,"","---","","## Implementation Prompt Seed","","Use this to start coding immediately with an agent:","","```bash",`infernoflow implement "${G}" --mode both`,"```","","### Cursor Agent Prompt","","```text",dt,"```","","### Generic Agent Prompt","","```text",ut,"```","","---","_Paste this block at the start of any new AI session._"].join(`
17
- `);if(it||(c.writeFileSync(D,N,"utf8"),console.log(d(`
18
- \u2714 Context written \u2192 `+D))),B){const t=jt(N);console.log(t?d(" \u2714 Copied to clipboard \u2014 paste with Ctrl+V"):_(" \u26A0 Clipboard copy failed \u2014 open inferno/CONTEXT.md manually"))}if(st&&(c.writeFileSync(".cursorrules",N,"utf8"),console.log(d(" \u2714 Written to .cursorrules \u2014 Cursor loads this automatically"))),rt&&(c.existsSync(".github")||c.mkdirSync(".github"),c.writeFileSync(".github/copilot-instructions.md",N,"utf8"),console.log(d(" \u2714 Written to .github/copilot-instructions.md \u2014 Copilot loads this automatically"))),console.log(`
19
- `+I("Context Summary")),console.log(" "+"\u2500".repeat(50)),console.log(" Project "+S.policyId+" \u2014 v"+W),console.log(" Capabilities "+O.length+" registered"),console.log(" Sync "+(V?d("\u2713 in sync"):_("\u26A0 check needed"))),console.log(" Working on "+(e.working?p(e.working):g("not set"))),console.log(" Intent "+(e.intent?p(e.intent):g("not set"))),console.log(" Decisions "+(e.decisions?e.decisions.length:0)+` recorded
20
- `),console.log(" "+I("Implementation Prompt")),console.log(" "+p("\u2192")+" Run "+p(`infernoflow implement "${G}" --mode both`)+`
21
- `),B?(console.log(" "+I("Ready to use:")),console.log(" "+p("\u2192")+" Paste into Claude / Cursor / Copilot with "+p("Ctrl+V")+`
22
- `)):(console.log(" "+I("Ready to use:")),console.log(" "+p("1.")+" Open "+p("inferno/CONTEXT.md")),console.log(" "+p("2.")+" Copy everything"),console.log(" "+p("3.")+" Paste at the start of your next AI session"),console.log(" "+g(" tip: use --copy to skip steps 1-2 automatically")+`
23
- `)),lt){let u=function(m){try{return C(m,{cwd:process.cwd(),encoding:"utf8",stdio:["ignore","pipe","pipe"]}),!0}catch{return!1}},k=function(m){try{return C(`git status --porcelain "${m}"`,{cwd:process.cwd(),encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()===""}catch{return!0}},z=function(m,r,v){const b=`chore: update context [${r.length>0?r.slice(0,3).join(", "):`${v} files`}]`;return u(`git add "${m}"`)?k(m)?{ok:!1,reason:"nothing to commit"}:u(`git commit -m "${b}"`)?{ok:!0,msg:b}:{ok:!1,reason:"git commit failed (lock?)"}:{ok:!1,reason:"git add failed"}},Q=function(){return u("git push")};var _t=u,xt=k,Ot=z,Nt=Q;const t=L?"auto-push":P?"auto-commit":"watch";console.log(" "+p("\u{1F441} Watch mode active")+g(` \u2014 polling every ${R/1e3}s`+(L?" \xB7 will commit + push on change":P?" \xB7 will commit on change":""))),console.log(" "+g(`Press Ctrl+C to stop
24
- `));let o="",f=null;const Y=async()=>{try{const m=process.cwd(),r=bt(m,{sinceCommits:1}),v=r.changedFiles.sort().join("|");if(v===o||(o=v,r.changedFiles.length===0))return;const $=r.affectedCapabilities.map(F=>F.id),b=$.length>0?`Working on: ${$.join(", ")} (${r.changedFiles.length} files changed)`:`${r.changedFiles.length} files changed \u2014 no capability match yet`,j=nt();if(j.working!==b){j.working=b,j.workingUpdated=new Date().toISOString(),ot(j),await It(n.filter(w=>w!=="--watch"&&w!=="--auto-commit"&&w!=="--auto-push"));const F=E(D),St=new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",second:"2-digit"});if(process.stderr.write(`
25
- ${d("\u2714")} [${St}] Context updated \u2014 ${$.length} capabilities affected
26
- ${g(r.changedFiles.slice(0,3).join(", ")+(r.changedFiles.length>3?` +${r.changedFiles.length-3} more`:""))}
27
- `),P&&F!==f){f=F;const w=z(D,$,r.changedFiles.length);if(w.ok){if(process.stderr.write(` ${d("\u2714")} Committed: ${g(w.msg)}
28
- `),L){const kt=Q();process.stderr.write(kt?` ${d("\u2714")} Pushed to origin
29
- `:` ${_("\u26A0")} Push failed \u2014 will retry next change
30
- `)}}else process.stderr.write(` ${_("\u26A0")} Commit skipped: ${g(w.reason)}
31
- `)}}}catch{}};await Y();const yt=setInterval(Y,R);process.on("SIGINT",()=>{clearInterval(yt),process.stderr.write(`
32
- `+g(`Watch stopped.
33
-
34
- `)),process.exit(0)}),await new Promise(()=>{})}}export{It as contextCommand};
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { bold, gray, cyan, red, green, yellow } from "../ui/output.mjs";
5
+ import { buildCursorImplementPrompt, buildGenericImplementPrompt } from "../ui/prompts.mjs";
6
+ import { detectDrift } from "../git/detect-drift.mjs";
7
+
8
+ function copyToClipboard(text) {
9
+ try {
10
+ const p = process.platform;
11
+ if (p === "win32") execSync("clip", { input: text });
12
+ else if (p === "darwin") execSync("pbcopy", { input: text });
13
+ else { try { execSync("xclip -selection clipboard", { input: text }); } catch { execSync("xsel --clipboard --input", { input: text }); } }
14
+ return true;
15
+ } catch { return false; }
16
+ }
17
+
18
+ const INFERNO_DIR = "inferno";
19
+ const CONTEXT_FILE = path.join(INFERNO_DIR, "CONTEXT.md");
20
+ const STATE_FILE = path.join(INFERNO_DIR, "context-state.json");
21
+
22
+ function readJSON(f) { try { return JSON.parse(fs.readFileSync(f,"utf8")); } catch { return null; } }
23
+ function readFile(f) { try { return fs.readFileSync(f,"utf8"); } catch { return null; } }
24
+ function loadState() { const r=readFile(STATE_FILE); if(!r) return {}; try { return JSON.parse(r); } catch { return {}; } }
25
+ function saveState(s) { fs.writeFileSync(STATE_FILE,JSON.stringify(s,null,2),"utf8"); }
26
+ function fmtDate(iso) { if(!iso) return "unknown"; return new Date(iso).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}); }
27
+ function parseChangelog(txt,max) {
28
+ if(!txt) return [];
29
+ const entries=[]; let cur=null;
30
+ for(const line of txt.split("\n")) {
31
+ if(line.startsWith("## ")) { if(cur&&entries.length<max) entries.push(cur); if(entries.length>=max) break; cur={title:line.replace("## ","").trim(),items:[]}; }
32
+ else if(cur&&line.startsWith("- ")) cur.items.push(line.replace("- ","").trim());
33
+ }
34
+ if(cur&&entries.length<max) entries.push(cur);
35
+ return entries.filter(e=>e.items.length>0);
36
+ }
37
+
38
+ export async function contextCommand(args) {
39
+ const has = (f) => args.includes(f);
40
+ const flag = (f) => { const i=args.indexOf(f); return i!==-1&&args[i+1]?args[i+1]:null; };
41
+
42
+ const intent = flag("--intent") || flag("-i");
43
+ const working = flag("--working") || flag("-w");
44
+ const decision = flag("--decision") || flag("-d");
45
+ const showOnly = has("--show") || has("-s");
46
+ const copyFlag = has("--copy") || has("-c");
47
+ const cursorFlag = has("--cursor");
48
+ const copilotFlag = has("--copilot");
49
+ const resetFlag= has("--reset");
50
+ const watchFlag = has("--watch");
51
+ const autoCommit = has("--auto-commit") || has("--auto-push");
52
+ const autoPush = has("--auto-push");
53
+ const statsFlag = has("--stats");
54
+ const watchInterval = parseInt(flag("--interval") || "30", 10) * 1000;
55
+
56
+ console.log("\n "+bold("��� infernoflow — context"));
57
+ console.log(" "+"─".repeat(50)+"\n");
58
+
59
+ if(!fs.existsSync(INFERNO_DIR)){
60
+ console.error(red(" ✘ inferno/ not found"));
61
+ console.error(gray(" → Run: infernoflow init\n"));
62
+ process.exit(1);
63
+ }
64
+
65
+ const contract = readJSON(path.join(INFERNO_DIR,"contract.json"));
66
+ const capabilities = readJSON(path.join(INFERNO_DIR,"capabilities.json"));
67
+ const changelog = readFile(path.join(INFERNO_DIR,"CHANGELOG.md"));
68
+
69
+ if(!contract||!capabilities){
70
+ console.error(red(" ✘ Missing contract.json or capabilities.json\n"));
71
+ process.exit(1);
72
+ }
73
+
74
+ let state = loadState();
75
+ if(resetFlag){ state={}; console.log(yellow(" ⚠ State reset\n")); }
76
+ if(intent) { state.intent=intent; state.intentUpdated=new Date().toISOString(); console.log(green(' ✔ Intent saved: "'+intent+'"')); }
77
+ if(working) { state.working=working; state.workingUpdated=new Date().toISOString(); console.log(green(' ✔ Working on: "'+working+'"')); }
78
+ if(decision) { if(!state.decisions) state.decisions=[]; state.decisions.push({text:decision,date:new Date().toISOString()}); console.log(green(' ✔ Decision recorded: "'+decision+'"')); }
79
+ if(intent||working||decision) saveState(state);
80
+
81
+ const capList = capabilities.capabilities||[];
82
+ const allInSync = capList.length===(contract.capabilities||[]).length;
83
+ const recent = parseChangelog(changelog,3);
84
+ const version = String(contract.policyVersion).replace(/^v/i,"");
85
+ const now = new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"});
86
+ const syncBadge = allInSync?"✓ validated":"⚠ out of sync";
87
+ const implementTask = state.intent || "describe the exact task to implement";
88
+ const implementInput = { task: implementTask, contract, caps: capabilities, scenarios: [], state };
89
+ const cursorPrompt = buildCursorImplementPrompt(implementInput);
90
+ const genericPrompt = buildGenericImplementPrompt(implementInput);
91
+
92
+ const capLines = capList.map(c=>"- **"+c.id+"** — "+c.title).join("\n");
93
+ const chgLines = recent.length>0 ? recent.map(e=>"### "+e.title+"\n"+e.items.map(i=>" - "+i).join("\n")).join("\n\n") : "_No recent changes_";
94
+ const intentLine = state.intent ? state.intent+" _("+fmtDate(state.intentUpdated)+")_" : "_Not set — run: infernoflow context --intent \"...\"_";
95
+ const workingLine = state.working ? state.working+" _("+fmtDate(state.workingUpdated)+")_" : "_Not set — run: infernoflow context --working \"...\"_";
96
+ const decLines = state.decisions&&state.decisions.length>0 ? state.decisions.slice(-5).map(d=>"- "+d.text+" _("+fmtDate(d.date)+")_").join("\n") : "_No decisions recorded_";
97
+
98
+ // ── Session memory (what AI can't infer from code) ────────────────────────
99
+ const sessionsFile = path.join(INFERNO_DIR, "sessions.jsonl");
100
+ let sessionLines = "_No session history yet — use: infernoflow log \"<what happened>\"_";
101
+ if (fs.existsSync(sessionsFile)) {
102
+ const entries = fs.readFileSync(sessionsFile, "utf8")
103
+ .split("\n").filter(Boolean)
104
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
105
+ .filter(Boolean)
106
+ .slice(-10); // last 10 entries
107
+ if (entries.length) {
108
+ sessionLines = entries.map(e => {
109
+ const ts = new Date(e.ts).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});
110
+ const result = e.result ? ` [${e.result}]` : "";
111
+ return `- **${e.type||"note"}**${result} ${e.summary} _(${ts})_`;
112
+ }).join("\n");
113
+ }
114
+ }
115
+
116
+ // ── Design system (theme.json) ────────────────────────────────────────────
117
+ const themeFile = path.join(INFERNO_DIR, "theme.json");
118
+ let themeLines = "_Not scanned yet — run: infernoflow theme_";
119
+ if (fs.existsSync(themeFile)) {
120
+ try {
121
+ const theme = JSON.parse(fs.readFileSync(themeFile, "utf8"));
122
+ const lines = [];
123
+ if (theme.fonts?.primary) lines.push(`- **Font (primary):** ${theme.fonts.primary}`);
124
+ if (theme.fonts?.mono) lines.push(`- **Font (mono):** ${theme.fonts.mono}`);
125
+ if (theme.colors?.mode) lines.push(`- **Color mode:** ${theme.colors.mode}`);
126
+ if (theme.colors?.palette) {
127
+ const pal = Object.entries(theme.colors.palette).map(([k,v])=>`${k}=${v}`).join(" ");
128
+ lines.push(`- **Palette:** ${pal}`);
129
+ }
130
+ if (theme.cssVars && Object.keys(theme.cssVars).length) {
131
+ const vars = Object.entries(theme.cssVars).slice(0,8).map(([k,v])=>`${k}: ${v}`).join(" | ");
132
+ lines.push(`- **CSS vars:** ${vars}`);
133
+ }
134
+ if (theme.framework) lines.push(`- **Framework:** ${theme.framework}`);
135
+ lines.push("", "> ⚠ Always use these exact values. Do not introduce new colors or fonts.");
136
+ themeLines = lines.join("\n");
137
+ } catch {}
138
+ }
139
+
140
+ const md = [
141
+ "# Project Context — "+contract.policyId+" v"+version,
142
+ "> Generated by infernoflow | "+now+" | "+syncBadge,
143
+ "","---","",
144
+ "## Session Memory — what AI can't infer from code",
145
+ "_Decisions made, failed attempts, gotchas, preferences_","",
146
+ sessionLines,"","---","",
147
+ "## Design System",
148
+ "_Fonts, colors, CSS vars — always match these exactly_","",
149
+ themeLines,"","---","",
150
+ "## What this system does","",capLines,"","---","",
151
+ "## Recent changes","",chgLines,"","---","",
152
+ "## Current state","",
153
+ "- **Capabilities:** "+capList.length,
154
+ "- **Version:** v"+version,
155
+ "- **Sync:** "+syncBadge,
156
+ "","---","",
157
+ "## What I am working on right now","",workingLine,"","---","",
158
+ "## Intent — what I want to build next","",intentLine,"","---","",
159
+ "## Decisions & notes","",decLines,"","---",
160
+ "",
161
+ "## Implementation Prompt Seed","",
162
+ "Use this to start coding immediately with an agent:","",
163
+ "```bash",
164
+ `infernoflow implement "${implementTask}" --mode both`,
165
+ "```",
166
+ "",
167
+ "### Cursor Agent Prompt","",
168
+ "```text",
169
+ cursorPrompt,
170
+ "```",
171
+ "",
172
+ "### Generic Agent Prompt","",
173
+ "```text",
174
+ genericPrompt,
175
+ "```",
176
+ "",
177
+ "---",
178
+ "_Paste this block at the start of any new AI session._"
179
+ ].join("\n");
180
+
181
+ if(!showOnly){ fs.writeFileSync(CONTEXT_FILE,md,"utf8"); console.log(green("\n ✔ Context written → "+CONTEXT_FILE)); }
182
+
183
+ if(copyFlag){
184
+ const ok=copyToClipboard(md);
185
+ console.log(ok ? green(" ✔ Copied to clipboard — paste with Ctrl+V") : yellow(" ⚠ Clipboard copy failed — open inferno/CONTEXT.md manually"));
186
+ }
187
+
188
+ if (cursorFlag) {
189
+ fs.writeFileSync(".cursorrules", md, "utf8");
190
+ console.log(green(" ✔ Written to .cursorrules — Cursor loads this automatically"));
191
+ }
192
+ if (copilotFlag) {
193
+ if (!fs.existsSync(".github")) fs.mkdirSync(".github");
194
+ fs.writeFileSync(".github/copilot-instructions.md", md, "utf8");
195
+ console.log(green(" ✔ Written to .github/copilot-instructions.md — Copilot loads this automatically"));
196
+ }
197
+ console.log("\n "+bold("Context Summary"));
198
+ console.log(" "+"─".repeat(50));
199
+ console.log(" Project "+contract.policyId+" — v"+version);
200
+ console.log(" Capabilities "+capList.length+" registered");
201
+ console.log(" Sync "+(allInSync?green("✓ in sync"):yellow("⚠ check needed")));
202
+ console.log(" Working on "+(state.working?cyan(state.working):gray("not set")));
203
+ console.log(" Intent "+(state.intent ?cyan(state.intent) :gray("not set")));
204
+ console.log(" Decisions "+(state.decisions?state.decisions.length:0)+" recorded");
205
+
206
+ // Brief stats line when --stats flag is set
207
+ if (statsFlag) {
208
+ try {
209
+ const { statsCommand } = await import("./stats.mjs");
210
+ process.stdout.write("\n");
211
+ await statsCommand(["--brief"]);
212
+ } catch {}
213
+ }
214
+ console.log();
215
+ console.log(" "+bold("Implementation Prompt"));
216
+ console.log(" "+cyan("→")+" Run "+cyan(`infernoflow implement "${implementTask}" --mode both`)+"\n");
217
+
218
+ if(copyFlag){
219
+ console.log(" "+bold("Ready to use:"));
220
+ console.log(" "+cyan("→")+" Paste into Claude / Cursor / Copilot with "+cyan("Ctrl+V")+"\n");
221
+ } else {
222
+ console.log(" "+bold("Ready to use:"));
223
+ console.log(" "+cyan("1.")+" Open "+cyan("inferno/CONTEXT.md"));
224
+ console.log(" "+cyan("2.")+" Copy everything");
225
+ console.log(" "+cyan("3.")+" Paste at the start of your next AI session");
226
+ console.log(" "+gray(" tip: use --copy to skip steps 1-2 automatically")+"\n");
227
+ }
228
+
229
+ // ── Watch mode ────────────────────────────────────────────────────────────
230
+ if (watchFlag) {
231
+ const modeLabel = autoPush ? "auto-push" : autoCommit ? "auto-commit" : "watch";
232
+ console.log(" " + cyan("👁 Watch mode active") + gray(
233
+ ` — polling every ${watchInterval / 1000}s` +
234
+ (autoPush ? " · will commit + push on change" : autoCommit ? " · will commit on change" : "")
235
+ ));
236
+ console.log(" " + gray("Press Ctrl+C to stop\n"));
237
+
238
+ let lastChangedFiles = "";
239
+ let lastCommittedContent = null;
240
+
241
+ // ── git helpers ──────────────────────────────────────────────────────
242
+ function gitRun(cmd) {
243
+ try {
244
+ execSync(cmd, { cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
245
+ return true;
246
+ } catch { return false; }
247
+ }
248
+
249
+ function gitIsCleanFor(filePath) {
250
+ // Returns true if the file has no staged/unstaged changes (nothing to commit)
251
+ try {
252
+ const out = execSync(`git status --porcelain "${filePath}"`, {
253
+ cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
254
+ }).trim();
255
+ return out === "";
256
+ } catch { return true; }
257
+ }
258
+
259
+ function commitContext(contextPath, affectedCaps, changedCount) {
260
+ const caps = affectedCaps.length > 0 ? affectedCaps.slice(0, 3).join(", ") : `${changedCount} files`;
261
+ const msg = `chore: update context [${caps}]`;
262
+ const staged = gitRun(`git add "${contextPath}"`);
263
+ if (!staged) return { ok: false, reason: "git add failed" };
264
+ if (gitIsCleanFor(contextPath)) return { ok: false, reason: "nothing to commit" };
265
+ const committed = gitRun(`git commit -m "${msg}"`);
266
+ if (!committed) return { ok: false, reason: "git commit failed (lock?)" };
267
+ return { ok: true, msg };
268
+ }
269
+
270
+ function pushContext() {
271
+ const ok = gitRun("git push");
272
+ return ok;
273
+ }
274
+
275
+ // ── poll loop ────────────────────────────────────────────────────────
276
+ const poll = async () => {
277
+ try {
278
+ const cwd = process.cwd();
279
+ const drift = detectDrift(cwd, { sinceCommits: 1 });
280
+ const changedKey = drift.changedFiles.sort().join("|");
281
+
282
+ if (changedKey === lastChangedFiles) return;
283
+ lastChangedFiles = changedKey;
284
+ if (drift.changedFiles.length === 0) return;
285
+
286
+ // Update "working" field
287
+ const affected = drift.affectedCapabilities.map(c => c.id);
288
+ const newWorking = affected.length > 0
289
+ ? `Working on: ${affected.join(", ")} (${drift.changedFiles.length} files changed)`
290
+ : `${drift.changedFiles.length} files changed — no capability match yet`;
291
+
292
+ const currentState = loadState();
293
+ if (currentState.working !== newWorking) {
294
+ currentState.working = newWorking;
295
+ currentState.workingUpdated = new Date().toISOString();
296
+ saveState(currentState);
297
+
298
+ // Regenerate CONTEXT.md silently
299
+ await contextCommand(args.filter(a => a !== "--watch" && a !== "--auto-commit" && a !== "--auto-push"));
300
+
301
+ const newContent = readFile(CONTEXT_FILE);
302
+ const ts = new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
303
+
304
+ process.stderr.write(
305
+ `\n ${green("✔")} [${ts}] Context updated — ${affected.length} capabilities affected\n` +
306
+ ` ${gray(drift.changedFiles.slice(0, 3).join(", ") + (drift.changedFiles.length > 3 ? ` +${drift.changedFiles.length - 3} more` : ""))}\n`
307
+ );
308
+
309
+ // Auto-commit if enabled and content actually changed
310
+ if (autoCommit && newContent !== lastCommittedContent) {
311
+ lastCommittedContent = newContent;
312
+ const result = commitContext(CONTEXT_FILE, affected, drift.changedFiles.length);
313
+ if (result.ok) {
314
+ process.stderr.write(` ${green("✔")} Committed: ${gray(result.msg)}\n`);
315
+ if (autoPush) {
316
+ const pushed = pushContext();
317
+ process.stderr.write(
318
+ pushed
319
+ ? ` ${green("✔")} Pushed to origin\n`
320
+ : ` ${yellow("⚠")} Push failed — will retry next change\n`
321
+ );
322
+ }
323
+ } else {
324
+ process.stderr.write(` ${yellow("⚠")} Commit skipped: ${gray(result.reason)}\n`);
325
+ }
326
+ }
327
+ }
328
+ } catch {
329
+ // Silent — watch mode never crashes
330
+ }
331
+ };
332
+
333
+ // Poll immediately then on interval
334
+ await poll();
335
+ const timer = setInterval(poll, watchInterval);
336
+
337
+ process.on("SIGINT", () => {
338
+ clearInterval(timer);
339
+ process.stderr.write("\n " + gray("Watch stopped.\n\n"));
340
+ process.exit(0);
341
+ });
342
+
343
+ // Prevent Node from exiting
344
+ await new Promise(() => {});
345
+ }
346
+ }