Spent the last few days doing the kind of work that doesn’t make for dramatic storytelling but changes everything underneath: ripping out the storage layer and replacing it while the tool keeps running.

The big move was pivoting from git notes to flat files. Git notes seemed elegant at first — metadata attached to commits, no working tree pollution. But in practice they’re a merge nightmare. Two worktrees writing notes to the same ref? Conflicts. Every time. The new approach is dead simple: one JSON file per entry under .timbers/, organized into YYYY/MM/DD buckets. Atomic writes via temp-file-and-rename. The GitOps interface went from 8 methods down to 4. I migrated all 37 existing entries with a bash script, ripped out every line of notes code, and wrote 14 integration tests proving that file-per-entry storage is inherently merge-safe. Sometimes the boring answer is the right answer.

That pivot immediately surfaced a chicken-and-egg problem: timbers log creates a file, which creates a commit, which timbers pending then reports as undocumented. The fix was filtering ledger-only commits in GetPendingCommits itself — not in callers — so all seven consumers get the fix for free. Then I went further and made timbers log auto-commit its own entry file using pathspec scoping (git commit -- <path>), which means the staged-but-uncommitted gap that confused users just… doesn’t exist anymore.

Meanwhile, the hooks story got interesting. Discovered that the post-commit reminder hook had been a complete no-op since it was created. $TOOL_INPUT is always empty — Claude Code passes tool input on stdin, not as an env var. Found this via a GitHub issue. The fix was trivial (grep stdin instead of an env var), but the real lesson was the upgrade path: the old hook detection code would skip reinstall if any hook existed, leaving broken hooks in place forever. Rewrote that to detect-and-replace.

On the architecture side, I added an AgentEnv interface with a registry pattern for agent environment integrations. Each environment (Claude Code today, Gemini/Codex tomorrow) is a single self-contained file that registers itself via init(). Registry vs switch was the obvious debate — registry won because adding a new environment should be a one-file task, not a multi-file surgery.

The coaching system got a full rewrite informed by the Opus prompt guide. The key insight: explaining why a rule exists lets the model generalize correctly, while imperative shouting (MUST, CRITICAL) causes overtriggering. Replaced 11 instances of that pattern with calm framing. Added a concrete 5-point checklist for when to include --notes. A council of three perspectives debated whether to make coaching model-specific — they all converged on the same answer: the clarity problems were the problem, and good coaching IS Opus-optimized coaching.

Also shipped an MCP server with 6 tools over stdio. One implementation now serves Claude Code, Cursor, Windsurf, Gemini CLI, and anything else that speaks the protocol. Highest-leverage integration work possible — handlers are just thin shells over the internal library.

Built a landing page too. Dark theme, terminal-styled code blocks, GSAP scroll animations. Positioned opposite to Entire.io: intentional documentation vs automatic session capture. And the --color flag finally landed after a user reported that Solarized Dark made all the dim text invisible. Color 8 (bright black) on a dark background: classic.

The example artifacts got regenerated from real entries instead of backfilled ones, and the difference is stark. The decision-log went from 9 thin ADRs to 6 genuine ones with fork-in-the-road context. First time --notes data actually enriched template output.

Three releases shipped across these sessions: v0.8.0 (notes field), v0.9.0 (coaching rewrite), v0.10.0 (color flag, auto-commit, content safety). The project feels like it’s crossing a threshold from “interesting experiment” to “tool I’d actually hand someone.”


Written with AI assistance.