The day started with the biggest single chunk of work: timbers query grew --tag filtering. OR semantics, not AND — agents doing discovery want breadth, and you can always compose multiple queries to narrow down. Cobra’s StringSliceVar gives you repeated flags and comma separation for free, which is one of those small wins where the framework actually does the right thing. The filtering logic landed in entry_filter.go, extracted cleanly so export could pick it up immediately after. And it did — --tag on export was a quick follow-up, threading the flag through all three code paths (range, time, last) for consistency.
Then timbers amend dropped. Typos in ledger entries were permanent before this, which felt wrong for a tool that’s supposed to reduce friction around capturing context. Now --what/--why/--how/--tag flags do partial updates, --dry-run lets you preview, and storage.WriteEntry with force=true overwrites the git note cleanly. updated_at gets bumped. Simple, correct.
The changelog command was a surprisingly hefty chunk of plumbing. Markdown output over JSON because changelogs are for humans. Group-by-tag with intentional duplication — an entry tagged both feature and dx shows up in both sections, because discoverability in context beats deduplication. And it defaults to all entries, because a changelog that requires --last 999 to show anything is hostile UX. That design instinct — default to useful, not default to empty — felt right.
Then came the rename that should’ve happened sooner: prompt → draft. “Prompt” is our jargon; “draft” reads as the action it performs — draft a changelog, draft release notes. Alongside it, a decision-log.md template for ADR extraction. The why-field data in ledger entries is uniquely suited to this. prime got updated to actually mention document generation, because agents were reaching for export|pipe instead of discovering draft existed.
Pipe ergonomics got a fix that sounds boring but matters: errors and warnings were going to stdout, corrupting piped output. output.Printer with WithStderr() routes Warn() to stderr where it belongs. Small, correct, the kind of thing you don’t notice until it ruins your | jq pipeline.
The config story solidified: internal/config package with a proper Dir() function that checks TIMBERS_CONFIG_HOME, then XDG_CONFIG_HOME, then Windows AppData, then falls back to ~/.config/timbers. Two places had the path hardcoded before. Windows users would’ve gotten the wrong location. .env.local support landed too, solving a genuinely annoying problem — Claude Code gets confused when ANTHROPIC_API_KEY lives in the environment (conflicts with its OAuth flow), so loading from a file as fallback sidesteps the whole mess.
doctor learned about config: 4 new checks for config dir, env files, API keys, templates. Plus a version check against the GitHub releases API with a 5-second timeout and graceful failure for dev builds. Doctor went from “is git notes set up?” to actually useful diagnostics.
Release prep for v0.1.0 was the expected housekeeping — MIT license, README rewrite, CI workflow, CHANGELOG, CONTRIBUTING. The prompt→draft rename propagated across the codebase. Org owner references fixed in goreleaser config.
Post-v0.2.0, three independent reviewers all converged on the same thing: init didn’t create the notes ref, so prime silently exited for new users. That’s an adoption killer. The fix uses git plumbing directly — mktree + commit-tree + update-ref to create an empty notes namespace. Hooks flipped from global to project-level (global was firing in every repo, even uninitiated ones) and from default-on to opt-in --hooks (they were conflicting with other tools that need pre-commit). Lipgloss styling landed on doctor and init output, following the pattern uninstall already established.
The recurring theme: things that worked fine for the developer who built them, breaking for everyone else. The notes ref gap, global hooks, hardcoded config paths, stdout pollution in pipes. All invisible until someone else tries to use it.