infernoflow 0.4.3 → 0.6.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.
package/README.md CHANGED
@@ -25,16 +25,19 @@ npx infernoflow init
25
25
  ## Quick Start
26
26
 
27
27
  ```bash
28
- # In your project root:
28
+ # 1. Scaffold in your project root:
29
29
  npx infernoflow init
30
30
 
31
- # See your contract health:
31
+ # 2. See your contract health:
32
32
  infernoflow status
33
33
 
34
- # Validate everything:
34
+ # 3. When you add a feature, let AI update the docs:
35
+ infernoflow suggest "added email notifications and user profiles"
36
+
37
+ # 4. Validate everything:
35
38
  infernoflow check
36
39
 
37
- # In CI / pre-push hook:
40
+ # 5. In CI / pre-push hook:
38
41
  infernoflow doc-gate
39
42
  ```
40
43
 
@@ -44,48 +47,61 @@ infernoflow doc-gate
44
47
  |---|---|
45
48
  | `infernoflow init` | Interactive scaffold — creates `inferno/` in your project |
46
49
  | `infernoflow status` | At-a-glance health of your contract |
50
+ | `infernoflow suggest` | Generate an AI prompt, apply capability updates |
47
51
  | `infernoflow check` | Full validation: contract, capabilities, scenarios, changelog |
48
52
  | `infernoflow doc-gate` | Fails if code changed but docs weren't updated |
49
53
 
50
54
  ### Options
51
55
 
52
56
  ```bash
53
- infernoflow init --force # overwrite existing files
54
- infernoflow init --yes # skip prompts, use defaults
55
- infernoflow check --json # machine-readable output for CI
57
+ infernoflow init --force # overwrite existing files
58
+ infernoflow init --yes # skip prompts, use defaults
59
+ infernoflow suggest "..." # describe what changed
60
+ infernoflow check --json # machine-readable output for CI
56
61
  infernoflow check --skip-doc-gate
57
62
  ```
58
63
 
59
- ## Example: Todo App
64
+ ## `infernoflow suggest` — AI-powered updates
60
65
 
61
- After `infernoflow init` in a React Todo project:
66
+ When you add a feature, just describe it in plain language. infernoflow generates a prompt you can paste into **any AI** (Claude, ChatGPT, Copilot, Cursor, etc.), then applies the suggested changes automatically.
62
67
 
68
+ ```bash
69
+ infernoflow suggest "added payment processing and invoice generation"
63
70
  ```
64
- inferno/
65
- ├── contract.json
66
- │ {
67
- │ "policyId": "todo-app",
68
- │ "policyVersion": 1,
69
- │ "capabilities": ["CreateTask", "ReadTasks", "UpdateTask", "ToggleComplete", "DeleteTask"]
70
- │ }
71
- ├── capabilities.json
72
- │ { capabilities: [{ id: "CreateTask", title: "Create a task", since: "0.1.0" }, ...] }
73
- ├── scenarios/
74
- │ └── happy_path.json ← covers all 5 capabilities
75
- └── CHANGELOG.md
71
+
72
+ **What happens:**
73
+ 1. infernoflow reads your current contract state
74
+ 2. Generates a structured prompt with full context
75
+ 3. You paste it into your AI of choice
76
+ 4. Paste the JSON response back
77
+ 5. infernoflow previews the changes and asks for confirmation
78
+ 6. On approval — updates `contract.json`, `capabilities.json`, `scenarios/`, and `CHANGELOG.md`
79
+
80
+ **Example output:**
76
81
  ```
82
+ Proposed Changes
77
83
 
78
- Then in CI:
84
+ Summary: Added payment processing and invoice generation functionality.
79
85
 
80
- ```yaml
81
- - run: npm run inferno:check
86
+ + New capabilities:
87
+ ProcessPayment Process Payment
88
+ GenerateInvoice — Generate Invoice
89
+
90
+ ~ Scenario updates:
91
+ [update] happy_path.json
92
+
93
+ 📝 Changelog: - Add payment processing and invoice generation capabilities.
94
+
95
+ Apply these changes? (y/n)
82
96
  ```
83
97
 
98
+ Works with any AI — Claude, ChatGPT, GitHub Copilot, Cursor, or your own setup.
99
+
84
100
  ## Why infernoflow?
85
101
 
86
102
  **The problem:** AI-assisted development moves fast. Code changes daily. But what does the system *actually do*? What changed? What's covered?
87
103
 
88
- **The metaphor:** A forge (כיבשן). Metal becomes liquid — flexible, shapeable. The forge is the controlled environment where that change happens safely, with molds (contracts) and tempering (tests).
104
+ **The metaphor:** A forge (כיבשן). Metal becomes liquid — flexible, shapeable. The forge is the controlled environment where that change happens safely, with molds (contracts) and tempering (validation).
89
105
 
90
106
  **The principle:** Liquid where you want flexibility. Solid where you need safety.
91
107
 
@@ -1,46 +1,50 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { bold, gray, cyan, red } from "../lib/ui/output.mjs";
4
3
 
5
- const VERSION = "0.4.2";
4
+ const VERSION = "0.5.0";
6
5
 
7
6
  const HELP = `
8
- ${bold("��� infernoflow")} ${gray("v" + VERSION)}
9
- ${gray("The forge for liquid code")}
7
+ ${bold("🔥 infernoflow")} ${gray("v" + VERSION)}
8
+ ${gray("The forge for liquid code — keep every AI session in sync")}
10
9
 
11
10
  ${bold("Usage:")}
12
11
  infernoflow <command> [options]
13
12
 
14
13
  ${bold("Commands:")}
15
- init Scaffold inferno/ in your project (interactive)
14
+ init Scaffold inferno/ in your project
16
15
  check Validate contract, capabilities, scenarios, changelog
17
16
  status Show contract health at a glance
18
17
  doc-gate Fail if code changed but docs were not updated
19
18
  suggest Generate AI prompt + apply capability updates
19
+ context Generate AI-ready context for new sessions
20
20
 
21
- ${bold("Options:")}
22
- init:
23
- --force, -f Overwrite existing files
24
- --yes, -y Skip prompts, use defaults
21
+ ${bold("context options:")}
22
+ --intent "..." What you plan to build next
23
+ --working "..." What you are building right now
24
+ --decision "..." Record a decision or note
25
+ --show Print context without writing file
26
+ --reset Clear all stored state
25
27
 
26
- check:
27
- --skip-doc-gate Skip the git doc-gate check
28
- --json Machine-readable JSON output (for CI)
28
+ ${bold("Typical workflow:")}
29
+ ${gray('1. infernoflow context --intent "what I want to build"')}
30
+ ${gray("2. [paste inferno/CONTEXT.md into Claude / Cursor / Copilot]")}
31
+ ${gray("3. [build the feature]")}
32
+ ${gray('4. infernoflow suggest "what I built"')}
33
+ ${gray("5. infernoflow check")}
29
34
  `;
30
35
 
31
- const [,, cmd, ...rest] = process.argv;
36
+ const [, , cmd, ...rest] = process.argv;
32
37
 
33
38
  if (!cmd || cmd === "--help" || cmd === "-h") {
34
39
  console.log(HELP);
35
40
  process.exit(0);
36
41
  }
37
-
38
42
  if (cmd === "--version" || cmd === "-v") {
39
43
  console.log(VERSION);
40
44
  process.exit(0);
41
45
  }
42
46
 
43
- const commands = ["init", "check", "status", "doc-gate", "suggest"];
47
+ const commands = ["init", "check", "status", "doc-gate", "suggest", "context"];
44
48
 
45
49
  if (!commands.includes(cmd)) {
46
50
  console.error(red(`\nUnknown command: ${cmd}`));
@@ -53,27 +57,50 @@ const args = [cmd, ...rest];
53
57
  switch (cmd) {
54
58
  case "init":
55
59
  import("../lib/commands/init.mjs")
56
- .then(m => m.initCommand(args))
57
- .catch(err => { console.error(red("\nError: ") + err.message); process.exit(1); });
60
+ .then((m) => m.initCommand(args))
61
+ .catch((err) => {
62
+ console.error(red("\nError: ") + err.message);
63
+ process.exit(1);
64
+ });
58
65
  break;
59
66
  case "check":
60
67
  import("../lib/commands/check.mjs")
61
- .then(m => m.checkCommand(args))
62
- .catch(err => { console.error(red("\nError: ") + err.message); process.exit(1); });
68
+ .then((m) => m.checkCommand(args))
69
+ .catch((err) => {
70
+ console.error(red("\nError: ") + err.message);
71
+ process.exit(1);
72
+ });
63
73
  break;
64
74
  case "status":
65
75
  import("../lib/commands/status.mjs")
66
- .then(m => m.statusCommand(args))
67
- .catch(err => { console.error(red("\nError: ") + err.message); process.exit(1); });
76
+ .then((m) => m.statusCommand(args))
77
+ .catch((err) => {
78
+ console.error(red("\nError: ") + err.message);
79
+ process.exit(1);
80
+ });
68
81
  break;
69
82
  case "suggest":
70
83
  import("../lib/commands/suggest.mjs")
71
- .then(m => m.suggestCommand(args))
72
- .catch(err => { console.error(red("\nError: ") + err.message); process.exit(1); });
84
+ .then((m) => m.suggestCommand(args))
85
+ .catch((err) => {
86
+ console.error(red("\nError: ") + err.message);
87
+ process.exit(1);
88
+ });
89
+ break;
90
+ case "context":
91
+ import("../lib/commands/context.mjs")
92
+ .then((m) => m.contextCommand(args))
93
+ .catch((err) => {
94
+ console.error(red("\nError: ") + err.message);
95
+ process.exit(1);
96
+ });
73
97
  break;
74
98
  case "doc-gate":
75
99
  import("../lib/commands/docGate.mjs")
76
- .then(m => m.docGateCommand())
77
- .catch(err => { console.error(red("\nError: ") + err.message); process.exit(1); });
100
+ .then((m) => m.docGateCommand())
101
+ .catch((err) => {
102
+ console.error(red("\nError: ") + err.message);
103
+ process.exit(1);
104
+ });
78
105
  break;
79
106
  }
@@ -0,0 +1,170 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { bold, gray, cyan, red, green, yellow } from "../ui/output.mjs";
4
+
5
+ const INFERNO_DIR = "inferno";
6
+ const CONTEXT_FILE = path.join(INFERNO_DIR, "CONTEXT.md");
7
+ const STATE_FILE = path.join(INFERNO_DIR, "context-state.json");
8
+
9
+ function readJSON(filePath) {
10
+ try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
11
+ catch { return null; }
12
+ }
13
+
14
+ function readFile(filePath) {
15
+ try { return fs.readFileSync(filePath, "utf8"); }
16
+ catch { return null; }
17
+ }
18
+
19
+ function parseRecentChangelog(changelogContent, maxEntries = 3) {
20
+ if (!changelogContent) return [];
21
+ const lines = changelogContent.split("\n");
22
+ const entries = [];
23
+ let current = null;
24
+
25
+ for (const line of lines) {
26
+ if (line.startsWith("## ")) {
27
+ if (current && entries.length < maxEntries) entries.push(current);
28
+ if (entries.length >= maxEntries) break;
29
+ current = { title: line.replace("## ", "").trim(), items: [] };
30
+ } else if (current && line.startsWith("- ")) {
31
+ current.items.push(line.replace("- ", "").trim());
32
+ }
33
+ }
34
+ if (current && entries.length < maxEntries) entries.push(current);
35
+ return entries.filter(e => e.items.length > 0);
36
+ }
37
+
38
+ function loadState() {
39
+ const raw = readFile(STATE_FILE);
40
+ if (!raw) return {};
41
+ try { return JSON.parse(raw); } catch { return {}; }
42
+ }
43
+
44
+ function saveState(state) {
45
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf8");
46
+ }
47
+
48
+ function formatDate(iso) {
49
+ if (!iso) return "unknown";
50
+ return new Date(iso).toLocaleDateString("en-GB", {
51
+ day: "2-digit", month: "short", year: "numeric"
52
+ });
53
+ }
54
+
55
+ export async function contextCommand(args) {
56
+ const getFlagValue = (f) => {
57
+ const i = args.indexOf(f);
58
+ return i !== -1 && args[i + 1] ? args[i + 1] : null;
59
+ };
60
+ const hasFlag = (f) => args.includes(f);
61
+
62
+ const intent = getFlagValue("--intent") || getFlagValue("-i");
63
+ const working = getFlagValue("--working") || getFlagValue("-w");
64
+ const decision = getFlagValue("--decision") || getFlagValue("-d");
65
+ const showOnly = hasFlag("--show") || hasFlag("-s");
66
+ const resetState = hasFlag("--reset");
67
+
68
+ console.log(`\n ${bold("🔥 infernoflow — context")}`);
69
+ console.log(` ${"─".repeat(50)}\n`);
70
+
71
+ if (!fs.existsSync(INFERNO_DIR)) {
72
+ console.error(red(" ✘ inferno/ not found"));
73
+ console.error(gray(" → Run: infernoflow init\n"));
74
+ process.exit(1);
75
+ }
76
+
77
+ const contract = readJSON(path.join(INFERNO_DIR, "contract.json"));
78
+ const capabilities = readJSON(path.join(INFERNO_DIR, "capabilities.json"));
79
+ const changelog = readFile(path.join(INFERNO_DIR, "CHANGELOG.md"));
80
+
81
+ if (!contract || !capabilities) {
82
+ console.error(red(" ✘ Missing contract.json or capabilities.json"));
83
+ process.exit(1);
84
+ }
85
+
86
+ let state = loadState();
87
+ if (resetState) { state = {}; console.log(yellow(" ⚠ State reset\n")); }
88
+
89
+ if (intent) { state.intent = intent; state.intentUpdated = new Date().toISOString(); console.log(green(` ✔ Intent saved: "${intent}"`)); }
90
+ if (working) { state.working = working; state.workingUpdated = new Date().toISOString(); console.log(green(` ✔ Working on: "${working}"`)); }
91
+ if (decision) {
92
+ if (!state.decisions) state.decisions = [];
93
+ state.decisions.push({ text: decision, date: new Date().toISOString() });
94
+ console.log(green(` ✔ Decision recorded: "${decision}"`));
95
+ }
96
+ if (intent || working || decision) saveState(state);
97
+
98
+ const capList = capabilities.capabilities || [];
99
+ const contractCaps = contract.capabilities || [];
100
+ const allInSync = capList.length === contractCaps.length;
101
+ const recentChanges = parseRecentChangelog(changelog, 3);
102
+
103
+ const now = new Date().toLocaleDateString("en-GB", {
104
+ day: "2-digit", month: "short", year: "numeric"
105
+ });
106
+
107
+ const capLines = capList.map(c => `- **${c.id}** — ${c.title}`).join("\n");
108
+
109
+ const changelogLines = recentChanges.length > 0
110
+ ? recentChanges.map(e => `### ${e.title}\n${e.items.map(i => ` - ${i}`).join("\n")}`).join("\n\n")
111
+ : "_No recent changes recorded_";
112
+
113
+ const intentLine = state.intent
114
+ ? `${state.intent} _(updated: ${formatDate(state.intentUpdated)})_`
115
+ : "_Not set — run: `infernoflow context --intent \"what I want to do next\"`_";
116
+
117
+ const workingLine = state.working
118
+ ? `${state.working} _(updated: ${formatDate(state.workingUpdated)})_`
119
+ : "_Not set — run: `infernoflow context --working \"what I am building now\"`_";
120
+
121
+ const decisionsLines = state.decisions?.length > 0
122
+ ? state.decisions.slice(-5).map(d => `- ${d.text} _(${formatDate(d.date)})_`).join("\n")
123
+ : "_No decisions recorded — run: `infernoflow context --decision \"why I chose X\"`_";
124
+
125
+ const syncBadge = allInSync ? "✓ validated" : "⚠ out of sync — run infernoflow check";
126
+ const version = String(contract.policyVersion).replace(/^v/i, "");
127
+
128
+ const contextMd = `# Project Context — ${contract.policyId} v${version}
129
+ > Generated by infernoflow | ${now} | ${syncBadge}
130
+
131
+ ---
132
+
133
+ ## What this system does
134
+
135
+ ${capLines}
136
+
137
+ ---
138
+
139
+ ## Recent changes
140
+
141
+ ${changelogLines}
142
+
143
+ ---
144
+
145
+ ## Current state
146
+
147
+ - **Capabilities declared:** ${capList.length}
148
+ - **Contract version:** v${version}
149
+ - **Sync status:** ${syncBadge}
150
+
151
+ ---
152
+
153
+ ## What I am working on right now
154
+
155
+ ${workingLine}
156
+
157
+ ---
158
+
159
+ ## Intent — what I want to build next
160
+
161
+ ${intentLine}
162
+
163
+ ---
164
+
165
+ ## Decisions & notes
166
+
167
+ ${decisionsLines}
168
+
169
+ ---
170
+ _Paste this block at the start of any new AI session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {