muse shelf save: resolve created_by from user identity, not hardcoded human
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
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.CB_02— Test: helper returns explicit--byvalue askind=agentunchangedCB_03— Test: helper returns handle from config when--byis"human"and config is populatedCB_04— Test: helper falls back to{"handle": "human", "kind": "human"}when config has no handle
Phase 2 — Wire into run_save
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.CB_06— Test:muse shelf save --jsonwith a configured handle producescreated_by: {"handle": "<handle>", "kind": "human"}CB_07— Test:muse shelf save --by my-agent --jsonproducescreated_by: {"handle": "my-agent", "kind": "agent"}CB_08— Test:muse shelf save --jsonwith no configured handle producescreated_by: {"handle": "human", "kind": "human"}
Phase 3 — Wire into autoshelf call sites
CB_09—_shelf_push_programmaticnow calls_resolve_created_by; callers incheckout.pyandmerge.pypasscreated_by="muse"→ resolves to{"handle": "muse", "kind": "agent"}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
All 10 deliverables implemented and shipped in commit sha256:afb6f72afb8e on dev. 166/166 shelf tests pass. created_by is now {"handle": "...", "kind": "human|agent"} — old string entries normalised on load.