Generated report
Dev Log: 2026-06-02
The reframe came from a tired place. The gate was firing on commits the agent in the room couldn't possibly document — a teammate's work pulled in via merge, a bot's housekeeping commit, something from yesterday's session that had already shipped. The protocol said "document everything before the next commit." Reality said "you can only document what you actually did, in this session, with this context loaded." Those two contracts had been quietly diverging for months.
So v0.23.0 became a project about teaching the gate to tell the difference.
The first move was a tally, not code. Before locking heuristic defaults, I pointed at a sister repo with a long history of multi-agent, multi-author work and ran git log --first-parent with the author-email and commit-date fields against a hundred-commit window. The point was to validate guesses — 24-hour staleness window, OR-composed identity + freshness checks, strict email equality — against something real. The tally confirmed all of them. It also surfaced one thing I hadn't anticipated: that repo already had a .mailmap coalescing two of my own emails. If timbers used %ae (raw author email) instead of %aE (mailmap-resolved), the gate would silently misclassify 61% of my own commits as foreign-authored. Thirty minutes of shell work changed a load-bearing default.
The plan doc itself went through three independent reviewers — a pragmatist, a correctness-skeptic, and an agent-UX advocate — and each found something the others missed. The pragmatist cut four items I was going to over-build. The correctness-skeptic caught that AuthorDate is preserved across rebase and amend, which meant a staleness check anchored on it would auto-skip work the user had just touched. CommitDate was the right clock. The agent-UX advocate flagged the single most consequential decision in the whole release: pending.count is the field every agent prompt anchors on, and if it kept reporting the raw total under the new gate semantics, agents would burn tokens trying to document work that wasn't theirs. The count had to mean in-session blocking only. Same field name, reframed contract.
Implementation went in thin phases, each landable on its own. Extend git.Commit with CommitDate and switch to %aE. Add a pure provenance classifier with a fixed-time test setup and lock in its precedence (infra → identity → content → provenance, so a documented commit still wins). Wire the classifier into the gate path — which is where the predictable test-fixture bomb went off. NewStorage was loading the host's user.email at construction, which meant every mock commit in every test suite suddenly looked foreign-authored. The fix took two passes: my first instinct was to reset provenance inside the test helper, but that only patched one package. The cleaner answer was to split the constructor: NewStorage returns a zero-config Storage; NewDefaultStorage is the production entry point that calls LoadProvenanceConfig. Tests opt in to provenance the same way they already opted in to skip-message config. That pattern was already in the codebase; I just hadn't seen it as the template until the bomb forced me to look.
The session window got a .timbersignore directive (session-window: 8h) for the marathon-refactor case, with safe-degradation back to the 24-hour default and doctor warnings on malformed values. Then the prime semantics flip — the load-bearing one — split Pending.Count from out-of-session and stale buckets, with foreign-author commits deliberately count-only. The agent-UX review had been pointed about that: if you surface foreign commit subjects, agents will fabricate what/why/how rather than respect "not yours to document." Counts only. The temptation to be more informative is exactly the failure mode.
The last piece was the safety net. A long session that runs past its window will silently stale-skip its own work — the one failure mode the review called unrecoverable in-session. A blocking warning would defeat the whole lenience reframe, so it became a post-commit note: non-blocking, count-based, references the session window so the operator sees what threshold tripped. The agent gets the signal without the gate getting its teeth back.
The thing I'll remember from this release is the reframe-without-renaming pattern. pending.count kept its name and changed its meaning. Every agent prompt that anchored on it kept working — better, in fact, because the new meaning matched what those prompts were actually trying to do. The most consequential change in v0.23.0 is invisible to any caller whose work was already in-session. It only shows up for the cases the old contract handled badly. That's a good shape for a behavior change to have.
The mailmap finding still nags at me a little. Sixty-one percent. That's the kind of silent miss that doesn't show up until someone runs a tally, and it makes me wonder what else is sitting in the defaults that only a real-world corpus would reveal. Probably something. Probably more than one thing. The gate feels honest now in a way it didn't a week ago, but "honest" isn't "finished" — it's just a better place to keep working from.
Written with AI assistance from session logs.