Store tsk refs as commit-backed history
Each refs/tsk/<ns>/<key> ref now points at a commit chain instead of a
single blob. The commit's tree carries one file (`content`) holding the
blob; future writes append commits with the prior tip as parent. `git
log refs/tsk/<ns>/tasks/<id>` walks the audit trail; `git show
refs/tsk/<ns>/tasks/<id>:content` reads the current state.
Inbox refs (`inbox/<src>-<id>`) intentionally stay blob-backed — they
are short-lived (deleted on accept) and gain nothing from history.
Storage:
- Store gains `write_with_meta(key, data, event, detail)` (with `write`
now defaulting to `event="write"`). FileStore ignores the meta;
GitStore turns it into a commit message via format_commit_message,
which is a single function for easy format changes later.
- GitStore::read peels through commit→tree→`content`, falling back to
raw blob so it keeps reading legacy/inbox refs unchanged.
- merge_blob produces a merge commit (parents = local + remote) when
either side is commit-backed; falls back to a blob OID otherwise.
- Cross-namespace link rewrites in the id-collision rebase now go
through write_with_meta so each ref-update lands as a commit.
Migration: `tsk migrate-history` walks every refs/tsk/<ns>/<key>
(except inbox/*) and wraps any remaining blob ref in a single
"migrated" commit. Idempotent — already-commit refs are skipped.
A few high-traffic call sites pass rich event metadata through:
- new_task → `tsk(tasks/N): created`
- save_task → `tsk(tasks/N): edited` / `tsk(attrs/N): edited`
- set_property / unset_property → `tsk(attrs/N): prop-set <key>` etc.
Other writes still get the generic "write" subject; opting another
caller into rich messages is a one-call-site change.
Tests cover the commit-chain shape (4 commits after a create + 3
edits), inbox refs staying blob-backed, the migration converting
legacy blob refs, and idempotency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>