chore(wolf): checkpoint buglog.json (1062 bugs)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
This commit is contained in:
parent
13246a89c8
commit
d8bf8fb2e1
27 changed files with 7702 additions and 897158 deletions
|
|
@ -1,65 +1,181 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, readStdin, normalizePath } from "./shared.js";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readStdin, normalizePath, getProjectDir, emitHookJSON, recordInjection, hookMain, getSessionFilePath } from "./shared.js";
|
||||
import { lookupEntry } from "./anatomy-store.js";
|
||||
function duplicateMode(wolfDir) {
|
||||
const cfg = readJSON(path.join(wolfDir, "config.json"), {});
|
||||
const mode = cfg.openwolf?.reads?.duplicate_mode;
|
||||
return mode === "deny" || mode === "off" ? mode : "warn";
|
||||
}
|
||||
function skeletonHintsEnabled(wolfDir) {
|
||||
const cfg = readJSON(path.join(wolfDir, "config.json"), {});
|
||||
return cfg.openwolf?.reads?.skeleton_hints !== false;
|
||||
}
|
||||
async function main() {
|
||||
ensureWolfDir();
|
||||
const wolfDir = getWolfDir();
|
||||
const hooksDir = path.join(wolfDir, "hooks");
|
||||
const sessionFile = path.join(hooksDir, "_session.json");
|
||||
const raw = await readStdin();
|
||||
let input;
|
||||
try {
|
||||
input = JSON.parse(raw);
|
||||
}
|
||||
catch {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
const sessionFile = getSessionFilePath(input);
|
||||
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
||||
if (!filePath) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
// Ranged reads (offset/limit) are exactly what the symbol hints steer the
|
||||
// model toward — never warn about, deny, or record them as full reads (a
|
||||
// ranged first contact must not make a later legitimate full read look like
|
||||
// a duplicate).
|
||||
const isRangedRead = input.tool_input?.offset !== undefined || input.tool_input?.limit !== undefined;
|
||||
// Subagents share this session file but not the main thread's context: a
|
||||
// file the main thread read is unseen by a fresh subagent, so duplicate
|
||||
// handling stays hands-off for them.
|
||||
const isSubagent = typeof input.agent_id === "string" && input.agent_id.length > 0;
|
||||
const normalizedFile = normalizePath(filePath);
|
||||
// Skip tracking for .wolf/ internal files — they're infrastructure, not project files.
|
||||
// Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning.
|
||||
const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
||||
const projectDir = normalizePath(getProjectDir());
|
||||
const relToProject = normalizedFile.startsWith(projectDir)
|
||||
? normalizedFile.slice(projectDir.length).replace(/^\//, "")
|
||||
: "";
|
||||
if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) {
|
||||
process.exit(0);
|
||||
// 2.4: .wolf reads used to be invisible to OpenWolf itself (~147k tokens
|
||||
// of unmeasured self-inflicted reads, including whole-file reads of the
|
||||
// two files the rules say never to read whole). Measure them (tagged
|
||||
// separately so anatomy metrics stay clean) and steer the expensive
|
||||
// pattern to the cheap one, once per file per session.
|
||||
try {
|
||||
const base = path.basename(normalizedFile);
|
||||
const isRanged = input.tool_input?.offset !== undefined || input.tool_input?.limit !== undefined;
|
||||
if (!isRanged && (base === "anatomy.md" || base === "cerebrum.md" || base === "memory.md")) {
|
||||
let sizeTokens = 0;
|
||||
try {
|
||||
sizeTokens = Math.ceil(fs.statSync(filePath).size / 3.5);
|
||||
}
|
||||
catch { }
|
||||
if (sizeTokens > 1500) {
|
||||
const session = readJSON(sessionFile, {
|
||||
session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0, repeated_reads_warned: 0,
|
||||
});
|
||||
const warned = (session.wolf_read_advised ?? {});
|
||||
if (!warned[base]) {
|
||||
warned[base] = true;
|
||||
session.wolf_read_advised = warned;
|
||||
const note = base === "anatomy.md"
|
||||
? `OpenWolf: ${base} is ~${sizeTokens} tokens and is an index, not a document. \`openwolf find <symbol or path>\` answers location queries in under 1k tokens; grep it for a single path's line otherwise.`
|
||||
: `OpenWolf: ${base} is ~${sizeTokens} tokens. Grep the section you need (for example "## Do-Not-Repeat") or read with offset/limit; the whole file rarely pays for itself.`;
|
||||
recordInjection(session, "wolf_read_advice", note);
|
||||
writeJSON(sessionFile, session);
|
||||
emitHookJSON("PreToolUse", { additionalContext: note });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return;
|
||||
}
|
||||
const session = readJSON(sessionFile, {
|
||||
session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0,
|
||||
repeated_reads_warned: 0,
|
||||
});
|
||||
// Check if already read this session
|
||||
if (session.files_read[normalizedFile]) {
|
||||
const prev = session.files_read[normalizedFile];
|
||||
process.stderr.write(`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`);
|
||||
session.files_read[normalizedFile].count++;
|
||||
session.repeated_reads_warned++;
|
||||
writeJSON(sessionFile, session);
|
||||
process.exit(0);
|
||||
if (isRangedRead) {
|
||||
// Record the contact so dedupe knows about it, but flagged: a ranged
|
||||
// first contact must never make a later full read look like a duplicate
|
||||
// (the old asymmetry with post-read inflated warnings ~20x).
|
||||
if (!session.files_read[normalizedFile]) {
|
||||
session.files_read[normalizedFile] = {
|
||||
count: 1, tokens: 0, first_read: new Date().toISOString(), ranged: true,
|
||||
};
|
||||
writeJSON(sessionFile, session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Check anatomy.md for this file
|
||||
const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md"));
|
||||
const sections = parseAnatomy(anatomyContent);
|
||||
let found = false;
|
||||
for (const [sectionKey, entries] of sections) {
|
||||
for (const entry of entries) {
|
||||
// Build the full relative path from the section key + filename for accurate matching
|
||||
const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
|
||||
if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) {
|
||||
process.stderr.write(`📋 OpenWolf anatomy: ${entry.file} — ${entry.description} (~${entry.tokens} tok)\n`);
|
||||
found = true;
|
||||
break;
|
||||
// Model-visible notes for this run — flushed as ONE additionalContext at exit.
|
||||
const notes = [];
|
||||
// Check if already read this session. Only a prior FULL read counts —
|
||||
// ranged-only contact means the model has never seen the whole file.
|
||||
if (session.files_read[normalizedFile] && session.files_read[normalizedFile].ranged !== true) {
|
||||
const prev = session.files_read[normalizedFile];
|
||||
let modifiedSinceRead = true;
|
||||
try {
|
||||
const mtime = fs.statSync(filePath).mtimeMs;
|
||||
modifiedSinceRead = prev.read_mtime === undefined || mtime > prev.read_mtime;
|
||||
}
|
||||
catch { }
|
||||
if (!modifiedSinceRead) {
|
||||
const mode = duplicateMode(wolfDir);
|
||||
session.repeated_reads_warned++;
|
||||
// Denial is the only path that actually saves the tokens, but it must
|
||||
// never strand the model: full reads only (ranged reads exited above),
|
||||
// main thread only, only when the earlier read verifiably delivered
|
||||
// content (tokens > 0), never after a compaction evicted that content,
|
||||
// and only ONCE per file per session — the next attempt passes through.
|
||||
const denyEligible = mode === "deny" && !isSubagent && prev.tokens > 0 &&
|
||||
prev.denied_once !== true && prev.compacted !== true;
|
||||
if (denyEligible) {
|
||||
prev.denied_once = true;
|
||||
session.reads_denied = (session.reads_denied ?? 0) + 1;
|
||||
session.denied_tokens_saved = (session.denied_tokens_saved ?? 0) + prev.tokens;
|
||||
const reason = `OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tok) and is unchanged on disk. Reuse your earlier read, or use offset/limit for the exact lines you need. If you do need the full file again, a second attempt will pass through.`;
|
||||
recordInjection(session, "dup_deny", reason);
|
||||
writeJSON(sessionFile, session);
|
||||
emitHookJSON("PreToolUse", {
|
||||
permissionDecision: "deny",
|
||||
permissionDecisionReason: reason,
|
||||
});
|
||||
return;
|
||||
}
|
||||
prev.count++;
|
||||
if (mode !== "off") {
|
||||
notes.push(`OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tok), unchanged since. If you only need the gist, your earlier read may suffice; for exact text (edit anchors, line numbers), the re-read is fine.`);
|
||||
}
|
||||
if (notes.length > 0)
|
||||
recordInjection(session, "dup_warn", notes.join("\n"));
|
||||
writeJSON(sessionFile, session);
|
||||
if (notes.length > 0) {
|
||||
emitHookJSON("PreToolUse", { additionalContext: notes.join("\n") });
|
||||
}
|
||||
return;
|
||||
}
|
||||
delete session.files_read[normalizedFile];
|
||||
}
|
||||
// Anatomy lookup: O(1) against the durable store, legacy md scan fallback.
|
||||
const entry = lookupEntry(wolfDir, projectDir, normalizedFile);
|
||||
const found = entry !== null;
|
||||
if (entry) {
|
||||
if (entry.description) {
|
||||
notes.push(`OpenWolf anatomy: ${entry.file}: ${entry.description} (~${entry.tokens} tok)`);
|
||||
}
|
||||
// Symbol hint (F2b Phase B): point at slices of big files. Suppressed if
|
||||
// the on-disk file no longer matches what was indexed — a stale line
|
||||
// range that misdirects an offset read is worse than no hint at all.
|
||||
if (entry.symbols && entry.symbols.length > 0) {
|
||||
let fresh = false;
|
||||
try {
|
||||
const st = fs.statSync(filePath);
|
||||
fresh = (entry.size === undefined || st.size === entry.size) &&
|
||||
(entry.mtimeMs === undefined || Math.abs(st.mtimeMs - entry.mtimeMs) < 1);
|
||||
}
|
||||
catch { }
|
||||
if (fresh) {
|
||||
// J2 skeleton hint: for big files with an indexed signature outline,
|
||||
// the outline replaces the "largest sections" line — often enough to
|
||||
// skip the full read entirely. Config-gated (reads.skeleton_hints).
|
||||
if (entry.skeleton && entry.tokens > 2000 && skeletonHintsEnabled(wolfDir)) {
|
||||
notes.push(`Signature outline of ${entry.file} (~${entry.tokens} tok full; symbols with line ranges follow):\n${entry.skeleton}\nRead with offset/limit to fetch just the part you need.`);
|
||||
}
|
||||
else {
|
||||
const top = [...entry.symbols].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
||||
const list = top.map((s) => `${s.kind} ${s.name} L${s.startLine}-${s.endLine} ~${s.tokens} tok`).join("; ");
|
||||
notes.push(`Largest sections: ${list}. Read with offset/limit to fetch just the part you need.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found)
|
||||
break;
|
||||
}
|
||||
if (found) {
|
||||
session.anatomy_hits++;
|
||||
|
|
@ -68,13 +184,24 @@ async function main() {
|
|||
session.anatomy_misses++;
|
||||
}
|
||||
// Record initial read entry (tokens will be updated in post-read)
|
||||
let readMtime;
|
||||
try {
|
||||
readMtime = fs.statSync(filePath).mtimeMs;
|
||||
}
|
||||
catch { }
|
||||
session.files_read[normalizedFile] = {
|
||||
count: 1,
|
||||
tokens: 0,
|
||||
first_read: new Date().toISOString(),
|
||||
read_mtime: readMtime,
|
||||
anatomy_hit: found,
|
||||
};
|
||||
if (notes.length > 0)
|
||||
recordInjection(session, "anatomy_hint", notes.join("\n"));
|
||||
writeJSON(sessionFile, session);
|
||||
process.exit(0);
|
||||
if (notes.length > 0) {
|
||||
emitHookJSON("PreToolUse", { additionalContext: notes.join("\n") });
|
||||
}
|
||||
}
|
||||
main().catch(() => process.exit(0));
|
||||
hookMain("pre-read", main);
|
||||
//# sourceMappingURL=pre-read.js.map
|
||||
Loading…
Add table
Add a link
Reference in a new issue