From ba4e5fd2270ffe252b71f1005c22885731408b39 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 13 Aug 2026 04:28:44 +0000 Subject: [PATCH] chore(wolf): guard buglog.json integrity, add dedupe rule and auto-checkpoint Recovered .wolf/buglog.json (828 entries lost when this worktree's copy silently reset to bug-001 sometime after commit ce96fd6d, 3 days uncommitted with zero overlap detection) by merging the committed history back in and renumbering the 43 real entries logged since. To stop this recurring: - OPENWOLF.md: require searching for an existing same-file/root_cause entry before appending a new bug id (fixes the bug-1418 x4 duplication pattern found in the recovered history); never restart the id counter. - stop.js: validate buglog.json every turn (JSON validity, {version,bugs} wrapper, duplicate ids, count/max-id not regressing vs. the last commit) and auto-commit it standalone when it grew cleanly, so git history is always a recoverable backup instead of a 3-day-stale one. Co-Authored-By: Claude Sonnet 5 --- .wolf/OPENWOLF.md | 6 ++- .wolf/hooks/stop.js | 89 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/.wolf/OPENWOLF.md b/.wolf/OPENWOLF.md index aae2a472..3688ad22 100644 --- a/.wolf/OPENWOLF.md +++ b/.wolf/OPENWOLF.md @@ -66,7 +66,7 @@ OpenWolf's value comes from learning across sessions. You MUST update `.wolf/cer **Before fixing:** Read `.wolf/buglog.json` first — the fix may already be known. -**After fixing:** ALWAYS append to `.wolf/buglog.json` with this structure: +**Before appending a new entry:** search existing bugs for the same `file` + overlapping `root_cause`/`error_message`. If a match exists, update THAT entry (`occurrences += 1`, `last_seen = now`, append context to `fix` if the resolution differs) instead of creating a new `bug-NNN`. Never create a second entry under an id that already exists — assign the next unused number after the current max `bug-NNN` in the file (never restart from `bug-001`; a restarted counter collides with existing history and is unrecoverable without a git-history merge). If you fixed something genuinely new, append with this structure: ```json { "id": "bug-NNN", @@ -81,9 +81,12 @@ OpenWolf's value comes from learning across sessions. You MUST update `.wolf/cer "last_seen": "ISO date" } ``` +Always preserve the top-level `{"version": 1, "bugs": [...]}` wrapper — never write a bare array. **The threshold is LOW.** When in doubt, log it. A false positive in the bug log costs nothing. A missed bug means repeating the same mistake later. +**Integrity is automated, not optional:** a Stop hook (`.wolf/hooks/stop.js`) validates `.wolf/buglog.json` (JSON validity, wrapper format, duplicate IDs, ID count/max going backwards vs. the last commit) after every turn and, if it's valid and grew, commits it standalone (`chore(wolf): checkpoint buglog.json (N bugs)`) so git history is always a recoverable backup. If it shrank or fails validation, the hook refuses to commit and prints why — treat that warning as a stop-and-investigate signal, not noise to scroll past. + ## Token Discipline - Never re-read a file already read this session unless it was modified since. @@ -133,3 +136,4 @@ Before ending or when asked to wrap up: 1. Write a session summary to `.wolf/memory.md`. 2. Review the session: did you learn anything? Did the user correct you? Did you fix a bug? If yes, update `.wolf/cerebrum.md` and/or `.wolf/buglog.json`. +3. Do NOT manually `git add`/commit `.wolf/buglog.json` yourself as part of a feature/task commit — the Stop hook checkpoints it standalone once it validates cleanly (see Bug Logging above). Mixing it into a feature commit just duplicates that history under an unrelated message. diff --git a/.wolf/hooks/stop.js b/.wolf/hooks/stop.js index e215ac6d..a33cb439 100644 --- a/.wolf/hooks/stop.js +++ b/.wolf/hooks/stop.js @@ -1,11 +1,18 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { execFileSync } from "node:child_process"; import { getWolfDir, ensureWolfDir, readJSON, writeJSON, appendMarkdown, timeShort } from "./shared.js"; async function main() { ensureWolfDir(); const wolfDir = getWolfDir(); const hooksDir = path.join(wolfDir, "hooks"); const sessionFile = path.join(hooksDir, "_session.json"); + const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + // Runs every stop regardless of read/write activity this turn, and + // regardless of whether buglog.json was edited via the Write/Edit tool + // or via a raw script (the common path for a file this large) — this + // is the only guard that can't be bypassed by which tool touched it. + checkAndCheckpointBuglog(wolfDir, projectRoot); const session = readJSON(sessionFile, { session_id: "", started: "", @@ -143,5 +150,87 @@ function checkCerebrumFreshness(wolfDir, session) { // cerebrum.md doesn't exist, that's ok } } +/** + * Validate .wolf/buglog.json on disk and, if it's well-formed and grew + * (or stayed the same) relative to the last commit, checkpoint it as its + * own standalone commit. Never commits a shrinking or malformed file — + * that would enshrine data loss (or corruption) into git history, which + * is exactly the failure mode this guard exists to catch before it repeats. + */ +function checkAndCheckpointBuglog(wolfDir, projectRoot) { + const buglogPath = path.join(wolfDir, "buglog.json"); + if (!fs.existsSync(buglogPath)) + return; + let raw; + try { + raw = fs.readFileSync(buglogPath, "utf-8"); + } + catch { + return; + } + let parsed; + try { + parsed = JSON.parse(raw); + } + catch (e) { + process.stderr.write(`🛑 OpenWolf: .wolf/buglog.json is not valid JSON (${e.message}). NOT checkpointing — fix this before it's lost for good.\n`); + return; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !Array.isArray(parsed.bugs)) { + process.stderr.write(`🛑 OpenWolf: .wolf/buglog.json must be {"version":1,"bugs":[...]}, not a bare array or other shape. NOT checkpointing.\n`); + return; + } + const bugs = parsed.bugs; + const idNum = (id) => { + const m = /^bug-(\d+)$/.exec(id || ""); + return m ? parseInt(m[1], 10) : 0; + }; + const currentMaxId = bugs.reduce((max, b) => Math.max(max, idNum(b.id)), 0); + const idCounts = {}; + for (const b of bugs) + idCounts[b.id] = (idCounts[b.id] || 0) + 1; + const dupes = Object.keys(idCounts).filter((id) => idCounts[id] > 1); + if (dupes.length > 0) { + process.stderr.write(`⚠️ OpenWolf: buglog.json has duplicate ids (${dupes.slice(0, 5).join(", ")}${dupes.length > 5 ? ", ..." : ""}). Before adding a new bug, search for an existing entry with the same file+root_cause and bump its occurrences instead of appending a new one under a colliding or fresh id.\n`); + } + let lastCount = null; + let lastMaxId = null; + try { + const committed = execFileSync("git", ["show", "HEAD:.wolf/buglog.json"], { + cwd: projectRoot, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }); + const lastParsed = JSON.parse(committed); + const lastBugs = Array.isArray(lastParsed) ? lastParsed : (lastParsed.bugs || []); + lastCount = lastBugs.length; + lastMaxId = lastBugs.reduce((max, b) => Math.max(max, idNum(b.id)), 0); + } + catch { + // No committed version yet (or git unavailable) — nothing to compare against. + } + if (lastCount !== null && bugs.length < lastCount) { + process.stderr.write(`🛑 OpenWolf: buglog.json SHRANK from ${lastCount} to ${bugs.length} bugs since the last commit. NOT auto-committing — this looks like data loss, investigate (check git history / other worktrees) before committing manually.\n`); + return; + } + if (lastMaxId !== null && currentMaxId < lastMaxId && bugs.length > 0) { + process.stderr.write(`🛑 OpenWolf: buglog.json's highest id (bug-${currentMaxId}) is lower than the last commit's (bug-${lastMaxId}) — looks like the id counter was reset. NOT auto-committing.\n`); + return; + } + try { + const status = execFileSync("git", ["status", "--porcelain", "--", ".wolf/buglog.json"], { + cwd: projectRoot, + encoding: "utf-8", + }); + if (!status.trim()) + return; // nothing changed since last commit + execFileSync("git", ["add", ".wolf/buglog.json"], { cwd: projectRoot }); + execFileSync("git", ["commit", "-m", `chore(wolf): checkpoint buglog.json (${bugs.length} bugs)`], { cwd: projectRoot }); + process.stderr.write(`✅ OpenWolf: checkpointed buglog.json (${bugs.length} bugs) to git.\n`); + } + catch (e) { + process.stderr.write(`⚠️ OpenWolf: buglog.json is valid but the checkpoint commit failed: ${e.message}\n`); + } +} main().catch(() => process.exit(0)); //# sourceMappingURL=stop.js.map \ No newline at end of file