openwolf-enhanced 1.27.0 → 1.28.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.
- package/CHANGELOG.md +131 -0
- package/dist/dashboard/assets/{AISuggestions-DBUq3WQW.js → AISuggestions-C7WFvzLO.js} +1 -1
- package/dist/dashboard/assets/{ActivityLog-DzvER0xw.js → ActivityLog-SsRbpBFt.js} +1 -1
- package/dist/dashboard/assets/{ActivityTimeline-DVx1o9Of.js → ActivityTimeline-BoV30nDY.js} +1 -1
- package/dist/dashboard/assets/{AnatomyBrowser-CQGTp2MR.js → AnatomyBrowser-DwzsHn4z.js} +1 -1
- package/dist/dashboard/assets/{BugLog-Ka23MuzX.js → BugLog-CPl4Q-JG.js} +1 -1
- package/dist/dashboard/assets/{CerebrumViewer-2SO_QPQn.js → CerebrumViewer-_f6fBsNF.js} +1 -1
- package/dist/dashboard/assets/{CronStatus-BSJ7o-Xc.js → CronStatus-1iBEPJK5.js} +1 -1
- package/dist/dashboard/assets/{DesignQC-D7Btjv4l.js → DesignQC-D4UU7lca.js} +1 -1
- package/dist/dashboard/assets/{LinkGraph-Cj0m-9HI.js → LinkGraph-BWtI8Gh3.js} +1 -1
- package/dist/dashboard/assets/{MemoryViewer-DQ_OGTaW.js → MemoryViewer-BYRVG34g.js} +1 -1
- package/dist/dashboard/assets/{NativeMemory-BLa2fOaz.js → NativeMemory-Dphtrqjt.js} +1 -1
- package/dist/dashboard/assets/{ProjectOverview-DK4qaJsJ.js → ProjectOverview-CTYohvjD.js} +1 -1
- package/dist/dashboard/assets/{ProjectsAggregate-DW8TBuH2.js → ProjectsAggregate-CMxNs35H.js} +1 -1
- package/dist/dashboard/assets/{StatusBadge-Bclc5R-9.js → StatusBadge-CjrFCDOI.js} +1 -1
- package/dist/dashboard/assets/{TokenUsage-BwhsNFH4.js → TokenUsage-C5wJpMl8.js} +1 -1
- package/dist/dashboard/assets/{index-DsimHkSD.js → index-C74qneS9.js} +3 -3
- package/dist/dashboard/index.html +1 -1
- package/dist/hooks/post-bash.js +4 -4
- package/dist/hooks/post-read.js +5 -7
- package/dist/hooks/post-write.js +114 -101
- package/dist/hooks/pre-read.js +4 -7
- package/dist/hooks/pre-write.js +3 -3
- package/dist/hooks/precompact.js +2 -2
- package/dist/hooks/session-start.js +11 -4
- package/dist/hooks/shared.js +139 -13
- package/dist/hooks/stop.js +9 -3
- package/dist/src/cli/cron-cmd.js +2 -2
- package/dist/src/cli/cron-cmd.js.map +1 -1
- package/dist/src/cli/daemon-cmd.js +40 -14
- package/dist/src/cli/daemon-cmd.js.map +1 -1
- package/dist/src/cli/init.js +11 -4
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/registry.js +68 -33
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/cli/update.js +27 -4
- package/dist/src/cli/update.js.map +1 -1
- package/dist/src/daemon/cron-engine.js +23 -5
- package/dist/src/daemon/cron-engine.js.map +1 -1
- package/dist/src/daemon/file-watcher.js +4 -1
- package/dist/src/daemon/file-watcher.js.map +1 -1
- package/dist/src/daemon/wolf-daemon.js +17 -3
- package/dist/src/daemon/wolf-daemon.js.map +1 -1
- package/dist/src/hooks/post-bash.js +4 -4
- package/dist/src/hooks/post-bash.js.map +1 -1
- package/dist/src/hooks/post-read.js +5 -7
- package/dist/src/hooks/post-read.js.map +1 -1
- package/dist/src/hooks/post-write.js +114 -101
- package/dist/src/hooks/post-write.js.map +1 -1
- package/dist/src/hooks/pre-read.js +4 -7
- package/dist/src/hooks/pre-read.js.map +1 -1
- package/dist/src/hooks/pre-write.js +3 -3
- package/dist/src/hooks/pre-write.js.map +1 -1
- package/dist/src/hooks/precompact.js +2 -2
- package/dist/src/hooks/precompact.js.map +1 -1
- package/dist/src/hooks/session-start.js +11 -4
- package/dist/src/hooks/session-start.js.map +1 -1
- package/dist/src/hooks/shared.js +139 -13
- package/dist/src/hooks/shared.js.map +1 -1
- package/dist/src/hooks/stop.js +9 -3
- package/dist/src/hooks/stop.js.map +1 -1
- package/dist/src/scanner/anatomy-scanner.js +7 -3
- package/dist/src/scanner/anatomy-scanner.js.map +1 -1
- package/dist/src/utils/agent-hooks.js +50 -4
- package/dist/src/utils/agent-hooks.js.map +1 -1
- package/dist/src/utils/daemon-pid.js +56 -0
- package/dist/src/utils/daemon-pid.js.map +1 -0
- package/dist/src/utils/dashboard-auth.js +13 -1
- package/dist/src/utils/dashboard-auth.js.map +1 -1
- package/dist/src/utils/fs-safe.js +54 -10
- package/dist/src/utils/fs-safe.js.map +1 -1
- package/dist/src/utils/maintenance.js +56 -5
- package/dist/src/utils/maintenance.js.map +1 -1
- package/dist/src/utils/remote.js +2 -2
- package/dist/src/utils/remote.js.map +1 -1
- package/package.json +1 -1
- package/src/templates/config.json +32 -8
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
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-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-C74qneS9.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="./assets/index-BNi8lsJE.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
package/dist/hooks/post-bash.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getWolfDir, ensureWolfDir, getCaptureConfig, redactSecrets, isFileWritingCommand, isNotableCommand, tailWithinBytes, timeShort, readStdin,
|
|
3
|
+
import { getWolfDir, ensureWolfDir, getCaptureConfig, redactSecrets, isFileWritingCommand, isNotableCommand, tailWithinBytes, timeShort, readStdin, tryWithLock, readJSON, writeJSON, sessionFileFor, } from "./shared.js";
|
|
4
4
|
// PostToolUse:Bash — two jobs.
|
|
5
5
|
//
|
|
6
6
|
// 1. Count file-writing shell commands into the session tracker. post-write.ts only matches
|
|
@@ -84,8 +84,8 @@ async function main() {
|
|
|
84
84
|
// install exactly as blind as the bug it fixes. [bug-149]
|
|
85
85
|
if (!failed && isFileWritingCommand(cmd)) {
|
|
86
86
|
try {
|
|
87
|
-
const sessionFile = path.join(wolfDir, "hooks",
|
|
88
|
-
|
|
87
|
+
const sessionFile = sessionFileFor(path.join(wolfDir, "hooks"), input.session_id);
|
|
88
|
+
tryWithLock(sessionFile, () => {
|
|
89
89
|
const session = readJSON(sessionFile, { files_written: [], edit_counts: {} });
|
|
90
90
|
session.bash_writes = (session.bash_writes ?? 0) + 1;
|
|
91
91
|
writeJSON(sessionFile, session);
|
|
@@ -108,7 +108,7 @@ async function main() {
|
|
|
108
108
|
// concurrent Bash hooks don't clobber each other's read-modify-write.
|
|
109
109
|
try {
|
|
110
110
|
const logPath = path.join(wolfDir, "activity.log");
|
|
111
|
-
|
|
111
|
+
tryWithLock(logPath, () => {
|
|
112
112
|
let existing = "";
|
|
113
113
|
try {
|
|
114
114
|
existing = fs.readFileSync(logPath, "utf8");
|
package/dist/hooks/post-read.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { getWolfDir, ensureWolfDir, updateSession, readMarkdown, parseAnatomy, estimateFileTokens, getTokenRatios, readStdin, normalizePath, loadIgnore, isSecretFile, matchesAnatomyEntry } from "./shared.js";
|
|
2
|
+
import { getWolfDir, ensureWolfDir, updateSession, readMarkdown, parseAnatomy, estimateFileTokens, getTokenRatios, readStdin, normalizePath, loadIgnore, isSecretFile, matchesAnatomyEntry, relativeToProject, sessionFileFor } from "./shared.js";
|
|
3
3
|
async function main() {
|
|
4
4
|
ensureWolfDir();
|
|
5
5
|
const wolfDir = getWolfDir();
|
|
6
6
|
const hooksDir = path.join(wolfDir, "hooks");
|
|
7
|
-
const sessionFile = path.join(hooksDir, "_session.json");
|
|
8
7
|
const raw = await readStdin();
|
|
9
8
|
let input;
|
|
10
9
|
try {
|
|
@@ -14,6 +13,7 @@ async function main() {
|
|
|
14
13
|
process.exit(0);
|
|
15
14
|
return;
|
|
16
15
|
}
|
|
16
|
+
const sessionFile = sessionFileFor(hooksDir, input.session_id);
|
|
17
17
|
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
|
18
18
|
const content = input.tool_output?.content ?? "";
|
|
19
19
|
if (!filePath) {
|
|
@@ -23,11 +23,9 @@ async function main() {
|
|
|
23
23
|
const normalizedFile = normalizePath(filePath);
|
|
24
24
|
// Skip tracking for .wolf/ internal files — consistent with pre-read
|
|
25
25
|
const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
const relToProject =
|
|
29
|
-
? normalizedFile.slice(projectDir.length + 1)
|
|
30
|
-
: "";
|
|
26
|
+
// Shared with pre-read: separator-anchored, with a realpath second opinion so a project reached
|
|
27
|
+
// through a symlink is not silently dropped from tracking.
|
|
28
|
+
const relToProject = relativeToProject(filePath, projectDir);
|
|
31
29
|
// Don't track reads of files outside the project root (upstream #56). relToProject is ""
|
|
32
30
|
// both when the path is outside projectDir and when it equals the root itself — neither
|
|
33
31
|
// is a trackable project file.
|
package/dist/hooks/post-write.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getWolfDir, ensureWolfDir, writeJSON, writeAtomic, parseAnatomy, serializeAnatomy, extractDescription, estimateFileTokens, getTokenRatios, appendMarkdown, timeShort, readStdin, normalizePath, getRetention, loadIgnore, readBugLog, isSecretFile, updateSession, isOutsideProject } from "./shared.js";
|
|
3
|
+
import { getWolfDir, ensureWolfDir, writeJSON, writeAtomic, parseAnatomy, serializeAnatomy, extractDescription, estimateFileTokens, getTokenRatios, appendMarkdown, timeShort, readStdin, normalizePath, getRetention, loadIgnore, readBugLog, isSecretFile, updateSession, isOutsideProject, tryWithLock, sessionFileFor } from "./shared.js";
|
|
4
4
|
async function main() {
|
|
5
5
|
ensureWolfDir();
|
|
6
6
|
const wolfDir = getWolfDir();
|
|
7
7
|
const hooksDir = path.join(wolfDir, "hooks");
|
|
8
|
-
const sessionFile = path.join(hooksDir, "_session.json");
|
|
9
8
|
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
10
9
|
const raw = await readStdin();
|
|
11
10
|
let input;
|
|
@@ -16,6 +15,7 @@ async function main() {
|
|
|
16
15
|
process.exit(0);
|
|
17
16
|
return;
|
|
18
17
|
}
|
|
18
|
+
const sessionFile = sessionFileFor(hooksDir, input.session_id);
|
|
19
19
|
const toolName = input.tool_name ?? "Write";
|
|
20
20
|
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
|
21
21
|
if (!filePath) {
|
|
@@ -65,57 +65,65 @@ async function main() {
|
|
|
65
65
|
? multiEdits.map((e) => e?.new_string ?? "").join("\n")
|
|
66
66
|
: newStr;
|
|
67
67
|
// 1. Update anatomy.md
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
anatomyContent = "# anatomy.md\n\n> Auto-maintained by OpenWolf.\n";
|
|
76
|
-
}
|
|
77
|
-
const sections = parseAnatomy(anatomyContent);
|
|
78
|
-
const relPathLocal = normalizePath(path.relative(projectRoot, absolutePath));
|
|
79
|
-
const dir = path.dirname(relPathLocal);
|
|
80
|
-
const fileName = path.basename(relPathLocal);
|
|
81
|
-
const sectionKey = dir === "." ? "./" : dir + "/";
|
|
82
|
-
let fileContent = "";
|
|
68
|
+
//
|
|
69
|
+
// Under the lock for its whole read-modify-write cycle. writeAtomic() prevents a TORN file, not a
|
|
70
|
+
// LOST update: two post-write hooks that each parse the index, add their own entry and serialise
|
|
71
|
+
// it both produce valid markdown, and the second one silently drops the first one's file. On a
|
|
72
|
+
// multi-edit turn that is the normal case, and the missing entry only shows up much later as an
|
|
73
|
+
// anatomy miss for a file that was indexed.
|
|
74
|
+
tryWithLock(path.join(wolfDir, "anatomy.md"), () => {
|
|
83
75
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
const desc = extractDescription(absolutePath, fileContent).slice(0, 100);
|
|
90
|
-
const tokens = estimateFileTokens(fileContent, absolutePath, getTokenRatios(wolfDir));
|
|
91
|
-
if (!sections.has(sectionKey))
|
|
92
|
-
sections.set(sectionKey, []);
|
|
93
|
-
const entries = sections.get(sectionKey);
|
|
94
|
-
const idx = entries.findIndex((e) => e.file === fileName);
|
|
95
|
-
const existing = idx !== -1 ? entries[idx] : undefined;
|
|
96
|
-
// Skip the full anatomy.md rewrite when nothing changed for this file.
|
|
97
|
-
// Repeated edits to the same file (same description + token estimate) are the common
|
|
98
|
-
// case; rewriting the entire anatomy.md on every one of them is pure overhead.
|
|
99
|
-
if (!existing || existing.description !== desc || existing.tokens !== tokens) {
|
|
100
|
-
if (idx !== -1) {
|
|
101
|
-
entries[idx] = { file: fileName, description: desc, tokens };
|
|
76
|
+
const anatomyPath = path.join(wolfDir, "anatomy.md");
|
|
77
|
+
let anatomyContent;
|
|
78
|
+
try {
|
|
79
|
+
anatomyContent = fs.readFileSync(anatomyPath, "utf-8");
|
|
102
80
|
}
|
|
103
|
-
|
|
104
|
-
|
|
81
|
+
catch {
|
|
82
|
+
anatomyContent = "# anatomy.md\n\n> Auto-maintained by OpenWolf.\n";
|
|
83
|
+
}
|
|
84
|
+
const sections = parseAnatomy(anatomyContent);
|
|
85
|
+
const relPathLocal = normalizePath(path.relative(projectRoot, absolutePath));
|
|
86
|
+
const dir = path.dirname(relPathLocal);
|
|
87
|
+
const fileName = path.basename(relPathLocal);
|
|
88
|
+
const sectionKey = dir === "." ? "./" : dir + "/";
|
|
89
|
+
let fileContent = "";
|
|
90
|
+
try {
|
|
91
|
+
fileContent = fs.readFileSync(absolutePath, "utf-8");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
fileContent = input.tool_input?.content ?? "";
|
|
95
|
+
}
|
|
96
|
+
const desc = extractDescription(absolutePath, fileContent).slice(0, 100);
|
|
97
|
+
const tokens = estimateFileTokens(fileContent, absolutePath, getTokenRatios(wolfDir));
|
|
98
|
+
if (!sections.has(sectionKey))
|
|
99
|
+
sections.set(sectionKey, []);
|
|
100
|
+
const entries = sections.get(sectionKey);
|
|
101
|
+
const idx = entries.findIndex((e) => e.file === fileName);
|
|
102
|
+
const existing = idx !== -1 ? entries[idx] : undefined;
|
|
103
|
+
// Skip the full anatomy.md rewrite when nothing changed for this file.
|
|
104
|
+
// Repeated edits to the same file (same description + token estimate) are the common
|
|
105
|
+
// case; rewriting the entire anatomy.md on every one of them is pure overhead.
|
|
106
|
+
if (!existing || existing.description !== desc || existing.tokens !== tokens) {
|
|
107
|
+
if (idx !== -1) {
|
|
108
|
+
entries[idx] = { file: fileName, description: desc, tokens };
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
entries.push({ file: fileName, description: desc, tokens });
|
|
112
|
+
}
|
|
113
|
+
let fileCount = 0;
|
|
114
|
+
for (const [, list] of sections)
|
|
115
|
+
fileCount += list.length;
|
|
116
|
+
const serialized = serializeAnatomy(sections, {
|
|
117
|
+
lastScanned: new Date().toISOString(),
|
|
118
|
+
fileCount,
|
|
119
|
+
hits: 0,
|
|
120
|
+
misses: 0,
|
|
121
|
+
});
|
|
122
|
+
writeAtomic(anatomyPath, () => serialized);
|
|
105
123
|
}
|
|
106
|
-
let fileCount = 0;
|
|
107
|
-
for (const [, list] of sections)
|
|
108
|
-
fileCount += list.length;
|
|
109
|
-
const serialized = serializeAnatomy(sections, {
|
|
110
|
-
lastScanned: new Date().toISOString(),
|
|
111
|
-
fileCount,
|
|
112
|
-
hits: 0,
|
|
113
|
-
misses: 0,
|
|
114
|
-
});
|
|
115
|
-
writeAtomic(anatomyPath, () => serialized);
|
|
116
124
|
}
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
catch { }
|
|
126
|
+
});
|
|
119
127
|
// 2. Append richer entry to memory.md
|
|
120
128
|
try {
|
|
121
129
|
const action = toolName === "Write" ? "Created" : toolName === "MultiEdit" ? "Multi-edited" : "Edited";
|
|
@@ -258,61 +266,66 @@ function extractCalls(code) {
|
|
|
258
266
|
// ─── Auto Bug Detection ──────────────────────────────────────────
|
|
259
267
|
function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) {
|
|
260
268
|
const bugLogPath = path.join(wolfDir, "buglog.json");
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (
|
|
286
|
-
recentDupe.
|
|
269
|
+
// The read has to happen INSIDE the lock together with the write. Reading first and writing later
|
|
270
|
+
// is the lost-update shape: two edits in the same turn both computed `bug-041` from the same
|
|
271
|
+
// snapshot, and the second write erased the first entry while reusing its id.
|
|
272
|
+
tryWithLock(bugLogPath, () => {
|
|
273
|
+
const bugLog = readBugLog(wolfDir); // tolerates legacy array format
|
|
274
|
+
const relFile = normalizePath(path.relative(projectRoot, absolutePath));
|
|
275
|
+
const basename = path.basename(absolutePath);
|
|
276
|
+
const ext = path.extname(basename).toLowerCase();
|
|
277
|
+
// Detect what kind of fix this is
|
|
278
|
+
const detection = detectFixPattern(oldStr, newStr, ext, path.basename(relFile));
|
|
279
|
+
if (!detection)
|
|
280
|
+
return; // returns from the locked callback, not from autoDetectBugFix
|
|
281
|
+
// Check for recent duplicate (same file + same category within 30 min).
|
|
282
|
+
// Widened from 5 min to cut down on repeated auto-detected entries for the same file.
|
|
283
|
+
const recentDupe = bugLog.bugs.find(b => {
|
|
284
|
+
if (path.basename(b.file) !== basename)
|
|
285
|
+
return false;
|
|
286
|
+
if (!b.tags.includes("auto-detected"))
|
|
287
|
+
return false;
|
|
288
|
+
if (!b.tags.includes(detection.category))
|
|
289
|
+
return false;
|
|
290
|
+
const bugTime = new Date(b.last_seen).getTime();
|
|
291
|
+
return (Date.now() - bugTime) < 30 * 60 * 1000;
|
|
292
|
+
});
|
|
293
|
+
if (recentDupe) {
|
|
294
|
+
recentDupe.occurrences++;
|
|
295
|
+
recentDupe.last_seen = new Date().toISOString();
|
|
296
|
+
// Append additional context
|
|
297
|
+
if (detection.context && !recentDupe.fix.includes(detection.context)) {
|
|
298
|
+
recentDupe.fix += ` | Also: ${detection.context}`;
|
|
299
|
+
}
|
|
300
|
+
writeJSON(bugLogPath, bugLog);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Derive id from the max existing numeric id so it stays unique even after the
|
|
304
|
+
// bugs[] array is trimmed below (bugs.length + 1 would collide after a trim).
|
|
305
|
+
const maxId = bugLog.bugs.reduce((m, b) => {
|
|
306
|
+
const n = parseInt(String(b.id).replace(/\D/g, ""), 10);
|
|
307
|
+
return Number.isFinite(n) && n > m ? n : m;
|
|
308
|
+
}, 0);
|
|
309
|
+
const nextId = `bug-${String(maxId + 1).padStart(3, "0")}`;
|
|
310
|
+
bugLog.bugs.push({
|
|
311
|
+
id: nextId,
|
|
312
|
+
timestamp: new Date().toISOString(),
|
|
313
|
+
error_message: detection.summary,
|
|
314
|
+
file: relFile,
|
|
315
|
+
root_cause: detection.rootCause,
|
|
316
|
+
fix: detection.fix,
|
|
317
|
+
tags: ["auto-detected", detection.category, ext.replace(".", "") || "unknown"],
|
|
318
|
+
related_bugs: [],
|
|
319
|
+
occurrences: 1,
|
|
320
|
+
last_seen: new Date().toISOString(),
|
|
321
|
+
});
|
|
322
|
+
// Keep buglog.json bounded — auto-detection can otherwise append on nearly every edit.
|
|
323
|
+
const maxBugs = getRetention(wolfDir).buglog_max_entries;
|
|
324
|
+
if (bugLog.bugs.length > maxBugs) {
|
|
325
|
+
bugLog.bugs = bugLog.bugs.slice(-maxBugs);
|
|
287
326
|
}
|
|
288
327
|
writeJSON(bugLogPath, bugLog);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
// Derive id from the max existing numeric id so it stays unique even after the
|
|
292
|
-
// bugs[] array is trimmed below (bugs.length + 1 would collide after a trim).
|
|
293
|
-
const maxId = bugLog.bugs.reduce((m, b) => {
|
|
294
|
-
const n = parseInt(String(b.id).replace(/\D/g, ""), 10);
|
|
295
|
-
return Number.isFinite(n) && n > m ? n : m;
|
|
296
|
-
}, 0);
|
|
297
|
-
const nextId = `bug-${String(maxId + 1).padStart(3, "0")}`;
|
|
298
|
-
bugLog.bugs.push({
|
|
299
|
-
id: nextId,
|
|
300
|
-
timestamp: new Date().toISOString(),
|
|
301
|
-
error_message: detection.summary,
|
|
302
|
-
file: relFile,
|
|
303
|
-
root_cause: detection.rootCause,
|
|
304
|
-
fix: detection.fix,
|
|
305
|
-
tags: ["auto-detected", detection.category, ext.replace(".", "") || "unknown"],
|
|
306
|
-
related_bugs: [],
|
|
307
|
-
occurrences: 1,
|
|
308
|
-
last_seen: new Date().toISOString(),
|
|
309
328
|
});
|
|
310
|
-
// Keep buglog.json bounded — auto-detection can otherwise append on nearly every edit.
|
|
311
|
-
const maxBugs = getRetention(wolfDir).buglog_max_entries;
|
|
312
|
-
if (bugLog.bugs.length > maxBugs) {
|
|
313
|
-
bugLog.bugs = bugLog.bugs.slice(-maxBugs);
|
|
314
|
-
}
|
|
315
|
-
writeJSON(bugLogPath, bugLog);
|
|
316
329
|
}
|
|
317
330
|
function detectFixPattern(oldStr, newStr, ext, fileLabel) {
|
|
318
331
|
const oldLines = oldStr.split("\n");
|
package/dist/hooks/pre-read.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getWolfDir, ensureWolfDir, readJSON, updateSession, readMarkdown, parseAnatomy, readStdin, normalizePath, loadIgnore, isSecretFile, matchesAnatomyEntry } from "./shared.js";
|
|
3
|
+
import { getWolfDir, ensureWolfDir, readJSON, updateSession, readMarkdown, parseAnatomy, readStdin, normalizePath, loadIgnore, isSecretFile, matchesAnatomyEntry, relativeToProject, sessionFileFor } from "./shared.js";
|
|
4
4
|
const SESSION_FALLBACK = {
|
|
5
5
|
session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0, repeated_reads_warned: 0,
|
|
6
6
|
};
|
|
@@ -8,7 +8,6 @@ async function main() {
|
|
|
8
8
|
ensureWolfDir();
|
|
9
9
|
const wolfDir = getWolfDir();
|
|
10
10
|
const hooksDir = path.join(wolfDir, "hooks");
|
|
11
|
-
const sessionFile = path.join(hooksDir, "_session.json");
|
|
12
11
|
const raw = await readStdin();
|
|
13
12
|
let input;
|
|
14
13
|
try {
|
|
@@ -18,6 +17,8 @@ async function main() {
|
|
|
18
17
|
process.exit(0);
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
20
|
+
// After the parse, not before: the payload is what says which session this is.
|
|
21
|
+
const sessionFile = sessionFileFor(hooksDir, input.session_id);
|
|
21
22
|
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
|
22
23
|
if (!filePath) {
|
|
23
24
|
process.exit(0);
|
|
@@ -33,11 +34,7 @@ async function main() {
|
|
|
33
34
|
// Skip tracking for .wolf/ internal files — they're infrastructure, not project files.
|
|
34
35
|
// Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning.
|
|
35
36
|
const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
36
|
-
|
|
37
|
-
// sibling directories sharing the prefix (/root/orderflow2 counted as inside /root/orderflow).
|
|
38
|
-
const relToProject = normalizedFile.startsWith(projectDir + "/")
|
|
39
|
-
? normalizedFile.slice(projectDir.length + 1)
|
|
40
|
-
: "";
|
|
37
|
+
const relToProject = relativeToProject(filePath, projectDir);
|
|
41
38
|
// Don't track files outside the project root (upstream #56).
|
|
42
39
|
if (!relToProject) {
|
|
43
40
|
process.exit(0);
|
package/dist/hooks/pre-write.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, readStdin, readBugLog,
|
|
3
|
+
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, readStdin, readBugLog, tryWithLock, sessionFileFor } from "./shared.js";
|
|
4
4
|
async function main() {
|
|
5
5
|
ensureWolfDir();
|
|
6
6
|
const wolfDir = getWolfDir();
|
|
@@ -29,8 +29,8 @@ async function main() {
|
|
|
29
29
|
// ever incremented it, so the ledger/dashboard always showed 0.
|
|
30
30
|
if (cerebrumWarnings > 0) {
|
|
31
31
|
try {
|
|
32
|
-
const sessionFile = path.join(wolfDir, "hooks",
|
|
33
|
-
|
|
32
|
+
const sessionFile = sessionFileFor(path.join(wolfDir, "hooks"), input.session_id);
|
|
33
|
+
tryWithLock(sessionFile, () => {
|
|
34
34
|
const session = readJSON(sessionFile, {});
|
|
35
35
|
session.cerebrum_warnings = (session.cerebrum_warnings ?? 0) + cerebrumWarnings;
|
|
36
36
|
writeJSON(sessionFile, session);
|
package/dist/hooks/precompact.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readStdin, timestamp } from "./shared.js";
|
|
2
|
+
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readStdin, timestamp, sessionFileFor } from "./shared.js";
|
|
3
3
|
// PreCompact hook — compaction survival.
|
|
4
4
|
//
|
|
5
5
|
// Fires just before Claude Code / Codex compacts the context window. The in-flight session state
|
|
@@ -17,7 +17,7 @@ async function main() {
|
|
|
17
17
|
}
|
|
18
18
|
catch { }
|
|
19
19
|
try {
|
|
20
|
-
const session = readJSON(
|
|
20
|
+
const session = readJSON(sessionFileFor(hooksDir, input.session_id), {});
|
|
21
21
|
writeJSON(path.join(hooksDir, "_precompact-snapshot.json"), {
|
|
22
22
|
at: timestamp(),
|
|
23
23
|
trigger: input.trigger ?? "unknown",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort, buildResumeDigest, readStdin,
|
|
3
|
+
import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort, buildResumeDigest, readStdin, tryWithLock, sessionFileFor, pruneOldSessionFiles, SESSION_SUMMARY_SCAFFOLD, bookInjection } from "./shared.js";
|
|
4
4
|
import { estimateTokens, getTokenRatios } from "./token-estimator.js";
|
|
5
5
|
async function main() {
|
|
6
6
|
ensureWolfDir();
|
|
@@ -19,7 +19,6 @@ async function main() {
|
|
|
19
19
|
}
|
|
20
20
|
catch { }
|
|
21
21
|
const hooksDir = path.join(wolfDir, "hooks");
|
|
22
|
-
const sessionFile = path.join(hooksDir, "_session.json");
|
|
23
22
|
const now = new Date();
|
|
24
23
|
const sessionId = `session-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
|
25
24
|
// SessionStart fires for startup/resume/clear/compact. Only startup and clear begin a genuinely
|
|
@@ -27,18 +26,26 @@ async function main() {
|
|
|
27
26
|
// would wipe this session's read/write tracking mid-flight and append a spurious memory header
|
|
28
27
|
// on every compaction (compaction survival).
|
|
29
28
|
let source = "startup";
|
|
29
|
+
let harnessSessionId;
|
|
30
30
|
try {
|
|
31
31
|
const hookInput = JSON.parse(await readStdin());
|
|
32
32
|
if (typeof hookInput.source === "string")
|
|
33
33
|
source = hookInput.source;
|
|
34
|
+
if (typeof hookInput.session_id === "string")
|
|
35
|
+
harnessSessionId = hookInput.session_id;
|
|
34
36
|
}
|
|
35
37
|
catch { }
|
|
38
|
+
// One state file per session (see sessionFileFor). Two agents in the same project used to share
|
|
39
|
+
// one, so the second one's startup wiped the first one's in-flight tracking.
|
|
40
|
+
const sessionFile = sessionFileFor(hooksDir, harnessSessionId);
|
|
41
|
+
// One file per session accumulates; a finished session is already folded into the ledger.
|
|
42
|
+
pruneOldSessionFiles(hooksDir);
|
|
36
43
|
const continuing = (source === "compact" || source === "resume") && fs.existsSync(sessionFile);
|
|
37
44
|
if (!continuing) {
|
|
38
45
|
// Create fresh session state. Under the lock like every other writer of this file: the reset is
|
|
39
46
|
// a whole-file replacement, so an unlocked write here could land in the middle of another
|
|
40
47
|
// hook's read-modify-write and hand it a half-written file to parse.
|
|
41
|
-
|
|
48
|
+
tryWithLock(sessionFile, () => writeJSON(sessionFile, {
|
|
42
49
|
session_id: sessionId,
|
|
43
50
|
started: timestamp(),
|
|
44
51
|
files_read: {},
|
|
@@ -89,7 +96,7 @@ async function main() {
|
|
|
89
96
|
// Same lock as the stop hook's ledger write — a session ending in one process while another
|
|
90
97
|
// starts must not clobber the read-modify-write.
|
|
91
98
|
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
|
92
|
-
|
|
99
|
+
tryWithLock(ledgerPath, () => {
|
|
93
100
|
const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } });
|
|
94
101
|
ledger.lifetime.total_sessions++;
|
|
95
102
|
writeJSON(ledgerPath, ledger);
|