infernoflow 0.32.8 → 0.33.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.
Files changed (81) 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 +34 -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 -321
  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/log.mjs +16 -0
  37. package/dist/lib/commands/monorepo.mjs +4 -428
  38. package/dist/lib/commands/notify.mjs +4 -258
  39. package/dist/lib/commands/onboard.mjs +4 -296
  40. package/dist/lib/commands/prComment.mjs +2 -361
  41. package/dist/lib/commands/prImpact.mjs +2 -157
  42. package/dist/lib/commands/publish.mjs +15 -316
  43. package/dist/lib/commands/report.mjs +28 -272
  44. package/dist/lib/commands/review.mjs +9 -223
  45. package/dist/lib/commands/run.mjs +8 -336
  46. package/dist/lib/commands/scaffold.mjs +54 -419
  47. package/dist/lib/commands/scan.mjs +5 -558
  48. package/dist/lib/commands/scout.mjs +2 -291
  49. package/dist/lib/commands/setup.mjs +5 -310
  50. package/dist/lib/commands/share.mjs +13 -196
  51. package/dist/lib/commands/snapshot.mjs +3 -383
  52. package/dist/lib/commands/stability.mjs +2 -293
  53. package/dist/lib/commands/status.mjs +4 -172
  54. package/dist/lib/commands/suggest.mjs +21 -563
  55. package/dist/lib/commands/syncAuto.mjs +1 -96
  56. package/dist/lib/commands/synthesize.mjs +10 -228
  57. package/dist/lib/commands/teamSync.mjs +2 -388
  58. package/dist/lib/commands/test.mjs +6 -363
  59. package/dist/lib/commands/theme.mjs +18 -0
  60. package/dist/lib/commands/version.mjs +2 -282
  61. package/dist/lib/commands/vibe.mjs +7 -357
  62. package/dist/lib/commands/watch.mjs +4 -203
  63. package/dist/lib/commands/why.mjs +4 -358
  64. package/dist/lib/cursorHooksInstall.mjs +1 -60
  65. package/dist/lib/draftToolingInstall.mjs +7 -68
  66. package/dist/lib/git/detect-drift.mjs +4 -208
  67. package/dist/lib/learning/adapt.mjs +6 -101
  68. package/dist/lib/learning/observe.mjs +1 -119
  69. package/dist/lib/learning/patternDetector.mjs +1 -298
  70. package/dist/lib/learning/profile.mjs +2 -279
  71. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  72. package/dist/lib/templates/index.mjs +1 -131
  73. package/dist/lib/theme/scanner.mjs +4 -0
  74. package/dist/lib/ui/errors.mjs +1 -142
  75. package/dist/lib/ui/output.mjs +6 -72
  76. package/dist/lib/ui/prompts.mjs +6 -147
  77. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  78. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  79. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  80. package/dist/templates/github-app/app-manifest.json +20 -0
  81. package/package.json +1 -1
@@ -1,69 +1,8 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
-
4
- const GITIGNORE_SNIPPET = `
5
- # infernoflow: agent draft (IDE hooks — review before commit)
1
+ import*as t from"node:fs";import*as n from"node:path";const d=`
2
+ # infernoflow: agent draft (IDE hooks \u2014 review before commit)
6
3
  inferno/CONTEXT.draft.md
7
- `.trimStart();
8
-
9
- function upsertPromoteScript(cwd, silent, logOk) {
10
- const pkgPath = path.join(cwd, "package.json");
11
- if (!fs.existsSync(pkgPath)) {
12
- if (!silent) logOk("No package.json — add script manually: inferno:promote-draft");
13
- return;
14
- }
15
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
16
- pkg.scripts = pkg.scripts || {};
17
- if (!pkg.scripts["inferno:promote-draft"]) {
18
- pkg.scripts["inferno:promote-draft"] = "node scripts/inferno-promote-draft.mjs";
19
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
20
- if (!silent) logOk("Updated package.json script: inferno:promote-draft");
21
- }
22
- }
23
-
24
- /**
25
- * inferno/CONTEXT.draft.md gitignore + promote script (shared by Cursor and VS Code installers).
26
- * @param {object} opts
27
- * @param {string} opts.cwd
28
- * @param {string} opts.templatesRoot
29
- * @param {boolean} opts.force
30
- * @param {boolean} opts.silent
31
- * @param {(msg: string) => void} [opts.logOk]
32
- * @param {(msg: string) => void} [opts.logWarn]
33
- */
34
- export function installInfernoDraftTooling(opts) {
35
- const { cwd, templatesRoot, force, silent } = opts;
36
- const logOk = opts.logOk || (() => {});
37
- const logWarn = opts.logWarn || (() => {});
38
-
39
- function copyFile(src, dst) {
40
- if (fs.existsSync(dst) && !force) {
41
- if (!silent) logWarn("Skipped (exists): " + path.relative(cwd, dst));
42
- return false;
43
- }
44
- fs.mkdirSync(path.dirname(dst), { recursive: true });
45
- fs.copyFileSync(src, dst);
46
- if (!silent) logOk("Created: " + path.relative(cwd, dst));
47
- return true;
48
- }
49
-
50
- const srcPromote = path.join(templatesRoot, "scripts", "inferno-promote-draft.mjs");
51
- const dstPromote = path.join(cwd, "scripts", "inferno-promote-draft.mjs");
52
- copyFile(srcPromote, dstPromote);
53
-
54
- upsertPromoteScript(cwd, silent, logOk);
55
-
56
- const gi = path.join(cwd, ".gitignore");
57
- if (fs.existsSync(gi)) {
58
- const cur = fs.readFileSync(gi, "utf8");
59
- if (cur.includes("CONTEXT.draft.md")) {
60
- if (!silent) logOk(".gitignore already mentions CONTEXT.draft.md");
61
- } else {
62
- fs.appendFileSync(gi, `\n${GITIGNORE_SNIPPET}\n`, "utf8");
63
- if (!silent) logOk("Updated: " + path.relative(cwd, gi));
64
- }
65
- } else {
66
- fs.writeFileSync(gi, `${GITIGNORE_SNIPPET}\n`, "utf8");
67
- if (!silent) logOk("Created: " + path.relative(cwd, gi));
68
- }
69
- }
4
+ `.trimStart();function S(o,e,c){const s=n.join(o,"package.json");if(!t.existsSync(s)){e||c("No package.json \u2014 add script manually: inferno:promote-draft");return}const r=JSON.parse(t.readFileSync(s,"utf8"));r.scripts=r.scripts||{},r.scripts["inferno:promote-draft"]||(r.scripts["inferno:promote-draft"]="node scripts/inferno-promote-draft.mjs",t.writeFileSync(s,JSON.stringify(r,null,2)+`
5
+ `,"utf8"),e||c("Updated package.json script: inferno:promote-draft"))}function y(o){const{cwd:e,templatesRoot:c,force:s,silent:r}=o,f=o.logOk||(()=>{}),l=o.logWarn||(()=>{});function m(p,a){return t.existsSync(a)&&!s?(r||l("Skipped (exists): "+n.relative(e,a)),!1):(t.mkdirSync(n.dirname(a),{recursive:!0}),t.copyFileSync(p,a),r||f("Created: "+n.relative(e,a)),!0)}const u=n.join(c,"scripts","inferno-promote-draft.mjs"),g=n.join(e,"scripts","inferno-promote-draft.mjs");m(u,g),S(e,r,f);const i=n.join(e,".gitignore");t.existsSync(i)?t.readFileSync(i,"utf8").includes("CONTEXT.draft.md")?r||f(".gitignore already mentions CONTEXT.draft.md"):(t.appendFileSync(i,`
6
+ ${d}
7
+ `,"utf8"),r||f("Updated: "+n.relative(e,i))):(t.writeFileSync(i,`${d}
8
+ `,"utf8"),r||f("Created: "+n.relative(e,i)))}export{y as installInfernoDraftTooling};
@@ -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};