# muse shelf: resolve created_by from user identity instead of hardcoding "human" ## Background `muse shelf save` recorded `created_by: "human"` (a plain string) for every human-initiated shelf entry. This lost the caller's username and made it impossible to distinguish gabriel's entries from any other user's. The commit command already solved this correctly: it calls `get_config_value("user.handle", root)` and uses the result as the `author` field. We went further than the original plan: `created_by` is now a structured object `{"handle": "", "kind": "human|agent"}` instead of a plain string. This makes the identity richer (you can filter by kind, not just by name) and brings shelf entries in line with how commit records carry agent provenance. **Breaking change**: `created_by` in all shelf JSON output is now `{"handle": "...", "kind": "..."}` instead of a plain string. Old entries stored as a string are normalised on load — the deserialization in `_load_shelf` handles both formats gracefully. ## Goal - `muse shelf save` (and `muse shelf` bare) emit `created_by: {"handle": "gabriel", "kind": "human"}` when `user.handle` is configured. - When no handle is configured (CI, fresh machine, anonymous), falls back to `{"handle": "human", "kind": "human"}` — same observable behaviour as before. - The `--by` flag still overrides everything and produces `{"handle": "", "kind": "agent"}`. - `_shelf_push_programmatic` (autoshelf) routes through the same helper; `created_by="muse"` → `{"handle": "muse", "kind": "agent"}`. - Old shelf entries stored with a plain string `created_by` are normalised on read — no migration needed. ## Phases ### Phase 1 — Extract a helper - [x] `CB_01` — Add `_resolve_created_by(root, explicit_by)` helper in `muse/cli/commands/shelf.py`. Returns `{"handle": explicit_by, "kind": "agent"}` when `explicit_by != "human"`; otherwise calls `get_config_value("user.handle", root)` and returns `{"handle": handle, "kind": "human"}`, falling back to `"human"` when unconfigured. Also introduced `_CreatedBy` TypedDict. - [x] `CB_02` — Test: helper returns explicit `--by` value as `kind=agent` unchanged - [x] `CB_03` — Test: helper returns handle from config when `--by` is `"human"` and config is populated - [x] `CB_04` — Test: helper falls back to `{"handle": "human", "kind": "human"}` when config has no handle ### Phase 2 — Wire into run_save - [x] `CB_05` — `run_save` and `_shelf_push_programmatic`: replace bare `created_by` string with `_resolve_created_by(root, created_by)` call. Updated all TypedDicts (`_ShelfSaveJson`, `_ShelfListEntryJson`, `_ShelfReadJson`, `ShelfEntry`) and `_load_shelf` backward-compat normalisation. - [x] `CB_06` — Test: `muse shelf save --json` with a configured handle produces `created_by: {"handle": "", "kind": "human"}` - [x] `CB_07` — Test: `muse shelf save --by my-agent --json` produces `created_by: {"handle": "my-agent", "kind": "agent"}` - [x] `CB_08` — Test: `muse shelf save --json` with no configured handle produces `created_by: {"handle": "human", "kind": "human"}` ### Phase 3 — Wire into autoshelf call sites - [x] `CB_09` — `_shelf_push_programmatic` now calls `_resolve_created_by`; callers in `checkout.py` and `merge.py` pass `created_by="muse"` → resolves to `{"handle": "muse", "kind": "agent"}` - [x] `CB_10` — Test: `_shelf_push_programmatic(root, created_by="muse")` records `{"handle": "muse", "kind": "agent"}` ## Acceptance criteria - `muse shelf save --json | jq '.created_by'` returns `{"handle":"gabriel","kind":"human"}` on a machine where `user.handle = gabriel` is configured ✅ - `muse shelf save --by my-agent --json | jq '.created_by'` returns `{"handle":"my-agent","kind":"agent"}` ✅ - `muse shelf list --json | jq '.[].created_by.handle'` shows the handle on all new entries ✅ - All CB_0x tests pass (9/9) ✅ - No regression on machines without a configured handle ✅ - 166 shelf tests pass ✅ ## Out of scope - Backfilling `created_by` on existing shelf entries — old entries are normalised on read - Reading identity from `~/.muse/identity.toml` directly — `get_config_value` is the correct single source of truth; no new I/O path needed