infernoflow 0.10.7 → 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 +58 -4
- package/bin/infernoflow.mjs +10 -0
- package/lib/ai/ideDetection.mjs +31 -0
- package/lib/ai/localProvider.mjs +88 -0
- package/lib/ai/providerRouter.mjs +73 -0
- package/lib/commands/adopt.mjs +768 -768
- package/lib/commands/implement.mjs +103 -103
- package/lib/commands/init.mjs +1 -0
- package/lib/commands/prImpact.mjs +157 -157
- package/lib/commands/run.mjs +315 -0
- package/lib/commands/suggest.mjs +42 -12
- package/lib/commands/syncAuto.mjs +96 -96
- package/package.json +2 -2
- package/templates/ci/github-inferno-check.yml +30 -36
- package/templates/scripts/inferno-install-hooks.mjs +36 -36
package/README.md
CHANGED
|
@@ -166,6 +166,7 @@ infernoflow doc-gate --json
|
|
|
166
166
|
| `infernoflow status` | At-a-glance health of your contract |
|
|
167
167
|
| `infernoflow suggest` | Generate an AI prompt, apply capability updates |
|
|
168
168
|
| `infernoflow implement` | Generate implementation prompts for coding agents |
|
|
169
|
+
| `infernoflow run` | One-command local-model flow with validation and rollback |
|
|
169
170
|
| `infernoflow pr-impact` | Analyze changed files and infer capability/doc drift |
|
|
170
171
|
| `infernoflow sync --auto` | Deterministic sync flow for agents (skeleton) |
|
|
171
172
|
| `infernoflow check` | Full validation: contract, capabilities, scenarios, changelog |
|
|
@@ -185,6 +186,9 @@ infernoflow implement "..." --mode both
|
|
|
185
186
|
infernoflow implement "..." --mode cursor
|
|
186
187
|
infernoflow implement "..." --mode generic
|
|
187
188
|
infernoflow implement "..." --mode both --copy
|
|
189
|
+
infernoflow run "add favorite badge to tasks"
|
|
190
|
+
infernoflow run "sync check" --dry-run
|
|
191
|
+
infernoflow run "sync check" --json
|
|
188
192
|
infernoflow pr-impact
|
|
189
193
|
infernoflow pr-impact --json
|
|
190
194
|
infernoflow sync --auto
|
|
@@ -232,6 +236,58 @@ Proposed Changes
|
|
|
232
236
|
|
|
233
237
|
Works with any AI — Claude, ChatGPT, GitHub Copilot, Cursor, or your own setup.
|
|
234
238
|
|
|
239
|
+
## `infernoflow run` — zero copy/paste flow
|
|
240
|
+
|
|
241
|
+
Run one command and infernoflow will:
|
|
242
|
+
1. Detect drift (`pr-impact`)
|
|
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
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
infernoflow run "add favorite badge to tasks and filter by favorite"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Machine mode:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
infernoflow run "sync check" --json
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Provider options:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
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
|
|
277
|
+
set INFERNO_LOCAL_PROVIDER=ollama
|
|
278
|
+
set INFERNO_LOCAL_ENDPOINT=http://127.0.0.1:11434/api/generate
|
|
279
|
+
set INFERNO_LOCAL_MODEL=llama3.1:8b
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Optional OpenAI-compatible local server:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
set INFERNO_LOCAL_PROVIDER=openai
|
|
286
|
+
set INFERNO_LOCAL_ENDPOINT=http://127.0.0.1:1234/v1/chat/completions
|
|
287
|
+
set INFERNO_LOCAL_MODEL=local-model
|
|
288
|
+
set INFERNO_LOCAL_API_KEY=local
|
|
289
|
+
```
|
|
290
|
+
|
|
235
291
|
## `infernoflow implement` — code-agent execution prompts
|
|
236
292
|
|
|
237
293
|
Generate coding prompts from your project context and inferno contract:
|
|
@@ -279,10 +335,8 @@ Recommended chain:
|
|
|
279
335
|
|
|
280
336
|
```yaml
|
|
281
337
|
# .github/workflows/ci.yml
|
|
282
|
-
- name: infernoflow
|
|
283
|
-
run:
|
|
284
|
-
npx infernoflow pr-impact --json
|
|
285
|
-
npx infernoflow check --json
|
|
338
|
+
- name: infernoflow run (headless)
|
|
339
|
+
run: npx infernoflow run "sync check" --provider prompt --json
|
|
286
340
|
env:
|
|
287
341
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
288
342
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
package/bin/infernoflow.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
13
13
|
status: "Show contract health at a glance",
|
|
14
14
|
"pr-impact": "Summarize PR impact on capabilities and docs",
|
|
15
15
|
sync: "Run deterministic inferno sync flow",
|
|
16
|
+
run: "One-command detect/propose/apply/validate flow",
|
|
16
17
|
"doc-gate": "Fail if code changed but docs were not updated",
|
|
17
18
|
suggest: "Generate AI prompt + apply capability updates",
|
|
18
19
|
implement: "Generate code-agent implementation prompt(s)",
|
|
@@ -25,6 +26,7 @@ const COMMAND_HANDLERS = {
|
|
|
25
26
|
status: async (args) => (await import("../lib/commands/status.mjs")).statusCommand(args),
|
|
26
27
|
"pr-impact": async (args) => (await import("../lib/commands/prImpact.mjs")).prImpactCommand(args),
|
|
27
28
|
sync: async (args) => (await import("../lib/commands/syncAuto.mjs")).syncCommand(args),
|
|
29
|
+
run: async (args) => (await import("../lib/commands/run.mjs")).runCommand(args),
|
|
28
30
|
suggest: async (args) => (await import("../lib/commands/suggest.mjs")).suggestCommand(args),
|
|
29
31
|
implement: async (args) => (await import("../lib/commands/implement.mjs")).implementCommand(args),
|
|
30
32
|
context: async (args) => (await import("../lib/commands/context.mjs")).contextCommand(args),
|
|
@@ -70,6 +72,13 @@ ${formatCommandsHelp()}
|
|
|
70
72
|
--mode <type> cursor | generic | both (default: both)
|
|
71
73
|
--copy, -c Copy generated prompt(s) to clipboard
|
|
72
74
|
|
|
75
|
+
${bold("run options:")}
|
|
76
|
+
--dry-run Execute full flow without writing files
|
|
77
|
+
--json Emit machine-readable events and result payload
|
|
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)
|
|
81
|
+
|
|
73
82
|
${bold("Typical workflow:")}
|
|
74
83
|
${gray('1. infernoflow context --intent "what I want to build"')}
|
|
75
84
|
${gray("2. [paste inferno/CONTEXT.md into Claude / Cursor / Copilot]")}
|
|
@@ -83,6 +92,7 @@ ${formatCommandsHelp()}
|
|
|
83
92
|
${gray("doc-gate --json")}
|
|
84
93
|
${gray("pr-impact --json")}
|
|
85
94
|
${gray("sync --auto --json")}
|
|
95
|
+
${gray('run "task" --json')}
|
|
86
96
|
`;
|
|
87
97
|
|
|
88
98
|
const [, , cmd, ...rest] = process.argv;
|
|
@@ -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,88 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 45000;
|
|
2
|
+
|
|
3
|
+
function withTimeout(ms) {
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
6
|
+
return { controller, timer };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function callOllama(prompt, timeoutMs) {
|
|
10
|
+
const endpoint = process.env.INFERNO_LOCAL_ENDPOINT || "http://127.0.0.1:11434/api/generate";
|
|
11
|
+
const model = process.env.INFERNO_LOCAL_MODEL || "llama3.1:8b";
|
|
12
|
+
const { controller, timer } = withTimeout(timeoutMs);
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(endpoint, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
model,
|
|
20
|
+
prompt,
|
|
21
|
+
stream: false,
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const body = await res.text();
|
|
26
|
+
throw new Error(`local_model_http_${res.status}: ${body.slice(0, 240)}`);
|
|
27
|
+
}
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
if (!data?.response || typeof data.response !== "string") {
|
|
30
|
+
throw new Error("local_model_invalid_response");
|
|
31
|
+
}
|
|
32
|
+
return data.response.trim();
|
|
33
|
+
} finally {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function callOpenAICompat(prompt, timeoutMs) {
|
|
39
|
+
const endpoint = process.env.INFERNO_LOCAL_ENDPOINT || "http://127.0.0.1:1234/v1/chat/completions";
|
|
40
|
+
const model = process.env.INFERNO_LOCAL_MODEL || "local-model";
|
|
41
|
+
const apiKey = process.env.INFERNO_LOCAL_API_KEY || "local";
|
|
42
|
+
const { controller, timer } = withTimeout(timeoutMs);
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(endpoint, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
Authorization: `Bearer ${apiKey}`,
|
|
49
|
+
},
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
model,
|
|
53
|
+
temperature: 0.1,
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: "system", content: "Return JSON only." },
|
|
56
|
+
{ role: "user", content: prompt },
|
|
57
|
+
],
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const body = await res.text();
|
|
62
|
+
throw new Error(`local_model_http_${res.status}: ${body.slice(0, 240)}`);
|
|
63
|
+
}
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
const text = data?.choices?.[0]?.message?.content;
|
|
66
|
+
if (!text || typeof text !== "string") {
|
|
67
|
+
throw new Error("local_model_invalid_response");
|
|
68
|
+
}
|
|
69
|
+
return text.trim();
|
|
70
|
+
} finally {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function generateWithLocalModel(prompt, options = {}) {
|
|
76
|
+
if (process.env.INFERNO_LOCAL_MOCK_RESPONSE) {
|
|
77
|
+
return process.env.INFERNO_LOCAL_MOCK_RESPONSE;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const provider = (process.env.INFERNO_LOCAL_PROVIDER || "ollama").toLowerCase();
|
|
81
|
+
const timeoutMs = Number(options.timeoutMs || process.env.INFERNO_LOCAL_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
|
|
82
|
+
|
|
83
|
+
if (provider === "openai") {
|
|
84
|
+
return callOpenAICompat(prompt, timeoutMs);
|
|
85
|
+
}
|
|
86
|
+
return callOllama(prompt, timeoutMs);
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -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
|
+
|