infernoflow 0.33.0 → 0.34.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 +208 -120
- package/dist/bin/infernoflow.mjs +271 -85
- package/dist/lib/adopters/angular.mjs +128 -1
- package/dist/lib/adopters/css.mjs +111 -1
- package/dist/lib/adopters/react.mjs +104 -1
- package/dist/lib/ai/ideDetection.mjs +31 -1
- package/dist/lib/ai/localProvider.mjs +88 -1
- package/dist/lib/ai/providerRouter.mjs +295 -2
- package/dist/lib/commands/adopt.mjs +869 -20
- package/dist/lib/commands/adoptWizard.mjs +320 -9
- package/dist/lib/commands/agent.mjs +191 -5
- package/dist/lib/commands/ai.mjs +407 -2
- package/dist/lib/commands/ask.mjs +299 -0
- package/dist/lib/commands/audit.mjs +300 -13
- package/dist/lib/commands/changelog.mjs +594 -26
- package/dist/lib/commands/check.mjs +184 -3
- package/dist/lib/commands/ci.mjs +208 -3
- package/dist/lib/commands/claudeMd.mjs +139 -28
- package/dist/lib/commands/cloud.mjs +521 -5
- package/dist/lib/commands/context.mjs +346 -34
- package/dist/lib/commands/coverage.mjs +282 -2
- package/dist/lib/commands/dashboard.mjs +635 -123
- package/dist/lib/commands/demo.mjs +465 -8
- package/dist/lib/commands/diff.mjs +274 -5
- package/dist/lib/commands/docGate.mjs +81 -2
- package/dist/lib/commands/doctor.mjs +321 -3
- package/dist/lib/commands/explain.mjs +438 -8
- package/dist/lib/commands/export.mjs +239 -10
- package/dist/lib/commands/generateSkills.mjs +163 -38
- package/dist/lib/commands/graph.mjs +378 -11
- package/dist/lib/commands/health.mjs +309 -2
- package/dist/lib/commands/impact.mjs +325 -2
- package/dist/lib/commands/implement.mjs +103 -7
- package/dist/lib/commands/init.mjs +545 -23
- package/dist/lib/commands/installCursorHooks.mjs +36 -1
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
- package/dist/lib/commands/link.mjs +342 -2
- package/dist/lib/commands/log.mjs +164 -16
- package/dist/lib/commands/monorepo.mjs +428 -4
- package/dist/lib/commands/notify.mjs +258 -4
- package/dist/lib/commands/onboard.mjs +296 -4
- package/dist/lib/commands/prComment.mjs +361 -2
- package/dist/lib/commands/prImpact.mjs +157 -2
- package/dist/lib/commands/publish.mjs +316 -15
- package/dist/lib/commands/recap.mjs +359 -0
- package/dist/lib/commands/report.mjs +272 -28
- package/dist/lib/commands/review.mjs +223 -9
- package/dist/lib/commands/run.mjs +336 -8
- package/dist/lib/commands/scaffold.mjs +419 -54
- package/dist/lib/commands/scan.mjs +1118 -5
- package/dist/lib/commands/scout.mjs +291 -2
- package/dist/lib/commands/setup.mjs +310 -5
- package/dist/lib/commands/share.mjs +196 -13
- package/dist/lib/commands/snapshot.mjs +383 -3
- package/dist/lib/commands/stability.mjs +293 -2
- package/dist/lib/commands/stats.mjs +402 -0
- package/dist/lib/commands/status.mjs +172 -4
- package/dist/lib/commands/suggest.mjs +563 -21
- package/dist/lib/commands/switch.mjs +310 -0
- package/dist/lib/commands/syncAuto.mjs +96 -1
- package/dist/lib/commands/synthesize.mjs +228 -10
- package/dist/lib/commands/teamSync.mjs +388 -2
- package/dist/lib/commands/test.mjs +363 -6
- package/dist/lib/commands/theme.mjs +195 -18
- package/dist/lib/commands/upgrade.mjs +153 -0
- package/dist/lib/commands/version.mjs +282 -2
- package/dist/lib/commands/vibe.mjs +357 -7
- package/dist/lib/commands/watch.mjs +203 -4
- package/dist/lib/commands/why.mjs +358 -4
- package/dist/lib/cursorHooksInstall.mjs +60 -1
- package/dist/lib/draftToolingInstall.mjs +68 -7
- package/dist/lib/git/detect-drift.mjs +208 -4
- package/dist/lib/learning/adapt.mjs +101 -6
- package/dist/lib/learning/observe.mjs +119 -1
- package/dist/lib/learning/patternDetector.mjs +298 -1
- package/dist/lib/learning/profile.mjs +279 -2
- package/dist/lib/learning/skillSynthesizer.mjs +145 -24
- package/dist/lib/templates/index.mjs +131 -1
- package/dist/lib/theme/scanner.mjs +343 -4
- package/dist/lib/ui/errors.mjs +142 -1
- package/dist/lib/ui/output.mjs +72 -6
- package/dist/lib/ui/prompts.mjs +147 -6
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
- package/package.json +1 -1
|
@@ -1,10 +1,239 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow export
|
|
3
|
+
*
|
|
4
|
+
* Export the capability contract to external formats so it can travel
|
|
5
|
+
* outside the repo — into API docs, service catalogs, spreadsheets, wikis.
|
|
6
|
+
*
|
|
7
|
+
* Formats:
|
|
8
|
+
* openapi OpenAPI 3.1 JSON (stubs for each capability)
|
|
9
|
+
* backstage Backstage catalog-info.yaml
|
|
10
|
+
* csv Spreadsheet-ready CSV
|
|
11
|
+
* markdown Confluence/Notion-ready Markdown table
|
|
12
|
+
* json Clean JSON (normalised, no internal infernoflow fields)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* infernoflow export --format openapi
|
|
16
|
+
* infernoflow export --format backstage --out catalog-info.yaml
|
|
17
|
+
* infernoflow export --format csv --out capabilities.csv
|
|
18
|
+
* infernoflow export --format markdown
|
|
19
|
+
* infernoflow export --format json --out contract-export.json
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { done, warn, info, bold, cyan, gray } from "../ui/output.mjs";
|
|
25
|
+
|
|
26
|
+
// ── Contract reader ───────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function readContract(infernoDir) {
|
|
29
|
+
for (const f of ["contract.json", "capabilities.json"]) {
|
|
30
|
+
const p = path.join(infernoDir, f);
|
|
31
|
+
if (!fs.existsSync(p)) continue;
|
|
32
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normaliseCaps(contract) {
|
|
38
|
+
const raw = contract?.capabilities || [];
|
|
39
|
+
return raw.map(c => {
|
|
40
|
+
if (typeof c === "string") return { id: c, description: "", tags: [], status: "active" };
|
|
41
|
+
return {
|
|
42
|
+
id: c.id || c.name || "unknown",
|
|
43
|
+
description: c.description || "",
|
|
44
|
+
tags: c.tags || [],
|
|
45
|
+
status: c.status || "active",
|
|
46
|
+
since: c.since || "",
|
|
47
|
+
owner: c.owner || "",
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readMeta(infernoDir) {
|
|
53
|
+
const pkgPath = path.join(path.dirname(infernoDir), "package.json");
|
|
54
|
+
try {
|
|
55
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
56
|
+
return { name: pkg.name || "my-service", version: pkg.version || "0.0.0", description: pkg.description || "" };
|
|
57
|
+
} catch {
|
|
58
|
+
return { name: path.basename(path.dirname(infernoDir)), version: "0.0.0", description: "" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Formatters ────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function toOpenApi(caps, meta) {
|
|
65
|
+
const paths = {};
|
|
66
|
+
for (const cap of caps) {
|
|
67
|
+
const route = `/${cap.id.replace(/_/g, "-")}`;
|
|
68
|
+
paths[route] = {
|
|
69
|
+
get: {
|
|
70
|
+
operationId: cap.id,
|
|
71
|
+
summary: cap.description || cap.id,
|
|
72
|
+
tags: cap.tags.length ? cap.tags : [meta.name],
|
|
73
|
+
parameters: [],
|
|
74
|
+
responses: {
|
|
75
|
+
"200": { description: "Success", content: { "application/json": { schema: { type: "object" } } } },
|
|
76
|
+
"401": { description: "Unauthorized" },
|
|
77
|
+
"500": { description: "Internal Server Error" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
openapi: "3.1.0",
|
|
85
|
+
info: {
|
|
86
|
+
title: meta.name,
|
|
87
|
+
version: meta.version,
|
|
88
|
+
description: meta.description || `Capability contract for ${meta.name}`,
|
|
89
|
+
},
|
|
90
|
+
paths,
|
|
91
|
+
components: { schemas: {}, securitySchemes: {} },
|
|
92
|
+
}, null, 2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toBackstage(caps, meta) {
|
|
96
|
+
const tags = [...new Set(caps.flatMap(c => c.tags))].slice(0, 10);
|
|
97
|
+
const lines = [
|
|
98
|
+
`apiVersion: backstage.io/v1alpha1`,
|
|
99
|
+
`kind: Component`,
|
|
100
|
+
`metadata:`,
|
|
101
|
+
` name: ${meta.name}`,
|
|
102
|
+
` description: "${(meta.description || meta.name).replace(/"/g, '\\"')}"`,
|
|
103
|
+
` annotations:`,
|
|
104
|
+
` infernoflow/capability-count: "${caps.length}"`,
|
|
105
|
+
` infernoflow/generated-at: "${new Date().toISOString()}"`,
|
|
106
|
+
tags.length ? ` tags:\n${tags.map(t => ` - ${t}`).join("\n")}` : "",
|
|
107
|
+
`spec:`,
|
|
108
|
+
` type: service`,
|
|
109
|
+
` lifecycle: production`,
|
|
110
|
+
` owner: team-default`,
|
|
111
|
+
` providesApis: []`,
|
|
112
|
+
`---`,
|
|
113
|
+
`# Capabilities`,
|
|
114
|
+
...caps.map(c => [
|
|
115
|
+
`# ${c.id}`,
|
|
116
|
+
`# ${c.description || "(no description)"}`,
|
|
117
|
+
`# tags: ${c.tags.join(", ") || "none"}`,
|
|
118
|
+
`# status: ${c.status}`,
|
|
119
|
+
].join("\n")),
|
|
120
|
+
].filter(Boolean);
|
|
121
|
+
return lines.join("\n") + "\n";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toCsv(caps) {
|
|
125
|
+
const header = ["id", "description", "tags", "status", "since", "owner"];
|
|
126
|
+
const rows = caps.map(c => [
|
|
127
|
+
c.id,
|
|
128
|
+
(c.description || "").replace(/"/g, '""'),
|
|
129
|
+
c.tags.join("|"),
|
|
130
|
+
c.status,
|
|
131
|
+
c.since,
|
|
132
|
+
c.owner,
|
|
133
|
+
].map(v => `"${v}"`).join(","));
|
|
134
|
+
return [header.join(","), ...rows].join("\n") + "\n";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function toMarkdown(caps, meta) {
|
|
138
|
+
const lines = [
|
|
139
|
+
`# ${meta.name} — Capability Contract`,
|
|
140
|
+
``,
|
|
141
|
+
`> Generated by infernoflow on ${new Date().toISOString().slice(0, 10)}`,
|
|
142
|
+
`> ${caps.length} capabilities tracked`,
|
|
143
|
+
``,
|
|
144
|
+
`| Capability | Description | Tags | Status |`,
|
|
145
|
+
`|---|---|---|---|`,
|
|
146
|
+
...caps.map(c =>
|
|
147
|
+
`| \`${c.id}\` | ${c.description || ""} | ${c.tags.join(", ") || "—"} | ${c.status} |`
|
|
148
|
+
),
|
|
149
|
+
``,
|
|
150
|
+
];
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function toCleanJson(caps, meta) {
|
|
155
|
+
return JSON.stringify({
|
|
156
|
+
name: meta.name,
|
|
157
|
+
version: meta.version,
|
|
158
|
+
exportedAt: new Date().toISOString(),
|
|
159
|
+
capabilities: caps,
|
|
160
|
+
}, null, 2) + "\n";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
const FORMAT_EXT = {
|
|
166
|
+
openapi: "openapi.json",
|
|
167
|
+
backstage: "catalog-info.yaml",
|
|
168
|
+
csv: "capabilities.csv",
|
|
169
|
+
markdown: "capabilities.md",
|
|
170
|
+
json: "capabilities-export.json",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export async function exportCommand(rawArgs) {
|
|
174
|
+
const args = rawArgs.slice(1);
|
|
175
|
+
const jsonMode = args.includes("--json");
|
|
176
|
+
const cwd = process.cwd();
|
|
177
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
178
|
+
|
|
179
|
+
if (!fs.existsSync(infernoDir)) {
|
|
180
|
+
const msg = "inferno/ not found. Run: infernoflow init";
|
|
181
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
|
|
182
|
+
else warn(msg);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const fmtIdx = args.indexOf("--format");
|
|
187
|
+
const format = fmtIdx !== -1 ? args[fmtIdx + 1] : null;
|
|
188
|
+
const outIdx = args.indexOf("--out");
|
|
189
|
+
const outArg = outIdx !== -1 ? args[outIdx + 1] : null;
|
|
190
|
+
|
|
191
|
+
const validFormats = Object.keys(FORMAT_EXT);
|
|
192
|
+
|
|
193
|
+
if (!format || !validFormats.includes(format)) {
|
|
194
|
+
const msg = `Usage: infernoflow export --format <${validFormats.join("|")}> [--out <path>]`;
|
|
195
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
|
|
196
|
+
else {
|
|
197
|
+
warn(msg);
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(` ${gray("Available formats:")}`);
|
|
200
|
+
console.log(` ${bold("openapi")} — OpenAPI 3.1 JSON with a stub path per capability`);
|
|
201
|
+
console.log(` ${bold("backstage")} — Backstage catalog-info.yaml component definition`);
|
|
202
|
+
console.log(` ${bold("csv")} — Spreadsheet-ready CSV`);
|
|
203
|
+
console.log(` ${bold("markdown")} — Confluence/Notion table`);
|
|
204
|
+
console.log(` ${bold("json")} — Clean normalised JSON`);
|
|
205
|
+
console.log();
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const contract = readContract(infernoDir);
|
|
211
|
+
if (!contract) {
|
|
212
|
+
const msg = "No contract.json or capabilities.json found.";
|
|
213
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
|
|
214
|
+
else warn(msg);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const caps = normaliseCaps(contract);
|
|
219
|
+
const meta = readMeta(infernoDir);
|
|
220
|
+
const outPath = outArg || path.join(cwd, FORMAT_EXT[format]);
|
|
221
|
+
|
|
222
|
+
let output;
|
|
223
|
+
switch (format) {
|
|
224
|
+
case "openapi": output = toOpenApi(caps, meta); break;
|
|
225
|
+
case "backstage": output = toBackstage(caps, meta); break;
|
|
226
|
+
case "csv": output = toCsv(caps); break;
|
|
227
|
+
case "markdown": output = toMarkdown(caps, meta); break;
|
|
228
|
+
case "json": output = toCleanJson(caps, meta); break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fs.writeFileSync(outPath, output);
|
|
232
|
+
|
|
233
|
+
if (jsonMode) {
|
|
234
|
+
console.log(JSON.stringify({ ok: true, format, file: outPath, capabilities: caps.length }));
|
|
235
|
+
} else {
|
|
236
|
+
done(`Exported ${bold(String(caps.length))} capabilities → ${cyan(path.relative(cwd, outPath))}`);
|
|
237
|
+
console.log();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -1,40 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow generate-skills
|
|
3
|
+
*
|
|
4
|
+
* Reads inferno/developer-profile.json and generates personalised skill files:
|
|
5
|
+
* - inferno/generated-skills/cursor-rules.md → copy to .cursor/rules/infernoflow.md
|
|
6
|
+
* - inferno/generated-skills/quick-restore.md → session startup skill
|
|
7
|
+
* - inferno/generated-skills/naming-guide.md → detected naming conventions
|
|
8
|
+
* - inferno/generated-skills/feature-scaffold.md → personal feature checklist
|
|
9
|
+
*
|
|
10
|
+
* Run: infernoflow generate-skills [--cursor] [--force]
|
|
11
|
+
* --cursor Also copy generated rules to .cursor/rules/infernoflow.md
|
|
12
|
+
* --force Overwrite existing generated files
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { readProfile } from "../learning/profile.mjs";
|
|
18
|
+
import { header, ok, warn, done, nextSteps, cyan, yellow, bold, gray } from "../ui/output.mjs";
|
|
19
|
+
|
|
20
|
+
const SKILLS_DIR = path.join("inferno", "generated-skills");
|
|
21
|
+
|
|
22
|
+
function write(filePath, content, force) {
|
|
23
|
+
if (fs.existsSync(filePath) && !force) {
|
|
24
|
+
warn(`Already exists (use --force to overwrite): ${path.relative(process.cwd(), filePath)}`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
28
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
29
|
+
ok(`Generated: ${path.relative(process.cwd(), filePath)}`);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildCursorRules(profile, contract) {
|
|
34
|
+
const naming = profile.namingStyle !== "unknown" ? profile.namingStyle : "PascalCase";
|
|
35
|
+
const verbs = profile.preferredVerbs.length > 0 ? profile.preferredVerbs.slice(0, 5).join(", ") : "Add, Update, Remove";
|
|
36
|
+
const framework = profile.stack?.framework || "unknown";
|
|
37
|
+
const clusters = profile.featureClusters.slice(0, 3);
|
|
38
|
+
const sessionStyle = profile.sessionCount >= 20 ? "experienced" : profile.sessionCount >= 5 ? "intermediate" : "new";
|
|
39
|
+
|
|
40
|
+
const clusterRules = clusters.map(cluster =>
|
|
41
|
+
`- When touching [${cluster.slice(0, 3).join(", ")}], check whether related capabilities also need updating`
|
|
42
|
+
).join("\n");
|
|
43
|
+
|
|
44
|
+
return `# infernoflow — Cursor Rules (auto-generated)
|
|
45
|
+
# Project: ${contract?.policyId || "unknown"} | Stack: ${framework}
|
|
4
46
|
# Regenerate with: infernoflow generate-skills --cursor
|
|
5
47
|
|
|
6
48
|
## Contract awareness
|
|
7
49
|
- This project uses infernoflow to track capability contracts
|
|
8
50
|
- After implementing any feature, always call \`infernoflow_run\` then \`infernoflow_apply\`
|
|
9
51
|
- Run \`infernoflow_check\` before every commit
|
|
10
|
-
- Current capabilities: [${(
|
|
52
|
+
- Current capabilities: [${(contract?.capabilities || []).join(", ")}]
|
|
11
53
|
|
|
12
54
|
## Naming conventions (detected from this developer's history)
|
|
13
|
-
- Capability IDs use **${
|
|
14
|
-
- Preferred action verbs: ${
|
|
55
|
+
- Capability IDs use **${naming}** (e.g. ${verbs.split(", ").slice(0, 2).map(v => v + "Item").join(", ")})
|
|
56
|
+
- Preferred action verbs: ${verbs}
|
|
15
57
|
- Match this style when suggesting new capability names
|
|
16
58
|
|
|
17
59
|
## Feature clusters (capabilities this developer adds together)
|
|
18
|
-
${
|
|
60
|
+
${clusterRules || "- No clusters detected yet — build more features to train this"}
|
|
19
61
|
|
|
20
|
-
## Session style (${
|
|
21
|
-
${
|
|
22
|
-
- Be direct and minimal in responses
|
|
23
|
-
- Explain infernoflow commands when used
|
|
62
|
+
## Session style (${sessionStyle})
|
|
63
|
+
${sessionStyle === "experienced"
|
|
64
|
+
? "- Skip basic explanations — this developer knows the codebase well\n- Be direct and minimal in responses"
|
|
65
|
+
: "- Include brief context for non-obvious decisions\n- Explain infernoflow commands when used"}
|
|
24
66
|
|
|
25
67
|
## Workflow reminders
|
|
26
68
|
- Start sessions with: \`infernoflow context --show\`
|
|
27
69
|
- Use \`infernoflow_git_drift\` to check what's changed before starting work
|
|
28
70
|
- Use \`infernoflow_implement\` to get a structured coding prompt before writing code
|
|
29
|
-
- Changelog entries should be ${
|
|
30
|
-
|
|
71
|
+
- Changelog entries should be ${profile.changelogVerbosity === "detailed" ? "detailed (include context and impact)" : "brief (one line, action-focused)"}
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildQuickRestore(profile, contract) {
|
|
76
|
+
const working = ""; // will be filled at runtime from context-state.json
|
|
77
|
+
const framework = profile.stack?.framework || "unknown";
|
|
78
|
+
const capabilities = (contract?.capabilities || []).slice(0, 6);
|
|
79
|
+
|
|
80
|
+
return `# Quick Restore — ${contract?.policyId || "this project"}
|
|
31
81
|
# Paste this at the start of any new AI session to restore context instantly.
|
|
32
82
|
# Regenerate with: infernoflow generate-skills
|
|
33
83
|
|
|
34
84
|
## Project snapshot
|
|
35
|
-
- **Project:** ${
|
|
36
|
-
- **Stack:** ${
|
|
37
|
-
- **Capabilities:** ${
|
|
85
|
+
- **Project:** ${contract?.policyId || "unknown"}
|
|
86
|
+
- **Stack:** ${framework} / ${profile.stack?.language || "unknown"} (${profile.stack?.projectType || "unknown"})
|
|
87
|
+
- **Capabilities:** ${capabilities.join(", ")}${(contract?.capabilities || []).length > 6 ? ` +${(contract?.capabilities || []).length - 6} more` : ""}
|
|
38
88
|
|
|
39
89
|
## How to start a session
|
|
40
90
|
1. Run: \`infernoflow context --show\`
|
|
@@ -43,42 +93,64 @@ ${a==="experienced"?`- Skip basic explanations \u2014 this developer knows the c
|
|
|
43
93
|
4. Pick up where you left off
|
|
44
94
|
|
|
45
95
|
## infernoflow tools available (in Cursor / VS Code Agent mode)
|
|
46
|
-
- \`infernoflow_run\`
|
|
47
|
-
- \`infernoflow_apply\`
|
|
48
|
-
- \`infernoflow_implement\`
|
|
49
|
-
- \`infernoflow_git_drift\`
|
|
50
|
-
- \`infernoflow_check\`
|
|
51
|
-
- \`infernoflow_status\`
|
|
96
|
+
- \`infernoflow_run\` — generate a contract update prompt
|
|
97
|
+
- \`infernoflow_apply\` — apply a JSON response
|
|
98
|
+
- \`infernoflow_implement\` — get a structured coding prompt
|
|
99
|
+
- \`infernoflow_git_drift\` — see what capabilities may have drifted
|
|
100
|
+
- \`infernoflow_check\` — validate contract is in sync
|
|
101
|
+
- \`infernoflow_status\` — quick health check
|
|
52
102
|
|
|
53
103
|
## Definition of done (every feature branch)
|
|
54
104
|
- [ ] Code works as intended
|
|
55
|
-
- [ ] \`infernoflow_run\`
|
|
105
|
+
- [ ] \`infernoflow_run\` → \`infernoflow_apply\` completed
|
|
56
106
|
- [ ] \`infernoflow_check\` passes
|
|
57
107
|
- [ ] Commit message references the capability changed
|
|
58
|
-
|
|
59
|
-
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildNamingGuide(profile) {
|
|
112
|
+
const naming = profile.namingStyle !== "unknown" ? profile.namingStyle : "PascalCase";
|
|
113
|
+
const verbs = profile.preferredVerbs.length > 0 ? profile.preferredVerbs : ["Create", "Read", "Update", "Delete", "Search"];
|
|
114
|
+
|
|
115
|
+
const examples = verbs.slice(0, 5).map(v => {
|
|
116
|
+
if (naming === "PascalCase") return ` - ${v}Item, ${v}Task, ${v}User`;
|
|
117
|
+
if (naming === "camelCase") return ` - ${v.toLowerCase()}Item, ${v.toLowerCase()}Task`;
|
|
118
|
+
return ` - ${v.toLowerCase()}-item, ${v.toLowerCase()}-task`;
|
|
119
|
+
}).join("\n");
|
|
60
120
|
|
|
61
|
-
|
|
121
|
+
return `# Naming Guide — auto-generated from your capability history
|
|
122
|
+
|
|
123
|
+
## Detected style: ${naming}
|
|
62
124
|
|
|
63
125
|
### Your preferred action verbs
|
|
64
|
-
${
|
|
65
|
-
`)}
|
|
126
|
+
${verbs.map(v => `- **${v}**`).join("\n")}
|
|
66
127
|
|
|
67
128
|
### Examples matching your style
|
|
68
|
-
${
|
|
129
|
+
${examples}
|
|
69
130
|
|
|
70
131
|
### Rules
|
|
71
132
|
- All capability IDs in \`inferno/contract.json\` must follow this style
|
|
72
|
-
- New capabilities suggested by AI should match
|
|
133
|
+
- New capabilities suggested by AI should match — reject any that don't
|
|
73
134
|
- If you rename a capability, update contract.json + capabilities.json + any scenarios
|
|
74
135
|
|
|
75
136
|
### When naming a new capability, ask:
|
|
76
137
|
1. Does it describe a single, discrete behavior? (If not, split it)
|
|
77
138
|
2. Does it start with one of your preferred verbs?
|
|
78
|
-
3. Is it in ${
|
|
79
|
-
4. Is it unique
|
|
80
|
-
|
|
81
|
-
|
|
139
|
+
3. Is it in ${naming}?
|
|
140
|
+
4. Is it unique — not already in contract.json?
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildFeatureScaffold(profile, contract) {
|
|
145
|
+
const clusters = profile.featureClusters.slice(0, 2);
|
|
146
|
+
const topCluster = clusters[0] || [];
|
|
147
|
+
const framework = profile.stack?.framework || "unknown";
|
|
148
|
+
|
|
149
|
+
const clusterChecks = topCluster.slice(0, 4).map(id =>
|
|
150
|
+
`- [ ] Does **${id}** need updating? (check inferno/capabilities.json)`
|
|
151
|
+
).join("\n");
|
|
152
|
+
|
|
153
|
+
return `# Feature Scaffold — ${framework} project
|
|
82
154
|
# Use this checklist whenever starting a new feature.
|
|
83
155
|
# Regenerate with: infernoflow generate-skills
|
|
84
156
|
|
|
@@ -94,13 +166,13 @@ ${r}
|
|
|
94
166
|
- [ ] Verify it works end-to-end
|
|
95
167
|
|
|
96
168
|
## Capability cluster check
|
|
97
|
-
${
|
|
169
|
+
${clusterChecks || "- [ ] Review existing capabilities — do any need updating?"}
|
|
98
170
|
|
|
99
171
|
## Contract update (required before merge)
|
|
100
172
|
- [ ] Run \`infernoflow_run\` with a description of what changed
|
|
101
173
|
- [ ] Review the suggested JSON
|
|
102
174
|
- [ ] Run \`infernoflow_apply\` with the JSON
|
|
103
|
-
- [ ] Run \`infernoflow_check\`
|
|
175
|
+
- [ ] Run \`infernoflow_check\` — must pass
|
|
104
176
|
|
|
105
177
|
## Commit message
|
|
106
178
|
- Reference the capability: "feat: add SearchItems endpoint (#42)"
|
|
@@ -110,5 +182,58 @@ ${o||"- [ ] Review existing capabilities \u2014 do any need updating?"}
|
|
|
110
182
|
- [ ] Feature works
|
|
111
183
|
- [ ] \`infernoflow_check\` passes
|
|
112
184
|
- [ ] PR description mentions which capabilities changed
|
|
113
|
-
|
|
114
|
-
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function generateSkillsCommand(args) {
|
|
189
|
+
const cwd = process.cwd();
|
|
190
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
191
|
+
const installCursor = args.includes("--cursor");
|
|
192
|
+
|
|
193
|
+
header("generate-skills");
|
|
194
|
+
|
|
195
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
196
|
+
if (!fs.existsSync(infernoDir)) {
|
|
197
|
+
console.error(" ✘ inferno/ not found — run: infernoflow init\n");
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const profile = readProfile(infernoDir);
|
|
202
|
+
let contract = null;
|
|
203
|
+
try { contract = JSON.parse(fs.readFileSync(path.join(infernoDir, "contract.json"), "utf8")); } catch {}
|
|
204
|
+
|
|
205
|
+
const skillsDir = path.join(cwd, SKILLS_DIR);
|
|
206
|
+
|
|
207
|
+
// Generate all four skill files
|
|
208
|
+
write(path.join(skillsDir, "cursor-rules.md"), buildCursorRules(profile, contract), force);
|
|
209
|
+
write(path.join(skillsDir, "quick-restore.md"), buildQuickRestore(profile, contract), force);
|
|
210
|
+
write(path.join(skillsDir, "naming-guide.md"), buildNamingGuide(profile), force);
|
|
211
|
+
write(path.join(skillsDir, "feature-scaffold.md"), buildFeatureScaffold(profile, contract), force);
|
|
212
|
+
|
|
213
|
+
// Optionally install cursor rules
|
|
214
|
+
if (installCursor) {
|
|
215
|
+
const rulesDir = path.join(cwd, ".cursor", "rules");
|
|
216
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
217
|
+
const src = path.join(skillsDir, "cursor-rules.md");
|
|
218
|
+
const dst = path.join(rulesDir, "infernoflow.md");
|
|
219
|
+
fs.copyFileSync(src, dst);
|
|
220
|
+
ok(`Installed to: .cursor/rules/infernoflow.md`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const profileSummary = [
|
|
224
|
+
profile.namingStyle !== "unknown" ? `naming: ${profile.namingStyle}` : null,
|
|
225
|
+
profile.stack?.framework !== "unknown" ? `stack: ${profile.stack.framework}` : null,
|
|
226
|
+
profile.sessionCount > 0 ? `sessions: ${profile.sessionCount}` : null,
|
|
227
|
+
].filter(Boolean).join(" · ");
|
|
228
|
+
|
|
229
|
+
done(`Skills generated${profileSummary ? ` (${profileSummary})` : ""}`);
|
|
230
|
+
|
|
231
|
+
nextSteps([
|
|
232
|
+
`Review files in ${yellow(SKILLS_DIR + "/")}`,
|
|
233
|
+
`Copy to Cursor: ${cyan("infernoflow generate-skills --cursor")}`,
|
|
234
|
+
`Re-run any time to refresh after more sessions: ${cyan("infernoflow generate-skills --force")}`,
|
|
235
|
+
profile.sessionCount < 5
|
|
236
|
+
? `Run more commands to improve personalisation (${profile.sessionCount} sessions so far)`
|
|
237
|
+
: `Profile has ${profile.sessionCount} sessions — personalisation is well-trained`,
|
|
238
|
+
]);
|
|
239
|
+
}
|