openwolf 1.0.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 (112) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +232 -0
  3. package/dist/bin/openwolf.js +10 -0
  4. package/dist/bin/openwolf.js.map +1 -0
  5. package/dist/dashboard/assets/AISuggestions-DzE-DQkR.js +1 -0
  6. package/dist/dashboard/assets/ActivityTimeline-DGVjujnt.js +1 -0
  7. package/dist/dashboard/assets/AnatomyBrowser-S-2rmYtw.js +1 -0
  8. package/dist/dashboard/assets/BugLog-CG2zDHJc.js +1 -0
  9. package/dist/dashboard/assets/CerebrumViewer-Dlgoy69U.js +1 -0
  10. package/dist/dashboard/assets/CronStatus-DxUF1iW_.js +1 -0
  11. package/dist/dashboard/assets/DesignQC-BGXn_aq8.js +1 -0
  12. package/dist/dashboard/assets/MemoryViewer-CGqkTyvQ.js +1 -0
  13. package/dist/dashboard/assets/ProjectOverview-DlFhu69i.js +1 -0
  14. package/dist/dashboard/assets/TokenUsage-DDsQiVIq.js +68 -0
  15. package/dist/dashboard/assets/index-CzK9GUjV.css +1 -0
  16. package/dist/dashboard/assets/index-PYeNGjkN.js +52 -0
  17. package/dist/dashboard/index.html +16 -0
  18. package/dist/hooks/post-read.js +68 -0
  19. package/dist/hooks/post-write.js +502 -0
  20. package/dist/hooks/pre-read.js +79 -0
  21. package/dist/hooks/pre-write.js +120 -0
  22. package/dist/hooks/session-start.js +76 -0
  23. package/dist/hooks/shared.js +613 -0
  24. package/dist/hooks/stop.js +146 -0
  25. package/dist/src/buglog/bug-matcher.js +3 -0
  26. package/dist/src/buglog/bug-matcher.js.map +1 -0
  27. package/dist/src/buglog/bug-tracker.js +81 -0
  28. package/dist/src/buglog/bug-tracker.js.map +1 -0
  29. package/dist/src/cli/bug-cmd.js +28 -0
  30. package/dist/src/cli/bug-cmd.js.map +1 -0
  31. package/dist/src/cli/cron-cmd.js +106 -0
  32. package/dist/src/cli/cron-cmd.js.map +1 -0
  33. package/dist/src/cli/daemon-cmd.js +177 -0
  34. package/dist/src/cli/daemon-cmd.js.map +1 -0
  35. package/dist/src/cli/dashboard.js +84 -0
  36. package/dist/src/cli/dashboard.js.map +1 -0
  37. package/dist/src/cli/designqc-cmd.js +31 -0
  38. package/dist/src/cli/designqc-cmd.js.map +1 -0
  39. package/dist/src/cli/index.js +149 -0
  40. package/dist/src/cli/index.js.map +1 -0
  41. package/dist/src/cli/init.js +506 -0
  42. package/dist/src/cli/init.js.map +1 -0
  43. package/dist/src/cli/registry.js +93 -0
  44. package/dist/src/cli/registry.js.map +1 -0
  45. package/dist/src/cli/scan.js +39 -0
  46. package/dist/src/cli/scan.js.map +1 -0
  47. package/dist/src/cli/status.js +85 -0
  48. package/dist/src/cli/status.js.map +1 -0
  49. package/dist/src/cli/update.js +414 -0
  50. package/dist/src/cli/update.js.map +1 -0
  51. package/dist/src/daemon/cron-engine.js +300 -0
  52. package/dist/src/daemon/cron-engine.js.map +1 -0
  53. package/dist/src/daemon/file-watcher.js +53 -0
  54. package/dist/src/daemon/file-watcher.js.map +1 -0
  55. package/dist/src/daemon/health.js +23 -0
  56. package/dist/src/daemon/health.js.map +1 -0
  57. package/dist/src/daemon/wolf-daemon.js +294 -0
  58. package/dist/src/daemon/wolf-daemon.js.map +1 -0
  59. package/dist/src/designqc/designqc-capture.js +235 -0
  60. package/dist/src/designqc/designqc-capture.js.map +1 -0
  61. package/dist/src/designqc/designqc-engine.js +141 -0
  62. package/dist/src/designqc/designqc-engine.js.map +1 -0
  63. package/dist/src/designqc/designqc-types.js +5 -0
  64. package/dist/src/designqc/designqc-types.js.map +1 -0
  65. package/dist/src/hooks/post-read.js +69 -0
  66. package/dist/src/hooks/post-read.js.map +1 -0
  67. package/dist/src/hooks/post-write.js +503 -0
  68. package/dist/src/hooks/post-write.js.map +1 -0
  69. package/dist/src/hooks/pre-read.js +80 -0
  70. package/dist/src/hooks/pre-read.js.map +1 -0
  71. package/dist/src/hooks/pre-write.js +121 -0
  72. package/dist/src/hooks/pre-write.js.map +1 -0
  73. package/dist/src/hooks/session-start.js +77 -0
  74. package/dist/src/hooks/session-start.js.map +1 -0
  75. package/dist/src/hooks/shared.js +614 -0
  76. package/dist/src/hooks/shared.js.map +1 -0
  77. package/dist/src/hooks/stop.js +147 -0
  78. package/dist/src/hooks/stop.js.map +1 -0
  79. package/dist/src/scanner/anatomy-scanner.js +260 -0
  80. package/dist/src/scanner/anatomy-scanner.js.map +1 -0
  81. package/dist/src/scanner/description-extractor.js +1007 -0
  82. package/dist/src/scanner/description-extractor.js.map +1 -0
  83. package/dist/src/scanner/project-root.js +42 -0
  84. package/dist/src/scanner/project-root.js.map +1 -0
  85. package/dist/src/tracker/token-estimator.js +20 -0
  86. package/dist/src/tracker/token-estimator.js.map +1 -0
  87. package/dist/src/tracker/token-ledger.js +45 -0
  88. package/dist/src/tracker/token-ledger.js.map +1 -0
  89. package/dist/src/tracker/waste-detector.js +101 -0
  90. package/dist/src/tracker/waste-detector.js.map +1 -0
  91. package/dist/src/utils/fs-safe.js +74 -0
  92. package/dist/src/utils/fs-safe.js.map +1 -0
  93. package/dist/src/utils/logger.js +48 -0
  94. package/dist/src/utils/logger.js.map +1 -0
  95. package/dist/src/utils/paths.js +23 -0
  96. package/dist/src/utils/paths.js.map +1 -0
  97. package/dist/src/utils/platform.js +14 -0
  98. package/dist/src/utils/platform.js.map +1 -0
  99. package/package.json +77 -0
  100. package/src/templates/OPENWOLF.md +135 -0
  101. package/src/templates/anatomy.md +5 -0
  102. package/src/templates/buglog.json +4 -0
  103. package/src/templates/cerebrum.md +22 -0
  104. package/src/templates/claude-md-snippet.md +5 -0
  105. package/src/templates/claude-rules-openwolf.md +15 -0
  106. package/src/templates/config.json +73 -0
  107. package/src/templates/cron-manifest.json +97 -0
  108. package/src/templates/cron-state.json +7 -0
  109. package/src/templates/identity.md +9 -0
  110. package/src/templates/memory.md +4 -0
  111. package/src/templates/reframe-frameworks.md +597 -0
  112. package/src/templates/token-ledger.json +21 -0
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>OpenWolf Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
+ <script type="module" crossorigin src="./assets/index-PYeNGjkN.js"></script>
11
+ <link rel="stylesheet" crossorigin href="./assets/index-CzK9GUjV.css">
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,68 @@
1
+ import * as path from "node:path";
2
+ import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, estimateTokens, readStdin, normalizePath } 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
+ if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) {
30
+ process.exit(0);
31
+ return;
32
+ }
33
+ const ext = path.extname(filePath).toLowerCase();
34
+ const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".css", ".json", ".yaml", ".yml"]);
35
+ const proseExts = new Set([".md", ".txt", ".rst"]);
36
+ const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed";
37
+ let tokens = content ? estimateTokens(content, type) : 0;
38
+ // Fallback: if tool_output had no content, use anatomy token estimate
39
+ if (tokens === 0) {
40
+ const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md"));
41
+ const sections = parseAnatomy(anatomyContent);
42
+ for (const [sectionKey, entries] of sections) {
43
+ for (const entry of entries) {
44
+ const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
45
+ if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) {
46
+ tokens = entry.tokens;
47
+ break;
48
+ }
49
+ }
50
+ if (tokens > 0)
51
+ break;
52
+ }
53
+ }
54
+ const session = readJSON(sessionFile, { files_read: {} });
55
+ if (session.files_read[normalizedFile]) {
56
+ session.files_read[normalizedFile].tokens = tokens;
57
+ }
58
+ else {
59
+ session.files_read[normalizedFile] = {
60
+ count: 1,
61
+ tokens,
62
+ first_read: new Date().toISOString(),
63
+ };
64
+ }
65
+ writeJSON(sessionFile, session);
66
+ process.exit(0);
67
+ }
68
+ main().catch(() => process.exit(0));
@@ -0,0 +1,502 @@
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 } 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 .env files in anatomy — they contain secrets
34
+ const baseName = path.basename(absolutePath);
35
+ if (baseName === ".env" || baseName.startsWith(".env.")) {
36
+ process.exit(0);
37
+ return;
38
+ }
39
+ const oldStr = input.tool_input?.old_string ?? "";
40
+ const newStr = input.tool_input?.new_string ?? "";
41
+ // 1. Update anatomy.md
42
+ try {
43
+ const anatomyPath = path.join(wolfDir, "anatomy.md");
44
+ let anatomyContent;
45
+ try {
46
+ anatomyContent = fs.readFileSync(anatomyPath, "utf-8");
47
+ }
48
+ catch {
49
+ anatomyContent = "# anatomy.md\n\n> Auto-maintained by OpenWolf.\n";
50
+ }
51
+ const sections = parseAnatomy(anatomyContent);
52
+ const relPathLocal = normalizePath(path.relative(projectRoot, absolutePath));
53
+ const dir = path.dirname(relPathLocal);
54
+ const fileName = path.basename(relPathLocal);
55
+ const sectionKey = dir === "." ? "./" : dir + "/";
56
+ let fileContent = "";
57
+ try {
58
+ fileContent = fs.readFileSync(absolutePath, "utf-8");
59
+ }
60
+ catch {
61
+ fileContent = input.tool_input?.content ?? "";
62
+ }
63
+ const desc = extractDescription(absolutePath).slice(0, 100);
64
+ const ext = path.extname(absolutePath).toLowerCase();
65
+ const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css"]);
66
+ const proseExts = new Set([".md", ".txt", ".rst"]);
67
+ const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed";
68
+ const tokens = estimateTokens(fileContent, type);
69
+ if (!sections.has(sectionKey))
70
+ sections.set(sectionKey, []);
71
+ const entries = sections.get(sectionKey);
72
+ const idx = entries.findIndex((e) => e.file === fileName);
73
+ if (idx !== -1) {
74
+ entries[idx] = { file: fileName, description: desc, tokens };
75
+ }
76
+ else {
77
+ entries.push({ file: fileName, description: desc, tokens });
78
+ }
79
+ let fileCount = 0;
80
+ for (const [, list] of sections)
81
+ fileCount += list.length;
82
+ const serialized = serializeAnatomy(sections, {
83
+ lastScanned: new Date().toISOString(),
84
+ fileCount,
85
+ hits: 0,
86
+ misses: 0,
87
+ });
88
+ const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
89
+ try {
90
+ fs.writeFileSync(tmp, serialized, "utf-8");
91
+ fs.renameSync(tmp, anatomyPath);
92
+ }
93
+ catch {
94
+ try {
95
+ fs.writeFileSync(anatomyPath, serialized, "utf-8");
96
+ }
97
+ catch { }
98
+ try {
99
+ fs.unlinkSync(tmp);
100
+ }
101
+ catch { }
102
+ }
103
+ }
104
+ catch { }
105
+ // 2. Append richer entry to memory.md
106
+ try {
107
+ const action = toolName === "Write" ? "Created" : toolName === "MultiEdit" ? "Multi-edited" : "Edited";
108
+ const relFile = normalizePath(path.relative(projectRoot, absolutePath));
109
+ const fileContent = input.tool_input?.content ?? "";
110
+ const ext = path.extname(absolutePath).toLowerCase();
111
+ const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css"]);
112
+ const type = codeExts.has(ext) ? "code" : "mixed";
113
+ const writeTokens = estimateTokens(fileContent || newStr, type);
114
+ let changeDesc = "";
115
+ if (oldStr && newStr) {
116
+ changeDesc = summarizeEdit(oldStr, newStr, baseName);
117
+ }
118
+ const memoryPath = path.join(wolfDir, "memory.md");
119
+ const outcome = changeDesc || "—";
120
+ appendMarkdown(memoryPath, `| ${timeShort()} | ${action} ${relFile} | ${outcome} | ~${writeTokens} |\n`);
121
+ }
122
+ catch { }
123
+ // 3. Record in session tracker + track edit counts
124
+ try {
125
+ const session = readJSON(sessionFile, { files_written: [], edit_counts: {} });
126
+ if (!session.edit_counts)
127
+ session.edit_counts = {};
128
+ const normalizedFile = normalizePath(filePath);
129
+ const action = toolName === "Write" ? "create" : "edit";
130
+ const fileContent = input.tool_input?.content ?? "";
131
+ const tokens = estimateTokens(fileContent || newStr, "code");
132
+ session.files_written.push({
133
+ file: normalizedFile,
134
+ action,
135
+ tokens,
136
+ at: new Date().toISOString(),
137
+ });
138
+ const editKey = normalizePath(path.relative(projectRoot, absolutePath));
139
+ session.edit_counts[editKey] = (session.edit_counts[editKey] || 0) + 1;
140
+ writeJSON(sessionFile, session);
141
+ if (session.edit_counts[editKey] >= 3) {
142
+ 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`);
143
+ }
144
+ }
145
+ catch { }
146
+ // 4. Auto-detect bug-fix patterns and log them
147
+ try {
148
+ if (oldStr && newStr) {
149
+ autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr);
150
+ }
151
+ }
152
+ catch { }
153
+ process.exit(0);
154
+ }
155
+ // ─── Edit Summarizer ─────────────────────────────────────────────
156
+ function summarizeEdit(oldStr, newStr, filename) {
157
+ const oldLines = oldStr.split("\n");
158
+ const newLines = newStr.split("\n");
159
+ const oldCount = oldLines.length;
160
+ const newCount = newLines.length;
161
+ const ext = path.extname(filename).toLowerCase();
162
+ // --- Structural fixes ---
163
+ if (newStr.includes("try") && newStr.includes("catch") && !oldStr.includes("catch")) {
164
+ return "added error handling";
165
+ }
166
+ if (newStr.includes("?.") && !oldStr.includes("?."))
167
+ return "added optional chaining";
168
+ if (newStr.includes("?? ") && !oldStr.includes("?? "))
169
+ return "added nullish coalescing";
170
+ // --- Deleted code ---
171
+ if (!newStr.trim() || newStr.trim().length < oldStr.trim().length * 0.2) {
172
+ return `removed ${oldCount} lines`;
173
+ }
174
+ // --- Import changes ---
175
+ const oldImports = oldLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length;
176
+ const newImports = newLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length;
177
+ if (newImports > oldImports && Math.abs(newCount - oldCount) <= newImports - oldImports + 1) {
178
+ return `added ${newImports - oldImports} import(s)`;
179
+ }
180
+ // --- Value/string replacement (common bug fix: wrong value) ---
181
+ if (oldCount === 1 && newCount === 1) {
182
+ const o = oldStr.trim();
183
+ const n = newStr.trim();
184
+ // String literal change
185
+ const oStr = o.match(/['"`]([^'"`]+)['"`]/);
186
+ const nStr = n.match(/['"`]([^'"`]+)['"`]/);
187
+ if (oStr && nStr && oStr[1] !== nStr[1]) {
188
+ return `"${oStr[1].slice(0, 25)}" → "${nStr[1].slice(0, 25)}"`;
189
+ }
190
+ // Number change
191
+ const oNum = o.match(/\b(\d+\.?\d*)\b/);
192
+ const nNum = n.match(/\b(\d+\.?\d*)\b/);
193
+ if (oNum && nNum && oNum[1] !== nNum[1] && o.replace(oNum[1], "") === n.replace(nNum[1], "")) {
194
+ return `${oNum[1]} → ${nNum[1]}`;
195
+ }
196
+ return "inline fix";
197
+ }
198
+ // --- Method/function call changes ---
199
+ const oldCalls = extractCalls(oldStr);
200
+ const newCalls = extractCalls(newStr);
201
+ const addedCalls = newCalls.filter(c => !oldCalls.includes(c));
202
+ const removedCalls = oldCalls.filter(c => !newCalls.includes(c));
203
+ if (removedCalls.length === 1 && addedCalls.length === 1) {
204
+ return `${removedCalls[0]}() → ${addedCalls[0]}()`;
205
+ }
206
+ // --- CSS/style changes ---
207
+ if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") {
208
+ const oldProps = (oldStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, ""));
209
+ const newProps = (newStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, ""));
210
+ const changed = newProps.filter(p => !oldProps.includes(p));
211
+ if (changed.length > 0 && changed.length <= 3) {
212
+ return `CSS: ${changed.join(", ")}`;
213
+ }
214
+ }
215
+ // --- Condition changes ---
216
+ const oldConds = (oldStr.match(/if\s*\(([^)]+)\)/g) || []);
217
+ const newConds = (newStr.match(/if\s*\(([^)]+)\)/g) || []);
218
+ if (newConds.length > oldConds.length) {
219
+ return `added ${newConds.length - oldConds.length} condition(s)`;
220
+ }
221
+ // --- Function modified ---
222
+ const fnMatch = newStr.match(/(?:function|def|fn|func|async\s+function)\s+(\w+)/);
223
+ if (fnMatch) {
224
+ return `modified ${fnMatch[1]}()`;
225
+ }
226
+ // --- Class/method context ---
227
+ const methodMatch = newStr.match(/(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
228
+ if (methodMatch) {
229
+ return `modified ${methodMatch[1]}()`;
230
+ }
231
+ // --- Size-based fallback ---
232
+ if (newCount > oldCount + 5)
233
+ return `expanded (+${newCount - oldCount} lines)`;
234
+ if (oldCount > newCount + 5)
235
+ return `reduced (-${oldCount - newCount} lines)`;
236
+ return `${oldCount}→${newCount} lines`;
237
+ }
238
+ function extractCalls(code) {
239
+ return [...new Set((code.match(/(\w+)\s*\(/g) || [])
240
+ .map(m => m.match(/(\w+)/)?.[1] || "")
241
+ .filter(n => n.length > 2 && !["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "instanceof", "const", "let", "var"].includes(n)))];
242
+ }
243
+ // ─── Auto Bug Detection ──────────────────────────────────────────
244
+ function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) {
245
+ const bugLogPath = path.join(wolfDir, "buglog.json");
246
+ const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] });
247
+ const relFile = normalizePath(path.relative(projectRoot, absolutePath));
248
+ const basename = path.basename(absolutePath);
249
+ const ext = path.extname(basename).toLowerCase();
250
+ // Detect what kind of fix this is
251
+ const detection = detectFixPattern(oldStr, newStr, ext);
252
+ if (!detection)
253
+ return;
254
+ // Check for recent duplicate (same file + same category within 5 min)
255
+ const recentDupe = bugLog.bugs.find(b => {
256
+ if (path.basename(b.file) !== basename)
257
+ return false;
258
+ if (!b.tags.includes("auto-detected"))
259
+ return false;
260
+ if (!b.tags.includes(detection.category))
261
+ return false;
262
+ const bugTime = new Date(b.last_seen).getTime();
263
+ return (Date.now() - bugTime) < 5 * 60 * 1000;
264
+ });
265
+ if (recentDupe) {
266
+ recentDupe.occurrences++;
267
+ recentDupe.last_seen = new Date().toISOString();
268
+ // Append additional context
269
+ if (detection.context && !recentDupe.fix.includes(detection.context)) {
270
+ recentDupe.fix += ` | Also: ${detection.context}`;
271
+ }
272
+ writeJSON(bugLogPath, bugLog);
273
+ return;
274
+ }
275
+ const nextId = `bug-${String(bugLog.bugs.length + 1).padStart(3, "0")}`;
276
+ bugLog.bugs.push({
277
+ id: nextId,
278
+ timestamp: new Date().toISOString(),
279
+ error_message: detection.summary,
280
+ file: relFile,
281
+ root_cause: detection.rootCause,
282
+ fix: detection.fix,
283
+ tags: ["auto-detected", detection.category, ext.replace(".", "") || "unknown"],
284
+ related_bugs: [],
285
+ occurrences: 1,
286
+ last_seen: new Date().toISOString(),
287
+ });
288
+ writeJSON(bugLogPath, bugLog);
289
+ }
290
+ function detectFixPattern(oldStr, newStr, ext) {
291
+ const oldLines = oldStr.split("\n");
292
+ const newLines = newStr.split("\n");
293
+ // --- Error handling added ---
294
+ if (newStr.includes("catch") && !oldStr.includes("catch")) {
295
+ const fn = newStr.match(/(?:function|def|async)\s+(\w+)/)?.[1] || "unknown";
296
+ return {
297
+ category: "error-handling",
298
+ summary: `Missing error handling in ${path.basename(fn)}`,
299
+ rootCause: "Code path had no error handling — exceptions would propagate uncaught",
300
+ fix: `Added try/catch block`,
301
+ context: extractChangedLines(oldStr, newStr),
302
+ };
303
+ }
304
+ // --- Null/undefined safety ---
305
+ if ((newStr.includes("?.") && !oldStr.includes("?.")) ||
306
+ (newStr.includes("?? ") && !oldStr.includes("?? ")) ||
307
+ (/!==?\s*(null|undefined)/.test(newStr) && !/!==?\s*(null|undefined)/.test(oldStr))) {
308
+ return {
309
+ category: "null-safety",
310
+ summary: `Null/undefined access in ${path.basename(path.basename(""))}`,
311
+ rootCause: "Property access on potentially null/undefined value",
312
+ fix: `Added null safety (optional chaining or null check)`,
313
+ context: extractChangedLines(oldStr, newStr),
314
+ };
315
+ }
316
+ // --- Guard clause / early return added ---
317
+ if (/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(newStr) &&
318
+ !/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(oldStr)) {
319
+ const condition = newStr.match(/if\s*\(([^)]+)\)/)?.[1]?.trim().slice(0, 60) || "condition";
320
+ return {
321
+ category: "guard-clause",
322
+ summary: `Missing guard clause`,
323
+ rootCause: `No early return/throw for edge case: ${condition}`,
324
+ fix: `Added guard clause: if (${condition.slice(0, 40)})`,
325
+ };
326
+ }
327
+ // --- Wrong value / string fix (very common bug) ---
328
+ if (oldLines.length <= 3 && newLines.length <= 3) {
329
+ const oldJoined = oldStr.trim();
330
+ const newJoined = newStr.trim();
331
+ // String literal changed
332
+ const oStrs = oldJoined.match(/['"`]([^'"`]{2,})['"`]/g) || [];
333
+ const nStrs = newJoined.match(/['"`]([^'"`]{2,})['"`]/g) || [];
334
+ if (oStrs.length > 0 && nStrs.length > 0) {
335
+ for (let i = 0; i < Math.min(oStrs.length, nStrs.length); i++) {
336
+ if (oStrs[i] !== nStrs[i]) {
337
+ return {
338
+ category: "wrong-value",
339
+ summary: `Incorrect value in code`,
340
+ rootCause: `Had ${oStrs[i].slice(0, 50)}`,
341
+ fix: `Changed to ${nStrs[i].slice(0, 50)}`,
342
+ };
343
+ }
344
+ }
345
+ }
346
+ // Variable name / method call changed
347
+ const oldTokens = tokenizeCode(oldJoined);
348
+ const newTokens = tokenizeCode(newJoined);
349
+ const changed = [];
350
+ for (let i = 0; i < Math.min(oldTokens.length, newTokens.length); i++) {
351
+ if (oldTokens[i] !== newTokens[i]) {
352
+ changed.push([oldTokens[i], newTokens[i]]);
353
+ }
354
+ }
355
+ if (changed.length === 1 && changed[0][0].length > 2) {
356
+ return {
357
+ category: "wrong-reference",
358
+ summary: `Wrong reference: ${changed[0][0]} should be ${changed[0][1]}`,
359
+ rootCause: `Used "${changed[0][0]}" instead of "${changed[0][1]}"`,
360
+ fix: `Changed ${changed[0][0]} → ${changed[0][1]}`,
361
+ };
362
+ }
363
+ }
364
+ // --- Logic fix (condition changed) ---
365
+ const oldCond = oldStr.match(/if\s*\(([^)]+)\)/)?.[1];
366
+ const newCond = newStr.match(/if\s*\(([^)]+)\)/)?.[1];
367
+ if (oldCond && newCond && oldCond !== newCond && oldLines.length <= 5) {
368
+ return {
369
+ category: "logic-fix",
370
+ summary: `Wrong condition in logic`,
371
+ rootCause: `Condition was: if (${oldCond.slice(0, 50)})`,
372
+ fix: `Changed to: if (${newCond.slice(0, 50)})`,
373
+ };
374
+ }
375
+ // --- Operator fix (=== vs ==, > vs >=, etc.) ---
376
+ const opChange = findOperatorChange(oldStr, newStr);
377
+ if (opChange) {
378
+ return {
379
+ category: "operator-fix",
380
+ summary: `Wrong operator: ${opChange.old} should be ${opChange.new}`,
381
+ rootCause: `Used "${opChange.old}" instead of "${opChange.new}"`,
382
+ fix: `Changed operator ${opChange.old} → ${opChange.new}`,
383
+ };
384
+ }
385
+ // --- Missing import/require ---
386
+ const oldImports = new Set((oldStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []).map(m => m));
387
+ const newImports = (newStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []);
388
+ const addedImports = newImports.filter(i => !oldImports.has(i));
389
+ if (addedImports.length > 0 && newLines.length - oldLines.length <= addedImports.length + 2) {
390
+ const modules = addedImports.map(i => i.match(/['"]([^'"]+)['"]/)?.[1] || "").filter(Boolean);
391
+ return {
392
+ category: "missing-import",
393
+ summary: `Missing import: ${modules.join(", ")}`,
394
+ rootCause: `Module(s) not imported: ${modules.join(", ")}`,
395
+ fix: `Added import(s) for ${modules.join(", ")}`,
396
+ };
397
+ }
398
+ // --- Return value fix ---
399
+ const oldReturn = oldStr.match(/return\s+(.+)/)?.[1]?.trim();
400
+ const newReturn = newStr.match(/return\s+(.+)/)?.[1]?.trim();
401
+ if (oldReturn && newReturn && oldReturn !== newReturn && oldLines.length <= 5) {
402
+ return {
403
+ category: "return-value",
404
+ summary: `Wrong return value`,
405
+ rootCause: `Was returning: ${oldReturn.slice(0, 50)}`,
406
+ fix: `Now returns: ${newReturn.slice(0, 50)}`,
407
+ };
408
+ }
409
+ // --- Async/await fix ---
410
+ if (newStr.includes("await ") && !oldStr.includes("await ")) {
411
+ return {
412
+ category: "async-fix",
413
+ summary: `Missing await`,
414
+ rootCause: `Async call without await — returned Promise instead of value`,
415
+ fix: `Added await to async call`,
416
+ context: extractChangedLines(oldStr, newStr),
417
+ };
418
+ }
419
+ if (newStr.includes("async ") && !oldStr.includes("async ")) {
420
+ return {
421
+ category: "async-fix",
422
+ summary: `Function not marked async`,
423
+ rootCause: `Function uses await but wasn't declared async`,
424
+ fix: `Added async modifier`,
425
+ };
426
+ }
427
+ // --- Type annotation/cast fix ---
428
+ if (ext === ".ts" || ext === ".tsx") {
429
+ if ((newStr.includes(" as ") && !oldStr.includes(" as ")) ||
430
+ (newStr.includes(": ") && !oldStr.includes(": ") && oldLines.length <= 3)) {
431
+ return {
432
+ category: "type-fix",
433
+ summary: `Type error`,
434
+ rootCause: `Missing or incorrect type annotation`,
435
+ fix: `Added type assertion/annotation`,
436
+ context: extractChangedLines(oldStr, newStr),
437
+ };
438
+ }
439
+ }
440
+ // --- CSS/style fix ---
441
+ if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") {
442
+ const oldProps = extractCSSProps(oldStr);
443
+ const newProps = extractCSSProps(newStr);
444
+ const changedProps = [...newProps.entries()].filter(([k, v]) => oldProps.get(k) !== v && oldProps.has(k));
445
+ if (changedProps.length > 0 && changedProps.length <= 3) {
446
+ const desc = changedProps.map(([k, v]) => `${k}: ${oldProps.get(k)} → ${v}`).join("; ");
447
+ return {
448
+ category: "style-fix",
449
+ summary: `CSS fix: ${changedProps.map(([k]) => k).join(", ")}`,
450
+ rootCause: desc,
451
+ fix: `Changed ${desc}`,
452
+ };
453
+ }
454
+ }
455
+ // --- Significant diff (catch-all for substantial edits) ---
456
+ const diffRatio = Math.abs(newStr.length - oldStr.length) / Math.max(oldStr.length, 1);
457
+ if (diffRatio > 0.3 && oldLines.length >= 3 && newLines.length >= 3) {
458
+ // Only log if there's meaningful structural change, not just additions
459
+ const removedLines = oldLines.filter(l => l.trim() && !newLines.some(nl => nl.trim() === l.trim()));
460
+ if (removedLines.length >= 2) {
461
+ return {
462
+ category: "refactor",
463
+ summary: `Significant refactor of ${path.basename("")}`,
464
+ rootCause: `${removedLines.length} lines replaced/restructured`,
465
+ fix: `Rewrote ${oldLines.length}→${newLines.length} lines (${removedLines.length} removed)`,
466
+ context: removedLines.slice(0, 2).map(l => l.trim().slice(0, 50)).join("; "),
467
+ };
468
+ }
469
+ }
470
+ return null;
471
+ }
472
+ function extractChangedLines(oldStr, newStr) {
473
+ const oldLines = new Set(oldStr.split("\n").map(l => l.trim()).filter(Boolean));
474
+ const newLines = newStr.split("\n").map(l => l.trim()).filter(Boolean);
475
+ const added = newLines.filter(l => !oldLines.has(l));
476
+ return added.slice(0, 2).map(l => l.slice(0, 60)).join("; ");
477
+ }
478
+ function tokenizeCode(code) {
479
+ return code.replace(/[^\w$]/g, " ").split(/\s+/).filter(t => t.length > 0);
480
+ }
481
+ function findOperatorChange(oldStr, newStr) {
482
+ const operators = ["===", "!==", "==", "!=", ">=", "<=", ">>", "<<", "&&", "||", "??"];
483
+ for (const op of operators) {
484
+ if (oldStr.includes(op) && !newStr.includes(op)) {
485
+ for (const op2 of operators) {
486
+ if (op2 !== op && newStr.includes(op2) && !oldStr.includes(op2)) {
487
+ return { old: op, new: op2 };
488
+ }
489
+ }
490
+ }
491
+ }
492
+ return null;
493
+ }
494
+ function extractCSSProps(code) {
495
+ const props = new Map();
496
+ const matches = code.matchAll(/([\w-]+)\s*:\s*([^;}\n]+)/g);
497
+ for (const m of matches) {
498
+ props.set(m[1].trim(), m[2].trim());
499
+ }
500
+ return props;
501
+ }
502
+ main().catch(() => process.exit(0));
@@ -0,0 +1,79 @@
1
+ import * as path from "node:path";
2
+ import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, readStdin, normalizePath } 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
+ if (!filePath) {
19
+ process.exit(0);
20
+ return;
21
+ }
22
+ const normalizedFile = normalizePath(filePath);
23
+ // Skip tracking for .wolf/ internal files — they're infrastructure, not project files.
24
+ // Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning.
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
+ if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) {
30
+ process.exit(0);
31
+ return;
32
+ }
33
+ const session = readJSON(sessionFile, {
34
+ session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0,
35
+ repeated_reads_warned: 0,
36
+ });
37
+ // Check if already read this session
38
+ if (session.files_read[normalizedFile]) {
39
+ const prev = session.files_read[normalizedFile];
40
+ process.stderr.write(`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`);
41
+ session.files_read[normalizedFile].count++;
42
+ session.repeated_reads_warned++;
43
+ writeJSON(sessionFile, session);
44
+ process.exit(0);
45
+ return;
46
+ }
47
+ // Check anatomy.md for this file
48
+ const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md"));
49
+ const sections = parseAnatomy(anatomyContent);
50
+ let found = false;
51
+ for (const [sectionKey, entries] of sections) {
52
+ for (const entry of entries) {
53
+ // Build the full relative path from the section key + filename for accurate matching
54
+ const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
55
+ if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) {
56
+ process.stderr.write(`📋 OpenWolf anatomy: ${entry.file} — ${entry.description} (~${entry.tokens} tok)\n`);
57
+ found = true;
58
+ break;
59
+ }
60
+ }
61
+ if (found)
62
+ break;
63
+ }
64
+ if (found) {
65
+ session.anatomy_hits++;
66
+ }
67
+ else {
68
+ session.anatomy_misses++;
69
+ }
70
+ // Record initial read entry (tokens will be updated in post-read)
71
+ session.files_read[normalizedFile] = {
72
+ count: 1,
73
+ tokens: 0,
74
+ first_read: new Date().toISOString(),
75
+ };
76
+ writeJSON(sessionFile, session);
77
+ process.exit(0);
78
+ }
79
+ main().catch(() => process.exit(0));