Generated report
Dev Log: 2026-07-10
A bug report came in from a parallel-agent worktree repo: the claude-stop hook was blocking session end, but timbers pending insisted the ledger was clean. The report had a theory — something about transcript scanning versus doc-set resolution — and a proposed fix: unify the gate and the display so they compute the same thing.
The theory was wrong. The fix would have been worse.
The real story, once I started pulling on the thread, was three defects stacked on top of each other, each one masking the next. The Stop hook was the only cross-agent-debt gate that quietly ignored TIMBERS_SKIP_CROSS_AGENT_DEBT. The "manual run exits 0" behavior everyone had been reassured by? That was the empty-stdin parse bailout hitting first — the hook was never actually running the check when invoked by hand, so nobody noticed it wasn't honoring the opt-out when it did run.
Adjacent to that, two more things were wrong. timbers log --anchor HEAD was storing the literal string "HEAD" in the entry, quietly defeating the whole since-anchor model. And timbers log itself was tripping the pre-commit gate on a sibling agent's debt — which meant the act of paying the debt was being blocked by the debt gate. A perfect little deadlock.
The tempting move was the reporter's move: collapse the gate and the display into one computation. One source of truth, less code, cleaner mental model. I turned it down. The gate uses first-parent history on purpose — agent A shouldn't be blocked by debt that agent B has already merged. The display walks the full DAG on purpose — you want to see everything pending across the repo. They look like duplication and they're not. Collapsing them would regress the parallel-worktree story that motivated the report in the first place.
So instead: three narrower fixes, one commit. claude-stop now honors the same env-var opt-out as its sibling hooks, with the constant aliased from ledger.SkipCrossAgentDebtEnv so there's exactly one string to grep for. timbers log resolves symbolic anchors through a new mockable GitOps.ResolveCommit before writing anything, and errors cleanly on unresolvable refs instead of persisting a phantom "HEAD". And the entry commit itself sets TIMBERS_SKIP_CROSS_AGENT_DEBT=1 on its own git invocation — not --no-verify, because foreign hooks in the repo still deserve to run — so paying the debt no longer trips the gate that exists to enforce paying the debt.
Test-first on all three failure modes. Independent review turned up nothing.
The thing I want to remember from this one: when a bug report proposes a fix, the fix is a hypothesis, not a spec. The reporter had done real work narrowing down where the pain was, and I owe them the credit for that. But the proposed remedy — unify the two computations — would have solved the visible symptom by dismantling a distinction that was load-bearing for the multi-agent case. The right move was to treat the report as a pointer to a region of the code that was misbehaving, then re-derive the fix from first principles once I was standing in that region.
I also thought about splitting this into three commits, one per defect. Couldn't cleanly — git.go carries helpers for two of them, and non-interactive staging can't split a single file surgically. So it landed as one cohesive unit, revertable as a whole, with all three work items referenced. Not ideal, but honest about what the code actually looks like.
Feels solid. The gate/display split now has a comment's worth of intent behind it in my head, even if the code was already doing the right thing. And the self-exemption on the entry commit closes a loop that should never have been open.
Written with AI assistance from session notes.