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 <noreply@anthropic.com>
This commit is contained in:
parent
d170623bd0
commit
ba4e5fd227
2 changed files with 94 additions and 1 deletions
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue