openwolf-enhanced 1.9.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 (127) hide show
  1. package/CHANGELOG.md +386 -0
  2. package/LICENSE +663 -0
  3. package/NOTICE +48 -0
  4. package/README.md +283 -0
  5. package/dist/bin/openwolf.js +10 -0
  6. package/dist/bin/openwolf.js.map +1 -0
  7. package/dist/dashboard/assets/AISuggestions-DHXOgHMM.js +9 -0
  8. package/dist/dashboard/assets/ActivityTimeline-BXLwacZV.js +1 -0
  9. package/dist/dashboard/assets/AnatomyBrowser-CUyC8M2I.js +1 -0
  10. package/dist/dashboard/assets/BugLog-Bxvy1sPW.js +1 -0
  11. package/dist/dashboard/assets/CerebrumViewer-DCySVmzA.js +1 -0
  12. package/dist/dashboard/assets/CronStatus-BA4fmQX3.js +1 -0
  13. package/dist/dashboard/assets/DesignQC-BhNEKryS.js +1 -0
  14. package/dist/dashboard/assets/MemoryViewer-0y9lsDUO.js +1 -0
  15. package/dist/dashboard/assets/ProjectOverview-CC51rVRe.js +1 -0
  16. package/dist/dashboard/assets/StatusBadge-4NJI_F2G.js +1 -0
  17. package/dist/dashboard/assets/TokenUsage-7pCxlnCX.js +55 -0
  18. package/dist/dashboard/assets/index-4O5rk-qB.css +2 -0
  19. package/dist/dashboard/assets/index-jfgWxxsn.js +13 -0
  20. package/dist/dashboard/assets/utils-BqE_aDxV.js +1 -0
  21. package/dist/dashboard/index.html +16 -0
  22. package/dist/hooks/post-read.js +85 -0
  23. package/dist/hooks/post-write.js +520 -0
  24. package/dist/hooks/pre-read.js +107 -0
  25. package/dist/hooks/pre-write.js +120 -0
  26. package/dist/hooks/session-start.js +76 -0
  27. package/dist/hooks/shared.js +0 -0
  28. package/dist/hooks/stop.js +219 -0
  29. package/dist/src/buglog/bug-matcher.js +3 -0
  30. package/dist/src/buglog/bug-matcher.js.map +1 -0
  31. package/dist/src/buglog/bug-tracker.js +89 -0
  32. package/dist/src/buglog/bug-tracker.js.map +1 -0
  33. package/dist/src/cli/bug-cmd.js +31 -0
  34. package/dist/src/cli/bug-cmd.js.map +1 -0
  35. package/dist/src/cli/cron-cmd.js +107 -0
  36. package/dist/src/cli/cron-cmd.js.map +1 -0
  37. package/dist/src/cli/daemon-cmd.js +180 -0
  38. package/dist/src/cli/daemon-cmd.js.map +1 -0
  39. package/dist/src/cli/dashboard.js +88 -0
  40. package/dist/src/cli/dashboard.js.map +1 -0
  41. package/dist/src/cli/designqc-cmd.js +31 -0
  42. package/dist/src/cli/designqc-cmd.js.map +1 -0
  43. package/dist/src/cli/doctor-cmd.js +94 -0
  44. package/dist/src/cli/doctor-cmd.js.map +1 -0
  45. package/dist/src/cli/index.js +158 -0
  46. package/dist/src/cli/index.js.map +1 -0
  47. package/dist/src/cli/init.js +493 -0
  48. package/dist/src/cli/init.js.map +1 -0
  49. package/dist/src/cli/registry.js +93 -0
  50. package/dist/src/cli/registry.js.map +1 -0
  51. package/dist/src/cli/scan.js +39 -0
  52. package/dist/src/cli/scan.js.map +1 -0
  53. package/dist/src/cli/status.js +98 -0
  54. package/dist/src/cli/status.js.map +1 -0
  55. package/dist/src/cli/update.js +435 -0
  56. package/dist/src/cli/update.js.map +1 -0
  57. package/dist/src/daemon/cron-engine.js +321 -0
  58. package/dist/src/daemon/cron-engine.js.map +1 -0
  59. package/dist/src/daemon/file-watcher.js +64 -0
  60. package/dist/src/daemon/file-watcher.js.map +1 -0
  61. package/dist/src/daemon/health.js +23 -0
  62. package/dist/src/daemon/health.js.map +1 -0
  63. package/dist/src/daemon/wolf-daemon.js +440 -0
  64. package/dist/src/daemon/wolf-daemon.js.map +1 -0
  65. package/dist/src/designqc/designqc-capture.js +278 -0
  66. package/dist/src/designqc/designqc-capture.js.map +1 -0
  67. package/dist/src/designqc/designqc-engine.js +159 -0
  68. package/dist/src/designqc/designqc-engine.js.map +1 -0
  69. package/dist/src/designqc/designqc-types.js +5 -0
  70. package/dist/src/designqc/designqc-types.js.map +1 -0
  71. package/dist/src/hooks/post-read.js +86 -0
  72. package/dist/src/hooks/post-read.js.map +1 -0
  73. package/dist/src/hooks/post-write.js +521 -0
  74. package/dist/src/hooks/post-write.js.map +1 -0
  75. package/dist/src/hooks/pre-read.js +108 -0
  76. package/dist/src/hooks/pre-read.js.map +1 -0
  77. package/dist/src/hooks/pre-write.js +121 -0
  78. package/dist/src/hooks/pre-write.js.map +1 -0
  79. package/dist/src/hooks/session-start.js +77 -0
  80. package/dist/src/hooks/session-start.js.map +1 -0
  81. package/dist/src/hooks/shared.js +0 -0
  82. package/dist/src/hooks/shared.js.map +1 -0
  83. package/dist/src/hooks/stop.js +220 -0
  84. package/dist/src/hooks/stop.js.map +1 -0
  85. package/dist/src/scanner/anatomy-scanner.js +278 -0
  86. package/dist/src/scanner/anatomy-scanner.js.map +1 -0
  87. package/dist/src/scanner/description-extractor.js +1007 -0
  88. package/dist/src/scanner/description-extractor.js.map +1 -0
  89. package/dist/src/scanner/project-root.js +52 -0
  90. package/dist/src/scanner/project-root.js.map +1 -0
  91. package/dist/src/tracker/token-estimator.js +20 -0
  92. package/dist/src/tracker/token-estimator.js.map +1 -0
  93. package/dist/src/tracker/token-ledger.js +55 -0
  94. package/dist/src/tracker/token-ledger.js.map +1 -0
  95. package/dist/src/tracker/waste-detector.js +101 -0
  96. package/dist/src/tracker/waste-detector.js.map +1 -0
  97. package/dist/src/utils/dashboard-auth.js +50 -0
  98. package/dist/src/utils/dashboard-auth.js.map +1 -0
  99. package/dist/src/utils/fs-safe.js +135 -0
  100. package/dist/src/utils/fs-safe.js.map +1 -0
  101. package/dist/src/utils/hooks-deploy.js +70 -0
  102. package/dist/src/utils/hooks-deploy.js.map +1 -0
  103. package/dist/src/utils/logger.js +48 -0
  104. package/dist/src/utils/logger.js.map +1 -0
  105. package/dist/src/utils/maintenance.js +385 -0
  106. package/dist/src/utils/maintenance.js.map +1 -0
  107. package/dist/src/utils/paths.js +23 -0
  108. package/dist/src/utils/paths.js.map +1 -0
  109. package/dist/src/utils/platform.js +14 -0
  110. package/dist/src/utils/platform.js.map +1 -0
  111. package/dist/src/utils/seed.js +61 -0
  112. package/dist/src/utils/seed.js.map +1 -0
  113. package/package.json +82 -0
  114. package/src/templates/OPENWOLF.md +147 -0
  115. package/src/templates/STATUS.md +48 -0
  116. package/src/templates/anatomy.md +5 -0
  117. package/src/templates/buglog.json +4 -0
  118. package/src/templates/cerebrum.md +22 -0
  119. package/src/templates/claude-md-snippet.md +5 -0
  120. package/src/templates/claude-rules-openwolf.md +17 -0
  121. package/src/templates/config.json +83 -0
  122. package/src/templates/cron-manifest.json +97 -0
  123. package/src/templates/cron-state.json +7 -0
  124. package/src/templates/identity.md +9 -0
  125. package/src/templates/memory.md +4 -0
  126. package/src/templates/reframe-frameworks.md +597 -0
  127. package/src/templates/token-ledger.json +21 -0
@@ -0,0 +1,85 @@
1
+ import * as path from "node:path";
2
+ import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, estimateTokens, readStdin, normalizePath, loadIgnore, isSecretFile } from "./shared.js";
3
+ async function main() {
4
+ ensureWolfDir();
5
+ const wolfDir = getWolfDir();
6
+ const hooksDir = path.join(wolfDir, "hooks");
7
+ const sessionFile = path.join(hooksDir, "_session.json");
8
+ const raw = await readStdin();
9
+ let input;
10
+ try {
11
+ input = JSON.parse(raw);
12
+ }
13
+ catch {
14
+ process.exit(0);
15
+ return;
16
+ }
17
+ const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
18
+ const content = input.tool_output?.content ?? "";
19
+ if (!filePath) {
20
+ process.exit(0);
21
+ return;
22
+ }
23
+ const normalizedFile = normalizePath(filePath);
24
+ // Skip tracking for .wolf/ internal files — consistent with pre-read
25
+ const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
26
+ const relToProject = normalizedFile.startsWith(projectDir)
27
+ ? normalizedFile.slice(projectDir.length).replace(/^\//, "")
28
+ : "";
29
+ // Don't track reads of files outside the project root (upstream #56). relToProject is ""
30
+ // both when the path is outside projectDir and when it equals the root itself — neither
31
+ // is a trackable project file.
32
+ if (!relToProject) {
33
+ process.exit(0);
34
+ return;
35
+ }
36
+ if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) {
37
+ process.exit(0);
38
+ return;
39
+ }
40
+ // Skip anything matched by .gitignore / .wolfignore — don't track ignored reads.
41
+ if (loadIgnore(projectDir)(relToProject)) {
42
+ process.exit(0);
43
+ return;
44
+ }
45
+ // Never track secret-bearing files in the ledger (#54).
46
+ if (isSecretFile(normalizedFile)) {
47
+ process.exit(0);
48
+ return;
49
+ }
50
+ const ext = path.extname(filePath).toLowerCase();
51
+ const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".css", ".json", ".yaml", ".yml", ".dart"]);
52
+ const proseExts = new Set([".md", ".txt", ".rst"]);
53
+ const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed";
54
+ let tokens = content ? estimateTokens(content, type) : 0;
55
+ // Fallback: if tool_output had no content, use anatomy token estimate
56
+ if (tokens === 0) {
57
+ const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md"));
58
+ const sections = parseAnatomy(anatomyContent);
59
+ for (const [sectionKey, entries] of sections) {
60
+ for (const entry of entries) {
61
+ const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
62
+ if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) {
63
+ tokens = entry.tokens;
64
+ break;
65
+ }
66
+ }
67
+ if (tokens > 0)
68
+ break;
69
+ }
70
+ }
71
+ const session = readJSON(sessionFile, { files_read: {} });
72
+ if (session.files_read[normalizedFile]) {
73
+ session.files_read[normalizedFile].tokens = tokens;
74
+ }
75
+ else {
76
+ session.files_read[normalizedFile] = {
77
+ count: 1,
78
+ tokens,
79
+ first_read: new Date().toISOString(),
80
+ };
81
+ }
82
+ writeJSON(sessionFile, session);
83
+ process.exit(0);
84
+ }
85
+ main().catch(() => process.exit(0));
@@ -0,0 +1,520 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as crypto from "node:crypto";
4
+ import { getWolfDir, ensureWolfDir, readJSON, writeJSON, parseAnatomy, serializeAnatomy, extractDescription, estimateTokens, appendMarkdown, timeShort, readStdin, normalizePath, getRetention, loadIgnore, readBugLog, isSecretFile } from "./shared.js";
5
+ async function main() {
6
+ ensureWolfDir();
7
+ const wolfDir = getWolfDir();
8
+ const hooksDir = path.join(wolfDir, "hooks");
9
+ const sessionFile = path.join(hooksDir, "_session.json");
10
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
11
+ const raw = await readStdin();
12
+ let input;
13
+ try {
14
+ input = JSON.parse(raw);
15
+ }
16
+ catch {
17
+ process.exit(0);
18
+ return;
19
+ }
20
+ const toolName = input.tool_name ?? "Write";
21
+ const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
22
+ if (!filePath) {
23
+ process.exit(0);
24
+ return;
25
+ }
26
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
27
+ // Skip processing for .wolf/ internal files to avoid slow self-referential updates
28
+ const relPath = normalizePath(path.relative(projectRoot, absolutePath));
29
+ if (relPath.startsWith(".wolf/")) {
30
+ process.exit(0);
31
+ return;
32
+ }
33
+ // Never track files outside the project root — an absolute path elsewhere (or ../foo)
34
+ // must not leak into anatomy/memory (upstream #56).
35
+ if (relPath === "" || relPath.startsWith("..")) {
36
+ process.exit(0);
37
+ return;
38
+ }
39
+ // Skip anything matched by .gitignore / .wolfignore — scopes anatomy + session tracking.
40
+ if (loadIgnore(projectRoot)(relPath)) {
41
+ process.exit(0);
42
+ return;
43
+ }
44
+ // Never track secret-bearing files (.env, private keys, certs, keystores…) — upstream #54.
45
+ const baseName = path.basename(absolutePath);
46
+ if (isSecretFile(baseName)) {
47
+ process.exit(0);
48
+ return;
49
+ }
50
+ const oldStr = input.tool_input?.old_string ?? "";
51
+ const newStr = input.tool_input?.new_string ?? "";
52
+ // 1. Update anatomy.md
53
+ try {
54
+ const anatomyPath = path.join(wolfDir, "anatomy.md");
55
+ let anatomyContent;
56
+ try {
57
+ anatomyContent = fs.readFileSync(anatomyPath, "utf-8");
58
+ }
59
+ catch {
60
+ anatomyContent = "# anatomy.md\n\n> Auto-maintained by OpenWolf.\n";
61
+ }
62
+ const sections = parseAnatomy(anatomyContent);
63
+ const relPathLocal = normalizePath(path.relative(projectRoot, absolutePath));
64
+ const dir = path.dirname(relPathLocal);
65
+ const fileName = path.basename(relPathLocal);
66
+ const sectionKey = dir === "." ? "./" : dir + "/";
67
+ let fileContent = "";
68
+ try {
69
+ fileContent = fs.readFileSync(absolutePath, "utf-8");
70
+ }
71
+ catch {
72
+ fileContent = input.tool_input?.content ?? "";
73
+ }
74
+ const desc = extractDescription(absolutePath, fileContent).slice(0, 100);
75
+ const ext = path.extname(absolutePath).toLowerCase();
76
+ const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css", ".dart"]);
77
+ const proseExts = new Set([".md", ".txt", ".rst"]);
78
+ const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed";
79
+ const tokens = estimateTokens(fileContent, type);
80
+ if (!sections.has(sectionKey))
81
+ sections.set(sectionKey, []);
82
+ const entries = sections.get(sectionKey);
83
+ const idx = entries.findIndex((e) => e.file === fileName);
84
+ const existing = idx !== -1 ? entries[idx] : undefined;
85
+ // Skip the full anatomy.md rewrite when nothing changed for this file.
86
+ // Repeated edits to the same file (same description + token estimate) are the common
87
+ // case; rewriting the entire anatomy.md on every one of them is pure overhead.
88
+ if (!existing || existing.description !== desc || existing.tokens !== tokens) {
89
+ if (idx !== -1) {
90
+ entries[idx] = { file: fileName, description: desc, tokens };
91
+ }
92
+ else {
93
+ entries.push({ file: fileName, description: desc, tokens });
94
+ }
95
+ let fileCount = 0;
96
+ for (const [, list] of sections)
97
+ fileCount += list.length;
98
+ const serialized = serializeAnatomy(sections, {
99
+ lastScanned: new Date().toISOString(),
100
+ fileCount,
101
+ hits: 0,
102
+ misses: 0,
103
+ });
104
+ const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
105
+ try {
106
+ fs.writeFileSync(tmp, serialized, "utf-8");
107
+ fs.renameSync(tmp, anatomyPath);
108
+ }
109
+ catch {
110
+ try {
111
+ fs.writeFileSync(anatomyPath, serialized, "utf-8");
112
+ }
113
+ catch { }
114
+ try {
115
+ fs.unlinkSync(tmp);
116
+ }
117
+ catch { }
118
+ }
119
+ }
120
+ }
121
+ catch { }
122
+ // 2. Append richer entry to memory.md
123
+ try {
124
+ const action = toolName === "Write" ? "Created" : toolName === "MultiEdit" ? "Multi-edited" : "Edited";
125
+ const relFile = normalizePath(path.relative(projectRoot, absolutePath));
126
+ const fileContent = input.tool_input?.content ?? "";
127
+ const ext = path.extname(absolutePath).toLowerCase();
128
+ const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css", ".dart"]);
129
+ const type = codeExts.has(ext) ? "code" : "mixed";
130
+ const writeTokens = estimateTokens(fileContent || newStr, type);
131
+ let changeDesc = "";
132
+ if (oldStr && newStr) {
133
+ changeDesc = summarizeEdit(oldStr, newStr, baseName);
134
+ }
135
+ const memoryPath = path.join(wolfDir, "memory.md");
136
+ const outcome = changeDesc || "—";
137
+ appendMarkdown(memoryPath, `| ${timeShort()} | ${action} ${relFile} | ${outcome} | ~${writeTokens} |\n`);
138
+ }
139
+ catch { }
140
+ // 3. Record in session tracker + track edit counts
141
+ try {
142
+ const session = readJSON(sessionFile, { files_written: [], edit_counts: {} });
143
+ if (!session.edit_counts)
144
+ session.edit_counts = {};
145
+ const normalizedFile = normalizePath(filePath);
146
+ const action = toolName === "Write" ? "create" : "edit";
147
+ const fileContent = input.tool_input?.content ?? "";
148
+ const tokens = estimateTokens(fileContent || newStr, "code");
149
+ session.files_written.push({
150
+ file: normalizedFile,
151
+ action,
152
+ tokens,
153
+ at: new Date().toISOString(),
154
+ });
155
+ const editKey = normalizePath(path.relative(projectRoot, absolutePath));
156
+ session.edit_counts[editKey] = (session.edit_counts[editKey] || 0) + 1;
157
+ writeJSON(sessionFile, session);
158
+ if (session.edit_counts[editKey] >= 3) {
159
+ process.stderr.write(`⚠️ OpenWolf: ${baseName} has been edited ${session.edit_counts[editKey]} times this session. If you're fixing a bug, remember to log it to .wolf/buglog.json.\n`);
160
+ }
161
+ }
162
+ catch { }
163
+ // 4. Auto-detect bug-fix patterns and log them
164
+ try {
165
+ if (oldStr && newStr) {
166
+ autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr);
167
+ }
168
+ }
169
+ catch { }
170
+ process.exit(0);
171
+ }
172
+ // ─── Edit Summarizer ─────────────────────────────────────────────
173
+ function summarizeEdit(oldStr, newStr, filename) {
174
+ const oldLines = oldStr.split("\n");
175
+ const newLines = newStr.split("\n");
176
+ const oldCount = oldLines.length;
177
+ const newCount = newLines.length;
178
+ const ext = path.extname(filename).toLowerCase();
179
+ // --- Structural fixes ---
180
+ if (newStr.includes("try") && newStr.includes("catch") && !oldStr.includes("catch")) {
181
+ return "added error handling";
182
+ }
183
+ if (newStr.includes("?.") && !oldStr.includes("?."))
184
+ return "added optional chaining";
185
+ if (newStr.includes("?? ") && !oldStr.includes("?? "))
186
+ return "added nullish coalescing";
187
+ // --- Deleted code ---
188
+ if (!newStr.trim() || newStr.trim().length < oldStr.trim().length * 0.2) {
189
+ return `removed ${oldCount} lines`;
190
+ }
191
+ // --- Import changes ---
192
+ const oldImports = oldLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length;
193
+ const newImports = newLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length;
194
+ if (newImports > oldImports && Math.abs(newCount - oldCount) <= newImports - oldImports + 1) {
195
+ return `added ${newImports - oldImports} import(s)`;
196
+ }
197
+ // --- Value/string replacement (common bug fix: wrong value) ---
198
+ if (oldCount === 1 && newCount === 1) {
199
+ const o = oldStr.trim();
200
+ const n = newStr.trim();
201
+ // String literal change
202
+ const oStr = o.match(/['"`]([^'"`]+)['"`]/);
203
+ const nStr = n.match(/['"`]([^'"`]+)['"`]/);
204
+ if (oStr && nStr && oStr[1] !== nStr[1]) {
205
+ return `"${oStr[1].slice(0, 25)}" → "${nStr[1].slice(0, 25)}"`;
206
+ }
207
+ // Number change
208
+ const oNum = o.match(/\b(\d+\.?\d*)\b/);
209
+ const nNum = n.match(/\b(\d+\.?\d*)\b/);
210
+ if (oNum && nNum && oNum[1] !== nNum[1] && o.replace(oNum[1], "") === n.replace(nNum[1], "")) {
211
+ return `${oNum[1]} → ${nNum[1]}`;
212
+ }
213
+ return "inline fix";
214
+ }
215
+ // --- Method/function call changes ---
216
+ const oldCalls = extractCalls(oldStr);
217
+ const newCalls = extractCalls(newStr);
218
+ const addedCalls = newCalls.filter(c => !oldCalls.includes(c));
219
+ const removedCalls = oldCalls.filter(c => !newCalls.includes(c));
220
+ if (removedCalls.length === 1 && addedCalls.length === 1) {
221
+ return `${removedCalls[0]}() → ${addedCalls[0]}()`;
222
+ }
223
+ // --- CSS/style changes ---
224
+ if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") {
225
+ const oldProps = (oldStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, ""));
226
+ const newProps = (newStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, ""));
227
+ const changed = newProps.filter(p => !oldProps.includes(p));
228
+ if (changed.length > 0 && changed.length <= 3) {
229
+ return `CSS: ${changed.join(", ")}`;
230
+ }
231
+ }
232
+ // --- Condition changes ---
233
+ const oldConds = (oldStr.match(/if\s*\(([^)]+)\)/g) || []);
234
+ const newConds = (newStr.match(/if\s*\(([^)]+)\)/g) || []);
235
+ if (newConds.length > oldConds.length) {
236
+ return `added ${newConds.length - oldConds.length} condition(s)`;
237
+ }
238
+ // --- Function modified ---
239
+ const fnMatch = newStr.match(/(?:function|def|fn|func|async\s+function)\s+(\w+)/);
240
+ if (fnMatch) {
241
+ return `modified ${fnMatch[1]}()`;
242
+ }
243
+ // --- Class/method context ---
244
+ const methodMatch = newStr.match(/(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
245
+ if (methodMatch) {
246
+ return `modified ${methodMatch[1]}()`;
247
+ }
248
+ // --- Size-based fallback ---
249
+ if (newCount > oldCount + 5)
250
+ return `expanded (+${newCount - oldCount} lines)`;
251
+ if (oldCount > newCount + 5)
252
+ return `reduced (-${oldCount - newCount} lines)`;
253
+ return `${oldCount}→${newCount} lines`;
254
+ }
255
+ function extractCalls(code) {
256
+ return [...new Set((code.match(/(\w+)\s*\(/g) || [])
257
+ .map(m => m.match(/(\w+)/)?.[1] || "")
258
+ .filter(n => n.length > 2 && !["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "instanceof", "const", "let", "var"].includes(n)))];
259
+ }
260
+ // ─── Auto Bug Detection ──────────────────────────────────────────
261
+ function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) {
262
+ const bugLogPath = path.join(wolfDir, "buglog.json");
263
+ const bugLog = readBugLog(wolfDir); // tolerates legacy array format
264
+ const relFile = normalizePath(path.relative(projectRoot, absolutePath));
265
+ const basename = path.basename(absolutePath);
266
+ const ext = path.extname(basename).toLowerCase();
267
+ // Detect what kind of fix this is
268
+ const detection = detectFixPattern(oldStr, newStr, ext);
269
+ if (!detection)
270
+ return;
271
+ // Check for recent duplicate (same file + same category within 30 min).
272
+ // Widened from 5 min to cut down on repeated auto-detected entries for the same file.
273
+ const recentDupe = bugLog.bugs.find(b => {
274
+ if (path.basename(b.file) !== basename)
275
+ return false;
276
+ if (!b.tags.includes("auto-detected"))
277
+ return false;
278
+ if (!b.tags.includes(detection.category))
279
+ return false;
280
+ const bugTime = new Date(b.last_seen).getTime();
281
+ return (Date.now() - bugTime) < 30 * 60 * 1000;
282
+ });
283
+ if (recentDupe) {
284
+ recentDupe.occurrences++;
285
+ recentDupe.last_seen = new Date().toISOString();
286
+ // Append additional context
287
+ if (detection.context && !recentDupe.fix.includes(detection.context)) {
288
+ recentDupe.fix += ` | Also: ${detection.context}`;
289
+ }
290
+ writeJSON(bugLogPath, bugLog);
291
+ return;
292
+ }
293
+ // Derive id from the max existing numeric id so it stays unique even after the
294
+ // bugs[] array is trimmed below (bugs.length + 1 would collide after a trim).
295
+ const maxId = bugLog.bugs.reduce((m, b) => {
296
+ const n = parseInt(String(b.id).replace(/\D/g, ""), 10);
297
+ return Number.isFinite(n) && n > m ? n : m;
298
+ }, 0);
299
+ const nextId = `bug-${String(maxId + 1).padStart(3, "0")}`;
300
+ bugLog.bugs.push({
301
+ id: nextId,
302
+ timestamp: new Date().toISOString(),
303
+ error_message: detection.summary,
304
+ file: relFile,
305
+ root_cause: detection.rootCause,
306
+ fix: detection.fix,
307
+ tags: ["auto-detected", detection.category, ext.replace(".", "") || "unknown"],
308
+ related_bugs: [],
309
+ occurrences: 1,
310
+ last_seen: new Date().toISOString(),
311
+ });
312
+ // Keep buglog.json bounded — auto-detection can otherwise append on nearly every edit.
313
+ const maxBugs = getRetention(wolfDir).buglog_max_entries;
314
+ if (bugLog.bugs.length > maxBugs) {
315
+ bugLog.bugs = bugLog.bugs.slice(-maxBugs);
316
+ }
317
+ writeJSON(bugLogPath, bugLog);
318
+ }
319
+ function detectFixPattern(oldStr, newStr, ext) {
320
+ const oldLines = oldStr.split("\n");
321
+ const newLines = newStr.split("\n");
322
+ // --- Error handling added ---
323
+ if (newStr.includes("catch") && !oldStr.includes("catch")) {
324
+ const fn = newStr.match(/(?:function|def|async)\s+(\w+)/)?.[1] || "unknown";
325
+ return {
326
+ category: "error-handling",
327
+ summary: `Missing error handling in ${path.basename(fn)}`,
328
+ rootCause: "Code path had no error handling — exceptions would propagate uncaught",
329
+ fix: `Added try/catch block`,
330
+ context: extractChangedLines(oldStr, newStr),
331
+ };
332
+ }
333
+ // --- Null/undefined safety ---
334
+ if ((newStr.includes("?.") && !oldStr.includes("?.")) ||
335
+ (newStr.includes("?? ") && !oldStr.includes("?? ")) ||
336
+ (/!==?\s*(null|undefined)/.test(newStr) && !/!==?\s*(null|undefined)/.test(oldStr))) {
337
+ return {
338
+ category: "null-safety",
339
+ summary: `Null/undefined access in ${path.basename(path.basename(""))}`,
340
+ rootCause: "Property access on potentially null/undefined value",
341
+ fix: `Added null safety (optional chaining or null check)`,
342
+ context: extractChangedLines(oldStr, newStr),
343
+ };
344
+ }
345
+ // --- Guard clause / early return added ---
346
+ if (/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(newStr) &&
347
+ !/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(oldStr)) {
348
+ const condition = newStr.match(/if\s*\(([^)]+)\)/)?.[1]?.trim().slice(0, 60) || "condition";
349
+ return {
350
+ category: "guard-clause",
351
+ summary: `Missing guard clause`,
352
+ rootCause: `No early return/throw for edge case: ${condition}`,
353
+ fix: `Added guard clause: if (${condition.slice(0, 40)})`,
354
+ };
355
+ }
356
+ // --- Wrong value / string fix (very common bug) ---
357
+ if (oldLines.length <= 3 && newLines.length <= 3) {
358
+ const oldJoined = oldStr.trim();
359
+ const newJoined = newStr.trim();
360
+ // String literal changed
361
+ const oStrs = oldJoined.match(/['"`]([^'"`]{2,})['"`]/g) || [];
362
+ const nStrs = newJoined.match(/['"`]([^'"`]{2,})['"`]/g) || [];
363
+ if (oStrs.length > 0 && nStrs.length > 0) {
364
+ for (let i = 0; i < Math.min(oStrs.length, nStrs.length); i++) {
365
+ if (oStrs[i] !== nStrs[i]) {
366
+ return {
367
+ category: "wrong-value",
368
+ summary: `Incorrect value in code`,
369
+ rootCause: `Had ${oStrs[i].slice(0, 50)}`,
370
+ fix: `Changed to ${nStrs[i].slice(0, 50)}`,
371
+ };
372
+ }
373
+ }
374
+ }
375
+ // Variable name / method call changed
376
+ const oldTokens = tokenizeCode(oldJoined);
377
+ const newTokens = tokenizeCode(newJoined);
378
+ const changed = [];
379
+ for (let i = 0; i < Math.min(oldTokens.length, newTokens.length); i++) {
380
+ if (oldTokens[i] !== newTokens[i]) {
381
+ changed.push([oldTokens[i], newTokens[i]]);
382
+ }
383
+ }
384
+ if (changed.length === 1 && changed[0][0].length > 2) {
385
+ return {
386
+ category: "wrong-reference",
387
+ summary: `Wrong reference: ${changed[0][0]} should be ${changed[0][1]}`,
388
+ rootCause: `Used "${changed[0][0]}" instead of "${changed[0][1]}"`,
389
+ fix: `Changed ${changed[0][0]} → ${changed[0][1]}`,
390
+ };
391
+ }
392
+ }
393
+ // --- Logic fix (condition changed) ---
394
+ const oldCond = oldStr.match(/if\s*\(([^)]+)\)/)?.[1];
395
+ const newCond = newStr.match(/if\s*\(([^)]+)\)/)?.[1];
396
+ if (oldCond && newCond && oldCond !== newCond && oldLines.length <= 5) {
397
+ return {
398
+ category: "logic-fix",
399
+ summary: `Wrong condition in logic`,
400
+ rootCause: `Condition was: if (${oldCond.slice(0, 50)})`,
401
+ fix: `Changed to: if (${newCond.slice(0, 50)})`,
402
+ };
403
+ }
404
+ // --- Operator fix (=== vs ==, > vs >=, etc.) ---
405
+ const opChange = findOperatorChange(oldStr, newStr);
406
+ if (opChange) {
407
+ return {
408
+ category: "operator-fix",
409
+ summary: `Wrong operator: ${opChange.old} should be ${opChange.new}`,
410
+ rootCause: `Used "${opChange.old}" instead of "${opChange.new}"`,
411
+ fix: `Changed operator ${opChange.old} → ${opChange.new}`,
412
+ };
413
+ }
414
+ // --- Missing import/require ---
415
+ const oldImports = new Set((oldStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []).map(m => m));
416
+ const newImports = (newStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []);
417
+ const addedImports = newImports.filter(i => !oldImports.has(i));
418
+ if (addedImports.length > 0 && newLines.length - oldLines.length <= addedImports.length + 2) {
419
+ const modules = addedImports.map(i => i.match(/['"]([^'"]+)['"]/)?.[1] || "").filter(Boolean);
420
+ return {
421
+ category: "missing-import",
422
+ summary: `Missing import: ${modules.join(", ")}`,
423
+ rootCause: `Module(s) not imported: ${modules.join(", ")}`,
424
+ fix: `Added import(s) for ${modules.join(", ")}`,
425
+ };
426
+ }
427
+ // --- Return value fix ---
428
+ const oldReturn = oldStr.match(/return\s+(.+)/)?.[1]?.trim();
429
+ const newReturn = newStr.match(/return\s+(.+)/)?.[1]?.trim();
430
+ if (oldReturn && newReturn && oldReturn !== newReturn && oldLines.length <= 5) {
431
+ return {
432
+ category: "return-value",
433
+ summary: `Wrong return value`,
434
+ rootCause: `Was returning: ${oldReturn.slice(0, 50)}`,
435
+ fix: `Now returns: ${newReturn.slice(0, 50)}`,
436
+ };
437
+ }
438
+ // --- Async/await fix ---
439
+ if (newStr.includes("await ") && !oldStr.includes("await ")) {
440
+ return {
441
+ category: "async-fix",
442
+ summary: `Missing await`,
443
+ rootCause: `Async call without await — returned Promise instead of value`,
444
+ fix: `Added await to async call`,
445
+ context: extractChangedLines(oldStr, newStr),
446
+ };
447
+ }
448
+ if (newStr.includes("async ") && !oldStr.includes("async ")) {
449
+ return {
450
+ category: "async-fix",
451
+ summary: `Function not marked async`,
452
+ rootCause: `Function uses await but wasn't declared async`,
453
+ fix: `Added async modifier`,
454
+ };
455
+ }
456
+ // --- Type annotation/cast fix ---
457
+ if (ext === ".ts" || ext === ".tsx") {
458
+ if ((newStr.includes(" as ") && !oldStr.includes(" as ")) ||
459
+ (newStr.includes(": ") && !oldStr.includes(": ") && oldLines.length <= 3)) {
460
+ return {
461
+ category: "type-fix",
462
+ summary: `Type error`,
463
+ rootCause: `Missing or incorrect type annotation`,
464
+ fix: `Added type assertion/annotation`,
465
+ context: extractChangedLines(oldStr, newStr),
466
+ };
467
+ }
468
+ }
469
+ // --- CSS/style fix ---
470
+ if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") {
471
+ const oldProps = extractCSSProps(oldStr);
472
+ const newProps = extractCSSProps(newStr);
473
+ const changedProps = [...newProps.entries()].filter(([k, v]) => oldProps.get(k) !== v && oldProps.has(k));
474
+ if (changedProps.length > 0 && changedProps.length <= 3) {
475
+ const desc = changedProps.map(([k, v]) => `${k}: ${oldProps.get(k)} → ${v}`).join("; ");
476
+ return {
477
+ category: "style-fix",
478
+ summary: `CSS fix: ${changedProps.map(([k]) => k).join(", ")}`,
479
+ rootCause: desc,
480
+ fix: `Changed ${desc}`,
481
+ };
482
+ }
483
+ }
484
+ // NOTE: the old "significant diff" catch-all (category: "refactor") was removed —
485
+ // it fired on nearly every legitimate multi-line edit (diffRatio > 0.3 with >=2 removed
486
+ // lines), flooding buglog.json with non-bugs. Auto-detection now only reports edits that
487
+ // match a specific fix pattern above.
488
+ return null;
489
+ }
490
+ function extractChangedLines(oldStr, newStr) {
491
+ const oldLines = new Set(oldStr.split("\n").map(l => l.trim()).filter(Boolean));
492
+ const newLines = newStr.split("\n").map(l => l.trim()).filter(Boolean);
493
+ const added = newLines.filter(l => !oldLines.has(l));
494
+ return added.slice(0, 2).map(l => l.slice(0, 60)).join("; ");
495
+ }
496
+ function tokenizeCode(code) {
497
+ return code.replace(/[^\w$]/g, " ").split(/\s+/).filter(t => t.length > 0);
498
+ }
499
+ function findOperatorChange(oldStr, newStr) {
500
+ const operators = ["===", "!==", "==", "!=", ">=", "<=", ">>", "<<", "&&", "||", "??"];
501
+ for (const op of operators) {
502
+ if (oldStr.includes(op) && !newStr.includes(op)) {
503
+ for (const op2 of operators) {
504
+ if (op2 !== op && newStr.includes(op2) && !oldStr.includes(op2)) {
505
+ return { old: op, new: op2 };
506
+ }
507
+ }
508
+ }
509
+ }
510
+ return null;
511
+ }
512
+ function extractCSSProps(code) {
513
+ const props = new Map();
514
+ const matches = code.matchAll(/([\w-]+)\s*:\s*([^;}\n]+)/g);
515
+ for (const m of matches) {
516
+ props.set(m[1].trim(), m[2].trim());
517
+ }
518
+ return props;
519
+ }
520
+ main().catch(() => process.exit(0));