openwolf-enhanced 1.26.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 +306 -0
- package/dist/dashboard/assets/{AISuggestions-DZmzksSA.js → AISuggestions-C7WFvzLO.js} +1 -1
- package/dist/dashboard/assets/{ActivityLog-BJ0omuzu.js → ActivityLog-SsRbpBFt.js} +1 -1
- package/dist/dashboard/assets/{ActivityTimeline-BWhxretI.js → ActivityTimeline-BoV30nDY.js} +1 -1
- package/dist/dashboard/assets/{AnatomyBrowser-DNVp6-_r.js → AnatomyBrowser-DwzsHn4z.js} +1 -1
- package/dist/dashboard/assets/{BugLog-DMJTuU9p.js → BugLog-CPl4Q-JG.js} +1 -1
- package/dist/dashboard/assets/{CerebrumViewer-BwcGr6pu.js → CerebrumViewer-_f6fBsNF.js} +1 -1
- package/dist/dashboard/assets/{CronStatus-Dwf_M4-S.js → CronStatus-1iBEPJK5.js} +1 -1
- package/dist/dashboard/assets/{DesignQC-DzHkh94I.js → DesignQC-D4UU7lca.js} +1 -1
- package/dist/dashboard/assets/{LinkGraph-kqQD--Yj.js → LinkGraph-BWtI8Gh3.js} +1 -1
- package/dist/dashboard/assets/{MemoryViewer-BCn9Fr-w.js → MemoryViewer-BYRVG34g.js} +1 -1
- package/dist/dashboard/assets/{NativeMemory-DPMZaP5E.js → NativeMemory-Dphtrqjt.js} +1 -1
- package/dist/dashboard/assets/{ProjectOverview-PWENJiC4.js → ProjectOverview-CTYohvjD.js} +1 -1
- package/dist/dashboard/assets/{ProjectsAggregate-Baj44DVk.js → ProjectsAggregate-CMxNs35H.js} +1 -1
- package/dist/dashboard/assets/{StatusBadge-Db61D51H.js → StatusBadge-CjrFCDOI.js} +1 -1
- package/dist/dashboard/assets/{TokenUsage-UefDh9w4.js → TokenUsage-C5wJpMl8.js} +1 -1
- package/dist/dashboard/assets/index-BNi8lsJE.css +2 -0
- package/dist/dashboard/assets/{index-e2PfOI0b.js → index-C74qneS9.js} +5 -5
- package/dist/dashboard/index.html +2 -2
- package/dist/hooks/post-bash.js +10 -5
- package/dist/hooks/post-read.js +24 -26
- package/dist/hooks/post-write.js +135 -118
- package/dist/hooks/pre-read.js +42 -36
- package/dist/hooks/pre-write.js +3 -3
- package/dist/hooks/precompact.js +2 -2
- package/dist/hooks/session-start.js +15 -6
- package/dist/hooks/shared.js +221 -14
- package/dist/hooks/stop.js +33 -7
- 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 +49 -21
- package/dist/src/cli/daemon-cmd.js.map +1 -1
- package/dist/src/cli/doctor-cmd.js +2 -1
- package/dist/src/cli/doctor-cmd.js.map +1 -1
- package/dist/src/cli/export-cmd.js +8 -0
- package/dist/src/cli/export-cmd.js.map +1 -1
- package/dist/src/cli/init.js +34 -8
- 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 +37 -6
- package/dist/src/cli/update.js.map +1 -1
- package/dist/src/daemon/cron-engine.js +70 -8
- 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/llm-provider.js +9 -6
- package/dist/src/daemon/llm-provider.js.map +1 -1
- package/dist/src/daemon/wolf-daemon.js +69 -15
- package/dist/src/daemon/wolf-daemon.js.map +1 -1
- package/dist/src/designqc/designqc-capture.js +116 -6
- package/dist/src/designqc/designqc-capture.js.map +1 -1
- package/dist/src/designqc/designqc-engine.js +9 -3
- package/dist/src/designqc/designqc-engine.js.map +1 -1
- package/dist/src/hooks/post-bash.js +10 -5
- package/dist/src/hooks/post-bash.js.map +1 -1
- package/dist/src/hooks/post-read.js +24 -26
- package/dist/src/hooks/post-read.js.map +1 -1
- package/dist/src/hooks/post-write.js +135 -118
- package/dist/src/hooks/post-write.js.map +1 -1
- package/dist/src/hooks/pre-read.js +42 -36
- 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 +15 -6
- package/dist/src/hooks/session-start.js.map +1 -1
- package/dist/src/hooks/shared.js +221 -14
- package/dist/src/hooks/shared.js.map +1 -1
- package/dist/src/hooks/stop.js +33 -7
- 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/embeddings.js +9 -1
- package/dist/src/utils/embeddings.js.map +1 -1
- package/dist/src/utils/fs-safe.js +69 -11
- package/dist/src/utils/fs-safe.js.map +1 -1
- package/dist/src/utils/logger.js +21 -6
- package/dist/src/utils/logger.js.map +1 -1
- package/dist/src/utils/maintenance.js +100 -6
- package/dist/src/utils/maintenance.js.map +1 -1
- package/dist/src/utils/platform.js +39 -0
- package/dist/src/utils/platform.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 +34 -9
- package/dist/dashboard/assets/index-CVfvTpSA.css +0 -2
|
@@ -7,8 +7,8 @@
|
|
|
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-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-C74qneS9.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BNi8lsJE.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
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
|
|
@@ -66,7 +66,12 @@ async function main() {
|
|
|
66
66
|
// COMMAND position of each shell segment, not the whole string — the old `\bopenwolf\b` skipped
|
|
67
67
|
// every command that merely mentioned an openwolf PATH, which made the write counter blind for
|
|
68
68
|
// exactly the sessions that work on an openwolf checkout.
|
|
69
|
-
|
|
69
|
+
// Newlines separate commands exactly like `;` does, and they were missing from this split: a
|
|
70
|
+
// two-line Bash block whose second line ran `openwolf push` was tested as ONE segment starting
|
|
71
|
+
// with the first line's command, so the filter saw no openwolf invocation and captured the whole
|
|
72
|
+
// thing. Multi-line blocks are the normal shape of an agent's shell call, so this was the common
|
|
73
|
+
// case, not the edge one.
|
|
74
|
+
const invokesOpenwolf = cmd.split(/&&|\|\||[;|\n\r]/).some(segmentInvokesOpenwolf);
|
|
70
75
|
if (invokesOpenwolf) {
|
|
71
76
|
process.exit(0);
|
|
72
77
|
return;
|
|
@@ -79,8 +84,8 @@ async function main() {
|
|
|
79
84
|
// install exactly as blind as the bug it fixes. [bug-149]
|
|
80
85
|
if (!failed && isFileWritingCommand(cmd)) {
|
|
81
86
|
try {
|
|
82
|
-
const sessionFile = path.join(wolfDir, "hooks",
|
|
83
|
-
|
|
87
|
+
const sessionFile = sessionFileFor(path.join(wolfDir, "hooks"), input.session_id);
|
|
88
|
+
tryWithLock(sessionFile, () => {
|
|
84
89
|
const session = readJSON(sessionFile, { files_written: [], edit_counts: {} });
|
|
85
90
|
session.bash_writes = (session.bash_writes ?? 0) + 1;
|
|
86
91
|
writeJSON(sessionFile, session);
|
|
@@ -103,7 +108,7 @@ async function main() {
|
|
|
103
108
|
// concurrent Bash hooks don't clobber each other's read-modify-write.
|
|
104
109
|
try {
|
|
105
110
|
const logPath = path.join(wolfDir, "activity.log");
|
|
106
|
-
|
|
111
|
+
tryWithLock(logPath, () => {
|
|
107
112
|
let existing = "";
|
|
108
113
|
try {
|
|
109
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,
|
|
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.
|
|
@@ -57,7 +55,7 @@ async function main() {
|
|
|
57
55
|
for (const [sectionKey, entries] of sections) {
|
|
58
56
|
for (const entry of entries) {
|
|
59
57
|
const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
|
|
60
|
-
if (
|
|
58
|
+
if (matchesAnatomyEntry(normalizedFile, entryRelPath)) {
|
|
61
59
|
tokens = entry.tokens;
|
|
62
60
|
break;
|
|
63
61
|
}
|
|
@@ -66,24 +64,24 @@ async function main() {
|
|
|
66
64
|
break;
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
67
|
+
updateSession(sessionFile, { files_read: {} }, (session) => {
|
|
68
|
+
if (session.files_read[normalizedFile]) {
|
|
69
|
+
// Never let a re-read shrink the estimate to zero. A repeat read often arrives with an
|
|
70
|
+
// empty tool_output.content, and if the file has no anatomy entry the fallback above
|
|
71
|
+
// cannot recover a number — overwriting unconditionally would wipe the good first-read
|
|
72
|
+
// estimate. That deflates inputTokens in stop.ts and, because savedFromRepeats multiplies
|
|
73
|
+
// by (count - 1), silently zeroes the repeat-savings metric too.
|
|
74
|
+
const prev = session.files_read[normalizedFile].tokens ?? 0;
|
|
75
|
+
session.files_read[normalizedFile].tokens = Math.max(prev, tokens);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
session.files_read[normalizedFile] = {
|
|
79
|
+
count: 1,
|
|
80
|
+
tokens,
|
|
81
|
+
first_read: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
87
85
|
process.exit(0);
|
|
88
86
|
}
|
|
89
87
|
main().catch(() => process.exit(0));
|
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,
|
|
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) {
|
|
@@ -37,10 +37,10 @@ async function main() {
|
|
|
37
37
|
// nothing happened, and the STATUS.md reminder never fires. That is exactly how a handoff doc goes
|
|
38
38
|
// stale for eleven slices without anyone noticing. The counter is a number only: no path, no name,
|
|
39
39
|
// so #56 still holds.
|
|
40
|
-
if (relPath
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
if (isOutsideProject(relPath)) {
|
|
41
|
+
updateSession(sessionFile, { files_written: [], edit_counts: {} }, (session) => {
|
|
42
|
+
session.external_writes = (session.external_writes ?? 0) + 1;
|
|
43
|
+
});
|
|
44
44
|
process.exit(0);
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
@@ -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";
|
|
@@ -133,24 +141,28 @@ async function main() {
|
|
|
133
141
|
catch { }
|
|
134
142
|
// 3. Record in session tracker + track edit counts
|
|
135
143
|
try {
|
|
136
|
-
const session = readJSON(sessionFile, { files_written: [], edit_counts: {} });
|
|
137
|
-
if (!session.edit_counts)
|
|
138
|
-
session.edit_counts = {};
|
|
139
144
|
const normalizedFile = normalizePath(filePath);
|
|
140
145
|
const action = toolName === "Write" ? "create" : "edit";
|
|
141
146
|
const fileContent = input.tool_input?.content ?? "";
|
|
142
147
|
const tokens = estimateFileTokens(fileContent || writtenText, absolutePath, getTokenRatios(wolfDir));
|
|
143
|
-
session.files_written.push({
|
|
144
|
-
file: normalizedFile,
|
|
145
|
-
action,
|
|
146
|
-
tokens,
|
|
147
|
-
at: new Date().toISOString(),
|
|
148
|
-
});
|
|
149
148
|
const editKey = normalizePath(path.relative(projectRoot, absolutePath));
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
let edits = 0;
|
|
150
|
+
updateSession(sessionFile, { files_written: [], edit_counts: {} }, (session) => {
|
|
151
|
+
if (!session.files_written)
|
|
152
|
+
session.files_written = [];
|
|
153
|
+
if (!session.edit_counts)
|
|
154
|
+
session.edit_counts = {};
|
|
155
|
+
session.files_written.push({
|
|
156
|
+
file: normalizedFile,
|
|
157
|
+
action,
|
|
158
|
+
tokens,
|
|
159
|
+
at: new Date().toISOString(),
|
|
160
|
+
});
|
|
161
|
+
session.edit_counts[editKey] = (session.edit_counts[editKey] || 0) + 1;
|
|
162
|
+
edits = session.edit_counts[editKey];
|
|
163
|
+
});
|
|
164
|
+
if (edits >= 3) {
|
|
165
|
+
process.stderr.write(`⚠️ OpenWolf: ${baseName} has been edited ${edits} times this session. If you're fixing a bug, remember to log it to .wolf/buglog.json.\n`);
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
168
|
catch { }
|
|
@@ -254,61 +266,66 @@ function extractCalls(code) {
|
|
|
254
266
|
// ─── Auto Bug Detection ──────────────────────────────────────────
|
|
255
267
|
function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) {
|
|
256
268
|
const bugLogPath = path.join(wolfDir, "buglog.json");
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
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);
|
|
283
326
|
}
|
|
284
327
|
writeJSON(bugLogPath, bugLog);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
// Derive id from the max existing numeric id so it stays unique even after the
|
|
288
|
-
// bugs[] array is trimmed below (bugs.length + 1 would collide after a trim).
|
|
289
|
-
const maxId = bugLog.bugs.reduce((m, b) => {
|
|
290
|
-
const n = parseInt(String(b.id).replace(/\D/g, ""), 10);
|
|
291
|
-
return Number.isFinite(n) && n > m ? n : m;
|
|
292
|
-
}, 0);
|
|
293
|
-
const nextId = `bug-${String(maxId + 1).padStart(3, "0")}`;
|
|
294
|
-
bugLog.bugs.push({
|
|
295
|
-
id: nextId,
|
|
296
|
-
timestamp: new Date().toISOString(),
|
|
297
|
-
error_message: detection.summary,
|
|
298
|
-
file: relFile,
|
|
299
|
-
root_cause: detection.rootCause,
|
|
300
|
-
fix: detection.fix,
|
|
301
|
-
tags: ["auto-detected", detection.category, ext.replace(".", "") || "unknown"],
|
|
302
|
-
related_bugs: [],
|
|
303
|
-
occurrences: 1,
|
|
304
|
-
last_seen: new Date().toISOString(),
|
|
305
328
|
});
|
|
306
|
-
// Keep buglog.json bounded — auto-detection can otherwise append on nearly every edit.
|
|
307
|
-
const maxBugs = getRetention(wolfDir).buglog_max_entries;
|
|
308
|
-
if (bugLog.bugs.length > maxBugs) {
|
|
309
|
-
bugLog.bugs = bugLog.bugs.slice(-maxBugs);
|
|
310
|
-
}
|
|
311
|
-
writeJSON(bugLogPath, bugLog);
|
|
312
329
|
}
|
|
313
330
|
function detectFixPattern(oldStr, newStr, ext, fileLabel) {
|
|
314
331
|
const oldLines = oldStr.split("\n");
|
package/dist/hooks/pre-read.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getWolfDir, ensureWolfDir, readJSON,
|
|
3
|
+
import { getWolfDir, ensureWolfDir, readJSON, updateSession, readMarkdown, parseAnatomy, readStdin, normalizePath, loadIgnore, isSecretFile, matchesAnatomyEntry, relativeToProject, sessionFileFor } from "./shared.js";
|
|
4
|
+
const SESSION_FALLBACK = {
|
|
5
|
+
session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0, repeated_reads_warned: 0,
|
|
6
|
+
};
|
|
4
7
|
async function main() {
|
|
5
8
|
ensureWolfDir();
|
|
6
9
|
const wolfDir = getWolfDir();
|
|
7
10
|
const hooksDir = path.join(wolfDir, "hooks");
|
|
8
|
-
const sessionFile = path.join(hooksDir, "_session.json");
|
|
9
11
|
const raw = await readStdin();
|
|
10
12
|
let input;
|
|
11
13
|
try {
|
|
@@ -15,6 +17,8 @@ async function main() {
|
|
|
15
17
|
process.exit(0);
|
|
16
18
|
return;
|
|
17
19
|
}
|
|
20
|
+
// After the parse, not before: the payload is what says which session this is.
|
|
21
|
+
const sessionFile = sessionFileFor(hooksDir, input.session_id);
|
|
18
22
|
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
|
19
23
|
if (!filePath) {
|
|
20
24
|
process.exit(0);
|
|
@@ -30,11 +34,7 @@ async function main() {
|
|
|
30
34
|
// Skip tracking for .wolf/ internal files — they're infrastructure, not project files.
|
|
31
35
|
// Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning.
|
|
32
36
|
const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
33
|
-
|
|
34
|
-
// sibling directories sharing the prefix (/root/orderflow2 counted as inside /root/orderflow).
|
|
35
|
-
const relToProject = normalizedFile.startsWith(projectDir + "/")
|
|
36
|
-
? normalizedFile.slice(projectDir.length + 1)
|
|
37
|
-
: "";
|
|
37
|
+
const relToProject = relativeToProject(filePath, projectDir);
|
|
38
38
|
// Don't track files outside the project root (upstream #56).
|
|
39
39
|
if (!relToProject) {
|
|
40
40
|
process.exit(0);
|
|
@@ -56,27 +56,34 @@ async function main() {
|
|
|
56
56
|
process.exit(0);
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
59
|
+
// Snapshot read, used only to DECIDE what to print. Every mutation below re-reads under the lock
|
|
60
|
+
// and applies a delta, so a concurrent hook's update is never overwritten with this snapshot.
|
|
61
|
+
const session = readJSON(sessionFile, SESSION_FALLBACK);
|
|
63
62
|
// Check if already read this session
|
|
64
63
|
if (session.files_read[normalizedFile]) {
|
|
65
64
|
const prev = session.files_read[normalizedFile];
|
|
66
65
|
// If the file changed since we last read it (e.g. it was edited during the session),
|
|
67
66
|
// a re-read is legitimate — refresh state and don't warn about a repeated read (upstream #41).
|
|
68
67
|
if (fileMtime !== undefined && prev.mtime !== undefined && fileMtime !== prev.mtime) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
updateSession(sessionFile, SESSION_FALLBACK, (s) => {
|
|
69
|
+
const e = s.files_read[normalizedFile];
|
|
70
|
+
if (e) {
|
|
71
|
+
e.count++;
|
|
72
|
+
e.mtime = fileMtime;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
72
75
|
process.exit(0);
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
75
78
|
process.stderr.write(`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
updateSession(sessionFile, SESSION_FALLBACK, (s) => {
|
|
80
|
+
const e = s.files_read[normalizedFile];
|
|
81
|
+
if (e) {
|
|
82
|
+
e.count++;
|
|
83
|
+
e.mtime = fileMtime ?? e.mtime;
|
|
84
|
+
}
|
|
85
|
+
s.repeated_reads_warned++;
|
|
86
|
+
});
|
|
80
87
|
process.exit(0);
|
|
81
88
|
return;
|
|
82
89
|
}
|
|
@@ -90,7 +97,7 @@ async function main() {
|
|
|
90
97
|
for (const entry of entries) {
|
|
91
98
|
// Build the full relative path from the section key + filename for accurate matching
|
|
92
99
|
const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
|
|
93
|
-
if (
|
|
100
|
+
if (matchesAnatomyEntry(normalizedFile, entryRelPath)) {
|
|
94
101
|
process.stderr.write(`📋 OpenWolf anatomy: ${entry.file} — ${entry.description} (~${entry.tokens} tok)\n`);
|
|
95
102
|
// If we indexed this big file's symbols, offer line ranges so the agent can read one
|
|
96
103
|
// function with offset/limit instead of the whole file.
|
|
@@ -107,23 +114,22 @@ async function main() {
|
|
|
107
114
|
if (found)
|
|
108
115
|
break;
|
|
109
116
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
};
|
|
126
|
-
writeJSON(sessionFile, session);
|
|
117
|
+
updateSession(sessionFile, SESSION_FALLBACK, (s) => {
|
|
118
|
+
if (found)
|
|
119
|
+
s.anatomy_hits++;
|
|
120
|
+
else
|
|
121
|
+
s.anatomy_misses++;
|
|
122
|
+
// Record initial read entry (tokens will be updated in post-read)
|
|
123
|
+
s.files_read[normalizedFile] = {
|
|
124
|
+
count: 1,
|
|
125
|
+
tokens: 0,
|
|
126
|
+
first_read: new Date().toISOString(),
|
|
127
|
+
// Whether anatomy.md already described this file. The ledger used to hardcode this as
|
|
128
|
+
// false, which made the waste detector's "anatomy would have sufficed" pattern unreachable.
|
|
129
|
+
anatomy_had_description: found,
|
|
130
|
+
mtime: fileMtime,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
127
133
|
process.exit(0);
|
|
128
134
|
}
|
|
129
135
|
main().catch(() => 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",
|