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,4 +1,208 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* detect-drift.mjs
|
|
3
|
+
* Compares git-changed files to capability source maps and returns
|
|
4
|
+
* a list of capabilities that may need contract updates.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get files changed since the last commit (staged + unstaged),
|
|
13
|
+
* or optionally since the last N commits.
|
|
14
|
+
*/
|
|
15
|
+
export function getChangedFiles(cwd, opts = {}) {
|
|
16
|
+
const { sinceCommits = 1, includeStagedOnly = false } = opts;
|
|
17
|
+
const changed = new Set();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Staged + unstaged modifications vs HEAD
|
|
21
|
+
const unstaged = execSync("git diff --name-only HEAD", {
|
|
22
|
+
cwd, encoding: "utf8", timeout: 10_000,
|
|
23
|
+
});
|
|
24
|
+
for (const f of unstaged.split("\n").map(l => l.trim()).filter(Boolean)) {
|
|
25
|
+
changed.add(f);
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Files changed in the last N commits
|
|
31
|
+
const committed = execSync(`git diff --name-only HEAD~${sinceCommits} HEAD`, {
|
|
32
|
+
cwd, encoding: "utf8", timeout: 10_000,
|
|
33
|
+
});
|
|
34
|
+
for (const f of committed.split("\n").map(l => l.trim()).filter(Boolean)) {
|
|
35
|
+
changed.add(f);
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Untracked new files
|
|
41
|
+
const untracked = execSync("git ls-files --others --exclude-standard", {
|
|
42
|
+
cwd, encoding: "utf8", timeout: 10_000,
|
|
43
|
+
});
|
|
44
|
+
for (const f of untracked.split("\n").map(l => l.trim()).filter(Boolean)) {
|
|
45
|
+
changed.add(f);
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
|
|
49
|
+
return Array.from(changed).sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load capability-map.json if it exists.
|
|
54
|
+
* Format: { "src/search/": ["SearchItems"], "src/auth/": ["Login"] }
|
|
55
|
+
*/
|
|
56
|
+
export function loadCapabilityMap(infernoDir) {
|
|
57
|
+
const mapPath = path.join(infernoDir, "capability-map.json");
|
|
58
|
+
if (!fs.existsSync(mapPath)) return null;
|
|
59
|
+
try { return JSON.parse(fs.readFileSync(mapPath, "utf8")); } catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load adoption_profile.json (has sourceFiles per capability from --adopt).
|
|
64
|
+
*/
|
|
65
|
+
export function loadAdoptionProfile(infernoDir) {
|
|
66
|
+
const profilePath = path.join(infernoDir, "adoption_profile.json");
|
|
67
|
+
if (!fs.existsSync(profilePath)) return null;
|
|
68
|
+
try { return JSON.parse(fs.readFileSync(profilePath, "utf8")); } catch { return null; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load capabilities.json to get all registered capabilities.
|
|
73
|
+
*/
|
|
74
|
+
export function loadCapabilities(infernoDir) {
|
|
75
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
76
|
+
if (!fs.existsSync(capsPath)) return [];
|
|
77
|
+
try {
|
|
78
|
+
const data = JSON.parse(fs.readFileSync(capsPath, "utf8"));
|
|
79
|
+
return data.capabilities || [];
|
|
80
|
+
} catch { return []; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Main: detect which capabilities are affected by changed files.
|
|
85
|
+
*
|
|
86
|
+
* Returns:
|
|
87
|
+
* {
|
|
88
|
+
* changedFiles: string[],
|
|
89
|
+
* affectedCapabilities: { id, title, matchedFiles: string[], confidence: "high"|"medium"|"low" }[],
|
|
90
|
+
* unmappedFiles: string[], // changed files with no capability match
|
|
91
|
+
* hasCapabilityMap: boolean,
|
|
92
|
+
* }
|
|
93
|
+
*/
|
|
94
|
+
export function detectDrift(cwd, opts = {}) {
|
|
95
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
96
|
+
const changedFiles = getChangedFiles(cwd, opts);
|
|
97
|
+
|
|
98
|
+
if (!changedFiles.length) {
|
|
99
|
+
return { changedFiles: [], affectedCapabilities: [], unmappedFiles: [], hasCapabilityMap: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const capMap = loadCapabilityMap(infernoDir);
|
|
103
|
+
const profile = loadAdoptionProfile(infernoDir);
|
|
104
|
+
const capabilities = loadCapabilities(infernoDir);
|
|
105
|
+
|
|
106
|
+
const capHits = new Map(); // capId → { id, title, matchedFiles: Set }
|
|
107
|
+
|
|
108
|
+
const addHit = (cap, file) => {
|
|
109
|
+
if (!capHits.has(cap.id)) {
|
|
110
|
+
capHits.set(cap.id, { id: cap.id, title: cap.title || cap.id, matchedFiles: new Set() });
|
|
111
|
+
}
|
|
112
|
+
capHits.get(cap.id).matchedFiles.add(file);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const mappedFiles = new Set();
|
|
116
|
+
|
|
117
|
+
// ── Strategy 1: capability-map.json (explicit, highest confidence) ────────
|
|
118
|
+
if (capMap) {
|
|
119
|
+
for (const changedFile of changedFiles) {
|
|
120
|
+
for (const [prefix, capIds] of Object.entries(capMap)) {
|
|
121
|
+
if (changedFile.startsWith(prefix.replace(/\\/g, "/"))) {
|
|
122
|
+
for (const capId of capIds) {
|
|
123
|
+
const cap = capabilities.find(c => c.id === capId) || { id: capId, title: capId };
|
|
124
|
+
addHit(cap, changedFile);
|
|
125
|
+
mappedFiles.add(changedFile);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Strategy 2: adoption_profile sourceFiles (from --adopt) ──────────────
|
|
133
|
+
// The profile doesn't directly store sourceFiles per cap (that's in capabilities.json via adopt).
|
|
134
|
+
// We use the capabilities sourceFiles stored during writeAdoptionBaseline.
|
|
135
|
+
// We re-read the raw capabilities with sourceFiles from the capabilities stored in inferno/.
|
|
136
|
+
const capsWithFiles = [];
|
|
137
|
+
if (profile) {
|
|
138
|
+
// Try to load a richer version from capabilities.json that includes sourceFiles
|
|
139
|
+
const capsPath = path.join(infernoDir, "capabilities.json");
|
|
140
|
+
try {
|
|
141
|
+
const raw = JSON.parse(fs.readFileSync(capsPath, "utf8"));
|
|
142
|
+
for (const c of raw.capabilities || []) {
|
|
143
|
+
if (c.sourceFiles && c.sourceFiles.length > 0) capsWithFiles.push(c);
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (capsWithFiles.length > 0) {
|
|
149
|
+
for (const cap of capsWithFiles) {
|
|
150
|
+
for (const srcFile of cap.sourceFiles || []) {
|
|
151
|
+
const normalized = srcFile.replace(/\\/g, "/");
|
|
152
|
+
for (const changedFile of changedFiles) {
|
|
153
|
+
const changedNorm = changedFile.replace(/\\/g, "/");
|
|
154
|
+
if (changedNorm === normalized || changedNorm.startsWith(path.dirname(normalized) + "/")) {
|
|
155
|
+
addHit(cap, changedFile);
|
|
156
|
+
mappedFiles.add(changedFile);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Strategy 3: filename heuristics (fallback) ────────────────────────────
|
|
164
|
+
const HEURISTIC_KEYWORDS = [
|
|
165
|
+
{ keywords: ["search"], capId: "SearchItems" },
|
|
166
|
+
{ keywords: ["filter"], capId: "FilterItems" },
|
|
167
|
+
{ keywords: ["auth", "login", "logout", "signin", "signup"], capId: "Authentication" },
|
|
168
|
+
{ keywords: ["create", "add", "new"], capId: "CreateItem" },
|
|
169
|
+
{ keywords: ["update", "edit"], capId: "UpdateItem" },
|
|
170
|
+
{ keywords: ["delete", "remove"], capId: "DeleteItem" },
|
|
171
|
+
{ keywords: ["read", "list", "view"], capId: "ReadItems" },
|
|
172
|
+
{ keywords: ["due", "deadline", "date"], capId: "SetDueDate" },
|
|
173
|
+
{ keywords: ["priority"], capId: "SetPriority" },
|
|
174
|
+
{ keywords: ["complete", "done", "toggle"], capId: "ToggleComplete" },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (const changedFile of changedFiles) {
|
|
178
|
+
if (mappedFiles.has(changedFile)) continue;
|
|
179
|
+
const lower = changedFile.toLowerCase();
|
|
180
|
+
for (const rule of HEURISTIC_KEYWORDS) {
|
|
181
|
+
if (rule.keywords.some(kw => lower.includes(kw))) {
|
|
182
|
+
const cap = capabilities.find(c => c.id === rule.capId) || { id: rule.capId, title: rule.capId };
|
|
183
|
+
addHit(cap, changedFile);
|
|
184
|
+
mappedFiles.add(changedFile);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const unmappedFiles = changedFiles.filter(f => !mappedFiles.has(f));
|
|
191
|
+
|
|
192
|
+
// Score confidence
|
|
193
|
+
const affectedCapabilities = Array.from(capHits.values()).map(hit => ({
|
|
194
|
+
id: hit.id,
|
|
195
|
+
title: hit.title,
|
|
196
|
+
matchedFiles: Array.from(hit.matchedFiles),
|
|
197
|
+
confidence: hit.matchedFiles.size >= 3 ? "high"
|
|
198
|
+
: hit.matchedFiles.size >= 1 ? "medium"
|
|
199
|
+
: "low",
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
changedFiles,
|
|
204
|
+
affectedCapabilities,
|
|
205
|
+
unmappedFiles,
|
|
206
|
+
hasCapabilityMap: !!capMap,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -1,9 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* lib/learning/adapt.mjs
|
|
3
|
+
* Uses developer-profile.json to personalise infernoflow prompts.
|
|
4
|
+
*
|
|
5
|
+
* Called by suggest/run before generating AI prompts so the AI
|
|
6
|
+
* receives instructions that match how this developer actually works.
|
|
7
|
+
*/
|
|
3
8
|
|
|
4
|
-
|
|
9
|
+
import { readProfile, summarizeProfile } from "./profile.mjs";
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Build a personalisation block to inject into any AI prompt.
|
|
13
|
+
* Returns an empty string if the profile doesn't have enough data yet.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} infernoDir
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function buildPersonalisationBlock(infernoDir) {
|
|
19
|
+
let profile;
|
|
20
|
+
try { profile = readProfile(infernoDir); } catch { return ""; }
|
|
8
21
|
|
|
9
|
-
|
|
22
|
+
const lines = [];
|
|
23
|
+
|
|
24
|
+
// Only inject once there's real data (at least 1 session or seeded from adopt)
|
|
25
|
+
const hasData =
|
|
26
|
+
profile.namingStyle !== "unknown" ||
|
|
27
|
+
profile.preferredVerbs.length > 0 ||
|
|
28
|
+
profile.stack.framework !== "unknown";
|
|
29
|
+
|
|
30
|
+
if (!hasData) return "";
|
|
31
|
+
|
|
32
|
+
lines.push("## Developer profile (personalise your response to match these preferences)");
|
|
33
|
+
|
|
34
|
+
if (profile.namingStyle !== "unknown") {
|
|
35
|
+
lines.push(`- Capability naming style: **${profile.namingStyle}** — use this for all new capability IDs`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (profile.preferredVerbs.length > 0) {
|
|
39
|
+
lines.push(`- Preferred action verbs: ${profile.preferredVerbs.slice(0, 5).join(", ")} — prefer these when naming new capabilities`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (profile.stack.framework !== "unknown") {
|
|
43
|
+
lines.push(`- Stack: ${profile.stack.framework} / ${profile.stack.language} (${profile.stack.projectType})`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (profile.changelogVerbosity !== "unknown") {
|
|
47
|
+
const hint = profile.changelogVerbosity === "brief"
|
|
48
|
+
? "Keep changelog entries short (one line, action-focused)"
|
|
49
|
+
: "Write detailed changelog entries (include context and impact)";
|
|
50
|
+
lines.push(`- Changelog style: ${hint}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (profile.featureClusters.length > 0) {
|
|
54
|
+
const topCluster = profile.featureClusters[0];
|
|
55
|
+
if (topCluster.length >= 2) {
|
|
56
|
+
lines.push(`- Common capability cluster: [${topCluster.slice(0, 4).join(", ")}] — if the task touches one of these, consider whether others need updating too`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (profile.sessionCount >= 10) {
|
|
61
|
+
lines.push(`- Experienced user (${profile.sessionCount} sessions) — skip basic explanations, be direct`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Inject personalisation into an existing prompt string.
|
|
69
|
+
* Inserts the block just before the "## Instructions" section if present,
|
|
70
|
+
* otherwise appends it near the end.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} prompt
|
|
73
|
+
* @param {string} infernoDir
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function personalisePrompt(prompt, infernoDir) {
|
|
77
|
+
const block = buildPersonalisationBlock(infernoDir);
|
|
78
|
+
if (!block) return prompt;
|
|
79
|
+
|
|
80
|
+
// Insert before ## Instructions if that section exists
|
|
81
|
+
if (prompt.includes("## Instructions")) {
|
|
82
|
+
return prompt.replace("## Instructions", block + "\n\n## Instructions");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fallback: append before the closing JSON instructions
|
|
86
|
+
if (prompt.includes("Respond with ONLY")) {
|
|
87
|
+
return prompt.replace("Respond with ONLY", block + "\n\n---\nRespond with ONLY");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return prompt + "\n\n" + block;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Return a short status line for display in infernoflow status / context.
|
|
95
|
+
* e.g. "naming: PascalCase · verbs: Add, Update · stack: angular / ts · sessions: 12"
|
|
96
|
+
*/
|
|
97
|
+
export function profileStatusLine(infernoDir) {
|
|
98
|
+
try {
|
|
99
|
+
const profile = readProfile(infernoDir);
|
|
100
|
+
return summarizeProfile(profile) || null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1 +1,119 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* lib/learning/observe.mjs
|
|
3
|
+
* Silent behavior recorder — called at the start of every CLI command.
|
|
4
|
+
*
|
|
5
|
+
* Records:
|
|
6
|
+
* - Which command was run (commandUsage counts)
|
|
7
|
+
* - When it was run (session detection)
|
|
8
|
+
* - New capabilities introduced via suggest/run (updates naming style + verbs + clusters)
|
|
9
|
+
*
|
|
10
|
+
* Never throws — all observation is best-effort so it never breaks the real command.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
readProfile,
|
|
16
|
+
writeProfile,
|
|
17
|
+
recordCommandUse,
|
|
18
|
+
recordSessionCommand,
|
|
19
|
+
detectNamingStyle,
|
|
20
|
+
detectPreferredVerbs,
|
|
21
|
+
recordCapabilityCluster,
|
|
22
|
+
} from "./profile.mjs";
|
|
23
|
+
|
|
24
|
+
const SESSION_GAP_MS = 30 * 60 * 1000; // 30 minutes = new session
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Call this at the very start of every command handler.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} infernoDir — path to the inferno/ directory
|
|
30
|
+
* @param {string} command — the CLI command name (e.g. "suggest", "run", "check")
|
|
31
|
+
* @param {object} [extras] — optional: { task: string } for suggest/implement/run
|
|
32
|
+
*/
|
|
33
|
+
export function observeCommandStart(infernoDir, command, extras = {}) {
|
|
34
|
+
try {
|
|
35
|
+
const profile = readProfile(infernoDir);
|
|
36
|
+
|
|
37
|
+
// Record command usage
|
|
38
|
+
recordCommandUse(profile, command);
|
|
39
|
+
|
|
40
|
+
// Detect new session (gap > 30 min since last command)
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const lastTs = profile._lastCommandTs || 0;
|
|
43
|
+
if (now - lastTs > SESSION_GAP_MS) {
|
|
44
|
+
profile.sessionCount = (profile.sessionCount || 0) + 1;
|
|
45
|
+
}
|
|
46
|
+
profile._lastCommandTs = now;
|
|
47
|
+
|
|
48
|
+
// Sprint 5: record rich session event
|
|
49
|
+
recordSessionCommand(profile, command, extras);
|
|
50
|
+
|
|
51
|
+
writeProfile(infernoDir, profile);
|
|
52
|
+
} catch {
|
|
53
|
+
// Silent — never break the real command
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Call this after a suggest/run/apply that added new capabilities.
|
|
59
|
+
* Updates naming style, preferred verbs, and feature clusters in the profile.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} infernoDir — path to inferno/
|
|
62
|
+
* @param {string[]} newCapabilityIds — capability IDs that were just added
|
|
63
|
+
*/
|
|
64
|
+
export function observeCapabilitiesAdded(infernoDir, newCapabilityIds) {
|
|
65
|
+
if (!newCapabilityIds || newCapabilityIds.length === 0) return;
|
|
66
|
+
try {
|
|
67
|
+
const profile = readProfile(infernoDir);
|
|
68
|
+
|
|
69
|
+
// Update naming style (weighted: new observations vs existing preference)
|
|
70
|
+
const detectedStyle = detectNamingStyle(newCapabilityIds);
|
|
71
|
+
if (detectedStyle !== "unknown") {
|
|
72
|
+
// If we have enough sessions to be confident, lock it in
|
|
73
|
+
if (profile.sessionCount >= 3 || profile.namingStyle === "unknown") {
|
|
74
|
+
profile.namingStyle = detectedStyle;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Update preferred verbs
|
|
79
|
+
const newVerbs = detectPreferredVerbs(newCapabilityIds);
|
|
80
|
+
if (newVerbs.length > 0) {
|
|
81
|
+
const combined = [...new Set([...profile.preferredVerbs, ...newVerbs])];
|
|
82
|
+
profile.preferredVerbs = combined.slice(0, 8); // keep top 8
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Record as a feature cluster if multiple capabilities were added together
|
|
86
|
+
if (newCapabilityIds.length >= 2) {
|
|
87
|
+
recordCapabilityCluster(profile, newCapabilityIds);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeProfile(infernoDir, profile);
|
|
91
|
+
} catch {
|
|
92
|
+
// Silent
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Record changelog verbosity from a changelog entry string.
|
|
98
|
+
* Helps infernoflow learn whether this developer writes brief or detailed changelogs.
|
|
99
|
+
*/
|
|
100
|
+
export function observeChangelogEntry(infernoDir, entry) {
|
|
101
|
+
if (!entry) return;
|
|
102
|
+
try {
|
|
103
|
+
const profile = readProfile(infernoDir);
|
|
104
|
+
const wordCount = String(entry).trim().split(/\s+/).length;
|
|
105
|
+
const verbosity = wordCount >= 15 ? "detailed" : "brief";
|
|
106
|
+
|
|
107
|
+
// Use running average: weight new observation against history
|
|
108
|
+
if (profile.changelogVerbosity === "unknown") {
|
|
109
|
+
profile.changelogVerbosity = verbosity;
|
|
110
|
+
} else if (profile.sessionCount >= 5) {
|
|
111
|
+
// After enough sessions, trust the pattern
|
|
112
|
+
profile.changelogVerbosity = verbosity;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
writeProfile(infernoDir, profile);
|
|
116
|
+
} catch {
|
|
117
|
+
// Silent
|
|
118
|
+
}
|
|
119
|
+
}
|