infernoflow 0.10.21 → 0.10.23

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog — infernoflow
2
2
 
3
+ ## 0.10.23 — 2026-04-21
4
+
5
+ ### Added
6
+ - Release 0.10.23
7
+
8
+
9
+ ## 0.10.22 — 2026-04-21
10
+
11
+ ### Added
12
+ - Release 0.10.22
13
+
14
+
3
15
  ## 0.10.21 — 2026-04-21
4
16
 
5
17
  ### Added
@@ -1,21 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as m}from"node:fs";import{dirname as d,join as f}from"node:path";import{fileURLToPath as u}from"node:url";import{bold as t,gray as e,red as s}from"../lib/ui/output.mjs";const g=d(u(import.meta.url)),h=JSON.parse(m(f(g,"..","package.json"),"utf8")),r=h.version||"0.0.0",c={publish:"Bump version, update changelog, build, npm publish, git commit + push in one shot",diff:"Show what capabilities changed since the last git tag (or any ref)",changelog:"Draft a changelog entry from commits since the last tag",setup:"One command to get fully operational \u2014 detects IDE, inits, installs hooks + MCP",init:"Scaffold inferno/ in your project (or adopt existing project)","install-cursor-hooks":"Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md","install-vscode-copilot-hooks":"Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",check:"Validate contract, capabilities, scenarios, changelog",status:"Show contract health at a glance","pr-impact":"Summarize PR impact on capabilities and docs",sync:"Run deterministic inferno sync flow",run:"One-command detect/propose/apply/validate flow","doc-gate":"Fail if code changed but docs were not updated",suggest:"Generate AI prompt + apply capability updates",implement:"Generate code-agent implementation prompt(s)",context:"Generate AI-ready context for new sessions","generate-skills":"Generate personalised Cursor rules + skill files from your developer profile"},l={publish:async o=>(await import("../lib/commands/publish.mjs")).publishCommand(o),diff:async o=>(await import("../lib/commands/diff.mjs")).diffCommand(o),changelog:async o=>(await import("../lib/commands/changelog.mjs")).changelogCommand(o),setup:async o=>(await import("../lib/commands/setup.mjs")).setupCommand(o),init:async o=>(await import("../lib/commands/init.mjs")).initCommand(o),"install-cursor-hooks":async o=>(await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(o),"install-vscode-copilot-hooks":async o=>(await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(o),check:async o=>(await import("../lib/commands/check.mjs")).checkCommand(o),status:async o=>(await import("../lib/commands/status.mjs")).statusCommand(o),"pr-impact":async o=>(await import("../lib/commands/prImpact.mjs")).prImpactCommand(o),sync:async o=>(await import("../lib/commands/syncAuto.mjs")).syncCommand(o),run:async o=>(await import("../lib/commands/run.mjs")).runCommand(o),suggest:async o=>(await import("../lib/commands/suggest.mjs")).suggestCommand(o),implement:async o=>(await import("../lib/commands/implement.mjs")).implementCommand(o),context:async o=>(await import("../lib/commands/context.mjs")).contextCommand(o),"doc-gate":async o=>(await import("../lib/commands/docGate.mjs")).docGateCommand(o),"generate-skills":async o=>(await import("../lib/commands/generateSkills.mjs")).generateSkillsCommand(o)};function y(){const o=Object.keys(c),i=Math.max(...o.map(a=>a.length),8)+1;return Object.entries(c).map(([a,p])=>` ${a.padEnd(i," ")}${p}`).join(`
2
+ import{readFileSync as m}from"node:fs";import{dirname as d,join as u}from"node:path";import{fileURLToPath as f}from"node:url";import{bold as e,gray as t,red as i}from"../lib/ui/output.mjs";const g=d(f(import.meta.url)),h=JSON.parse(m(u(g,"..","package.json"),"utf8")),r=h.version||"0.0.0",c={publish:"Bump version, update changelog, build, npm publish, git commit + push in one shot",diff:"Show what capabilities changed since the last git tag (or any ref)",changelog:"Draft a changelog entry from commits since the last tag",setup:"One command to get fully operational \u2014 detects IDE, inits, installs hooks + MCP",init:"Scaffold inferno/ in your project (or adopt existing project)","install-cursor-hooks":"Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md","install-vscode-copilot-hooks":"Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",check:"Validate contract, capabilities, scenarios, changelog",status:"Show contract health at a glance","pr-impact":"Summarize PR impact on capabilities and docs",sync:"Run deterministic inferno sync flow",run:"One-command detect/propose/apply/validate flow","doc-gate":"Fail if code changed but docs were not updated",suggest:"Generate AI prompt + apply capability updates",implement:"Generate code-agent implementation prompt(s)",context:"Generate AI-ready context for new sessions","generate-skills":"Generate personalised Cursor rules + skill files from your developer profile"},l={publish:async o=>(await import("../lib/commands/publish.mjs")).publishCommand(o),diff:async o=>(await import("../lib/commands/diff.mjs")).diffCommand(o),changelog:async o=>(await import("../lib/commands/changelog.mjs")).changelogCommand(o),setup:async o=>(await import("../lib/commands/setup.mjs")).setupCommand(o),init:async o=>(await import("../lib/commands/init.mjs")).initCommand(o),"install-cursor-hooks":async o=>(await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(o),"install-vscode-copilot-hooks":async o=>(await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(o),check:async o=>(await import("../lib/commands/check.mjs")).checkCommand(o),status:async o=>(await import("../lib/commands/status.mjs")).statusCommand(o),"pr-impact":async o=>(await import("../lib/commands/prImpact.mjs")).prImpactCommand(o),sync:async o=>(await import("../lib/commands/syncAuto.mjs")).syncCommand(o),run:async o=>(await import("../lib/commands/run.mjs")).runCommand(o),suggest:async o=>(await import("../lib/commands/suggest.mjs")).suggestCommand(o),implement:async o=>(await import("../lib/commands/implement.mjs")).implementCommand(o),context:async o=>(await import("../lib/commands/context.mjs")).contextCommand(o),"doc-gate":async o=>(await import("../lib/commands/docGate.mjs")).docGateCommand(o),"generate-skills":async o=>(await import("../lib/commands/generateSkills.mjs")).generateSkillsCommand(o)};function y(){const o=Object.keys(c),s=Math.max(...o.map(a=>a.length),8)+1;return Object.entries(c).map(([a,p])=>` ${a.padEnd(s," ")}${p}`).join(`
3
3
  `)}const w=`
4
- ${t("\u{1F525} infernoflow")} ${e("v"+r)}
5
- ${e("The forge for liquid code \u2014 keep every AI session in sync")}
4
+ ${e("\u{1F525} infernoflow")} ${t("v"+r)}
5
+ ${t("The forge for liquid code \u2014 keep every AI session in sync")}
6
6
 
7
- ${t("Usage:")}
7
+ ${e("Usage:")}
8
8
  infernoflow <command> [options]
9
9
 
10
- ${t("Commands:")}
10
+ ${e("Commands:")}
11
11
  ${y()}
12
12
 
13
- ${t("diff options:")}
13
+ ${e("diff options:")}
14
14
  --ref <tag|commit> Compare against a specific ref (default: last git tag)
15
15
  --summary One-liner count only
16
16
  --json Machine-readable output
17
17
 
18
- ${t("changelog options:")}
18
+ ${e("changelog options:")}
19
19
  update Draft ## Unreleased from commits (default sub-command)
20
20
  show Print the current ## Unreleased block
21
21
  list List commits since last tag
@@ -24,7 +24,7 @@ ${y()}
24
24
  --append Append to existing ## Unreleased instead of replacing
25
25
  --json Machine-readable output
26
26
 
27
- ${t("publish options:")}
27
+ ${e("publish options:")}
28
28
  --bump patch|minor|major Version bump type (default: patch)
29
29
  --skip-build Skip the build step
30
30
  --skip-tests Skip smoke tests
@@ -33,11 +33,11 @@ ${y()}
33
33
  --dry-run Print all steps without executing
34
34
  --yes, -y Non-interactive (skip confirmation prompt)
35
35
 
36
- ${t("setup options:")}
36
+ ${e("setup options:")}
37
37
  --yes, -y Skip prompts (non-interactive)
38
38
  --force, -f Overwrite existing hook files
39
39
 
40
- ${t("init options:")}
40
+ ${e("init options:")}
41
41
  --cursor-hooks Also install Cursor hooks (draft \u2192 inferno/CONTEXT.draft.md)
42
42
  --vscode-copilot-hooks Also install VS Code + Copilot hooks (.github/hooks \u2014 Preview)
43
43
  --adopt Infer capabilities from an existing codebase
@@ -50,13 +50,13 @@ ${y()}
50
50
  --yes, -y Skip prompts and accept inferred/default values
51
51
  --force, -f Overwrite existing inferno/ files
52
52
 
53
- ${t("install-cursor-hooks options:")}
53
+ ${e("install-cursor-hooks options:")}
54
54
  --force, -f Overwrite .cursor/hooks.json and hook scripts if they exist
55
55
 
56
- ${t("install-vscode-copilot-hooks options:")}
56
+ ${e("install-vscode-copilot-hooks options:")}
57
57
  --force, -f Overwrite .github/hooks/infernoflow-drafts.json and scripts if they exist
58
58
 
59
- ${t("context options:")}
59
+ ${e("context options:")}
60
60
  --intent "..." What you plan to build next
61
61
  --working "..." What you are building right now
62
62
  --decision "..." Record a decision or note
@@ -65,37 +65,46 @@ ${y()}
65
65
  --reset Clear all stored state
66
66
  --watch Poll git diff every 30s and auto-update CONTEXT.md (living context)
67
67
  --interval <secs> Watch poll interval in seconds (default: 30)
68
+ --auto-commit Watch mode: commit CONTEXT.md to git on every change
69
+ --auto-push Watch mode: commit + push CONTEXT.md on every change
68
70
 
69
- ${t("generate-skills options:")}
71
+ ${e("generate-skills options:")}
70
72
  --cursor Also install rules to .cursor/rules/infernoflow.md
71
73
  --force, -f Overwrite existing generated skill files
72
74
 
73
- ${t("implement options:")}
75
+ ${e("implement options:")}
74
76
  --mode <type> cursor | generic | both (default: both)
75
77
  --copy, -c Copy generated prompt(s) to clipboard
76
78
 
77
- ${t("run options:")}
79
+ ${e("run options:")}
78
80
  --dry-run Execute full flow without writing files
79
81
  --json Emit machine-readable events and result payload
80
82
  --no-rollback Keep changes even if validation fails
81
83
  --provider <type> auto | agent | local | prompt (default: auto)
82
84
  --ide <name> auto | cursor | vscode | windsurf (default: auto)
83
85
 
84
- ${t("Typical workflow:")}
85
- ${e('1. infernoflow context --intent "what I want to build"')}
86
- ${e("2. [paste inferno/CONTEXT.md into Claude / Cursor / Copilot]")}
87
- ${e("3. [build the feature]")}
88
- ${e('4. infernoflow suggest "what I built"')}
89
- ${e("5. infernoflow check")}
90
-
91
- ${t("Machine output:")}
92
- ${e("status --json")}
93
- ${e("check --json")}
94
- ${e("doc-gate --json")}
95
- ${e("pr-impact --json")}
96
- ${e("sync --auto --json")}
97
- ${e('run "task" --json')}
98
- `;import*as k from"node:fs";import*as b from"node:path";try{const o=b.join(process.cwd(),"inferno");if(k.existsSync(o)){const{observeCommandStart:i}=await import("../lib/learning/observe.mjs"),a=process.argv[2];a&&!a.startsWith("-")&&i(o,a)}}catch{}const[,,n,...C]=process.argv;(!n||n==="--help"||n==="-h")&&(console.log(w),process.exit(0)),(n==="--version"||n==="-v")&&(console.log(r),process.exit(0));const v=Object.keys(l);v.includes(n)||(console.error(s(`
99
- Unknown command: ${n}`)),console.error(e(`Run: infernoflow --help
100
- `)),process.exit(1));const $=[n,...C];l[n]($).catch(o=>{console.error(s(`
86
+ ${e("Typical workflow:")}
87
+ ${t('1. infernoflow context --intent "what I want to build"')}
88
+ ${t("2. [paste inferno/CONTEXT.md into Claude / Cursor / Copilot]")}
89
+ ${t("3. [build the feature]")}
90
+ ${t('4. infernoflow suggest "what I built"')}
91
+ ${t("5. infernoflow check")}
92
+
93
+ ${e("suggest options:")}
94
+ --json Non-interactive: emit prompt as JSON, no readline prompts
95
+ --response <json|@file> Provide AI response directly (use with --json)
96
+ --apply Apply the response changes when using --json --response
97
+
98
+ ${e("Machine output:")}
99
+ ${t("status --json")}
100
+ ${t("check --json")}
101
+ ${t("doc-gate --json")}
102
+ ${t("pr-impact --json")}
103
+ ${t("sync --auto --json")}
104
+ ${t('run "task" --json')}
105
+ ${t('suggest "what changed" --json')}
106
+ ${t(`suggest "what changed" --json --response '{"newCapabilities":[...]}' --apply`)}
107
+ `;import*as k from"node:fs";import*as C from"node:path";try{const o=C.join(process.cwd(),"inferno");if(k.existsSync(o)){const{observeCommandStart:s}=await import("../lib/learning/observe.mjs"),a=process.argv[2];a&&!a.startsWith("-")&&s(o,a)}}catch{}const[,,n,...b]=process.argv;(!n||n==="--help"||n==="-h")&&(console.log(w),process.exit(0)),(n==="--version"||n==="-v")&&(console.log(r),process.exit(0));const v=Object.keys(l);v.includes(n)||(console.error(i(`
108
+ Unknown command: ${n}`)),console.error(t(`Run: infernoflow --help
109
+ `)),process.exit(1));const $=[n,...b];l[n]($).catch(o=>{console.error(i(`
101
110
  Error: `)+o.message),process.exit(1)});
@@ -1,27 +1,31 @@
1
- import l from"node:fs";import h from"node:path";import{execSync as b}from"node:child_process";import{bold as m,gray as d,cyan as r,red as G,green as a,yellow as _}from"../ui/output.mjs";import{buildCursorImplementPrompt as ct,buildGenericImplementPrompt as rt}from"../ui/prompts.mjs";import{detectDrift as lt}from"../git/detect-drift.mjs";function at(n){try{const o=process.platform;if(o==="win32")b("clip",{input:n});else if(o==="darwin")b("pbcopy",{input:n});else try{b("xclip -selection clipboard",{input:n})}catch{b("xsel --clipboard --input",{input:n})}return!0}catch{return!1}}const u="inferno",R=h.join(u,"CONTEXT.md"),A=h.join(u,"context-state.json");function U(n){try{return JSON.parse(l.readFileSync(n,"utf8"))}catch{return null}}function $(n){try{return l.readFileSync(n,"utf8")}catch{return null}}function J(){const n=$(A);if(!n)return{};try{return JSON.parse(n)}catch{return{}}}function V(n){l.writeFileSync(A,JSON.stringify(n,null,2),"utf8")}function O(n){return n?new Date(n).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}):"unknown"}function pt(n,o){if(!n)return[];const i=[];let s=null;for(const c of n.split(`
2
- `))if(c.startsWith("## ")){if(s&&i.length<o&&i.push(s),i.length>=o)break;s={title:c.replace("## ","").trim(),items:[]}}else s&&c.startsWith("- ")&&s.items.push(c.replace("- ","").trim());return s&&i.length<o&&i.push(s),i.filter(c=>c.items.length>0)}async function gt(n){const o=e=>n.includes(e),i=e=>{const p=n.indexOf(e);return p!==-1&&n[p+1]?n[p+1]:null},s=i("--intent")||i("-i"),c=i("--working")||i("-w"),w=i("--decision")||i("-d"),X=o("--show")||o("-s"),N=o("--copy")||o("-c"),B=o("--cursor"),H=o("--copilot"),K=o("--reset"),M=o("--watch"),v=parseInt(i("--interval")||"30",10)*1e3;console.log(`
3
- `+m("\uFFFD\uFFFD\uFFFD infernoflow \u2014 context")),console.log(" "+"\u2500".repeat(50)+`
4
- `),l.existsSync(u)||(console.error(G(" \u2718 inferno/ not found")),console.error(d(` \u2192 Run: infernoflow init
5
- `)),process.exit(1));const f=U(h.join(u,"contract.json")),k=U(h.join(u,"capabilities.json")),q=$(h.join(u,"CHANGELOG.md"));(!f||!k)&&(console.error(G(` \u2718 Missing contract.json or capabilities.json
6
- `)),process.exit(1));let t=J();K&&(t={},console.log(_(` \u26A0 State reset
7
- `))),s&&(t.intent=s,t.intentUpdated=new Date().toISOString(),console.log(a(' \u2714 Intent saved: "'+s+'"'))),c&&(t.working=c,t.workingUpdated=new Date().toISOString(),console.log(a(' \u2714 Working on: "'+c+'"'))),w&&(t.decisions||(t.decisions=[]),t.decisions.push({text:w,date:new Date().toISOString()}),console.log(a(' \u2714 Decision recorded: "'+w+'"'))),(s||c||w)&&V(t);const y=k.capabilities||[],P=y.length===(f.capabilities||[]).length,D=pt(q,3),I=String(f.policyVersion).replace(/^v/i,""),z=new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}),T=P?"\u2713 validated":"\u26A0 out of sync",F=t.intent||"describe the exact task to implement",W={task:F,contract:f,caps:k,scenarios:[],state:t},Q=ct(W),Y=rt(W),Z=y.map(e=>"- **"+e.id+"** \u2014 "+e.title).join(`
8
- `),tt=D.length>0?D.map(e=>"### "+e.title+`
9
- `+e.items.map(p=>" - "+p).join(`
1
+ import g from"node:fs";import b from"node:path";import{execSync as h}from"node:child_process";import{bold as I,gray as d,cyan as l,red as q,green as a,yellow as F}from"../ui/output.mjs";import{buildCursorImplementPrompt as wt,buildGenericImplementPrompt as yt}from"../ui/prompts.mjs";import{detectDrift as kt}from"../git/detect-drift.mjs";function Ct(e){try{const n=process.platform;if(n==="win32")h("clip",{input:e});else if(n==="darwin")h("pbcopy",{input:e});else try{h("xclip -selection clipboard",{input:e})}catch{h("xsel --clipboard --input",{input:e})}return!0}catch{return!1}}const w="inferno",P=b.join(w,"CONTEXT.md"),z=b.join(w,"context-state.json");function Q(e){try{return JSON.parse(g.readFileSync(e,"utf8"))}catch{return null}}function W(e){try{return g.readFileSync(e,"utf8")}catch{return null}}function Y(){const e=W(z);if(!e)return{};try{return JSON.parse(e)}catch{return{}}}function Z(e){g.writeFileSync(z,JSON.stringify(e,null,2),"utf8")}function E(e){return e?new Date(e).toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}):"unknown"}function St(e,n){if(!e)return[];const i=[];let c=null;for(const r of e.split(`
2
+ `))if(r.startsWith("## ")){if(c&&i.length<n&&i.push(c),i.length>=n)break;c={title:r.replace("## ","").trim(),items:[]}}else c&&r.startsWith("- ")&&c.items.push(r.replace("- ","").trim());return c&&i.length<n&&i.push(c),i.filter(r=>r.items.length>0)}async function bt(e){const n=o=>e.includes(o),i=o=>{const u=e.indexOf(o);return u!==-1&&e[u+1]?e[u+1]:null},c=i("--intent")||i("-i"),r=i("--working")||i("-w"),$=i("--decision")||i("-d"),tt=n("--show")||n("-s"),G=n("--copy")||n("-c"),et=n("--cursor"),nt=n("--copilot"),ot=n("--reset"),it=n("--watch"),N=n("--auto-commit")||n("--auto-push"),v=n("--auto-push"),R=parseInt(i("--interval")||"30",10)*1e3;console.log(`
3
+ `+I("\uFFFD\uFFFD\uFFFD infernoflow \u2014 context")),console.log(" "+"\u2500".repeat(50)+`
4
+ `),g.existsSync(w)||(console.error(q(" \u2718 inferno/ not found")),console.error(d(` \u2192 Run: infernoflow init
5
+ `)),process.exit(1));const f=Q(b.join(w,"contract.json")),D=Q(b.join(w,"capabilities.json")),st=W(b.join(w,"CHANGELOG.md"));(!f||!D)&&(console.error(q(` \u2718 Missing contract.json or capabilities.json
6
+ `)),process.exit(1));let t=Y();ot&&(t={},console.log(F(` \u26A0 State reset
7
+ `))),c&&(t.intent=c,t.intentUpdated=new Date().toISOString(),console.log(a(' \u2714 Intent saved: "'+c+'"'))),r&&(t.working=r,t.workingUpdated=new Date().toISOString(),console.log(a(' \u2714 Working on: "'+r+'"'))),$&&(t.decisions||(t.decisions=[]),t.decisions.push({text:$,date:new Date().toISOString()}),console.log(a(' \u2714 Decision recorded: "'+$+'"'))),(c||r||$)&&Z(t);const j=D.capabilities||[],A=j.length===(f.capabilities||[]).length,U=St(st,3),T=String(f.policyVersion).replace(/^v/i,""),ct=new Date().toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}),B=A?"\u2713 validated":"\u26A0 out of sync",L=t.intent||"describe the exact task to implement",J={task:L,contract:f,caps:D,scenarios:[],state:t},rt=wt(J),lt=yt(J),at=j.map(o=>"- **"+o.id+"** \u2014 "+o.title).join(`
8
+ `),dt=U.length>0?U.map(o=>"### "+o.title+`
9
+ `+o.items.map(u=>" - "+u).join(`
10
10
  `)).join(`
11
11
 
12
- `):"_No recent changes_",nt=t.intent?t.intent+" _("+O(t.intentUpdated)+")_":'_Not set \u2014 run: infernoflow context --intent "..."_',et=t.working?t.working+" _("+O(t.workingUpdated)+")_":'_Not set \u2014 run: infernoflow context --working "..."_',ot=t.decisions&&t.decisions.length>0?t.decisions.slice(-5).map(e=>"- "+e.text+" _("+O(e.date)+")_").join(`
13
- `):"_No decisions recorded_",S=["# Project Context \u2014 "+f.policyId+" v"+I,"> Generated by infernoflow | "+z+" | "+T,"","---","","## What this system does","",Z,"","---","","## Recent changes","",tt,"","---","","## Current state","","- **Capabilities:** "+y.length,"- **Version:** v"+I,"- **Sync:** "+T,"","---","","## What I am working on right now","",et,"","---","","## Intent \u2014 what I want to build next","",nt,"","---","","## Decisions & notes","",ot,"","---","","## Implementation Prompt Seed","","Use this to start coding immediately with an agent:","","```bash",`infernoflow implement "${F}" --mode both`,"```","","### Cursor Agent Prompt","","```text",Q,"```","","### Generic Agent Prompt","","```text",Y,"```","","---","_Paste this block at the start of any new AI session._"].join(`
14
- `);if(X||(l.writeFileSync(R,S,"utf8"),console.log(a(`
15
- \u2714 Context written \u2192 `+R))),N){const e=at(S);console.log(e?a(" \u2714 Copied to clipboard \u2014 paste with Ctrl+V"):_(" \u26A0 Clipboard copy failed \u2014 open inferno/CONTEXT.md manually"))}if(B&&(l.writeFileSync(".cursorrules",S,"utf8"),console.log(a(" \u2714 Written to .cursorrules \u2014 Cursor loads this automatically"))),H&&(l.existsSync(".github")||l.mkdirSync(".github"),l.writeFileSync(".github/copilot-instructions.md",S,"utf8"),console.log(a(" \u2714 Written to .github/copilot-instructions.md \u2014 Copilot loads this automatically"))),console.log(`
16
- `+m("Context Summary")),console.log(" "+"\u2500".repeat(50)),console.log(" Project "+f.policyId+" \u2014 v"+I),console.log(" Capabilities "+y.length+" registered"),console.log(" Sync "+(P?a("\u2713 in sync"):_("\u26A0 check needed"))),console.log(" Working on "+(t.working?r(t.working):d("not set"))),console.log(" Intent "+(t.intent?r(t.intent):d("not set"))),console.log(" Decisions "+(t.decisions?t.decisions.length:0)+` recorded
17
- `),console.log(" "+m("Implementation Prompt")),console.log(" "+r("\u2192")+" Run "+r(`infernoflow implement "${F}" --mode both`)+`
18
- `),N?(console.log(" "+m("Ready to use:")),console.log(" "+r("\u2192")+" Paste into Claude / Cursor / Copilot with "+r("Ctrl+V")+`
19
- `)):(console.log(" "+m("Ready to use:")),console.log(" "+r("1.")+" Open "+r("inferno/CONTEXT.md")),console.log(" "+r("2.")+" Copy everything"),console.log(" "+r("3.")+" Paste at the start of your next AI session"),console.log(" "+d(" tip: use --copy to skip steps 1-2 automatically")+`
20
- `)),M){console.log(" "+r("\u{1F441} Watch mode active")+d(" \u2014 polling git diff every "+v/1e3+"s")),console.log(" "+d(`Press Ctrl+C to stop
21
- `));let e="";const p=async()=>{try{const st=process.cwd(),g=lt(st,{sinceCommits:1}),L=g.changedFiles.sort().join("|");if(L===e||(e=L,g.changedFiles.length===0))return;const j=g.affectedCapabilities.map(x=>x.id),E=j.length>0?`Working on: ${j.join(", ")} (${g.changedFiles.length} files changed)`:`${g.changedFiles.length} files changed \u2014 no capability match yet`,C=J();C.working!==E&&(C.working=E,C.workingUpdated=new Date().toISOString(),V(C),await gt(n.filter(x=>x!=="--watch")),process.stderr.write(`
22
- `+a("\u2714")+" Context updated \u2014 "+j.length+` capabilities affected
23
- `+d(g.changedFiles.slice(0,3).join(", ")+(g.changedFiles.length>3?` +${g.changedFiles.length-3} more`:""))+`
24
- `))}catch{}};await p();const it=setInterval(p,v);process.on("SIGINT",()=>{clearInterval(it),process.stderr.write(`
12
+ `):"_No recent changes_",pt=t.intent?t.intent+" _("+E(t.intentUpdated)+")_":'_Not set \u2014 run: infernoflow context --intent "..."_',gt=t.working?t.working+" _("+E(t.workingUpdated)+")_":'_Not set \u2014 run: infernoflow context --working "..."_',ut=t.decisions&&t.decisions.length>0?t.decisions.slice(-5).map(o=>"- "+o.text+" _("+E(o.date)+")_").join(`
13
+ `):"_No decisions recorded_",x=["# Project Context \u2014 "+f.policyId+" v"+T,"> Generated by infernoflow | "+ct+" | "+B,"","---","","## What this system does","",at,"","---","","## Recent changes","",dt,"","---","","## Current state","","- **Capabilities:** "+j.length,"- **Version:** v"+T,"- **Sync:** "+B,"","---","","## What I am working on right now","",gt,"","---","","## Intent \u2014 what I want to build next","",pt,"","---","","## Decisions & notes","",ut,"","---","","## Implementation Prompt Seed","","Use this to start coding immediately with an agent:","","```bash",`infernoflow implement "${L}" --mode both`,"```","","### Cursor Agent Prompt","","```text",rt,"```","","### Generic Agent Prompt","","```text",lt,"```","","---","_Paste this block at the start of any new AI session._"].join(`
14
+ `);if(tt||(g.writeFileSync(P,x,"utf8"),console.log(a(`
15
+ \u2714 Context written \u2192 `+P))),G){const o=Ct(x);console.log(o?a(" \u2714 Copied to clipboard \u2014 paste with Ctrl+V"):F(" \u26A0 Clipboard copy failed \u2014 open inferno/CONTEXT.md manually"))}if(et&&(g.writeFileSync(".cursorrules",x,"utf8"),console.log(a(" \u2714 Written to .cursorrules \u2014 Cursor loads this automatically"))),nt&&(g.existsSync(".github")||g.mkdirSync(".github"),g.writeFileSync(".github/copilot-instructions.md",x,"utf8"),console.log(a(" \u2714 Written to .github/copilot-instructions.md \u2014 Copilot loads this automatically"))),console.log(`
16
+ `+I("Context Summary")),console.log(" "+"\u2500".repeat(50)),console.log(" Project "+f.policyId+" \u2014 v"+T),console.log(" Capabilities "+j.length+" registered"),console.log(" Sync "+(A?a("\u2713 in sync"):F("\u26A0 check needed"))),console.log(" Working on "+(t.working?l(t.working):d("not set"))),console.log(" Intent "+(t.intent?l(t.intent):d("not set"))),console.log(" Decisions "+(t.decisions?t.decisions.length:0)+` recorded
17
+ `),console.log(" "+I("Implementation Prompt")),console.log(" "+l("\u2192")+" Run "+l(`infernoflow implement "${L}" --mode both`)+`
18
+ `),G?(console.log(" "+I("Ready to use:")),console.log(" "+l("\u2192")+" Paste into Claude / Cursor / Copilot with "+l("Ctrl+V")+`
19
+ `)):(console.log(" "+I("Ready to use:")),console.log(" "+l("1.")+" Open "+l("inferno/CONTEXT.md")),console.log(" "+l("2.")+" Copy everything"),console.log(" "+l("3.")+" Paste at the start of your next AI session"),console.log(" "+d(" tip: use --copy to skip steps 1-2 automatically")+`
20
+ `)),it){let _=function(p){try{return h(p,{cwd:process.cwd(),encoding:"utf8",stdio:["ignore","pipe","pipe"]}),!0}catch{return!1}},X=function(p){try{return h(`git status --porcelain "${p}"`,{cwd:process.cwd(),encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()===""}catch{return!0}},H=function(p,s,O){const k=`chore: update context [${s.length>0?s.slice(0,3).join(", "):`${O} files`}]`;return _(`git add "${p}"`)?X(p)?{ok:!1,reason:"nothing to commit"}:_(`git commit -m "${k}"`)?{ok:!0,msg:k}:{ok:!1,reason:"git commit failed (lock?)"}:{ok:!1,reason:"git add failed"}},K=function(){return _("git push")};var It=_,Ft=X,$t=H,jt=K;const o=v?"auto-push":N?"auto-commit":"watch";console.log(" "+l("\u{1F441} Watch mode active")+d(` \u2014 polling every ${R/1e3}s`+(v?" \xB7 will commit + push on change":N?" \xB7 will commit on change":""))),console.log(" "+d(`Press Ctrl+C to stop
21
+ `));let u="",V=null;const M=async()=>{try{const p=process.cwd(),s=kt(p,{sinceCommits:1}),O=s.changedFiles.sort().join("|");if(O===u||(u=O,s.changedFiles.length===0))return;const y=s.affectedCapabilities.map(S=>S.id),k=y.length>0?`Working on: ${y.join(", ")} (${s.changedFiles.length} files changed)`:`${s.changedFiles.length} files changed \u2014 no capability match yet`,C=Y();if(C.working!==k){C.working=k,C.workingUpdated=new Date().toISOString(),Z(C),await bt(e.filter(m=>m!=="--watch"&&m!=="--auto-commit"&&m!=="--auto-push"));const S=W(P),ft=new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",second:"2-digit"});if(process.stderr.write(`
22
+ ${a("\u2714")} [${ft}] Context updated \u2014 ${y.length} capabilities affected
23
+ ${d(s.changedFiles.slice(0,3).join(", ")+(s.changedFiles.length>3?` +${s.changedFiles.length-3} more`:""))}
24
+ `),N&&S!==V){V=S;const m=H(P,y,s.changedFiles.length);if(m.ok){if(process.stderr.write(` ${a("\u2714")} Committed: ${d(m.msg)}
25
+ `),v){const ht=K();process.stderr.write(ht?` ${a("\u2714")} Pushed to origin
26
+ `:` ${F("\u26A0")} Push failed \u2014 will retry next change
27
+ `)}}else process.stderr.write(` ${F("\u26A0")} Commit skipped: ${d(m.reason)}
28
+ `)}}}catch{}};await M();const mt=setInterval(M,R);process.on("SIGINT",()=>{clearInterval(mt),process.stderr.write(`
25
29
  `+d(`Watch stopped.
26
30
 
27
- `)),process.exit(0)}),await new Promise(()=>{})}}export{gt as contextCommand};
31
+ `)),process.exit(0)}),await new Promise(()=>{})}}export{bt as contextCommand};
@@ -1,9 +1,9 @@
1
- import*as c from"node:fs";import*as l from"node:path";import*as J from"node:readline";import{header as U,ok as N,warn as G,info as R,done as W,section as D,nextSteps as H,bold as Y,cyan as C,gray as A,yellow as z,green as L,red as T,errorAndExit as E}from"../ui/output.mjs";import{personalisePrompt as M}from"../learning/adapt.mjs";function $(o){try{return JSON.parse(c.readFileSync(o,"utf8"))}catch{return null}}function V(o,e){return new Promise(i=>{o.question(e,p=>i(p.trim()))})}function ie(o){return o.replace(/[-_]+/g," ").split(" ").map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}function Z({description:o,contract:e,capabilities:i,scenarios:p}){const g=e.capabilities||[],d=(i?.capabilities||[]).map(f=>` - ${f.id}: ${f.title||f.id}`).join(`
2
- `),s=p.map(f=>{const w=(f.capabilitiesCovered||[]).join(", "),y=(f.steps||[]).map(h=>` {action: "${h.action}", expect: "${h.expect}"}`).join(`
3
- `);return` File: ${f._file}
4
- capabilitiesCovered: [${w}]
1
+ import*as p from"node:fs";import*as f from"node:path";import*as R from"node:readline";import{header as ee,ok as O,warn as M,info as ie,done as te,section as U,nextSteps as ne,bold as oe,cyan as S,gray as E,yellow as se,green as W,red as Z,errorAndExit as _}from"../ui/output.mjs";import{personalisePrompt as ae}from"../learning/adapt.mjs";function j(i){try{return JSON.parse(p.readFileSync(i,"utf8"))}catch{return null}}function K(i,e){return new Promise(t=>{i.question(e,r=>t(r.trim()))})}function fe(i){return i.replace(/[-_]+/g," ").split(" ").map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}function re({description:i,contract:e,capabilities:t,scenarios:r}){const b=e.capabilities||[],u=(t?.capabilities||[]).map(h=>` - ${h.id}: ${h.title||h.id}`).join(`
2
+ `),s=r.map(h=>{const v=(h.capabilitiesCovered||[]).join(", "),m=(h.steps||[]).map(c=>` {action: "${c.action}", expect: "${c.expect}"}`).join(`
3
+ `);return` File: ${h._file}
4
+ capabilitiesCovered: [${v}]
5
5
  steps:
6
- ${y}`}).join(`
6
+ ${m}`}).join(`
7
7
 
8
8
  `);return`You are a developer assistant for the infernoflow CLI tool.
9
9
 
@@ -13,16 +13,16 @@ Your job is to analyze a code change description and suggest updates to the infe
13
13
 
14
14
  policyId: ${e.policyId}
15
15
  policyVersion: ${e.policyVersion}
16
- capabilities: [${g.join(", ")}]
16
+ capabilities: [${b.join(", ")}]
17
17
 
18
18
  ## Current capabilities registry
19
- ${d||" (none)"}
19
+ ${u||" (none)"}
20
20
 
21
21
  ## Current scenarios
22
22
  ${s||" (none)"}
23
23
 
24
24
  ## Developer's description of what changed
25
- "${o}"
25
+ "${i}"
26
26
 
27
27
  ## Your task
28
28
 
@@ -52,11 +52,11 @@ Rules:
52
52
  - Capability IDs must be PascalCase (e.g. SendEmail, not send_email)
53
53
  - If nothing changed capability-wise, return empty arrays
54
54
  - changelogEntry should start with "- "
55
- - Keep it minimal and accurate`}function K(o){const e=[];if(!o||typeof o!="object")return["AI response must be a JSON object."];o.summary!=null&&typeof o.summary!="string"&&e.push('"summary" must be a string.'),Array.isArray(o.newCapabilities)||e.push('"newCapabilities" must be an array.'),Array.isArray(o.removedCapabilities)||e.push('"removedCapabilities" must be an array.'),Array.isArray(o.updatedScenarios)||e.push('"updatedScenarios" must be an array.'),o.changelogEntry!=null&&typeof o.changelogEntry!="string"&&e.push('"changelogEntry" must be a string.');for(const i of o.newCapabilities||[]){if(!i||typeof i!="object"){e.push('Each item in "newCapabilities" must be an object.');continue}(typeof i.id!="string"||!/^[A-Z][A-Za-z0-9]*$/.test(i.id))&&e.push("newCapabilities[].id must be PascalCase (example: SendEmail)."),(typeof i.title!="string"||!i.title.trim())&&e.push("newCapabilities[].title must be a non-empty string.")}for(const i of o.removedCapabilities||[])(typeof i!="string"||!i.trim())&&e.push("removedCapabilities[] must contain non-empty strings.");for(const i of o.updatedScenarios||[]){if(!i||typeof i!="object"){e.push('Each item in "updatedScenarios" must be an object.');continue}(typeof i.file!="string"||!i.file.endsWith(".json"))&&e.push("updatedScenarios[].file must be a .json filename."),typeof i.isNew!="boolean"&&e.push("updatedScenarios[].isNew must be boolean."),(!Array.isArray(i.capabilitiesCovered)||!Array.isArray(i.stepsToAdd))&&e.push("updatedScenarios[].capabilitiesCovered and stepsToAdd must be arrays.")}return e}function B(o,e){const i=[],p=new Set(o.capabilities||[]),g=new Set((e.newCapabilities||[]).map(s=>s.id)),d=new Set(e.removedCapabilities||[]);for(const s of g)d.has(s)&&i.push(`Capability "${s}" appears in both newCapabilities and removedCapabilities.`),p.has(s)&&i.push(`Capability "${s}" already exists in contract capabilities.`);for(const s of d)p.has(s)||i.push(`Capability "${s}" cannot be removed because it does not exist in contract.`);return i}function Q({cwd:o,contract:e,capabilities:i,suggestion:p,version:g,quiet:d=!1}){const s=l.join(o,"inferno"),f=l.join(s,"contract.json"),w=l.join(s,"capabilities.json"),y=l.join(s,"CHANGELOG.md"),h=l.join(s,"scenarios"),m=p.newCapabilities||[],S=p.removedCapabilities||[],k=p.updatedScenarios||[],O=p.changelogEntry||"";let x=!1;const P=[],j=(t,n)=>P.push({filePath:t,content:n});if(m.length>0||S.length>0){const t=[...e.capabilities.filter(b=>!S.includes(b)),...m.map(b=>b.id)],n=Number(e.policyVersion||1)+1,r={...e,capabilities:t,policyVersion:n};j(f,JSON.stringify(r,null,2)+`
56
- `),d||N(`contract.json updated \u2192 policyVersion: v${n}`),x=!0}if(m.length>0||S.length>0){const t=i?{...i}:{schemaVersion:1,capabilities:[]};t.capabilities=(t.capabilities||[]).filter(n=>!S.includes(n.id));for(const n of m)t.capabilities.find(r=>r.id===n.id)||t.capabilities.push({id:n.id,title:n.title,since:g});j(w,JSON.stringify(t,null,2)+`
57
- `),d||N("capabilities.json updated")}for(const t of k){const n=l.join(h,t.file);let r;if(t.isNew||!c.existsSync(n))r={scenarioId:t.file.replace(".json",""),description:p.summary||"",capabilitiesCovered:t.capabilitiesCovered||[],steps:t.stepsToAdd||[]},j(n,JSON.stringify(r,null,2)+`
58
- `),d||N(`Created scenario: ${C(t.file)}`);else{r=$(n);const b=new Set(r.capabilitiesCovered||[]);(t.capabilitiesCovered||[]).forEach(I=>b.add(I)),r.capabilitiesCovered=[...b],r.steps=[...r.steps||[],...t.stepsToAdd||[]],j(n,JSON.stringify(r,null,2)+`
59
- `),d||N(`Updated scenario: ${C(t.file)}`)}x=!0}if(O&&c.existsSync(y)){let t=c.readFileSync(y,"utf8");/##\s+Unreleased/i.test(t)&&(t=t.replace(/(##\s+Unreleased[^\n]*\n)/i,`$1
60
- ${O}
61
- `),j(y,t),d||N("CHANGELOG.md updated"),x=!0)}const u=new Map;try{for(const t of P){c.existsSync(t.filePath)?u.set(t.filePath,c.readFileSync(t.filePath,"utf8")):u.set(t.filePath,null);const n=`${t.filePath}.tmp`;c.writeFileSync(n,t.content),c.renameSync(n,t.filePath)}}catch(t){for(const[n,r]of u.entries())r===null?c.existsSync(n)&&c.unlinkSync(n):c.writeFileSync(n,r);throw new Error(`Failed applying changes. Rolled back. Details: ${t.message}`)}return x}function X(o){const e=String(o||"").trim().replace(/^```json?\n?/,"").replace(/\n?```$/,"");return JSON.parse(e)}function oe(o){const e=l.join(o,"inferno"),i=l.join(e,"contract.json"),p=l.join(e,"capabilities.json"),g=l.join(e,"scenarios"),d=$(i),s=$(p),f=[];if(c.existsSync(g))for(const h of c.readdirSync(g).filter(m=>m.endsWith(".json"))){const m=$(l.join(g,h));m&&f.push({...m,_file:h})}let w="0.1.0";const y=l.join(o,"package.json");if(c.existsSync(y)){const h=$(y);h?.version&&(w=h.version)}return{contract:d,capabilities:s,scenarios:f,version:w}}async function ne(o){const e=process.cwd(),i=l.join(e,"inferno");U("suggest"),c.existsSync(i)||E("inferno/ not found","Run: infernoflow init");const p=l.join(i,"contract.json"),g=l.join(i,"capabilities.json"),d=l.join(i,"scenarios"),s=$(p);s||E("contract.json not found or invalid");const f=$(g),w=[];if(c.existsSync(d))for(const a of c.readdirSync(d).filter(v=>v.endsWith(".json"))){const v=$(l.join(d,a));v&&w.push({...v,_file:a})}let y="0.1.0";const h=l.join(e,"package.json");if(c.existsSync(h)){const a=$(h);a?.version&&(y=a.version)}let S=o.filter(a=>!a.startsWith("-")).slice(1).join(" ");if(!S){const a=J.createInterface({input:process.stdin,output:process.stdout});console.log(A(" Describe what changed in your code (e.g. 'added email notifications'):")),S=await V(a,` ${C(">")} `),a.close(),console.log()}S||E("No description provided",'Usage: infernoflow suggest "what changed"');const k=Z({description:S,contract:s,capabilities:f,scenarios:w}),O=M(k,i);D("Generated Prompt"),console.log(),console.log(A("\u2500".repeat(50))),console.log(O),console.log(A("\u2500".repeat(50))),console.log(),R("Copy the prompt above and paste it into:"),console.log(` ${C("\u2022")} Claude \u2192 https://claude.ai`),console.log(` ${C("\u2022")} ChatGPT \u2192 https://chatgpt.com`),console.log(` ${C("\u2022")} Copilot, Cursor, or any AI you use`),console.log(),G("The AI will respond with a JSON object."),console.log();const x=J.createInterface({input:process.stdin,output:process.stdout});console.log(A(" Paste the AI's JSON response below, then press Enter twice:")),console.log();let P="",j=0;await new Promise(a=>{x.on("line",v=>{v.trim()===""?(j++,j>=2&&P.trim()&&a()):(j=0,P+=v+`
62
- `)}),x.on("close",a)}),x.close();let u;try{u=X(P)}catch{E("Could not parse the AI response as JSON","Make sure you copied the full JSON response from the AI")}const t=K(u);t.length>0&&E("AI response schema is invalid",t[0]+(t.length>1?` (+${t.length-1} more)`:""));const n=B(s,u);n.length>0&&E("AI response contains conflicting capability operations",n[0]+(n.length>1?` (+${n.length-1} more)`:"")),D("Proposed Changes"),console.log(),u.summary&&(console.log(` ${Y("Summary:")} ${u.summary}`),console.log());const r=u.newCapabilities||[],b=u.removedCapabilities||[],I=u.updatedScenarios||[];r.length===0&&b.length===0&&I.length===0&&(N("No capability changes detected \u2014 nothing to apply."),console.log(),process.exit(0)),r.length>0&&(console.log(` ${L("+")} New capabilities:`),r.forEach(a=>console.log(` ${L(a.id)} \u2014 ${A(a.title)}`)),console.log()),b.length>0&&(console.log(` ${T("-")} Removed capabilities:`),b.forEach(a=>console.log(` ${T(a)}`)),console.log()),I.length>0&&(console.log(` ${C("~")} Scenario updates:`),I.forEach(a=>{const v=a.isNew?L("[new]"):C("[update]");console.log(` ${v} ${a.file}`)}),console.log()),u.changelogEntry&&(console.log(` ${z("\u{1F4DD}")} Changelog: ${A(u.changelogEntry)}`),console.log());const _=J.createInterface({input:process.stdin,output:process.stdout}),F=await V(_,` Apply these changes? ${A("(y/n)")} `);_.close(),console.log(),F.toLowerCase()!=="y"&&F.toLowerCase()!=="yes"&&(G("Cancelled \u2014 no changes made."),console.log(),process.exit(0)),D("Applying Changes"),console.log(),Q({cwd:e,contract:s,capabilities:f,suggestion:u,version:y}),W("suggest complete!"),H([C("infernoflow status")+" \u2014 verify the updated contract",C("infernoflow check")+" \u2014 validate everything"])}export{Q as applyChanges,Z as buildPrompt,B as detectSuggestionConflicts,oe as loadSuggestContext,X as parseSuggestionJson,$ as readJson,ne as suggestCommand,K as validateSuggestion};
55
+ - Keep it minimal and accurate`}function B(i){const e=[];if(!i||typeof i!="object")return["AI response must be a JSON object."];i.summary!=null&&typeof i.summary!="string"&&e.push('"summary" must be a string.'),Array.isArray(i.newCapabilities)||e.push('"newCapabilities" must be an array.'),Array.isArray(i.removedCapabilities)||e.push('"removedCapabilities" must be an array.'),Array.isArray(i.updatedScenarios)||e.push('"updatedScenarios" must be an array.'),i.changelogEntry!=null&&typeof i.changelogEntry!="string"&&e.push('"changelogEntry" must be a string.');for(const t of i.newCapabilities||[]){if(!t||typeof t!="object"){e.push('Each item in "newCapabilities" must be an object.');continue}(typeof t.id!="string"||!/^[A-Z][A-Za-z0-9]*$/.test(t.id))&&e.push("newCapabilities[].id must be PascalCase (example: SendEmail)."),(typeof t.title!="string"||!t.title.trim())&&e.push("newCapabilities[].title must be a non-empty string.")}for(const t of i.removedCapabilities||[])(typeof t!="string"||!t.trim())&&e.push("removedCapabilities[] must contain non-empty strings.");for(const t of i.updatedScenarios||[]){if(!t||typeof t!="object"){e.push('Each item in "updatedScenarios" must be an object.');continue}(typeof t.file!="string"||!t.file.endsWith(".json"))&&e.push("updatedScenarios[].file must be a .json filename."),typeof t.isNew!="boolean"&&e.push("updatedScenarios[].isNew must be boolean."),(!Array.isArray(t.capabilitiesCovered)||!Array.isArray(t.stepsToAdd))&&e.push("updatedScenarios[].capabilitiesCovered and stepsToAdd must be arrays.")}return e}function Q(i,e){const t=[],r=new Set(i.capabilities||[]),b=new Set((e.newCapabilities||[]).map(s=>s.id)),u=new Set(e.removedCapabilities||[]);for(const s of b)u.has(s)&&t.push(`Capability "${s}" appears in both newCapabilities and removedCapabilities.`),r.has(s)&&t.push(`Capability "${s}" already exists in contract capabilities.`);for(const s of u)r.has(s)||t.push(`Capability "${s}" cannot be removed because it does not exist in contract.`);return t}function X({cwd:i,contract:e,capabilities:t,suggestion:r,version:b,quiet:u=!1}){const s=f.join(i,"inferno"),h=f.join(s,"contract.json"),v=f.join(s,"capabilities.json"),m=f.join(s,"CHANGELOG.md"),c=f.join(s,"scenarios"),y=r.newCapabilities||[],$=r.removedCapabilities||[],P=r.updatedScenarios||[],k=r.changelogEntry||"";let J=!1;const w=[],A=(n,a)=>w.push({filePath:n,content:a});if(y.length>0||$.length>0){const n=[...e.capabilities.filter(d=>!$.includes(d)),...y.map(d=>d.id)],a=Number(e.policyVersion||1)+1,l={...e,capabilities:n,policyVersion:a};A(h,JSON.stringify(l,null,2)+`
56
+ `),u||O(`contract.json updated \u2192 policyVersion: v${a}`),J=!0}if(y.length>0||$.length>0){const n=t?{...t}:{schemaVersion:1,capabilities:[]};n.capabilities=(n.capabilities||[]).filter(a=>!$.includes(a.id));for(const a of y)n.capabilities.find(l=>l.id===a.id)||n.capabilities.push({id:a.id,title:a.title,since:b});A(v,JSON.stringify(n,null,2)+`
57
+ `),u||O("capabilities.json updated")}for(const n of P){const a=f.join(c,n.file);let l;if(n.isNew||!p.existsSync(a))l={scenarioId:n.file.replace(".json",""),description:r.summary||"",capabilitiesCovered:n.capabilitiesCovered||[],steps:n.stepsToAdd||[]},A(a,JSON.stringify(l,null,2)+`
58
+ `),u||O(`Created scenario: ${S(n.file)}`);else{l=j(a);const d=new Set(l.capabilitiesCovered||[]);(n.capabilitiesCovered||[]).forEach(N=>d.add(N)),l.capabilitiesCovered=[...d],l.steps=[...l.steps||[],...n.stepsToAdd||[]],A(a,JSON.stringify(l,null,2)+`
59
+ `),u||O(`Updated scenario: ${S(n.file)}`)}J=!0}if(k&&p.existsSync(m)){let n=p.readFileSync(m,"utf8");/##\s+Unreleased/i.test(n)&&(n=n.replace(/(##\s+Unreleased[^\n]*\n)/i,`$1
60
+ ${k}
61
+ `),A(m,n),u||O("CHANGELOG.md updated"),J=!0)}const I=new Map;try{for(const n of w){p.existsSync(n.filePath)?I.set(n.filePath,p.readFileSync(n.filePath,"utf8")):I.set(n.filePath,null);const a=`${n.filePath}.tmp`;p.writeFileSync(a,n.content),p.renameSync(a,n.filePath)}}catch(n){for(const[a,l]of I.entries())l===null?p.existsSync(a)&&p.unlinkSync(a):p.writeFileSync(a,l);throw new Error(`Failed applying changes. Rolled back. Details: ${n.message}`)}return J}function q(i){const e=String(i||"").trim().replace(/^```json?\n?/,"").replace(/\n?```$/,"");return JSON.parse(e)}function ue(i){const e=f.join(i,"inferno"),t=f.join(e,"contract.json"),r=f.join(e,"capabilities.json"),b=f.join(e,"scenarios"),u=j(t),s=j(r),h=[];if(p.existsSync(b))for(const c of p.readdirSync(b).filter(y=>y.endsWith(".json"))){const y=j(f.join(b,c));y&&h.push({...y,_file:c})}let v="0.1.0";const m=f.join(i,"package.json");if(p.existsSync(m)){const c=j(m);c?.version&&(v=c.version)}return{contract:u,capabilities:s,scenarios:h,version:v}}function F(i){console.log(JSON.stringify(i,null,2))}function x(i,e,t){F({ok:!1,error:i,message:e,hint:t}),process.exit(1)}async function ce(){return new Promise(i=>{let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>i(e.trim())),setTimeout(()=>i(""),100)})}async function he(i){const e=process.cwd(),t=f.join(e,"inferno"),r=i.includes("--json"),b=i.includes("--apply"),u=i.indexOf("--response");let s=u!==-1?i[u+1]:null;r||ee("suggest"),p.existsSync(t)||(r&&x("inferno_not_found","inferno/ not found","Run: infernoflow init"),_("inferno/ not found","Run: infernoflow init"));const h=f.join(t,"contract.json"),v=f.join(t,"capabilities.json"),m=f.join(t,"scenarios"),c=j(h);c||(r&&x("contract_not_found","contract.json not found or invalid"),_("contract.json not found or invalid"));const y=j(v),$=[];if(p.existsSync(m))for(const o of p.readdirSync(m).filter(g=>g.endsWith(".json"))){const g=j(f.join(m,o));g&&$.push({...g,_file:o})}let P="0.1.0";const k=f.join(e,"package.json");if(p.existsSync(k)){const o=j(k);o?.version&&(P=o.version)}let w=i.filter(o=>!o.startsWith("-")).slice(1).join(" ");if(!w&&!r){const o=R.createInterface({input:process.stdin,output:process.stdout});console.log(E(" Describe what changed in your code (e.g. 'added email notifications'):")),w=await K(o,` ${S(">")} `),o.close(),console.log()}w||(r&&x("no_description","No description provided",'Usage: infernoflow suggest "what changed" --json'),_("No description provided",'Usage: infernoflow suggest "what changed"'));const A=re({description:w,contract:c,capabilities:y,scenarios:$}),I=ae(A,t);if(r){if(!s){const C=await ce();C&&(s=C)}if(!s){F({ok:!0,mode:"prompt",description:w,prompt:I,context:{policyId:c.policyId,policyVersion:c.policyVersion,capabilities:c.capabilities||[],scenarios:$.map(C=>C._file),version:P}});return}if(s.startsWith("@")){const C=s.slice(1);try{s=p.readFileSync(C,"utf8")}catch{x("file_not_found",`Cannot read response file: ${C}`)}}let o;try{o=q(s)}catch(C){x("parse_error","Could not parse AI response as JSON",C.message)}const g=B(o);g.length>0&&x("validation_error",g[0],g.join("; "));const T=Q(c,o);T.length>0&&x("conflict_error",T[0],T.join("; "));const z={summary:o.summary||"",newCapabilities:o.newCapabilities||[],removedCapabilities:o.removedCapabilities||[],updatedScenarios:o.updatedScenarios||[],changelogEntry:o.changelogEntry||""};if(!b){F({ok:!0,mode:"validate",description:w,changes:z,applied:!1});return}try{X({cwd:e,contract:c,capabilities:y,suggestion:o,version:P,quiet:!0}),F({ok:!0,mode:"apply",description:w,changes:z,applied:!0})}catch(C){x("apply_error",C.message)}return}U("Generated Prompt"),console.log(),console.log(E("\u2500".repeat(50))),console.log(I),console.log(E("\u2500".repeat(50))),console.log(),ie("Copy the prompt above and paste it into:"),console.log(` ${S("\u2022")} Claude \u2192 https://claude.ai`),console.log(` ${S("\u2022")} ChatGPT \u2192 https://chatgpt.com`),console.log(` ${S("\u2022")} Copilot, Cursor, or any AI you use`),console.log(),M("The AI will respond with a JSON object."),console.log();const n=R.createInterface({input:process.stdin,output:process.stdout});console.log(E(" Paste the AI's JSON response below, then press Enter twice:")),console.log();let a="",l=0;await new Promise(o=>{n.on("line",g=>{g.trim()===""?(l++,l>=2&&a.trim()&&o()):(l=0,a+=g+`
62
+ `)}),n.on("close",o)}),n.close();let d;try{d=q(a)}catch{_("Could not parse the AI response as JSON","Make sure you copied the full JSON response from the AI")}const N=B(d);N.length>0&&_("AI response schema is invalid",N[0]+(N.length>1?` (+${N.length-1} more)`:""));const D=Q(c,d);D.length>0&&_("AI response contains conflicting capability operations",D[0]+(D.length>1?` (+${D.length-1} more)`:"")),U("Proposed Changes"),console.log(),d.summary&&(console.log(` ${oe("Summary:")} ${d.summary}`),console.log());const L=d.newCapabilities||[],V=d.removedCapabilities||[],G=d.updatedScenarios||[];L.length===0&&V.length===0&&G.length===0&&(O("No capability changes detected \u2014 nothing to apply."),console.log(),process.exit(0)),L.length>0&&(console.log(` ${W("+")} New capabilities:`),L.forEach(o=>console.log(` ${W(o.id)} \u2014 ${E(o.title)}`)),console.log()),V.length>0&&(console.log(` ${Z("-")} Removed capabilities:`),V.forEach(o=>console.log(` ${Z(o)}`)),console.log()),G.length>0&&(console.log(` ${S("~")} Scenario updates:`),G.forEach(o=>{const g=o.isNew?W("[new]"):S("[update]");console.log(` ${g} ${o.file}`)}),console.log()),d.changelogEntry&&(console.log(` ${se("\u{1F4DD}")} Changelog: ${E(d.changelogEntry)}`),console.log());const H=R.createInterface({input:process.stdin,output:process.stdout}),Y=await K(H,` Apply these changes? ${E("(y/n)")} `);H.close(),console.log(),Y.toLowerCase()!=="y"&&Y.toLowerCase()!=="yes"&&(M("Cancelled \u2014 no changes made."),console.log(),process.exit(0)),U("Applying Changes"),console.log(),X({cwd:e,contract:c,capabilities:y,suggestion:d,version:P}),te("suggest complete!"),ne([S("infernoflow status")+" \u2014 verify the updated contract",S("infernoflow check")+" \u2014 validate everything"])}export{X as applyChanges,re as buildPrompt,Q as detectSuggestionConflicts,ue as loadSuggestContext,q as parseSuggestionJson,j as readJson,he as suggestCommand,B as validateSuggestion};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.10.21",
3
+ "version": "0.10.23",
4
4
  "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {