openwolf-enhanced 1.20.2 → 1.20.4

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 (52) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/dashboard/assets/{AISuggestions-CFK_2uAF.js → AISuggestions-CtOxHIl9.js} +1 -1
  3. package/dist/dashboard/assets/{ActivityLog-CCOHsoDc.js → ActivityLog-CFN5LTgT.js} +1 -1
  4. package/dist/dashboard/assets/{ActivityTimeline-BhZHCuoG.js → ActivityTimeline-DUFGSBZE.js} +1 -1
  5. package/dist/dashboard/assets/{AnatomyBrowser-B5Of1HOO.js → AnatomyBrowser-kTbfRkpC.js} +1 -1
  6. package/dist/dashboard/assets/{BugLog-BqlIbKOd.js → BugLog-B1OZI3_G.js} +1 -1
  7. package/dist/dashboard/assets/{CerebrumViewer-Do6Mh7gd.js → CerebrumViewer-BBNL_KNO.js} +1 -1
  8. package/dist/dashboard/assets/{CronStatus-y_GxP3-1.js → CronStatus-GEJfvNJ6.js} +1 -1
  9. package/dist/dashboard/assets/{DesignQC-BqcR7OWX.js → DesignQC-BL3xQxKL.js} +1 -1
  10. package/dist/dashboard/assets/{MemoryViewer-Dnj_oixJ.js → MemoryViewer-CM4NJTuH.js} +1 -1
  11. package/dist/dashboard/assets/{NativeMemory-DotOF3kS.js → NativeMemory-SQQ20A77.js} +1 -1
  12. package/dist/dashboard/assets/{ProjectOverview-BEzUoJk1.js → ProjectOverview-Bo1BwDba.js} +1 -1
  13. package/dist/dashboard/assets/{ProjectsAggregate-paEMmTgD.js → ProjectsAggregate-By41p6Ih.js} +1 -1
  14. package/dist/dashboard/assets/{StatusBadge-Cpbb5e1W.js → StatusBadge-BJrvWVqs.js} +1 -1
  15. package/dist/dashboard/assets/{TokenUsage-ByUBdw7G.js → TokenUsage-CloSdJB2.js} +1 -1
  16. package/dist/dashboard/assets/{index-Dp-E2wpx.js → index-HpYzFcBu.js} +3 -3
  17. package/dist/dashboard/index.html +1 -1
  18. package/dist/hooks/post-bash.js +11 -6
  19. package/dist/hooks/post-read.js +4 -2
  20. package/dist/hooks/post-write.js +24 -30
  21. package/dist/hooks/pre-read.js +12 -3
  22. package/dist/hooks/pre-write.js +29 -5
  23. package/dist/hooks/session-start.js +8 -4
  24. package/dist/hooks/shared.js +79 -30
  25. package/dist/hooks/stop.js +4 -1
  26. package/dist/src/buglog/bug-tracker.js +7 -3
  27. package/dist/src/buglog/bug-tracker.js.map +1 -1
  28. package/dist/src/daemon/cron-engine.js +7 -50
  29. package/dist/src/daemon/cron-engine.js.map +1 -1
  30. package/dist/src/daemon/file-watcher.js +5 -1
  31. package/dist/src/daemon/file-watcher.js.map +1 -1
  32. package/dist/src/hooks/post-bash.js +11 -6
  33. package/dist/src/hooks/post-bash.js.map +1 -1
  34. package/dist/src/hooks/post-read.js +4 -2
  35. package/dist/src/hooks/post-read.js.map +1 -1
  36. package/dist/src/hooks/post-write.js +24 -30
  37. package/dist/src/hooks/post-write.js.map +1 -1
  38. package/dist/src/hooks/pre-read.js +12 -3
  39. package/dist/src/hooks/pre-read.js.map +1 -1
  40. package/dist/src/hooks/pre-write.js +29 -5
  41. package/dist/src/hooks/pre-write.js.map +1 -1
  42. package/dist/src/hooks/session-start.js +8 -4
  43. package/dist/src/hooks/session-start.js.map +1 -1
  44. package/dist/src/hooks/shared.js +79 -30
  45. package/dist/src/hooks/shared.js.map +1 -1
  46. package/dist/src/hooks/stop.js +4 -1
  47. package/dist/src/hooks/stop.js.map +1 -1
  48. package/dist/src/tracker/token-ledger.js +5 -30
  49. package/dist/src/tracker/token-ledger.js.map +1 -1
  50. package/dist/src/utils/maintenance.js +44 -25
  51. package/dist/src/utils/maintenance.js.map +1 -1
  52. package/package.json +1 -1
@@ -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-Dp-E2wpx.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-HpYzFcBu.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="./assets/index-Cr3yWM5G.css">
12
12
  </head>
13
13
  <body>
@@ -12,10 +12,9 @@ function classifyOutcome(resp) {
12
12
  }
13
13
  if (r.interrupted === true || r.is_error === true || r.isError === true)
14
14
  return "error";
15
- const stdout = typeof r.stdout === "string" ? r.stdout : "";
16
- const stderr = typeof r.stderr === "string" ? r.stderr : "";
17
- if (stderr.trim() && !stdout.trim())
18
- return "error";
15
+ // NOTE: no "stderr but no stdout" heuristic plenty of successful commands write only to
16
+ // stderr (git checkout progress, curl -v, warnings). It misclassified them as failures,
17
+ // undercounting bash_writes and mislabeling activity.log entries. Explicit signals only.
19
18
  }
20
19
  if (typeof resp === "string" && /(^|\s)(error|failed|command not found)\b|exit code [1-9]/i.test(resp)) {
21
20
  return "error";
@@ -39,8 +38,14 @@ async function main() {
39
38
  process.exit(0);
40
39
  return;
41
40
  }
42
- // Never capture the capture-reading commands themselves, to avoid feedback noise.
43
- if (/\bopenwolf\b/.test(cmd)) {
41
+ // Never capture invocations of the openwolf CLI itself, to avoid feedback noise. Match the
42
+ // COMMAND position of each shell segment, not the whole string — the old `\bopenwolf\b` skipped
43
+ // every command that merely mentioned an openwolf PATH, which made the write counter blind for
44
+ // exactly the sessions that work on an openwolf checkout.
45
+ const invokesOpenwolf = cmd
46
+ .split(/&&|\|\||[;|]/)
47
+ .some((seg) => /^(?:\s*[A-Za-z_]\w*=\S*)*\s*(?:\S*\/)?openwolf(?:\s|$)/.test(seg));
48
+ if (invokesOpenwolf) {
44
49
  process.exit(0);
45
50
  return;
46
51
  }
@@ -23,8 +23,10 @@ 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
- const relToProject = normalizedFile.startsWith(projectDir)
27
- ? normalizedFile.slice(projectDir.length).replace(/^\//, "")
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)
28
30
  : "";
29
31
  // Don't track reads of files outside the project root (upstream #56). relToProject is ""
30
32
  // both when the path is outside projectDir and when it equals the root itself — neither
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import * as crypto from "node:crypto";
4
- import { getWolfDir, ensureWolfDir, readJSON, writeJSON, parseAnatomy, serializeAnatomy, extractDescription, estimateFileTokens, getTokenRatios, appendMarkdown, timeShort, readStdin, normalizePath, getRetention, loadIgnore, readBugLog, isSecretFile } from "./shared.js";
3
+ import { getWolfDir, ensureWolfDir, readJSON, writeJSON, writeAtomic, parseAnatomy, serializeAnatomy, extractDescription, estimateFileTokens, getTokenRatios, appendMarkdown, timeShort, readStdin, normalizePath, getRetention, loadIgnore, readBugLog, isSecretFile } from "./shared.js";
5
4
  async function main() {
6
5
  ensureWolfDir();
7
6
  const wolfDir = getWolfDir();
@@ -113,21 +112,7 @@ async function main() {
113
112
  hits: 0,
114
113
  misses: 0,
115
114
  });
116
- const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
117
- try {
118
- fs.writeFileSync(tmp, serialized, "utf-8");
119
- fs.renameSync(tmp, anatomyPath);
120
- }
121
- catch {
122
- try {
123
- fs.writeFileSync(anatomyPath, serialized, "utf-8");
124
- }
125
- catch { }
126
- try {
127
- fs.unlinkSync(tmp);
128
- }
129
- catch { }
130
- }
115
+ writeAtomic(anatomyPath, () => serialized);
131
116
  }
132
117
  }
133
118
  catch { }
@@ -465,15 +450,17 @@ function detectFixPattern(oldStr, newStr, ext, fileLabel) {
465
450
  fix: `Added async modifier`,
466
451
  };
467
452
  }
468
- // --- Type annotation/cast fix ---
453
+ // --- Type assertion fix ---
454
+ // Only an added `as`-assertion counts. The old second branch (`": "` newly appearing in a
455
+ // small edit) fired on adding ANY object property or label and flooded the buglog with
456
+ // fake "Type error" entries.
469
457
  if (ext === ".ts" || ext === ".tsx") {
470
- if ((newStr.includes(" as ") && !oldStr.includes(" as ")) ||
471
- (newStr.includes(": ") && !oldStr.includes(": ") && oldLines.length <= 3)) {
458
+ if (newStr.includes(" as ") && !oldStr.includes(" as ")) {
472
459
  return {
473
460
  category: "type-fix",
474
461
  summary: `Type error`,
475
- rootCause: `Missing or incorrect type annotation`,
476
- fix: `Added type assertion/annotation`,
462
+ rootCause: `Missing or incorrect type assertion`,
463
+ fix: `Added type assertion`,
477
464
  context: extractChangedLines(oldStr, newStr),
478
465
  };
479
466
  }
@@ -509,15 +496,22 @@ function tokenizeCode(code) {
509
496
  return code.replace(/[^\w$]/g, " ").split(/\s+/).filter(t => t.length > 0);
510
497
  }
511
498
  function findOperatorChange(oldStr, newStr) {
512
- const operators = ["===", "!==", "==", "!=", ">=", "<=", ">>", "<<", "&&", "||", "??"];
513
- for (const op of operators) {
514
- if (oldStr.includes(op) && !newStr.includes(op)) {
515
- for (const op2 of operators) {
516
- if (op2 !== op && newStr.includes(op2) && !oldStr.includes(op2)) {
517
- return { old: op, new: op2 };
518
- }
519
- }
499
+ // Tokenize operators longest-first instead of substring-testing: `"===".includes("==")` is true,
500
+ // so the old check could never see a `===` → `==` (or `!==` `!=`) change — the most classic
501
+ // operator bug of them all.
502
+ const extractOps = (s) => {
503
+ const counts = new Map();
504
+ for (const m of s.match(/===|!==|==|!=|>=|<=|>>|<<|&&|\|\||\?\?/g) ?? []) {
505
+ counts.set(m, (counts.get(m) ?? 0) + 1);
520
506
  }
507
+ return counts;
508
+ };
509
+ const oldOps = extractOps(oldStr);
510
+ const newOps = extractOps(newStr);
511
+ const removed = [...oldOps.keys()].filter((op) => (newOps.get(op) ?? 0) < oldOps.get(op));
512
+ const added = [...newOps.keys()].filter((op) => (oldOps.get(op) ?? 0) < newOps.get(op));
513
+ if (removed.length === 1 && added.length === 1) {
514
+ return { old: removed[0], new: added[0] };
521
515
  }
522
516
  return null;
523
517
  }
@@ -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, parseAnatomy, readStdin, normalizePath, loadIgnore } from "./shared.js";
3
+ import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, readStdin, normalizePath, loadIgnore, isSecretFile } from "./shared.js";
4
4
  async function main() {
5
5
  ensureWolfDir();
6
6
  const wolfDir = getWolfDir();
@@ -30,8 +30,10 @@ async function main() {
30
30
  // Skip tracking for .wolf/ internal files — they're infrastructure, not project files.
31
31
  // Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning.
32
32
  const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
33
- const relToProject = normalizedFile.startsWith(projectDir)
34
- ? normalizedFile.slice(projectDir.length).replace(/^\//, "")
33
+ // Require the path SEPARATOR after the root — a bare startsWith(projectDir) also matched
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)
35
37
  : "";
36
38
  // Don't track files outside the project root (upstream #56).
37
39
  if (!relToProject) {
@@ -47,6 +49,13 @@ async function main() {
47
49
  process.exit(0);
48
50
  return;
49
51
  }
52
+ // Never track secret-bearing files (#54). post-read already skips them, but the session entry
53
+ // (with the file's PATH) is created HERE — without this guard a read .env still landed in
54
+ // _session.json and from there in token-ledger.json.
55
+ if (isSecretFile(normalizedFile)) {
56
+ process.exit(0);
57
+ return;
58
+ }
50
59
  const session = readJSON(sessionFile, {
51
60
  session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0,
52
61
  repeated_reads_warned: 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, readMarkdown, readStdin, readBugLog } from "./shared.js";
3
+ import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, readStdin, readBugLog, withLock } from "./shared.js";
4
4
  async function main() {
5
5
  ensureWolfDir();
6
6
  const wolfDir = getWolfDir();
@@ -24,7 +24,20 @@ async function main() {
24
24
  return;
25
25
  }
26
26
  // 1. Cerebrum Do-Not-Repeat check
27
- checkCerebrum(wolfDir, allContent);
27
+ const cerebrumWarnings = checkCerebrum(wolfDir, allContent);
28
+ // Record fired warnings in the session tracker — the field existed since day one but nothing
29
+ // ever incremented it, so the ledger/dashboard always showed 0.
30
+ if (cerebrumWarnings > 0) {
31
+ try {
32
+ const sessionFile = path.join(wolfDir, "hooks", "_session.json");
33
+ withLock(sessionFile, () => {
34
+ const session = readJSON(sessionFile, {});
35
+ session.cerebrum_warnings = (session.cerebrum_warnings ?? 0) + cerebrumWarnings;
36
+ writeJSON(sessionFile, session);
37
+ });
38
+ }
39
+ catch { /* best-effort */ }
40
+ }
28
41
  // 2. Bug log: search for similar past bugs when editing code
29
42
  // This fires when Claude is about to edit a file — if the edit looks like a fix
30
43
  // (changing error handling, modifying catch blocks, etc.), check the bug log
@@ -37,9 +50,10 @@ function checkCerebrum(wolfDir, content) {
37
50
  const cerebrumContent = readMarkdown(path.join(wolfDir, "cerebrum.md"));
38
51
  const doNotRepeatSection = cerebrumContent.split("## Do-Not-Repeat")[1];
39
52
  if (!doNotRepeatSection)
40
- return;
53
+ return 0;
41
54
  const entries = doNotRepeatSection.split("## ")[0];
42
55
  const lines = entries.split("\n").filter((l) => l.trim().startsWith("[") || l.trim().startsWith("-"));
56
+ let fired = 0;
43
57
  for (const line of lines) {
44
58
  const trimmed = line.trim().replace(/^[-*]\s*/, "").replace(/^\[[\d-]+\]\s*/, "");
45
59
  if (!trimmed)
@@ -59,11 +73,14 @@ function checkCerebrum(wolfDir, content) {
59
73
  const regex = new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
60
74
  if (regex.test(content)) {
61
75
  process.stderr.write(`⚠️ OpenWolf cerebrum warning: "${trimmed}" — check your code before proceeding.\n`);
76
+ fired++;
77
+ break; // one warning per Do-Not-Repeat entry is enough
62
78
  }
63
79
  }
64
80
  catch { }
65
81
  }
66
82
  }
83
+ return fired;
67
84
  }
68
85
  // Common words that appear in most code — must be excluded from similarity matching
69
86
  const STOP_WORDS = new Set([
@@ -94,8 +111,15 @@ function checkBugLog(wolfDir, filePath, oldStr, newStr, content) {
94
111
  const editText = (oldStr + " " + newStr + " " + content).toLowerCase();
95
112
  const editTokens = tokenize(editText);
96
113
  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()));
114
+ // Check if any bug tag appears in the edit content — as a whole word, and only tags with some
115
+ // substance. A substring match on short tags ("ts", "s1", "api") hit virtually every edit and
116
+ // made this FYI fire constantly for any file that carried one.
117
+ const tagHit = (bug.tags ?? []).some(t => {
118
+ const tag = t.toLowerCase();
119
+ if (tag.length < 4)
120
+ return false;
121
+ return new RegExp(`\\b${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(editText);
122
+ });
99
123
  if (tagHit)
100
124
  return true;
101
125
  // Check meaningful word overlap (excluding stop words)
@@ -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, SESSION_SUMMARY_SCAFFOLD } from "./shared.js";
3
+ import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort, buildResumeDigest, readStdin, withLock, SESSION_SUMMARY_SCAFFOLD } from "./shared.js";
4
4
  async function main() {
5
5
  ensureWolfDir();
6
6
  const wolfDir = getWolfDir();
@@ -83,10 +83,14 @@ async function main() {
83
83
  // Increment total_sessions in token-ledger — only for a genuinely new session, not a
84
84
  // resume/compact continuation (which would double-count the same session).
85
85
  if (!continuing) {
86
+ // Same lock as the stop hook's ledger write — a session ending in one process while another
87
+ // starts must not clobber the read-modify-write.
86
88
  const ledgerPath = path.join(wolfDir, "token-ledger.json");
87
- const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } });
88
- ledger.lifetime.total_sessions++;
89
- writeJSON(ledgerPath, ledger);
89
+ withLock(ledgerPath, () => {
90
+ const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } });
91
+ ledger.lifetime.total_sessions++;
92
+ writeJSON(ledgerPath, ledger);
93
+ });
90
94
  }
91
95
  // Inject a compact resume digest as additionalContext so the model continues without
92
96
  // re-reading STATUS.md/cerebrum/memory. Bounded and config-gated. stdout is reserved
@@ -12,9 +12,28 @@ export function nativeMemoryDir(projectRoot) {
12
12
  if (override)
13
13
  return fs.existsSync(override) ? override : null;
14
14
  const root = projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
15
+ const base = path.join(os.homedir(), ".claude", "projects");
15
16
  const slug = root.replace(/\//g, "-");
16
- const dir = path.join(os.homedir(), ".claude", "projects", slug, "memory");
17
- return fs.existsSync(dir) ? dir : null;
17
+ const dir = path.join(base, slug, "memory");
18
+ if (fs.existsSync(dir))
19
+ return dir;
20
+ // The exact slugging Claude Code uses varies for paths with dots/underscores — the plain
21
+ // slash→dash guess above misses those and native memory silently vanished from recall. Fall
22
+ // back to matching any project dir whose name equals the root once BOTH are canonicalized
23
+ // (every non-alphanumeric run → "-"). Exact guess still wins; this only runs on a miss.
24
+ const canon = (s) => s.replace(/[^A-Za-z0-9]+/g, "-");
25
+ const want = canon(root);
26
+ try {
27
+ for (const name of fs.readdirSync(base)) {
28
+ if (canon(name) === want) {
29
+ const candidate = path.join(base, name, "memory");
30
+ if (fs.existsSync(candidate))
31
+ return candidate;
32
+ }
33
+ }
34
+ }
35
+ catch { /* no projects dir */ }
36
+ return null;
18
37
  }
19
38
  export function getWolfDir() {
20
39
  // Prefer an explicit project dir so hooks work even if CWD changes during a session.
@@ -169,39 +188,58 @@ export function getRetention(wolfDir) {
169
188
  // and ** globs. (Duplicated in utils/maintenance.ts — separate build roots.)
170
189
  // ---------------------------------------------------------------------------
171
190
  export function makeIgnoreMatcher(patterns) {
172
- const pats = patterns
191
+ // gitignore-lite, evaluated in ORDER with last-match-wins so `!` negations work
192
+ // ("dist/" then "!dist/keep.js"). A leading "/" anchors the pattern to the project root —
193
+ // "/dist" matches only the top-level dist, not src/dist. The old matcher dropped negations
194
+ // entirely and never gave the leading slash any meaning, so "/dist"-style .gitignore lines
195
+ // (the common form) matched nothing at all.
196
+ const rules = patterns
173
197
  .map((p) => p.trim())
174
- .filter((p) => p && !p.startsWith("#") && !p.startsWith("!"));
175
- if (pats.length === 0)
198
+ .filter((p) => p && !p.startsWith("#"))
199
+ .map((raw) => {
200
+ const negate = raw.startsWith("!");
201
+ const body = (negate ? raw.slice(1) : raw).trim();
202
+ const anchored = body.startsWith("/");
203
+ const pat = (anchored ? body.slice(1) : body).replace(/\/+$/, "");
204
+ return { negate, anchored, pat };
205
+ })
206
+ .filter((r) => r.pat.length > 0);
207
+ if (rules.length === 0)
176
208
  return () => false;
177
- return (relPath) => {
178
- const norm = relPath.replace(/\\/g, "/").replace(/^\.\//, "");
179
- const parts = norm.split("/");
180
- const base = parts[parts.length - 1];
181
- for (const raw of pats) {
182
- const pat = raw.replace(/\/+$/, "");
183
- if (norm === pat || base === pat)
209
+ const matchesRule = (norm, parts, base, r) => {
210
+ const { anchored, pat } = r;
211
+ if (norm === pat || norm.startsWith(pat + "/"))
212
+ return true;
213
+ if (!anchored) {
214
+ if (base === pat || parts.includes(pat))
184
215
  return true;
185
- if (parts.includes(pat))
186
- return true; // dir name anywhere in path
187
216
  if (pat.startsWith("*.") && norm.endsWith(pat.slice(1)))
188
217
  return true;
189
- if (norm.startsWith(pat + "/"))
190
- return true; // under a dir prefix
191
- if (pat.includes("*")) {
192
- const re = new RegExp("^" +
193
- pat
194
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
195
- .replace(/\*\*/g, "\u0000")
196
- .replace(/\*/g, "[^/]*")
197
- .replace(/\u0000/g, ".*") +
198
- "$");
199
- if (re.test(norm) || re.test(base))
200
- return true;
201
- }
218
+ }
219
+ if (pat.includes("*")) {
220
+ const re = new RegExp("^" +
221
+ pat
222
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
223
+ .replace(/\*\*/g, "\u0000")
224
+ .replace(/\*/g, "[^/]*")
225
+ .replace(/\u0000/g, ".*") +
226
+ "$");
227
+ if (re.test(norm) || (!anchored && re.test(base)))
228
+ return true;
202
229
  }
203
230
  return false;
204
231
  };
232
+ return (relPath) => {
233
+ const norm = relPath.replace(/\\/g, "/").replace(/^\.\//, "");
234
+ const parts = norm.split("/");
235
+ const base = parts[parts.length - 1];
236
+ let ignored = false;
237
+ for (const r of rules) {
238
+ if (matchesRule(norm, parts, base, r))
239
+ ignored = !r.negate;
240
+ }
241
+ return ignored;
242
+ };
205
243
  }
206
244
  // Secret-bearing files that must never be captured into anatomy.md / memory.md.
207
245
  // Upstream only excluded .env* (issue #54) — private keys, certs, keystores and
@@ -940,11 +978,22 @@ export function normalizePath(p) {
940
978
  export function countSemanticEntries(wolfDir) {
941
979
  try {
942
980
  const content = fs.readFileSync(path.join(wolfDir, "memory.md"), "utf-8");
981
+ const lines = content.split(/\r?\n/);
982
+ // Scope to the CURRENT session: everything after the last "## Session" header. The old code
983
+ // counted lines starting with "| YYYY-MM-DD" — a format no writer ever produces (rows start
984
+ // "| HH:MM |"), so the count was always 0 and the summary reminder fired regardless.
985
+ let start = 0;
986
+ for (let i = 0; i < lines.length; i++)
987
+ if (/^## Session\b/.test(lines[i]))
988
+ start = i + 1;
943
989
  const mechanical = /^\|\s*[\d:]+\s*\|\s*(Created|Edited|Multi-edited|Session end:|designqc:)/;
944
- const today = `| ${new Date().toISOString().slice(0, 10)}`;
945
990
  let count = 0;
946
- for (const line of content.split(/\r?\n/)) {
947
- if (line.startsWith(today) && !mechanical.test(line))
991
+ for (const line of lines.slice(start)) {
992
+ if (/^\*\*Did:\*\*/.test(line.trim())) {
993
+ count++;
994
+ continue;
995
+ }
996
+ if (/^\|\s*\d{1,2}:\d{2}\s*\|/.test(line) && !mechanical.test(line))
948
997
  count++;
949
998
  }
950
999
  return count;
@@ -90,6 +90,7 @@ async function main() {
90
90
  writes_count: writeCount,
91
91
  repeated_reads_blocked: session.repeated_reads_warned,
92
92
  anatomy_lookups: session.anatomy_hits,
93
+ ...(unnamedWrites > 0 ? { unnamed_writes: unnamedWrites } : {}),
93
94
  },
94
95
  };
95
96
  // Update token-ledger.json — lock the read-modify-write so concurrent sessions and the cron
@@ -130,7 +131,9 @@ async function main() {
130
131
  ledger.sessions = ledger.sessions.slice(-ret.token_ledger_max_sessions);
131
132
  }
132
133
  ledger.lifetime.total_reads += readCount;
133
- ledger.lifetime.total_writes += writeCount;
134
+ // Unnamed writes (shell / other working dirs) count toward the lifetime total — they were real
135
+ // work. They stay OUT of the session's named writes[] list (no path was recorded, by design).
136
+ ledger.lifetime.total_writes += writeCount + unnamedWrites;
134
137
  ledger.lifetime.total_tokens_estimated += inputTokens + outputTokens;
135
138
  ledger.lifetime.anatomy_hits += session.anatomy_hits;
136
139
  ledger.lifetime.anatomy_misses += session.anatomy_misses;
@@ -62,9 +62,13 @@ export function findSimilarBugs(wolfDir, errorMessage) {
62
62
  const results = [];
63
63
  for (const bug of bugLog.bugs) {
64
64
  let score = 0;
65
- // Exact substring match
66
- if (normalize(bug.error_message).includes(normalizedInput) ||
67
- normalizedInput.includes(normalize(bug.error_message))) {
65
+ // Exact substring match — but only when the CONTAINED string has real length. normalize()
66
+ // can reduce a short or punctuation-only message to "" or a few chars, and "".includes / a
67
+ // 3-char fragment matches everything: logBug would then fold unrelated new bugs into that
68
+ // entry as "duplicates".
69
+ const nb = normalize(bug.error_message);
70
+ if ((normalizedInput.length >= 12 && nb.includes(normalizedInput)) ||
71
+ (nb.length >= 12 && normalizedInput.includes(nb))) {
68
72
  score += 1.0;
69
73
  }
70
74
  // Word overlap (jaccard)
@@ -1 +1 @@
1
- {"version":3,"file":"bug-tracker.js","sourceRoot":"","sources":["../../../src/buglog/bug-tracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bug-tracker.js","sourceRoot":"","sources":["../../../src/buglog/bug-tracker.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import cron from "node-cron";
4
- import { readJSON, writeJSON, readText, writeText, withLock } from "../utils/fs-safe.js";
4
+ import { readJSON, writeJSON, writeText, withLock } from "../utils/fs-safe.js";
5
5
  import { scanProject } from "../scanner/anatomy-scanner.js";
6
6
  import { detectWaste } from "../tracker/waste-detector.js";
7
+ import { consolidateMemory } from "../utils/maintenance.js";
7
8
  import { resolveLlmConfig, callLlmDetailed, requiresApiKey } from "./llm-provider.js";
8
9
  /**
9
10
  * Split text into chunks that fit a context window — at paragraph boundaries, never mid-sentence.
@@ -222,7 +223,11 @@ export class CronEngine {
222
223
  scanProject(this.wolfDir, this.projectRoot);
223
224
  break;
224
225
  case "consolidate_memory":
225
- this.consolidateMemory(action.params?.older_than_days ?? 7);
226
+ // Delegates to utils/maintenance — the engine used to carry its own copy, which MISSED the
227
+ // idempotency guard: every daily run re-consolidated already-consolidated blocks and, finding
228
+ // no table rows in them, rewrote every count to "(0 actions)". 127 of 128 blocks in one live
229
+ // memory.md had lost their counts to this. One implementation now, guarded and locked.
230
+ consolidateMemory(this.wolfDir, action.params?.older_than_days ?? 7);
226
231
  break;
227
232
  case "generate_token_report":
228
233
  this.generateTokenReport();
@@ -234,54 +239,6 @@ export class CronEngine {
234
239
  throw new Error(`Unknown action type: ${action.type}`);
235
240
  }
236
241
  }
237
- consolidateMemory(olderThanDays) {
238
- const memoryPath = path.join(this.wolfDir, "memory.md");
239
- const content = readText(memoryPath);
240
- if (!content)
241
- return;
242
- const cutoff = new Date();
243
- cutoff.setDate(cutoff.getDate() - olderThanDays);
244
- const lines = content.split("\n");
245
- const result = [];
246
- let inOldSession = false;
247
- let oldSessionLines = [];
248
- let currentSessionDate = null;
249
- for (const line of lines) {
250
- const sessionMatch = line.match(/^## Session: (\d{4}-\d{2}-\d{2})/);
251
- if (sessionMatch) {
252
- // Flush previous old session
253
- if (inOldSession && oldSessionLines.length > 0) {
254
- const actionCount = oldSessionLines.filter((l) => l.startsWith("|") && !l.startsWith("|--") && !l.startsWith("| Time")).length;
255
- result.push(`> Consolidated session (${actionCount} actions)`);
256
- result.push("");
257
- }
258
- currentSessionDate = new Date(sessionMatch[1]);
259
- if (currentSessionDate < cutoff) {
260
- inOldSession = true;
261
- oldSessionLines = [];
262
- result.push(line); // Keep the header
263
- }
264
- else {
265
- inOldSession = false;
266
- result.push(line);
267
- }
268
- continue;
269
- }
270
- if (inOldSession) {
271
- oldSessionLines.push(line);
272
- }
273
- else {
274
- result.push(line);
275
- }
276
- }
277
- // Flush last old session
278
- if (inOldSession && oldSessionLines.length > 0) {
279
- const actionCount = oldSessionLines.filter((l) => l.startsWith("|") && !l.startsWith("|--") && !l.startsWith("| Time")).length;
280
- result.push(`> Consolidated session (${actionCount} actions)`);
281
- result.push("");
282
- }
283
- writeText(memoryPath, result.join("\n"));
284
- }
285
242
  generateTokenReport() {
286
243
  const flags = detectWaste(this.wolfDir);
287
244
  const ledgerPath = path.join(this.wolfDir, "token-ledger.json");