gabriel / musehub public
Open #207 Bug data-integrity
filed by gabriel human · 33 days ago

DATA LOSS: muse shelf save silently drops unstaged modifications when any other file is staged

0 Anchors
— Blast radius
— Churn 30d
0 Proposals
No description provided.
◎ Activity2
●
gabriel opened this issue 33 days ago
○
gabriel 33 days ago

HIGHEST SEVERITY — real, permanent, silent data loss of uncommitted work. Found while building Episode 15 ("Shelves") demo material. Confirmed 3 times, in 3 independently constructed minimal repros. Assigning to myself to circle back asap, per the same discipline as staging#90.

Repro (minimal, deterministic):

muse init
echo "line A" > file_a.txt
echo "line B" > file_b.txt
muse code add . && muse commit -m "Initial" ...

echo "line A -- EDITED, UNSTAGED" > file_a.txt      # never `muse code add`ed
echo "line B -- EDITED, STAGED"   > file_b.txt
muse code add file_b.txt                             # only file_b staged

muse shelf save --by claude-code --intent-type checkpoint -m "..." --json
# -> {"status": "shelved", "shelf_size": 0, ...}      <- claims success

muse shelf read main/000 --json
# -> "files": ["file_b.txt"]                          <- file_a.txt is ABSENT

cat file_a.txt
# -> "line A"                                         <- the edit is GONE.

What happened: file_a.txt's real, unstaged edit ("line A -- EDITED, UNSTAGED") is not present in the shelf's stored snapshot manifest (confirmed by reading the shelf's raw object off disk under .muse/shelf/sha256/... and decoding the blob-wrapped JSON — the snapshot dict has no file_a.txt key at all, staged, unstaged, or otherwise). It is not in the object store as any reachable reference. The working tree then reverts file_a.txt back to the last-committed content, exactly as if the checkpoint had succeeded. There is no warning, no nonzero exit code, no warnings field entry — status: "shelved" is reported as an unconditional success.

Isolated to the interaction, not either operation alone:

  • An unstaged modification to a tracked file, shelved alone (nothing else staged) — captured correctly, restores correctly via apply/pop. Confirmed working.
  • A staged addition, shelved alone — captured correctly. Confirmed working.
  • Both present simultaneously (one file staged, a DIFFERENT file has an unrelated unstaged modification) — the unstaged file is dropped entirely from the shelf snapshot, with no error.

Also observed: muse status --json immediately after the lossy shelve reports a confusing/inconsistent state — newfile.txt (the file that WAS staged and correctly shelved) shows up in both staged.added AND unstaged.deleted, and previously-tracked .museattributes/ .museignore show as untracked. This smells like the shelf-save snapshot-building code is operating over some intermediate/partial index representation rather than the true reconciled working-tree state, and whatever it consumes from gets left in a semi-mutated state afterward.

Why this is the most severe finding of the season: every other bug found in Build With Muse involved wrong metadata, an unreachable feature, or a wrong risk score — recoverable, or at worst misleading. This one silently destroys real uncommitted work during what the user believes is a safe checkpoint operation, which is the exact scenario muse shelf exists to protect against ("Full snapshots, not deltas. The complete working-tree manifest is stored, so restoration is always unambiguous" — directly contradicted).

Next step: read muse/cli/commands/shelf.py's snapshot-construction code (likely wherever it enumerates changed paths for the save operation) and determine whether it's reading from a stale/partial in-memory index vs. re-scanning the true working tree + stage state. Priority: fix before recommending muse shelf for any real work, agent or human.

Assigned to gabriel. Circling back on this immediately after wrapping the current episode script, not deferred to "after the season" like the other tickets.

○
gabriel 33 days ago

Second data point on the same underlying defect family — found while finalizing the Episode 15 demo script, reproduced cleanly:

muse init
echo "..." > pipeline.py && muse code add . && muse commit -m "Initial" ...
<edit pipeline.py>
muse code add pipeline.py        # STAGE it (nothing else touched)
muse shelf save --by claude-code --intent-type interrupt -m "test" --resumable

Shelf save reports success and correctly captures pipeline.py's new content in the shelf snapshot this time (files_count: 1, matches). But muse status --json immediately after shows clean: false, with pipeline.py listed as modified in BOTH staged.modified AND unstaged.modified simultaneously — the working tree was never reverted to HEAD at all. The change sits there uncommitted, still staged, exactly as if shelf save had never run its working-tree-reset step.

Contrast with the two clean cases already confirmed:

  • Change is unstaged only (never staged) -> shelf save reverts correctly, clean: true afterward. Works.
  • Change is staged only, and it's the ONLY change in the repo -> shelf save captures it correctly but does NOT revert the working tree/index. Broken, this comment.
  • Mixed staged + unstaged (different files) -> the unstaged file is dropped from the snapshot entirely. Data loss, main report above.

Working theory, not yet verified: shelf save's post-snapshot cleanup step only clears the working tree for paths it read from an UNSTAGED diff pass, and never resets the STAGED index/tree for paths that came from the staged side — which would explain both symptoms as one bug: whatever code path builds the snapshot from staged content never also schedules those same paths for the reset-to-HEAD step. Worth checking first when someone picks this up.