infernoflow 0.32.7 → 0.32.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.
Files changed (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -320
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -1,208 +1,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
+ import*as p from"node:fs";import*as m from"node:path";import{execSync as F}from"node:child_process";function k(i,o={}){const{sinceCommits:d=1,includeStagedOnly:l=!1}=o,r=new Set;try{const f=F("git diff --name-only HEAD",{cwd:i,encoding:"utf8",timeout:1e4});for(const c of f.split(`
2
+ `).map(n=>n.trim()).filter(Boolean))r.add(c)}catch{}try{const f=F(`git diff --name-only HEAD~${d} HEAD`,{cwd:i,encoding:"utf8",timeout:1e4});for(const c of f.split(`
3
+ `).map(n=>n.trim()).filter(Boolean))r.add(c)}catch{}try{const f=F("git ls-files --others --exclude-standard",{cwd:i,encoding:"utf8",timeout:1e4});for(const c of f.split(`
4
+ `).map(n=>n.trim()).filter(Boolean))r.add(c)}catch{}return Array.from(r).sort()}function C(i){const o=m.join(i,"capability-map.json");if(!p.existsSync(o))return null;try{return JSON.parse(p.readFileSync(o,"utf8"))}catch{return null}}function x(i){const o=m.join(i,"adoption_profile.json");if(!p.existsSync(o))return null;try{return JSON.parse(p.readFileSync(o,"utf8"))}catch{return null}}function j(i){const o=m.join(i,"capabilities.json");if(!p.existsSync(o))return[];try{return JSON.parse(p.readFileSync(o,"utf8")).capabilities||[]}catch{return[]}}function A(i,o={}){const d=m.join(i,"inferno"),l=k(i,o);if(!l.length)return{changedFiles:[],affectedCapabilities:[],unmappedFiles:[],hasCapabilityMap:!1};const r=C(d),f=x(d),c=j(d),n=new Map,y=(e,s)=>{n.has(e.id)||n.set(e.id,{id:e.id,title:e.title||e.id,matchedFiles:new Set}),n.get(e.id).matchedFiles.add(s)},h=new Set;if(r){for(const e of l)for(const[s,t]of Object.entries(r))if(e.startsWith(s.replace(/\\/g,"/")))for(const a of t){const u=c.find(b=>b.id===a)||{id:a,title:a};y(u,e),h.add(e)}}const g=[];if(f){const e=m.join(d,"capabilities.json");try{const s=JSON.parse(p.readFileSync(e,"utf8"));for(const t of s.capabilities||[])t.sourceFiles&&t.sourceFiles.length>0&&g.push(t)}catch{}}if(g.length>0)for(const e of g)for(const s of e.sourceFiles||[]){const t=s.replace(/\\/g,"/");for(const a of l){const u=a.replace(/\\/g,"/");(u===t||u.startsWith(m.dirname(t)+"/"))&&(y(e,a),h.add(a))}}const I=[{keywords:["search"],capId:"SearchItems"},{keywords:["filter"],capId:"FilterItems"},{keywords:["auth","login","logout","signin","signup"],capId:"Authentication"},{keywords:["create","add","new"],capId:"CreateItem"},{keywords:["update","edit"],capId:"UpdateItem"},{keywords:["delete","remove"],capId:"DeleteItem"},{keywords:["read","list","view"],capId:"ReadItems"},{keywords:["due","deadline","date"],capId:"SetDueDate"},{keywords:["priority"],capId:"SetPriority"},{keywords:["complete","done","toggle"],capId:"ToggleComplete"}];for(const e of l){if(h.has(e))continue;const s=e.toLowerCase();for(const t of I)if(t.keywords.some(a=>s.includes(a))){const a=c.find(u=>u.id===t.capId)||{id:t.capId,title:t.capId};y(a,e),h.add(e);break}}const S=l.filter(e=>!h.has(e)),w=Array.from(n.values()).map(e=>({id:e.id,title:e.title,matchedFiles:Array.from(e.matchedFiles),confidence:e.matchedFiles.size>=3?"high":e.matchedFiles.size>=1?"medium":"low"}));return{changedFiles:l,affectedCapabilities:w,unmappedFiles:S,hasCapabilityMap:!!r}}export{A as detectDrift,k as getChangedFiles,x as loadAdoptionProfile,j as loadCapabilities,C as loadCapabilityMap};
@@ -1,104 +1,9 @@
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
- */
1
+ import{readProfile as r,summarizeProfile as i}from"./profile.mjs";function o(t){let e;try{e=r(t)}catch{return""}const n=[];if(!(e.namingStyle!=="unknown"||e.preferredVerbs.length>0||e.stack.framework!=="unknown"))return"";if(n.push("## Developer profile (personalise your response to match these preferences)"),e.namingStyle!=="unknown"&&n.push(`- Capability naming style: **${e.namingStyle}** \u2014 use this for all new capability IDs`),e.preferredVerbs.length>0&&n.push(`- Preferred action verbs: ${e.preferredVerbs.slice(0,5).join(", ")} \u2014 prefer these when naming new capabilities`),e.stack.framework!=="unknown"&&n.push(`- Stack: ${e.stack.framework} / ${e.stack.language} (${e.stack.projectType})`),e.changelogVerbosity!=="unknown"){const s=e.changelogVerbosity==="brief"?"Keep changelog entries short (one line, action-focused)":"Write detailed changelog entries (include context and impact)";n.push(`- Changelog style: ${s}`)}if(e.featureClusters.length>0){const s=e.featureClusters[0];s.length>=2&&n.push(`- Common capability cluster: [${s.slice(0,4).join(", ")}] \u2014 if the task touches one of these, consider whether others need updating too`)}return e.sessionCount>=10&&n.push(`- Experienced user (${e.sessionCount} sessions) \u2014 skip basic explanations, be direct`),n.join(`
2
+ `)}function c(t,e){const n=o(e);return n?t.includes("## Instructions")?t.replace("## Instructions",n+`
8
3
 
9
- import { readProfile, summarizeProfile } from "./profile.mjs";
4
+ ## Instructions`):t.includes("Respond with ONLY")?t.replace("Respond with ONLY",n+`
10
5
 
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 ""; }
6
+ ---
7
+ Respond with ONLY`):t+`
21
8
 
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
- }
9
+ `+n:t}function u(t){try{const e=r(t);return i(e)||null}catch{return null}}export{o as buildPersonalisationBlock,c as personalisePrompt,u as profileStatusLine};
@@ -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
- }
1
+ import"node:path";import{readProfile as c,writeProfile as i,recordCommandUse as l,recordSessionCommand as f,detectNamingStyle as d,detectPreferredVerbs as a,recordCapabilityCluster as m}from"./profile.mjs";const g=1800*1e3;function S(n,t,e={}){try{const o=c(n);l(o,t);const r=Date.now(),s=o._lastCommandTs||0;r-s>g&&(o.sessionCount=(o.sessionCount||0)+1),o._lastCommandTs=r,f(o,t,e),i(n,o)}catch{}}function b(n,t){if(!(!t||t.length===0))try{const e=c(n),o=d(t);o!=="unknown"&&(e.sessionCount>=3||e.namingStyle==="unknown")&&(e.namingStyle=o);const r=a(t);if(r.length>0){const s=[...new Set([...e.preferredVerbs,...r])];e.preferredVerbs=s.slice(0,8)}t.length>=2&&m(e,t),i(n,e)}catch{}}function C(n,t){if(t)try{const e=c(n),r=String(t).trim().split(/\s+/).length>=15?"detailed":"brief";(e.changelogVerbosity==="unknown"||e.sessionCount>=5)&&(e.changelogVerbosity=r),i(n,e)}catch{}}export{b as observeCapabilitiesAdded,C as observeChangelogEntry,S as observeCommandStart};