The bug was invisible in the happy path. That’s what made it so irritating.

--range has two ways to find entries: anchor-based lookup (find the entry whose anchor commit lives in the range) and diff-based discovery (scan the actual changes). In v0.15.3, the diff-based path was a fallback — it only kicked in when anchor lookup returned zero results. Which sounds reasonable until you think about what happens when anchor lookup returns some results but not all of them.

A feature branch gets squash-merged. Half the anchors still point at commits that exist on the target branch. The other half point at commits that were rewritten during the squash — they’re ghosts now. Anchor lookup finds the survivors, returns a non-empty set, and the fallback never fires. The entries with stale anchors just… vanish. No error, no warning. Silent data loss.

The fix is conceptually simple: stop treating diff-based discovery as a fallback. Run both paths unconditionally, union the results, deduplicate by entry ID. The anchor path catches what it can; the diff path catches what it can; together they cover the full picture. It’s the kind of change where you look at the before and after and wonder why it was ever the other way — but of course it was the other way because the stale-anchor scenario hadn’t materialized yet when the original code was written. Fallback architectures are optimistic by nature. They assume failure is binary: everything works or nothing does. Partial failure, the most common kind of failure in distributed-ish systems, sneaks right past them.

But the --range fix was really the last domino. The deeper problem had already been churning for a while: squash and rebase workflows fundamentally break anchor tracking. An anchor commit is a promise — this SHA identifies where this entry was born. Rewriting history breaks that promise. And when the promise breaks, things get weird. pending would dump every reachable commit as “undocumented.” HasPendingCommits would error out, which blocked hooks. Agents — automated consumers of the ledger — would see hundreds of false-positive pending commits, dutifully try to re-document all of them, and create mountains of duplicates.

The response was a kind of graceful degradation philosophy applied across several surfaces at once. HasPendingCommits now returns false on ErrStaleAnchor instead of bubbling up the error — hooks don’t block, work doesn’t stop. pending reports zero actionable commits and shows a clear warning explaining the stale anchor situation, rather than vomiting an incomprehensible commit list. And doctor gained two new diagnostic checks: it inspects merge strategy configuration (pull.rebase, merge.ff) and detects stale anchors directly. The idea is that doctor tells you why things are degraded before you even notice the degradation.

The pattern here is worth naming: fail narrow, diagnose wide. When a subsystem hits a state it can’t fully trust, it should minimize the blast radius of its own confusion (return safe defaults, don’t block workflows) while maximizing the visibility of the underlying cause (surface it in a diagnostic tool, not inline with unrelated output).

This also prompted a proper integration guide — a document aimed at plugin maintainers and agents working with the ledger, explaining the anchor problem, the mechanics of hooks, and concrete recommendations for merge-friendly workflows. Documentation isn’t glamorous, but when your users are other tools, clarity about failure modes matters more than any feature.

The stale anchor problem isn’t solved in any permanent sense. History rewriting and content-addressed anchoring are in fundamental tension, and no amount of fallback logic makes that tension disappear. What is solved: the system no longer pretends everything is fine when it isn’t, and no longer panics when it could simply shrug and point you toward doctor. That’s solid ground — the kind where you know exactly which compromises you’re standing on.


This post was written with AI assistance.