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.
Files changed (77) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/dist/dashboard/assets/{AISuggestions-DBUq3WQW.js → AISuggestions-C7WFvzLO.js} +1 -1
  3. package/dist/dashboard/assets/{ActivityLog-DzvER0xw.js → ActivityLog-SsRbpBFt.js} +1 -1
  4. package/dist/dashboard/assets/{ActivityTimeline-DVx1o9Of.js → ActivityTimeline-BoV30nDY.js} +1 -1
  5. package/dist/dashboard/assets/{AnatomyBrowser-CQGTp2MR.js → AnatomyBrowser-DwzsHn4z.js} +1 -1
  6. package/dist/dashboard/assets/{BugLog-Ka23MuzX.js → BugLog-CPl4Q-JG.js} +1 -1
  7. package/dist/dashboard/assets/{CerebrumViewer-2SO_QPQn.js → CerebrumViewer-_f6fBsNF.js} +1 -1
  8. package/dist/dashboard/assets/{CronStatus-BSJ7o-Xc.js → CronStatus-1iBEPJK5.js} +1 -1
  9. package/dist/dashboard/assets/{DesignQC-D7Btjv4l.js → DesignQC-D4UU7lca.js} +1 -1
  10. package/dist/dashboard/assets/{LinkGraph-Cj0m-9HI.js → LinkGraph-BWtI8Gh3.js} +1 -1
  11. package/dist/dashboard/assets/{MemoryViewer-DQ_OGTaW.js → MemoryViewer-BYRVG34g.js} +1 -1
  12. package/dist/dashboard/assets/{NativeMemory-BLa2fOaz.js → NativeMemory-Dphtrqjt.js} +1 -1
  13. package/dist/dashboard/assets/{ProjectOverview-DK4qaJsJ.js → ProjectOverview-CTYohvjD.js} +1 -1
  14. package/dist/dashboard/assets/{ProjectsAggregate-DW8TBuH2.js → ProjectsAggregate-CMxNs35H.js} +1 -1
  15. package/dist/dashboard/assets/{StatusBadge-Bclc5R-9.js → StatusBadge-CjrFCDOI.js} +1 -1
  16. package/dist/dashboard/assets/{TokenUsage-BwhsNFH4.js → TokenUsage-C5wJpMl8.js} +1 -1
  17. package/dist/dashboard/assets/{index-DsimHkSD.js → index-C74qneS9.js} +3 -3
  18. package/dist/dashboard/index.html +1 -1
  19. package/dist/hooks/post-bash.js +4 -4
  20. package/dist/hooks/post-read.js +5 -7
  21. package/dist/hooks/post-write.js +114 -101
  22. package/dist/hooks/pre-read.js +4 -7
  23. package/dist/hooks/pre-write.js +3 -3
  24. package/dist/hooks/precompact.js +2 -2
  25. package/dist/hooks/session-start.js +11 -4
  26. package/dist/hooks/shared.js +139 -13
  27. package/dist/hooks/stop.js +9 -3
  28. package/dist/src/cli/cron-cmd.js +2 -2
  29. package/dist/src/cli/cron-cmd.js.map +1 -1
  30. package/dist/src/cli/daemon-cmd.js +40 -14
  31. package/dist/src/cli/daemon-cmd.js.map +1 -1
  32. package/dist/src/cli/init.js +11 -4
  33. package/dist/src/cli/init.js.map +1 -1
  34. package/dist/src/cli/registry.js +68 -33
  35. package/dist/src/cli/registry.js.map +1 -1
  36. package/dist/src/cli/update.js +27 -4
  37. package/dist/src/cli/update.js.map +1 -1
  38. package/dist/src/daemon/cron-engine.js +23 -5
  39. package/dist/src/daemon/cron-engine.js.map +1 -1
  40. package/dist/src/daemon/file-watcher.js +4 -1
  41. package/dist/src/daemon/file-watcher.js.map +1 -1
  42. package/dist/src/daemon/wolf-daemon.js +17 -3
  43. package/dist/src/daemon/wolf-daemon.js.map +1 -1
  44. package/dist/src/hooks/post-bash.js +4 -4
  45. package/dist/src/hooks/post-bash.js.map +1 -1
  46. package/dist/src/hooks/post-read.js +5 -7
  47. package/dist/src/hooks/post-read.js.map +1 -1
  48. package/dist/src/hooks/post-write.js +114 -101
  49. package/dist/src/hooks/post-write.js.map +1 -1
  50. package/dist/src/hooks/pre-read.js +4 -7
  51. package/dist/src/hooks/pre-read.js.map +1 -1
  52. package/dist/src/hooks/pre-write.js +3 -3
  53. package/dist/src/hooks/pre-write.js.map +1 -1
  54. package/dist/src/hooks/precompact.js +2 -2
  55. package/dist/src/hooks/precompact.js.map +1 -1
  56. package/dist/src/hooks/session-start.js +11 -4
  57. package/dist/src/hooks/session-start.js.map +1 -1
  58. package/dist/src/hooks/shared.js +139 -13
  59. package/dist/src/hooks/shared.js.map +1 -1
  60. package/dist/src/hooks/stop.js +9 -3
  61. package/dist/src/hooks/stop.js.map +1 -1
  62. package/dist/src/scanner/anatomy-scanner.js +7 -3
  63. package/dist/src/scanner/anatomy-scanner.js.map +1 -1
  64. package/dist/src/utils/agent-hooks.js +50 -4
  65. package/dist/src/utils/agent-hooks.js.map +1 -1
  66. package/dist/src/utils/daemon-pid.js +56 -0
  67. package/dist/src/utils/daemon-pid.js.map +1 -0
  68. package/dist/src/utils/dashboard-auth.js +13 -1
  69. package/dist/src/utils/dashboard-auth.js.map +1 -1
  70. package/dist/src/utils/fs-safe.js +54 -10
  71. package/dist/src/utils/fs-safe.js.map +1 -1
  72. package/dist/src/utils/maintenance.js +56 -5
  73. package/dist/src/utils/maintenance.js.map +1 -1
  74. package/dist/src/utils/remote.js +2 -2
  75. package/dist/src/utils/remote.js.map +1 -1
  76. package/package.json +1 -1
  77. 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-DsimHkSD.js"></script>
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>
@@ -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, withLock, readJSON, writeJSON, } from "./shared.js";
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", "_session.json");
88
- withLock(sessionFile, () => {
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
- withLock(logPath, () => {
111
+ tryWithLock(logPath, () => {
112
112
  let existing = "";
113
113
  try {
114
114
  existing = fs.readFileSync(logPath, "utf8");
@@ -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
- // Separator required after the root — see pre-read (sibling-prefix directories, e.g.
27
- // /root/orderflow2, must not count as inside /root/orderflow).
28
- const relToProject = normalizedFile.startsWith(projectDir + "/")
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.
@@ -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
- try {
69
- const anatomyPath = path.join(wolfDir, "anatomy.md");
70
- let anatomyContent;
71
- try {
72
- anatomyContent = fs.readFileSync(anatomyPath, "utf-8");
73
- }
74
- catch {
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
- fileContent = fs.readFileSync(absolutePath, "utf-8");
85
- }
86
- catch {
87
- fileContent = input.tool_input?.content ?? "";
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
- else {
104
- entries.push({ file: fileName, description: desc, tokens });
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
- catch { }
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
- const bugLog = readBugLog(wolfDir); // tolerates legacy array format
262
- const relFile = normalizePath(path.relative(projectRoot, absolutePath));
263
- const basename = path.basename(absolutePath);
264
- const ext = path.extname(basename).toLowerCase();
265
- // Detect what kind of fix this is
266
- const detection = detectFixPattern(oldStr, newStr, ext, path.basename(relFile));
267
- if (!detection)
268
- return;
269
- // Check for recent duplicate (same file + same category within 30 min).
270
- // Widened from 5 min to cut down on repeated auto-detected entries for the same file.
271
- const recentDupe = bugLog.bugs.find(b => {
272
- if (path.basename(b.file) !== basename)
273
- return false;
274
- if (!b.tags.includes("auto-detected"))
275
- return false;
276
- if (!b.tags.includes(detection.category))
277
- return false;
278
- const bugTime = new Date(b.last_seen).getTime();
279
- return (Date.now() - bugTime) < 30 * 60 * 1000;
280
- });
281
- if (recentDupe) {
282
- recentDupe.occurrences++;
283
- recentDupe.last_seen = new Date().toISOString();
284
- // Append additional context
285
- if (detection.context && !recentDupe.fix.includes(detection.context)) {
286
- recentDupe.fix += ` | Also: ${detection.context}`;
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");
@@ -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
- // Require the path SEPARATOR after the root — a bare startsWith(projectDir) also matched
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);
@@ -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, withLock } from "./shared.js";
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", "_session.json");
33
- withLock(sessionFile, () => {
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);
@@ -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(path.join(hooksDir, "_session.json"), {});
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, withLock, SESSION_SUMMARY_SCAFFOLD, bookInjection } from "./shared.js";
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
- withLock(sessionFile, () => writeJSON(sessionFile, {
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
- withLock(ledgerPath, () => {
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);