The pending-commit detector was too eager. It had one job — tell you which commits still need documentation — and it was doing that job with the enthusiasm of a smoke alarm that goes off when you make toast. Every .gitignore tweak, every go.sum update, every editor config change: pending, pending, pending. The signal-to-noise ratio was drifting toward noise.

This batch of work was about teaching the system what doesn’t matter — and then making sure it can’t lie to you about what it’s ignoring.

The first problem was structural. Skip rules were just prefix matches against file paths, which sounds fine until you realize strings.HasPrefix(".gitignore", ".gitignores") returns true. That’s the kind of bug that hides for months because the false positive is almost right. The fix was a proper typed rule system — prefix, exact, suffix — living in its own skiprules.go file. Exact means exact. .gitignore matches .gitignore and nothing else. This also opened the door for suffix rules (*.lock, *.sum) without bolting on a second filtering mechanism.

But hardcoded defaults can only go so far. Every repo has its own housekeeping: vendor/, third_party/, whatever bespoke directory structure you’ve accumulated over the years. So now there’s .timbers/.timbersignore — a simple newline-delimited file, # comments, reusing the same skip-rule grammar. No TOML, no YAML, no new parser dependency. The loader runs once at construction and merges with the built-in defaults. If the file is malformed, it falls back silently. A broken ignore file should never block the enforcement gate — that would invert the entire purpose of the tool.

The interesting design tension showed up around visibility. Once you let repos define their own skip rules, you’ve created a knob that can be cranked to “skip everything.” The minimum viable feedback channel: timbers status now reports how many commits were infrastructure-skipped since the last entry. Human output gets it under --verbose only (the reviewer correctly flagged that the extra git-walk would add latency people didn’t ask for). JSON always includes infra_skipped_since_entry, because machine consumers shouldn’t have to opt in to data they might need. Errors collapse to zero — status is a window, not a wall.

The most satisfying piece was revert handling. When someone runs git revert on an already-documented commit, the revert itself carries no new design intent. The original entry is the audit trail. Requiring a fresh entry for the revert just inflates the ledger with noise. So revert.go now parses the Revert "..." subject and This reverts commit <sha> trailer, cross-references against documented worksets, and quietly drops the match from pending. The conservative choice: if a revert touches any undocumented SHA, the whole thing stays pending. Silent loss of context is worse than an extra prompt.

Code review caught real bugs in all of this. The SHA prefix match for reverts was too short — collision risk at the lengths I’d picked. Bumped the minimum to 12 characters. The pending detection path was scanning entries twice where once would do, so a pure helper (documentedSHASetFromEntries) now threads a single ListEntries call through both latest-entry resolution and revert auto-skipping. The visibility count and the actual filter had diverged — they were computing “skipped” differently, which is exactly the kind of inconsistency that erodes trust in tooling. Unified them.

The reviewer also flagged that NewStorage now does I/O at construction (loading .timbersignore). Fair point. But lazy loading would hide state behind first-use side effects, and the I/O is a single os.Open on a tiny file. Documented it in the godoc and moved on.

The principle underneath all of this: an enforcement tool earns trust through transparency, not strictness. If you can’t see what’s being skipped, you can’t trust what’s being enforced. And if enforcement nags you about things that genuinely don’t matter, you stop listening — which is worse than having no enforcement at all. The skip-rule system is deliberately conservative in what it hides and deliberate about showing its work. It doesn’t cover every edge case (no doublestar globs yet, no --revert flag for manual entries), but it covers the cases that were actually causing friction.

The pending detector still has one job. It’s just better at knowing when to keep quiet.


This post was written with AI assistance.