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": "<name>", "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(andmuse shelfbare) emitcreated_by: {"handle": "gabriel", "kind": "human"}whenuser.handleis configured.- When no handle is configured (CI, fresh machine, anonymous), falls back to
{"handle": "human", "kind": "human"}— same observable behaviour as before. - The
--byflag still overrides everything and produces{"handle": "<id>", "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_byare normalised on read — no migration needed.
Phases
Phase 1 — Extract a helper
- [x]
CB_01— Add_resolve_created_by(root, explicit_by)helper inmuse/cli/commands/shelf.py. Returns{"handle": explicit_by, "kind": "agent"}whenexplicit_by != "human"; otherwise callsget_config_value("user.handle", root)and returns{"handle": handle, "kind": "human"}, falling back to"human"when unconfigured. Also introduced_CreatedByTypedDict. - [x]
CB_02— Test: helper returns explicit--byvalue askind=agentunchanged - [x]
CB_03— Test: helper returns handle from config when--byis"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_saveand_shelf_push_programmatic: replace barecreated_bystring with_resolve_created_by(root, created_by)call. Updated all TypedDicts (_ShelfSaveJson,_ShelfListEntryJson,_ShelfReadJson,ShelfEntry) and_load_shelfbackward-compat normalisation. - [x]
CB_06— Test:muse shelf save --jsonwith a configured handle producescreated_by: {"handle": "<handle>", "kind": "human"} - [x]
CB_07— Test:muse shelf save --by my-agent --jsonproducescreated_by: {"handle": "my-agent", "kind": "agent"} - [x]
CB_08— Test:muse shelf save --jsonwith no configured handle producescreated_by: {"handle": "human", "kind": "human"}
Phase 3 — Wire into autoshelf call sites
- [x]
CB_09—_shelf_push_programmaticnow calls_resolve_created_by; callers incheckout.pyandmerge.pypasscreated_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 whereuser.handle = gabrielis 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_byon existing shelf entries — old entries are normalised on read - Reading identity from
~/.muse/identity.tomldirectly —get_config_valueis the correct single source of truth; no new I/O path needed