infernoflow 0.37.0 → 0.37.3

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 (88) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -517
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -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};