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 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. Generate suggestion via local model
244
- 3. Apply inferno updates
245
- 4. Validate with `check`
246
- 5. Roll back automatically if validation fails
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
- Local model configuration (required):
259
+ Provider options:
259
260
 
260
261
  ```bash
261
- # default provider: ollama
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
  ```
@@ -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
+
@@ -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)) {
@@ -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 task = args.filter((a) => !a.startsWith("-")).slice(1).join(" ").trim() || "sync check";
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
- const raw = await generateWithLocalModel(prompt);
124
- suggestion = parseSuggestionJson(raw);
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: "local_model_failed", reason: String(err.message || err), events };
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(`local model failed`, err.message);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
4
4
  "description": "The forge for liquid code — keep capabilities, contracts, and docs in sync.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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) => {