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.
- package/CHANGELOG.md +386 -0
- package/LICENSE +663 -0
- package/NOTICE +48 -0
- package/README.md +283 -0
- package/dist/bin/openwolf.js +10 -0
- package/dist/bin/openwolf.js.map +1 -0
- package/dist/dashboard/assets/AISuggestions-DHXOgHMM.js +9 -0
- package/dist/dashboard/assets/ActivityTimeline-BXLwacZV.js +1 -0
- package/dist/dashboard/assets/AnatomyBrowser-CUyC8M2I.js +1 -0
- package/dist/dashboard/assets/BugLog-Bxvy1sPW.js +1 -0
- package/dist/dashboard/assets/CerebrumViewer-DCySVmzA.js +1 -0
- package/dist/dashboard/assets/CronStatus-BA4fmQX3.js +1 -0
- package/dist/dashboard/assets/DesignQC-BhNEKryS.js +1 -0
- package/dist/dashboard/assets/MemoryViewer-0y9lsDUO.js +1 -0
- package/dist/dashboard/assets/ProjectOverview-CC51rVRe.js +1 -0
- package/dist/dashboard/assets/StatusBadge-4NJI_F2G.js +1 -0
- package/dist/dashboard/assets/TokenUsage-7pCxlnCX.js +55 -0
- package/dist/dashboard/assets/index-4O5rk-qB.css +2 -0
- package/dist/dashboard/assets/index-jfgWxxsn.js +13 -0
- package/dist/dashboard/assets/utils-BqE_aDxV.js +1 -0
- package/dist/dashboard/index.html +16 -0
- package/dist/hooks/post-read.js +85 -0
- package/dist/hooks/post-write.js +520 -0
- package/dist/hooks/pre-read.js +107 -0
- package/dist/hooks/pre-write.js +120 -0
- package/dist/hooks/session-start.js +76 -0
- package/dist/hooks/shared.js +0 -0
- package/dist/hooks/stop.js +219 -0
- package/dist/src/buglog/bug-matcher.js +3 -0
- package/dist/src/buglog/bug-matcher.js.map +1 -0
- package/dist/src/buglog/bug-tracker.js +89 -0
- package/dist/src/buglog/bug-tracker.js.map +1 -0
- package/dist/src/cli/bug-cmd.js +31 -0
- package/dist/src/cli/bug-cmd.js.map +1 -0
- package/dist/src/cli/cron-cmd.js +107 -0
- package/dist/src/cli/cron-cmd.js.map +1 -0
- package/dist/src/cli/daemon-cmd.js +180 -0
- package/dist/src/cli/daemon-cmd.js.map +1 -0
- package/dist/src/cli/dashboard.js +88 -0
- package/dist/src/cli/dashboard.js.map +1 -0
- package/dist/src/cli/designqc-cmd.js +31 -0
- package/dist/src/cli/designqc-cmd.js.map +1 -0
- package/dist/src/cli/doctor-cmd.js +94 -0
- package/dist/src/cli/doctor-cmd.js.map +1 -0
- package/dist/src/cli/index.js +158 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/init.js +493 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/registry.js +93 -0
- package/dist/src/cli/registry.js.map +1 -0
- package/dist/src/cli/scan.js +39 -0
- package/dist/src/cli/scan.js.map +1 -0
- package/dist/src/cli/status.js +98 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/update.js +435 -0
- package/dist/src/cli/update.js.map +1 -0
- package/dist/src/daemon/cron-engine.js +321 -0
- package/dist/src/daemon/cron-engine.js.map +1 -0
- package/dist/src/daemon/file-watcher.js +64 -0
- package/dist/src/daemon/file-watcher.js.map +1 -0
- package/dist/src/daemon/health.js +23 -0
- package/dist/src/daemon/health.js.map +1 -0
- package/dist/src/daemon/wolf-daemon.js +440 -0
- package/dist/src/daemon/wolf-daemon.js.map +1 -0
- package/dist/src/designqc/designqc-capture.js +278 -0
- package/dist/src/designqc/designqc-capture.js.map +1 -0
- package/dist/src/designqc/designqc-engine.js +159 -0
- package/dist/src/designqc/designqc-engine.js.map +1 -0
- package/dist/src/designqc/designqc-types.js +5 -0
- package/dist/src/designqc/designqc-types.js.map +1 -0
- package/dist/src/hooks/post-read.js +86 -0
- package/dist/src/hooks/post-read.js.map +1 -0
- package/dist/src/hooks/post-write.js +521 -0
- package/dist/src/hooks/post-write.js.map +1 -0
- package/dist/src/hooks/pre-read.js +108 -0
- package/dist/src/hooks/pre-read.js.map +1 -0
- package/dist/src/hooks/pre-write.js +121 -0
- package/dist/src/hooks/pre-write.js.map +1 -0
- package/dist/src/hooks/session-start.js +77 -0
- package/dist/src/hooks/session-start.js.map +1 -0
- package/dist/src/hooks/shared.js +0 -0
- package/dist/src/hooks/shared.js.map +1 -0
- package/dist/src/hooks/stop.js +220 -0
- package/dist/src/hooks/stop.js.map +1 -0
- package/dist/src/scanner/anatomy-scanner.js +278 -0
- package/dist/src/scanner/anatomy-scanner.js.map +1 -0
- package/dist/src/scanner/description-extractor.js +1007 -0
- package/dist/src/scanner/description-extractor.js.map +1 -0
- package/dist/src/scanner/project-root.js +52 -0
- package/dist/src/scanner/project-root.js.map +1 -0
- package/dist/src/tracker/token-estimator.js +20 -0
- package/dist/src/tracker/token-estimator.js.map +1 -0
- package/dist/src/tracker/token-ledger.js +55 -0
- package/dist/src/tracker/token-ledger.js.map +1 -0
- package/dist/src/tracker/waste-detector.js +101 -0
- package/dist/src/tracker/waste-detector.js.map +1 -0
- package/dist/src/utils/dashboard-auth.js +50 -0
- package/dist/src/utils/dashboard-auth.js.map +1 -0
- package/dist/src/utils/fs-safe.js +135 -0
- package/dist/src/utils/fs-safe.js.map +1 -0
- package/dist/src/utils/hooks-deploy.js +70 -0
- package/dist/src/utils/hooks-deploy.js.map +1 -0
- package/dist/src/utils/logger.js +48 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/maintenance.js +385 -0
- package/dist/src/utils/maintenance.js.map +1 -0
- package/dist/src/utils/paths.js +23 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/src/utils/platform.js +14 -0
- package/dist/src/utils/platform.js.map +1 -0
- package/dist/src/utils/seed.js +61 -0
- package/dist/src/utils/seed.js.map +1 -0
- package/package.json +82 -0
- package/src/templates/OPENWOLF.md +147 -0
- package/src/templates/STATUS.md +48 -0
- package/src/templates/anatomy.md +5 -0
- package/src/templates/buglog.json +4 -0
- package/src/templates/cerebrum.md +22 -0
- package/src/templates/claude-md-snippet.md +5 -0
- package/src/templates/claude-rules-openwolf.md +17 -0
- package/src/templates/config.json +83 -0
- package/src/templates/cron-manifest.json +97 -0
- package/src/templates/cron-state.json +7 -0
- package/src/templates/identity.md +9 -0
- package/src/templates/memory.md +4 -0
- package/src/templates/reframe-frameworks.md +597 -0
- package/src/templates/token-ledger.json +21 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, readStdin, normalizePath, loadIgnore } from "./shared.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
ensureWolfDir();
|
|
6
|
+
const wolfDir = getWolfDir();
|
|
7
|
+
const hooksDir = path.join(wolfDir, "hooks");
|
|
8
|
+
const sessionFile = path.join(hooksDir, "_session.json");
|
|
9
|
+
const raw = await readStdin();
|
|
10
|
+
let input;
|
|
11
|
+
try {
|
|
12
|
+
input = JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
|
19
|
+
if (!filePath) {
|
|
20
|
+
process.exit(0);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const normalizedFile = normalizePath(filePath);
|
|
24
|
+
// Current on-disk mtime (used to allow a legitimate re-read after the file changed).
|
|
25
|
+
let fileMtime;
|
|
26
|
+
try {
|
|
27
|
+
fileMtime = fs.statSync(filePath).mtimeMs;
|
|
28
|
+
}
|
|
29
|
+
catch { /* not statable */ }
|
|
30
|
+
// Skip tracking for .wolf/ internal files — they're infrastructure, not project files.
|
|
31
|
+
// Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning.
|
|
32
|
+
const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
33
|
+
const relToProject = normalizedFile.startsWith(projectDir)
|
|
34
|
+
? normalizedFile.slice(projectDir.length).replace(/^\//, "")
|
|
35
|
+
: "";
|
|
36
|
+
// Don't track files outside the project root (upstream #56).
|
|
37
|
+
if (!relToProject) {
|
|
38
|
+
process.exit(0);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) {
|
|
42
|
+
process.exit(0);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Skip .gitignore / .wolfignore matches — no anatomy hint / repeated-read tracking for them.
|
|
46
|
+
if (loadIgnore(projectDir)(relToProject)) {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const session = readJSON(sessionFile, {
|
|
51
|
+
session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0,
|
|
52
|
+
repeated_reads_warned: 0,
|
|
53
|
+
});
|
|
54
|
+
// Check if already read this session
|
|
55
|
+
if (session.files_read[normalizedFile]) {
|
|
56
|
+
const prev = session.files_read[normalizedFile];
|
|
57
|
+
// If the file changed since we last read it (e.g. it was edited during the session),
|
|
58
|
+
// a re-read is legitimate — refresh state and don't warn about a repeated read (upstream #41).
|
|
59
|
+
if (fileMtime !== undefined && prev.mtime !== undefined && fileMtime !== prev.mtime) {
|
|
60
|
+
prev.count++;
|
|
61
|
+
prev.mtime = fileMtime;
|
|
62
|
+
writeJSON(sessionFile, session);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
process.stderr.write(`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`);
|
|
67
|
+
prev.count++;
|
|
68
|
+
prev.mtime = fileMtime ?? prev.mtime;
|
|
69
|
+
session.repeated_reads_warned++;
|
|
70
|
+
writeJSON(sessionFile, session);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Check anatomy.md for this file
|
|
75
|
+
const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md"));
|
|
76
|
+
const sections = parseAnatomy(anatomyContent);
|
|
77
|
+
let found = false;
|
|
78
|
+
for (const [sectionKey, entries] of sections) {
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
// Build the full relative path from the section key + filename for accurate matching
|
|
81
|
+
const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
|
|
82
|
+
if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) {
|
|
83
|
+
process.stderr.write(`📋 OpenWolf anatomy: ${entry.file} — ${entry.description} (~${entry.tokens} tok)\n`);
|
|
84
|
+
found = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (found)
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (found) {
|
|
92
|
+
session.anatomy_hits++;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
session.anatomy_misses++;
|
|
96
|
+
}
|
|
97
|
+
// Record initial read entry (tokens will be updated in post-read)
|
|
98
|
+
session.files_read[normalizedFile] = {
|
|
99
|
+
count: 1,
|
|
100
|
+
tokens: 0,
|
|
101
|
+
first_read: new Date().toISOString(),
|
|
102
|
+
mtime: fileMtime,
|
|
103
|
+
};
|
|
104
|
+
writeJSON(sessionFile, session);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getWolfDir, ensureWolfDir, readMarkdown, readStdin, readBugLog } from "./shared.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
ensureWolfDir();
|
|
6
|
+
const wolfDir = getWolfDir();
|
|
7
|
+
const raw = await readStdin();
|
|
8
|
+
let input;
|
|
9
|
+
try {
|
|
10
|
+
input = JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
process.exit(0);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
// For Edit tool, the meaningful content is old_string + new_string
|
|
17
|
+
const content = input.tool_input?.content ?? "";
|
|
18
|
+
const oldStr = input.tool_input?.old_string ?? "";
|
|
19
|
+
const newStr = input.tool_input?.new_string ?? "";
|
|
20
|
+
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
|
21
|
+
const allContent = [content, oldStr, newStr].join("\n");
|
|
22
|
+
if (!allContent.trim()) {
|
|
23
|
+
process.exit(0);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// 1. Cerebrum Do-Not-Repeat check
|
|
27
|
+
checkCerebrum(wolfDir, allContent);
|
|
28
|
+
// 2. Bug log: search for similar past bugs when editing code
|
|
29
|
+
// This fires when Claude is about to edit a file — if the edit looks like a fix
|
|
30
|
+
// (changing error handling, modifying catch blocks, etc.), check the bug log
|
|
31
|
+
if (filePath && (oldStr || content)) {
|
|
32
|
+
checkBugLog(wolfDir, filePath, oldStr, newStr, content);
|
|
33
|
+
}
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
function checkCerebrum(wolfDir, content) {
|
|
37
|
+
const cerebrumContent = readMarkdown(path.join(wolfDir, "cerebrum.md"));
|
|
38
|
+
const doNotRepeatSection = cerebrumContent.split("## Do-Not-Repeat")[1];
|
|
39
|
+
if (!doNotRepeatSection)
|
|
40
|
+
return;
|
|
41
|
+
const entries = doNotRepeatSection.split("## ")[0];
|
|
42
|
+
const lines = entries.split("\n").filter((l) => l.trim().startsWith("[") || l.trim().startsWith("-"));
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const trimmed = line.trim().replace(/^[-*]\s*/, "").replace(/^\[[\d-]+\]\s*/, "");
|
|
45
|
+
if (!trimmed)
|
|
46
|
+
continue;
|
|
47
|
+
const patterns = [];
|
|
48
|
+
const quotedMatches = trimmed.match(/"([^"]+)"/g) || trimmed.match(/'([^']+)'/g) || trimmed.match(/`([^`]+)`/g);
|
|
49
|
+
if (quotedMatches) {
|
|
50
|
+
for (const qm of quotedMatches) {
|
|
51
|
+
patterns.push(qm.replace(/["'`]/g, ""));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const neverMatch = trimmed.match(/(?:never use|avoid|don't use|do not use)\s+(\w+)/i);
|
|
55
|
+
if (neverMatch)
|
|
56
|
+
patterns.push(neverMatch[1]);
|
|
57
|
+
for (const pattern of patterns) {
|
|
58
|
+
try {
|
|
59
|
+
const regex = new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
60
|
+
if (regex.test(content)) {
|
|
61
|
+
process.stderr.write(`⚠️ OpenWolf cerebrum warning: "${trimmed}" — check your code before proceeding.\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Common words that appear in most code — must be excluded from similarity matching
|
|
69
|
+
const STOP_WORDS = new Set([
|
|
70
|
+
"error", "function", "return", "const", "this", "that", "with", "from",
|
|
71
|
+
"import", "export", "class", "interface", "type", "undefined", "null",
|
|
72
|
+
"true", "false", "string", "number", "object", "array", "value",
|
|
73
|
+
"file", "path", "name", "data", "response", "request", "result",
|
|
74
|
+
"should", "must", "does", "have", "been", "will", "would", "could",
|
|
75
|
+
"when", "then", "else", "each", "some", "every", "only",
|
|
76
|
+
]);
|
|
77
|
+
function checkBugLog(wolfDir, filePath, oldStr, newStr, content) {
|
|
78
|
+
const bugLogPath = path.join(wolfDir, "buglog.json");
|
|
79
|
+
if (!fs.existsSync(bugLogPath))
|
|
80
|
+
return;
|
|
81
|
+
const bugLog = readBugLog(wolfDir); // tolerates legacy array format
|
|
82
|
+
if (bugLog.bugs.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
const basename = path.basename(filePath);
|
|
85
|
+
// ONLY surface bugs that match the SAME file being edited.
|
|
86
|
+
// Cross-file matching is too noisy and risks misdirecting Claude.
|
|
87
|
+
const fileMatches = bugLog.bugs.filter(b => {
|
|
88
|
+
const bugBasename = path.basename(b.file);
|
|
89
|
+
return bugBasename === basename;
|
|
90
|
+
});
|
|
91
|
+
if (fileMatches.length === 0)
|
|
92
|
+
return;
|
|
93
|
+
// Further filter: require tag or error_message overlap with the edit content
|
|
94
|
+
const editText = (oldStr + " " + newStr + " " + content).toLowerCase();
|
|
95
|
+
const editTokens = tokenize(editText);
|
|
96
|
+
const relevant = fileMatches.filter(bug => {
|
|
97
|
+
// Check if any bug tag appears in the edit content
|
|
98
|
+
const tagHit = bug.tags.some(t => editText.includes(t.toLowerCase()));
|
|
99
|
+
if (tagHit)
|
|
100
|
+
return true;
|
|
101
|
+
// Check meaningful word overlap (excluding stop words)
|
|
102
|
+
const bugTokens = tokenize(bug.error_message + " " + bug.root_cause);
|
|
103
|
+
const overlap = [...editTokens].filter(t => bugTokens.has(t));
|
|
104
|
+
// Require at least 3 meaningful overlapping words
|
|
105
|
+
return overlap.length >= 3;
|
|
106
|
+
});
|
|
107
|
+
if (relevant.length === 0)
|
|
108
|
+
return;
|
|
109
|
+
// Surface as a FYI, not a directive — Claude should evaluate, not blindly apply
|
|
110
|
+
process.stderr.write(`📋 OpenWolf buglog: ${relevant.length} past bug(s) found for ${basename} — review for context, do NOT apply blindly:\n`);
|
|
111
|
+
for (const bug of relevant.slice(0, 2)) {
|
|
112
|
+
process.stderr.write(` [${bug.id}] "${bug.error_message.slice(0, 70)}"\n Cause: ${bug.root_cause.slice(0, 80)}\n Fix: ${bug.fix.slice(0, 80)}\n`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function tokenize(text) {
|
|
116
|
+
return new Set(text.replace(/[^\w\s]/g, " ").split(/\s+/)
|
|
117
|
+
.filter(w => w.length > 3 && !STOP_WORDS.has(w.toLowerCase()))
|
|
118
|
+
.map(w => w.toLowerCase()));
|
|
119
|
+
}
|
|
120
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort } from "./shared.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
ensureWolfDir();
|
|
6
|
+
const wolfDir = getWolfDir();
|
|
7
|
+
// Clean up stale .tmp files left from failed atomic writes
|
|
8
|
+
try {
|
|
9
|
+
const files = fs.readdirSync(wolfDir);
|
|
10
|
+
for (const f of files) {
|
|
11
|
+
if (f.endsWith(".tmp")) {
|
|
12
|
+
try {
|
|
13
|
+
fs.unlinkSync(path.join(wolfDir, f));
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
const hooksDir = path.join(wolfDir, "hooks");
|
|
21
|
+
const sessionFile = path.join(hooksDir, "_session.json");
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const sessionId = `session-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
|
24
|
+
// Create fresh session state
|
|
25
|
+
writeJSON(sessionFile, {
|
|
26
|
+
session_id: sessionId,
|
|
27
|
+
started: timestamp(),
|
|
28
|
+
files_read: {},
|
|
29
|
+
files_written: [],
|
|
30
|
+
edit_counts: {},
|
|
31
|
+
anatomy_hits: 0,
|
|
32
|
+
anatomy_misses: 0,
|
|
33
|
+
repeated_reads_warned: 0,
|
|
34
|
+
cerebrum_warnings: 0,
|
|
35
|
+
stop_count: 0,
|
|
36
|
+
});
|
|
37
|
+
// Append session header to memory.md
|
|
38
|
+
const memoryPath = path.join(wolfDir, "memory.md");
|
|
39
|
+
const header = `\n## Session: ${now.toISOString().slice(0, 10)} ${timeShort()}\n\n| Time | Action | File(s) | Outcome | ~Tokens |\n|------|--------|---------|---------|--------|\n`;
|
|
40
|
+
appendMarkdown(memoryPath, header);
|
|
41
|
+
// Check cerebrum freshness — remind Claude to learn
|
|
42
|
+
try {
|
|
43
|
+
const cerebrumPath = path.join(wolfDir, "cerebrum.md");
|
|
44
|
+
const cerebrumContent = fs.readFileSync(cerebrumPath, "utf-8");
|
|
45
|
+
const stat = fs.statSync(cerebrumPath);
|
|
46
|
+
const daysSinceUpdate = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24);
|
|
47
|
+
// Count actual entries (non-comment, non-empty lines in content sections)
|
|
48
|
+
const entryLines = cerebrumContent.split("\n").filter(l => {
|
|
49
|
+
const t = l.trim();
|
|
50
|
+
return t.startsWith("- ") || t.startsWith("* ") || (t.startsWith("[") && t.includes("]"));
|
|
51
|
+
});
|
|
52
|
+
if (entryLines.length < 3) {
|
|
53
|
+
process.stderr.write(`💡 OpenWolf: cerebrum.md has only ${entryLines.length} entries. Learn from this session — record user preferences, project conventions, and mistakes to .wolf/cerebrum.md.\n`);
|
|
54
|
+
}
|
|
55
|
+
else if (daysSinceUpdate > 3) {
|
|
56
|
+
process.stderr.write(`💡 OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(daysSinceUpdate)} days. Look for opportunities to add learnings this session.\n`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
// Check buglog — remind if empty
|
|
61
|
+
try {
|
|
62
|
+
const buglogPath = path.join(wolfDir, "buglog.json");
|
|
63
|
+
const buglog = readJSON(buglogPath, { bugs: [] });
|
|
64
|
+
if (buglog.bugs.length === 0) {
|
|
65
|
+
process.stderr.write(`📋 OpenWolf: buglog.json is empty. If you encounter or fix any bugs, errors, or failed tests this session, log them to .wolf/buglog.json.\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
// Increment total_sessions in token-ledger
|
|
70
|
+
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
|
71
|
+
const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } });
|
|
72
|
+
ledger.lifetime.total_sessions++;
|
|
73
|
+
writeJSON(ledgerPath, ledger);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
main().catch(() => process.exit(0));
|
|
Binary file
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, appendMarkdown, timeShort, getRetention, compactMemoryIfLarge, countSemanticEntries, withLock } from "./shared.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
ensureWolfDir();
|
|
6
|
+
const wolfDir = getWolfDir();
|
|
7
|
+
const hooksDir = path.join(wolfDir, "hooks");
|
|
8
|
+
const sessionFile = path.join(hooksDir, "_session.json");
|
|
9
|
+
const session = readJSON(sessionFile, {
|
|
10
|
+
session_id: "",
|
|
11
|
+
started: "",
|
|
12
|
+
files_read: {},
|
|
13
|
+
files_written: [],
|
|
14
|
+
edit_counts: {},
|
|
15
|
+
anatomy_hits: 0,
|
|
16
|
+
anatomy_misses: 0,
|
|
17
|
+
repeated_reads_warned: 0,
|
|
18
|
+
cerebrum_warnings: 0,
|
|
19
|
+
stop_count: 0,
|
|
20
|
+
});
|
|
21
|
+
session.stop_count++;
|
|
22
|
+
// Only write to ledger if there's been activity
|
|
23
|
+
const readCount = Object.keys(session.files_read).length;
|
|
24
|
+
const writeCount = session.files_written.length;
|
|
25
|
+
if (readCount === 0 && writeCount === 0) {
|
|
26
|
+
writeJSON(sessionFile, session);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Collect end-of-turn reminders. These are surfaced via additionalContext (stdout) at the
|
|
31
|
+
// very end so they land in Claude's next context window — stderr would only hit the terminal.
|
|
32
|
+
// Each reminder type fires at most once per session so it doesn't re-nag every turn.
|
|
33
|
+
const candidates = [
|
|
34
|
+
{ key: "buglog", msg: checkForMissingBugLogs(wolfDir, session) },
|
|
35
|
+
{ key: "cerebrum", msg: checkCerebrumFreshness(wolfDir, session) },
|
|
36
|
+
{ key: "summary", msg: checkSemanticSummaries(wolfDir, writeCount) },
|
|
37
|
+
{ key: "status", msg: checkStatusFreshness(wolfDir, session) },
|
|
38
|
+
].filter((c) => c.msg !== null);
|
|
39
|
+
const alreadyShown = new Set(session.reminders_shown ?? []);
|
|
40
|
+
const fresh = candidates.filter((c) => !alreadyShown.has(c.key));
|
|
41
|
+
for (const c of fresh)
|
|
42
|
+
alreadyShown.add(c.key);
|
|
43
|
+
session.reminders_shown = [...alreadyShown];
|
|
44
|
+
const reminders = fresh.map((c) => c.msg);
|
|
45
|
+
// Build session entry for ledger
|
|
46
|
+
const reads = Object.entries(session.files_read).map(([file, data]) => ({
|
|
47
|
+
file,
|
|
48
|
+
tokens_estimated: data.tokens,
|
|
49
|
+
was_repeated: data.count > 1,
|
|
50
|
+
anatomy_had_description: false, // simplified
|
|
51
|
+
}));
|
|
52
|
+
const writes = session.files_written.map((w) => ({
|
|
53
|
+
file: w.file,
|
|
54
|
+
tokens_estimated: w.tokens,
|
|
55
|
+
action: w.action,
|
|
56
|
+
}));
|
|
57
|
+
const inputTokens = reads.reduce((sum, r) => sum + r.tokens_estimated, 0);
|
|
58
|
+
const outputTokens = writes.reduce((sum, w) => sum + w.tokens_estimated, 0);
|
|
59
|
+
const sessionEntry = {
|
|
60
|
+
id: session.session_id,
|
|
61
|
+
started: session.started,
|
|
62
|
+
ended: new Date().toISOString(),
|
|
63
|
+
reads,
|
|
64
|
+
writes,
|
|
65
|
+
totals: {
|
|
66
|
+
input_tokens_estimated: inputTokens,
|
|
67
|
+
output_tokens_estimated: outputTokens,
|
|
68
|
+
reads_count: readCount,
|
|
69
|
+
writes_count: writeCount,
|
|
70
|
+
repeated_reads_blocked: session.repeated_reads_warned,
|
|
71
|
+
anatomy_lookups: session.anatomy_hits,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
// Update token-ledger.json — lock the read-modify-write so concurrent sessions and the cron
|
|
75
|
+
// token report don't clobber each other (M1).
|
|
76
|
+
const ret = getRetention(wolfDir);
|
|
77
|
+
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
|
78
|
+
withLock(ledgerPath, () => {
|
|
79
|
+
const ledger = readJSON(ledgerPath, {
|
|
80
|
+
version: 1,
|
|
81
|
+
created_at: "",
|
|
82
|
+
lifetime: {
|
|
83
|
+
total_tokens_estimated: 0,
|
|
84
|
+
total_reads: 0,
|
|
85
|
+
total_writes: 0,
|
|
86
|
+
total_sessions: 0,
|
|
87
|
+
anatomy_hits: 0,
|
|
88
|
+
anatomy_misses: 0,
|
|
89
|
+
repeated_reads_blocked: 0,
|
|
90
|
+
estimated_savings_vs_bare_cli: 0,
|
|
91
|
+
},
|
|
92
|
+
sessions: [],
|
|
93
|
+
daemon_usage: [],
|
|
94
|
+
waste_flags: [],
|
|
95
|
+
optimization_report: { last_generated: null, patterns: [] },
|
|
96
|
+
});
|
|
97
|
+
// Keep token-ledger.json bounded: cap per-session arrays and total session count.
|
|
98
|
+
// Without this, sessions[] (each embedding full reads[]/writes[]) grows without limit
|
|
99
|
+
// and writeJSON's full-file rewrite becomes quadratic over time. Limits are tunable
|
|
100
|
+
// via config.json openwolf.retention.
|
|
101
|
+
if (Array.isArray(sessionEntry.reads) && sessionEntry.reads.length > ret.session_io_max) {
|
|
102
|
+
sessionEntry.reads = sessionEntry.reads.slice(-ret.session_io_max);
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(sessionEntry.writes) && sessionEntry.writes.length > ret.session_io_max) {
|
|
105
|
+
sessionEntry.writes = sessionEntry.writes.slice(-ret.session_io_max);
|
|
106
|
+
}
|
|
107
|
+
ledger.sessions.push(sessionEntry);
|
|
108
|
+
if (ledger.sessions.length > ret.token_ledger_max_sessions) {
|
|
109
|
+
ledger.sessions = ledger.sessions.slice(-ret.token_ledger_max_sessions);
|
|
110
|
+
}
|
|
111
|
+
ledger.lifetime.total_reads += readCount;
|
|
112
|
+
ledger.lifetime.total_writes += writeCount;
|
|
113
|
+
ledger.lifetime.total_tokens_estimated += inputTokens + outputTokens;
|
|
114
|
+
ledger.lifetime.anatomy_hits += session.anatomy_hits;
|
|
115
|
+
ledger.lifetime.anatomy_misses += session.anatomy_misses;
|
|
116
|
+
ledger.lifetime.repeated_reads_blocked += session.repeated_reads_warned;
|
|
117
|
+
// Estimate savings: anatomy hits save ~200 tokens each, repeated reads blocked save their token count
|
|
118
|
+
const savedFromAnatomy = session.anatomy_hits * 200;
|
|
119
|
+
const savedFromRepeats = Object.values(session.files_read)
|
|
120
|
+
.filter((r) => r.count > 1)
|
|
121
|
+
.reduce((sum, r) => sum + r.tokens * (r.count - 1), 0);
|
|
122
|
+
ledger.lifetime.estimated_savings_vs_bare_cli += savedFromAnatomy + savedFromRepeats;
|
|
123
|
+
writeJSON(ledgerPath, ledger);
|
|
124
|
+
});
|
|
125
|
+
// Write a session summary line to memory.md if there was meaningful activity
|
|
126
|
+
if (writeCount > 0) {
|
|
127
|
+
try {
|
|
128
|
+
const uniqueFiles = new Set(session.files_written.map(w => path.basename(w.file)));
|
|
129
|
+
const fileList = [...uniqueFiles].slice(0, 5).join(", ");
|
|
130
|
+
const memoryPath = path.join(wolfDir, "memory.md");
|
|
131
|
+
appendMarkdown(memoryPath, `| ${timeShort()} | Session end: ${writeCount} writes across ${uniqueFiles.size} files (${fileList}) | ${readCount} reads | ~${inputTokens + outputTokens} tok |\n`);
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
}
|
|
135
|
+
// Opportunistic self-maintenance: keep memory.md bounded even when the daemon
|
|
136
|
+
// (which normally runs the consolidation cron) isn't running. Stat-gated → cheap.
|
|
137
|
+
try {
|
|
138
|
+
compactMemoryIfLarge(wolfDir, ret.memory_max_bytes);
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
writeJSON(sessionFile, session);
|
|
142
|
+
// Surface reminders into Claude's next context window (Stop hooks can inject via stdout JSON).
|
|
143
|
+
if (reminders.length > 0) {
|
|
144
|
+
const additionalContext = `⚠️ OpenWolf end-of-turn reminders:\n${reminders.map((r) => `• ${r}`).join("\n")}`;
|
|
145
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "Stop", additionalContext } }));
|
|
146
|
+
}
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check if files were edited multiple times but buglog.json wasn't updated.
|
|
151
|
+
* Emit a stderr reminder so Claude sees it in the next turn.
|
|
152
|
+
*/
|
|
153
|
+
function checkForMissingBugLogs(wolfDir, session) {
|
|
154
|
+
if (!session.edit_counts)
|
|
155
|
+
return null;
|
|
156
|
+
const multiEditFiles = Object.entries(session.edit_counts)
|
|
157
|
+
.filter(([, count]) => count >= 3)
|
|
158
|
+
.map(([file]) => path.basename(file));
|
|
159
|
+
if (multiEditFiles.length === 0)
|
|
160
|
+
return null;
|
|
161
|
+
// Check if buglog was written to this session
|
|
162
|
+
const buglogWritten = session.files_written.some(w => w.file.includes("buglog.json"));
|
|
163
|
+
if (!buglogWritten) {
|
|
164
|
+
return `Files edited 3+ times this session (${multiEditFiles.join(", ")}) but buglog.json was not updated. If you fixed bugs, log them to .wolf/buglog.json.`;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if cerebrum.md was updated recently. If it hasn't been updated in
|
|
170
|
+
* a while and there was significant activity, return a gentle reminder.
|
|
171
|
+
*/
|
|
172
|
+
function checkCerebrumFreshness(wolfDir, session) {
|
|
173
|
+
const cerebrumPath = path.join(wolfDir, "cerebrum.md");
|
|
174
|
+
try {
|
|
175
|
+
const stat = fs.statSync(cerebrumPath);
|
|
176
|
+
const hoursSinceUpdate = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60);
|
|
177
|
+
// If cerebrum hasn't been updated in 24h+ and there were significant writes
|
|
178
|
+
if (hoursSinceUpdate > 24 && session.files_written.length >= 3) {
|
|
179
|
+
return `cerebrum.md hasn't been updated in ${Math.floor(hoursSinceUpdate)}h. Did you learn any user preferences, conventions, or gotchas this session? Consider updating .wolf/cerebrum.md.`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// cerebrum.md doesn't exist, that's ok
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* If STATUS.md is older than the session start (or missing) and there was meaningful code
|
|
189
|
+
* activity, nudge Claude to update the handoff doc so the next /clear resumes cheaply. (upstream #40)
|
|
190
|
+
*/
|
|
191
|
+
function checkStatusFreshness(wolfDir, session) {
|
|
192
|
+
const statusPath = path.join(wolfDir, "STATUS.md");
|
|
193
|
+
const codeWrites = session.files_written.filter((w) => !w.file.includes("/.wolf/") && !w.file.endsWith(".tmp"));
|
|
194
|
+
if (codeWrites.length < 3)
|
|
195
|
+
return null;
|
|
196
|
+
try {
|
|
197
|
+
const stat = fs.statSync(statusPath);
|
|
198
|
+
const sessionStartMs = session.started ? Date.parse(session.started) : 0;
|
|
199
|
+
if (sessionStartMs && stat.mtimeMs < sessionStartMs) {
|
|
200
|
+
return `STATUS.md wasn't updated this session despite ${codeWrites.length} code writes. Update .wolf/STATUS.md (✅ done / 🚀 next quest) before /clear so the next session resumes in one read.`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return `.wolf/STATUS.md is missing. Create it with the current quest summary + next steps so /clear stays cheap.`;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* If there were meaningful edits this session but no non-mechanical memory.md summary
|
|
210
|
+
* was written, nudge Claude to record what it did. (upstream #55)
|
|
211
|
+
*/
|
|
212
|
+
function checkSemanticSummaries(wolfDir, writeCount) {
|
|
213
|
+
if (writeCount < 3)
|
|
214
|
+
return null;
|
|
215
|
+
if (countSemanticEntries(wolfDir) > 0)
|
|
216
|
+
return null;
|
|
217
|
+
return `${writeCount} files were changed this session but no meaningful summary was written to memory.md. Consider recording what you did and why.`;
|
|
218
|
+
}
|
|
219
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bug-matcher.js","sourceRoot":"","sources":["../../../src/buglog/bug-matcher.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { readJSON, writeJSON } from "../utils/fs-safe.js";
|
|
3
|
+
export function getBugLogPath(wolfDir) {
|
|
4
|
+
return path.join(wolfDir, "buglog.json");
|
|
5
|
+
}
|
|
6
|
+
export function readBugLog(wolfDir) {
|
|
7
|
+
return readJSON(getBugLogPath(wolfDir), { version: 1, bugs: [] });
|
|
8
|
+
}
|
|
9
|
+
export function logBug(wolfDir, bug) {
|
|
10
|
+
const bugLog = readBugLog(wolfDir);
|
|
11
|
+
const now = new Date().toISOString();
|
|
12
|
+
// Check for near-duplicate (score > 0.8)
|
|
13
|
+
const similar = findSimilarBugs(wolfDir, bug.error_message);
|
|
14
|
+
if (similar.length > 0 && similar[0].score > 0.8) {
|
|
15
|
+
const existing = bugLog.bugs.find((b) => b.id === similar[0].bug.id);
|
|
16
|
+
if (existing) {
|
|
17
|
+
existing.occurrences++;
|
|
18
|
+
existing.last_seen = now;
|
|
19
|
+
writeJSON(getBugLogPath(wolfDir), bugLog);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const id = `bug-${String(bugLog.bugs.length + 1).padStart(3, "0")}`;
|
|
24
|
+
bugLog.bugs.push({
|
|
25
|
+
id,
|
|
26
|
+
timestamp: now,
|
|
27
|
+
error_message: bug.error_message,
|
|
28
|
+
file: bug.file,
|
|
29
|
+
line: bug.line,
|
|
30
|
+
root_cause: bug.root_cause,
|
|
31
|
+
fix: bug.fix,
|
|
32
|
+
tags: bug.tags,
|
|
33
|
+
related_bugs: [],
|
|
34
|
+
occurrences: 1,
|
|
35
|
+
last_seen: now,
|
|
36
|
+
});
|
|
37
|
+
writeJSON(getBugLogPath(wolfDir), bugLog);
|
|
38
|
+
}
|
|
39
|
+
function normalize(text) {
|
|
40
|
+
// Tolerate entries missing fields (legacy / auto-detected / schema drift) — upstream #44.
|
|
41
|
+
return String(text ?? "").toLowerCase().replace(/\d+/g, "N").replace(/[^\w\s]/g, " ").trim();
|
|
42
|
+
}
|
|
43
|
+
function tokenize(text) {
|
|
44
|
+
return new Set(normalize(text).split(/\s+/).filter((w) => w.length > 2));
|
|
45
|
+
}
|
|
46
|
+
function jaccardSimilarity(a, b) {
|
|
47
|
+
const intersection = new Set([...a].filter((x) => b.has(x)));
|
|
48
|
+
const union = new Set([...a, ...b]);
|
|
49
|
+
return union.size === 0 ? 0 : intersection.size / union.size;
|
|
50
|
+
}
|
|
51
|
+
export function findSimilarBugs(wolfDir, errorMessage) {
|
|
52
|
+
const bugLog = readBugLog(wolfDir);
|
|
53
|
+
const normalizedInput = normalize(errorMessage);
|
|
54
|
+
const inputTokens = tokenize(errorMessage);
|
|
55
|
+
const results = [];
|
|
56
|
+
for (const bug of bugLog.bugs) {
|
|
57
|
+
let score = 0;
|
|
58
|
+
// Exact substring match
|
|
59
|
+
if (normalize(bug.error_message).includes(normalizedInput) ||
|
|
60
|
+
normalizedInput.includes(normalize(bug.error_message))) {
|
|
61
|
+
score += 1.0;
|
|
62
|
+
}
|
|
63
|
+
// Word overlap (jaccard)
|
|
64
|
+
const bugTokens = tokenize(bug.error_message);
|
|
65
|
+
score += jaccardSimilarity(inputTokens, bugTokens) * 0.5;
|
|
66
|
+
if (score > 0.3) {
|
|
67
|
+
results.push({ bug, score });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
results.sort((a, b) => b.score - a.score);
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
export function searchBugs(wolfDir, term) {
|
|
74
|
+
const bugLog = readBugLog(wolfDir);
|
|
75
|
+
const lower = term.toLowerCase();
|
|
76
|
+
// Null-safe across schema drift: any entry may miss a field, `tags` may be absent,
|
|
77
|
+
// and some tooling records `files: string[]` instead of a singular `file` (upstream #44).
|
|
78
|
+
const has = (v) => typeof v === "string" && v.toLowerCase().includes(lower);
|
|
79
|
+
return bugLog.bugs.filter((b) => {
|
|
80
|
+
const bug = b;
|
|
81
|
+
const files = Array.isArray(bug.files) ? bug.files : bug.file != null ? [bug.file] : [];
|
|
82
|
+
return (has(bug.error_message) ||
|
|
83
|
+
has(bug.root_cause) ||
|
|
84
|
+
has(bug.fix) ||
|
|
85
|
+
(Array.isArray(bug.tags) && bug.tags.some((t) => has(t))) ||
|
|
86
|
+
files.some((f) => has(f)));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=bug-tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bug-tracker.js","sourceRoot":"","sources":["../../../src/buglog/bug-tracker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { findProjectRoot } from "../scanner/project-root.js";
|
|
4
|
+
import { searchBugs } from "../buglog/bug-tracker.js";
|
|
5
|
+
export function bugSearch(term) {
|
|
6
|
+
const projectRoot = findProjectRoot();
|
|
7
|
+
const wolfDir = path.join(projectRoot, ".wolf");
|
|
8
|
+
if (!fs.existsSync(wolfDir)) {
|
|
9
|
+
console.log("OpenWolf not initialized. Run: openwolf init");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const results = searchBugs(wolfDir, term);
|
|
13
|
+
if (results.length === 0) {
|
|
14
|
+
console.log(`No bugs found matching "${term}".`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.log(`Found ${results.length} matching bug(s):\n`);
|
|
18
|
+
for (const b of results) {
|
|
19
|
+
// Null-safe display: entries can be missing fields or use files[] (schema drift, #44).
|
|
20
|
+
const bug = b;
|
|
21
|
+
const files = Array.isArray(bug.files) ? bug.files.join(", ") : (bug.file ?? "—");
|
|
22
|
+
console.log(` [${bug.id ?? "?"}] ${(bug.error_message ?? "(no message)").slice(0, 80)}`);
|
|
23
|
+
console.log(` File: ${files}${bug.line ? `:${bug.line}` : ""}`);
|
|
24
|
+
console.log(` Root cause: ${bug.root_cause ?? "—"}`);
|
|
25
|
+
console.log(` Fix: ${bug.fix ?? "—"}`);
|
|
26
|
+
console.log(` Tags: ${Array.isArray(bug.tags) ? bug.tags.filter(Boolean).join(", ") : "—"}`);
|
|
27
|
+
console.log(` Occurrences: ${bug.occurrences ?? 1} | Last seen: ${bug.last_seen ?? "—"}`);
|
|
28
|
+
console.log("");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=bug-cmd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bug-cmd.js","sourceRoot":"","sources":["../../../src/cli/bug-cmd.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,WAAW,GAAG,eAAe,EAAE,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAEhD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;QAC5D,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAE1C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,IAAI,CAAC,CAAC;QACjD,OAAO;IACT,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,SAAS,OAAO,CAAC,MAAM,qBAAqB,CAAC,CAAC;IAE1D,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,uFAAuF;QACvF,MAAM,GAAG,GAAG,CAAoC,CAAC;QACjD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;QAClF,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,aAAa,IAAI,cAAc,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAC1F,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACnE,OAAO,CAAC,GAAG,CAAC,mBAAmB,GAAG,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC,CAAC;QACxD,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QAChG,OAAO,CAAC,GAAG,CAAC,oBAAoB,GAAG,CAAC,WAAW,IAAI,CAAC,iBAAiB,GAAG,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC,CAAC;QAC7F,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
|