infernoflow 0.10.8 → 0.10.9
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 +23 -11
- package/bin/infernoflow.mjs +2 -0
- package/lib/ai/ideDetection.mjs +31 -0
- package/lib/ai/providerRouter.mjs +73 -0
- package/lib/commands/init.mjs +1 -1
- package/lib/commands/run.mjs +95 -7
- package/package.json +1 -1
- package/templates/ci/github-inferno-check.yml +2 -5
- package/templates/scripts/inferno-install-hooks.mjs +4 -4
package/README.md
CHANGED
|
@@ -240,10 +240,11 @@ Works with any AI — Claude, ChatGPT, GitHub Copilot, Cursor, or your own setup
|
|
|
240
240
|
|
|
241
241
|
Run one command and infernoflow will:
|
|
242
242
|
1. Detect drift (`pr-impact`)
|
|
243
|
-
2.
|
|
244
|
-
3.
|
|
245
|
-
4.
|
|
246
|
-
5.
|
|
243
|
+
2. Resolve provider (`auto` defaults to IDE agent)
|
|
244
|
+
3. Generate suggestion
|
|
245
|
+
4. Apply inferno updates
|
|
246
|
+
5. Validate with `check`
|
|
247
|
+
6. Roll back automatically if validation fails
|
|
247
248
|
|
|
248
249
|
```bash
|
|
249
250
|
infernoflow run "add favorite badge to tasks and filter by favorite"
|
|
@@ -255,10 +256,24 @@ Machine mode:
|
|
|
255
256
|
infernoflow run "sync check" --json
|
|
256
257
|
```
|
|
257
258
|
|
|
258
|
-
|
|
259
|
+
Provider options:
|
|
259
260
|
|
|
260
261
|
```bash
|
|
261
|
-
# default
|
|
262
|
+
infernoflow run "task" --provider auto # default (IDE agent first)
|
|
263
|
+
infernoflow run "task" --provider agent --ide cursor # require IDE agent
|
|
264
|
+
infernoflow run "task" --provider local # explicit local model
|
|
265
|
+
infernoflow run "task" --provider prompt # deterministic prompt fallback
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
IDE routing behavior:
|
|
269
|
+
- `auto` + agent available -> uses IDE agent
|
|
270
|
+
- `auto` + no agent -> falls back to prompt mode (`FALLBACK_PROMPT_MODE`)
|
|
271
|
+
- `agent` + no agent -> exits with `EXPLICIT_AGENT_REQUIRED`
|
|
272
|
+
|
|
273
|
+
Local model configuration (optional):
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# local provider example: ollama
|
|
262
277
|
set INFERNO_LOCAL_PROVIDER=ollama
|
|
263
278
|
set INFERNO_LOCAL_ENDPOINT=http://127.0.0.1:11434/api/generate
|
|
264
279
|
set INFERNO_LOCAL_MODEL=llama3.1:8b
|
|
@@ -320,12 +335,9 @@ Recommended chain:
|
|
|
320
335
|
|
|
321
336
|
```yaml
|
|
322
337
|
# .github/workflows/ci.yml
|
|
323
|
-
- name: infernoflow run
|
|
324
|
-
run: npx infernoflow run "sync check" --json
|
|
338
|
+
- name: infernoflow run (headless)
|
|
339
|
+
run: npx infernoflow run "sync check" --provider prompt --json
|
|
325
340
|
env:
|
|
326
|
-
INFERNO_LOCAL_PROVIDER: ollama
|
|
327
|
-
INFERNO_LOCAL_ENDPOINT: http://127.0.0.1:11434/api/generate
|
|
328
|
-
INFERNO_LOCAL_MODEL: llama3.1:8b
|
|
329
341
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
330
342
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
331
343
|
```
|
package/bin/infernoflow.mjs
CHANGED
|
@@ -76,6 +76,8 @@ ${formatCommandsHelp()}
|
|
|
76
76
|
--dry-run Execute full flow without writing files
|
|
77
77
|
--json Emit machine-readable events and result payload
|
|
78
78
|
--no-rollback Keep changes even if validation fails
|
|
79
|
+
--provider <type> auto | agent | local | prompt (default: auto)
|
|
80
|
+
--ide <name> auto | cursor | vscode | windsurf (default: auto)
|
|
79
81
|
|
|
80
82
|
${bold("Typical workflow:")}
|
|
81
83
|
${gray('1. infernoflow context --intent "what I want to build"')}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function detectIdeContext(preferredIde = "auto") {
|
|
2
|
+
const env = process.env;
|
|
3
|
+
const lowerPreferred = String(preferredIde || "auto").toLowerCase();
|
|
4
|
+
|
|
5
|
+
const hasCursor = !!(env.CURSOR_TRACE_ID || env.CURSOR_AGENT || env.CURSOR_SESSION_ID);
|
|
6
|
+
const hasVscode = !!(env.VSCODE_PID || env.VSCODE_CWD || env.GITHUB_COPILOT_AGENT);
|
|
7
|
+
const hasWindsurf = !!(env.WINDSURF || env.CODEIUM || env.WINDSURF_SESSION_ID);
|
|
8
|
+
|
|
9
|
+
let ideDetected = "unknown";
|
|
10
|
+
if (hasCursor) ideDetected = "cursor";
|
|
11
|
+
else if (hasVscode) ideDetected = "vscode";
|
|
12
|
+
else if (hasWindsurf) ideDetected = "windsurf";
|
|
13
|
+
|
|
14
|
+
if (lowerPreferred !== "auto" && ["cursor", "vscode", "windsurf"].includes(lowerPreferred)) {
|
|
15
|
+
ideDetected = lowerPreferred;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const explicitAgentAvailability = env.INFERNO_AGENT_AVAILABLE;
|
|
19
|
+
const agentAvailable = explicitAgentAvailability != null
|
|
20
|
+
? explicitAgentAvailability === "1" || explicitAgentAvailability === "true"
|
|
21
|
+
: ideDetected !== "unknown";
|
|
22
|
+
|
|
23
|
+
const reasonCodes = [];
|
|
24
|
+
if (ideDetected !== "unknown") reasonCodes.push(`IDE_${ideDetected.toUpperCase()}_DETECTED`);
|
|
25
|
+
else reasonCodes.push("IDE_UNKNOWN");
|
|
26
|
+
if (agentAvailable) reasonCodes.push("IDE_AGENT_AVAILABLE");
|
|
27
|
+
else reasonCodes.push("IDE_AGENT_UNAVAILABLE");
|
|
28
|
+
|
|
29
|
+
return { ideDetected, agentAvailable, reasonCodes };
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { detectIdeContext } from "./ideDetection.mjs";
|
|
2
|
+
|
|
3
|
+
export async function resolveProvider(requestedProvider = "auto", preferredIde = "auto") {
|
|
4
|
+
const providerRequested = String(requestedProvider || "auto").toLowerCase();
|
|
5
|
+
const ide = detectIdeContext(preferredIde);
|
|
6
|
+
const reasonCodes = [...ide.reasonCodes];
|
|
7
|
+
|
|
8
|
+
if (providerRequested === "local") {
|
|
9
|
+
reasonCodes.push("LOCAL_PROVIDER_SELECTED");
|
|
10
|
+
return {
|
|
11
|
+
providerRequested,
|
|
12
|
+
providerResolved: "local",
|
|
13
|
+
ideDetected: ide.ideDetected,
|
|
14
|
+
agentAvailable: ide.agentAvailable,
|
|
15
|
+
reasonCodes,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (providerRequested === "prompt") {
|
|
20
|
+
reasonCodes.push("PROMPT_PROVIDER_SELECTED");
|
|
21
|
+
return {
|
|
22
|
+
providerRequested,
|
|
23
|
+
providerResolved: "prompt",
|
|
24
|
+
ideDetected: ide.ideDetected,
|
|
25
|
+
agentAvailable: ide.agentAvailable,
|
|
26
|
+
reasonCodes,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (providerRequested === "agent") {
|
|
31
|
+
if (!ide.agentAvailable) {
|
|
32
|
+
reasonCodes.push("EXPLICIT_AGENT_REQUIRED");
|
|
33
|
+
return {
|
|
34
|
+
providerRequested,
|
|
35
|
+
providerResolved: "none",
|
|
36
|
+
ideDetected: ide.ideDetected,
|
|
37
|
+
agentAvailable: ide.agentAvailable,
|
|
38
|
+
reasonCodes,
|
|
39
|
+
error: "agent_unavailable",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
reasonCodes.push("IDE_AGENT_SELECTED");
|
|
43
|
+
return {
|
|
44
|
+
providerRequested,
|
|
45
|
+
providerResolved: "agent",
|
|
46
|
+
ideDetected: ide.ideDetected,
|
|
47
|
+
agentAvailable: ide.agentAvailable,
|
|
48
|
+
reasonCodes,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// auto
|
|
53
|
+
if (ide.agentAvailable) {
|
|
54
|
+
reasonCodes.push("IDE_AGENT_SELECTED");
|
|
55
|
+
return {
|
|
56
|
+
providerRequested: "auto",
|
|
57
|
+
providerResolved: "agent",
|
|
58
|
+
ideDetected: ide.ideDetected,
|
|
59
|
+
agentAvailable: ide.agentAvailable,
|
|
60
|
+
reasonCodes,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
reasonCodes.push("FALLBACK_PROMPT_MODE");
|
|
65
|
+
return {
|
|
66
|
+
providerRequested: "auto",
|
|
67
|
+
providerResolved: "prompt",
|
|
68
|
+
ideDetected: ide.ideDetected,
|
|
69
|
+
agentAvailable: ide.agentAvailable,
|
|
70
|
+
reasonCodes,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
package/lib/commands/init.mjs
CHANGED
|
@@ -68,7 +68,7 @@ function upsertScripts(cwd, silent = false) {
|
|
|
68
68
|
"inferno:gate": "infernoflow doc-gate",
|
|
69
69
|
"inferno:impact": "infernoflow pr-impact --json",
|
|
70
70
|
"inferno:sync": "infernoflow sync --auto --json",
|
|
71
|
-
"inferno:run": "infernoflow run \"sync check\" --json",
|
|
71
|
+
"inferno:run": "infernoflow run \"sync check\" --provider auto --json",
|
|
72
72
|
"inferno:hooks": "node scripts/inferno-install-hooks.mjs"
|
|
73
73
|
};
|
|
74
74
|
for (const [k, v] of Object.entries(toAdd)) {
|
package/lib/commands/run.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { generateWithLocalModel } from "../ai/localProvider.mjs";
|
|
6
|
+
import { resolveProvider } from "../ai/providerRouter.mjs";
|
|
6
7
|
import {
|
|
7
8
|
buildPrompt,
|
|
8
9
|
loadSuggestContext,
|
|
@@ -88,13 +89,59 @@ function writeRunArtifact(cwd, artifact) {
|
|
|
88
89
|
return path.relative(cwd, filePath);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function getOptionValue(args, flag, fallback = null) {
|
|
93
|
+
const idx = args.indexOf(flag);
|
|
94
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-")) return args[idx + 1];
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractTask(args) {
|
|
99
|
+
const takesValue = new Set(["--provider", "--ide"]);
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let i = 1; i < args.length; i++) {
|
|
102
|
+
const token = args[i];
|
|
103
|
+
if (token.startsWith("-")) {
|
|
104
|
+
if (takesValue.has(token)) i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
out.push(token);
|
|
108
|
+
}
|
|
109
|
+
return out.join(" ").trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildPromptFallbackSuggestion(task, contract) {
|
|
113
|
+
return {
|
|
114
|
+
summary: `Prompt fallback only: ${task}`,
|
|
115
|
+
newCapabilities: [],
|
|
116
|
+
removedCapabilities: [],
|
|
117
|
+
updatedScenarios: [],
|
|
118
|
+
changelogEntry: `- Prompt fallback mode for task: ${task} (no automatic contract mutation).`,
|
|
119
|
+
_meta: {
|
|
120
|
+
actionRequired: true,
|
|
121
|
+
nextStep: "Run infernoflow suggest or provide an agent bridge for automatic apply.",
|
|
122
|
+
capabilitiesCount: (contract?.capabilities || []).length,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function generateWithIdeAgent(prompt) {
|
|
128
|
+
if (process.env.INFERNO_AGENT_MOCK_RESPONSE) return process.env.INFERNO_AGENT_MOCK_RESPONSE;
|
|
129
|
+
if (process.env.INFERNO_AGENT_RESPONSE_FILE && fs.existsSync(process.env.INFERNO_AGENT_RESPONSE_FILE)) {
|
|
130
|
+
return fs.readFileSync(process.env.INFERNO_AGENT_RESPONSE_FILE, "utf8");
|
|
131
|
+
}
|
|
132
|
+
throw new Error("ide_agent_bridge_not_configured");
|
|
133
|
+
}
|
|
134
|
+
|
|
91
135
|
export async function runCommand(args = []) {
|
|
92
136
|
const asJson = args.includes("--json");
|
|
93
137
|
const dryRun = args.includes("--dry-run");
|
|
94
138
|
const noRollback = args.includes("--no-rollback");
|
|
95
|
-
const
|
|
139
|
+
const providerRequested = (getOptionValue(args, "--provider", "auto") || "auto").toLowerCase();
|
|
140
|
+
const ideRequested = (getOptionValue(args, "--ide", "auto") || "auto").toLowerCase();
|
|
141
|
+
const task = extractTask(args) || "sync check";
|
|
96
142
|
const cwd = process.cwd();
|
|
97
143
|
const events = [];
|
|
144
|
+
const reasonCodes = [];
|
|
98
145
|
|
|
99
146
|
if (!asJson) header("run");
|
|
100
147
|
stageEvent(asJson, events, "init", "info", { task, dryRun, noRollback });
|
|
@@ -103,6 +150,30 @@ export async function runCommand(args = []) {
|
|
|
103
150
|
const impact = runCliJson(["pr-impact", "--json"]);
|
|
104
151
|
stageEvent(asJson, events, "detect", impact.data?.ok ? "ok" : "warn", { confidence: impact.data?.confidence || "low" });
|
|
105
152
|
|
|
153
|
+
const routed = await resolveProvider(providerRequested, ideRequested);
|
|
154
|
+
reasonCodes.push(...(routed.reasonCodes || []));
|
|
155
|
+
if (routed.error === "agent_unavailable") {
|
|
156
|
+
const payload = {
|
|
157
|
+
ok: false,
|
|
158
|
+
error: "agent_unavailable",
|
|
159
|
+
providerRequested,
|
|
160
|
+
providerResolved: routed.providerResolved,
|
|
161
|
+
ideDetected: routed.ideDetected,
|
|
162
|
+
agentAvailable: routed.agentAvailable,
|
|
163
|
+
reasonCodes,
|
|
164
|
+
events,
|
|
165
|
+
};
|
|
166
|
+
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
167
|
+
else fail("provider agent unavailable", "Use --provider auto|local|prompt");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
stageEvent(asJson, events, "route", "ok", {
|
|
171
|
+
providerRequested,
|
|
172
|
+
providerResolved: routed.providerResolved,
|
|
173
|
+
ideDetected: routed.ideDetected,
|
|
174
|
+
agentAvailable: routed.agentAvailable,
|
|
175
|
+
});
|
|
176
|
+
|
|
106
177
|
const ctx = loadSuggestContext(cwd);
|
|
107
178
|
if (!ctx?.contract) {
|
|
108
179
|
const payload = { ok: false, error: "inferno_missing", events };
|
|
@@ -120,12 +191,19 @@ export async function runCommand(args = []) {
|
|
|
120
191
|
});
|
|
121
192
|
let suggestion;
|
|
122
193
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
194
|
+
if (routed.providerResolved === "local") {
|
|
195
|
+
const raw = await generateWithLocalModel(prompt);
|
|
196
|
+
suggestion = parseSuggestionJson(raw);
|
|
197
|
+
} else if (routed.providerResolved === "agent") {
|
|
198
|
+
const raw = await generateWithIdeAgent(prompt);
|
|
199
|
+
suggestion = parseSuggestionJson(raw);
|
|
200
|
+
} else {
|
|
201
|
+
suggestion = buildPromptFallbackSuggestion(task, ctx.contract);
|
|
202
|
+
}
|
|
125
203
|
} catch (err) {
|
|
126
|
-
const payload = { ok: false, error: "
|
|
204
|
+
const payload = { ok: false, error: "proposal_failed", reason: String(err.message || err), reasonCodes, events };
|
|
127
205
|
if (asJson) console.log(JSON.stringify(payload, null, 2));
|
|
128
|
-
else fail(
|
|
206
|
+
else fail("proposal generation failed", err.message);
|
|
129
207
|
process.exit(1);
|
|
130
208
|
}
|
|
131
209
|
stageEvent(asJson, events, "propose", "ok", {
|
|
@@ -133,8 +211,8 @@ export async function runCommand(args = []) {
|
|
|
133
211
|
removedCapabilities: (suggestion.removedCapabilities || []).length,
|
|
134
212
|
});
|
|
135
213
|
|
|
136
|
-
const schemaErrors = validateSuggestion(suggestion);
|
|
137
|
-
const conflictErrors = detectSuggestionConflicts(ctx.contract, suggestion);
|
|
214
|
+
const schemaErrors = routed.providerResolved === "prompt" ? [] : validateSuggestion(suggestion);
|
|
215
|
+
const conflictErrors = routed.providerResolved === "prompt" ? [] : detectSuggestionConflicts(ctx.contract, suggestion);
|
|
138
216
|
if (schemaErrors.length || conflictErrors.length) {
|
|
139
217
|
const payload = {
|
|
140
218
|
ok: false,
|
|
@@ -155,6 +233,11 @@ export async function runCommand(args = []) {
|
|
|
155
233
|
try {
|
|
156
234
|
if (dryRun) {
|
|
157
235
|
stageEvent(asJson, events, "apply", "info", { dryRun: true });
|
|
236
|
+
} else if (routed.providerResolved === "prompt") {
|
|
237
|
+
stageEvent(asJson, events, "apply", "warn", {
|
|
238
|
+
skipped: true,
|
|
239
|
+
reason: "prompt_fallback_requires_manual_step",
|
|
240
|
+
});
|
|
158
241
|
} else {
|
|
159
242
|
applyChanged = applyChanges({
|
|
160
243
|
cwd,
|
|
@@ -205,6 +288,11 @@ export async function runCommand(args = []) {
|
|
|
205
288
|
mode: "run",
|
|
206
289
|
task,
|
|
207
290
|
dryRun,
|
|
291
|
+
providerRequested,
|
|
292
|
+
providerResolved: routed.providerResolved,
|
|
293
|
+
ideDetected: routed.ideDetected,
|
|
294
|
+
agentAvailable: routed.agentAvailable,
|
|
295
|
+
reasonCodes: Array.from(new Set(reasonCodes)),
|
|
208
296
|
rolledBack,
|
|
209
297
|
applyChanged,
|
|
210
298
|
artifactPath,
|
package/package.json
CHANGED
|
@@ -22,12 +22,9 @@ jobs:
|
|
|
22
22
|
- name: Install dependencies
|
|
23
23
|
run: npm ci --ignore-scripts || npm install --ignore-scripts
|
|
24
24
|
|
|
25
|
-
- name: Inferno one-command run
|
|
26
|
-
run: npx infernoflow run "sync check" --json
|
|
25
|
+
- name: Inferno one-command run (headless prompt mode)
|
|
26
|
+
run: npx infernoflow run "sync check" --provider prompt --json
|
|
27
27
|
env:
|
|
28
|
-
INFERNO_LOCAL_PROVIDER: ollama
|
|
29
|
-
INFERNO_LOCAL_ENDPOINT: http://127.0.0.1:11434/api/generate
|
|
30
|
-
INFERNO_LOCAL_MODEL: llama3.1:8b
|
|
31
28
|
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
32
29
|
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
33
30
|
|
|
@@ -14,13 +14,13 @@ if (!fs.existsSync(gitDir)) {
|
|
|
14
14
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
15
15
|
|
|
16
16
|
const preCommit = `#!/bin/sh
|
|
17
|
-
echo "[inferno hooks] pre-commit: infernoflow run --dry-run"
|
|
18
|
-
npx infernoflow run "sync check" --dry-run
|
|
17
|
+
echo "[inferno hooks] pre-commit: infernoflow run --provider auto --dry-run"
|
|
18
|
+
npx infernoflow run "sync check" --provider auto --dry-run
|
|
19
19
|
`;
|
|
20
20
|
|
|
21
21
|
const prePush = `#!/bin/sh
|
|
22
|
-
echo "[inferno hooks] pre-push: infernoflow run --json"
|
|
23
|
-
npx infernoflow run "sync check" --json
|
|
22
|
+
echo "[inferno hooks] pre-push: infernoflow run --provider auto --json"
|
|
23
|
+
npx infernoflow run "sync check" --provider auto --json
|
|
24
24
|
`;
|
|
25
25
|
|
|
26
26
|
const writeHook = (name, content) => {
|