feat(shelf): --path scoped shelving, the native replacement for /tmp backups
Prompted by user feedback during this session: I had a recurring pattern of `cp file.py /tmp/file_fixed.py` before reverting a fix to verify its test genuinely fails without it (RED), then `cp /tmp/file_fixed.py file.py` to restore (GREEN) -- an anti-pattern since /tmp is cleared by the OS and the whole point of Muse is to never need an out-of-band backup mechanism. The user asked: can muse shelf just become the idiomatic drop-in replacement?
Investigated muse shelf save's actual semantics first (live-tested, not assumed): it shelves the ENTIRE uncommitted working tree -- tracked changes AND untracked files -- as one atomic unit, relative to HEAD. This made it unsuitable as-is for the RED/GREEN use case: shelving "the fix" would also sweep away a brand-new, not-yet-committed test file, since there was no way to scope the operation to specific paths. Also found two pre-existing documentation inaccuracies while investigating (docs/agent-guide.md's git stash -> muse shelf table claimed "shelves all TRACKED changes" -- wrong, verified live -- and referenced a nonexistent `muse shelf show` subcommand and a `--index` flag that `shelf pop` has never had).
Added `muse shelf save --path <p>` (repeatable, `-p` short form, same convention as `muse diff --path`), matching `git stash push -- <pathspec>` semantics: - Only changes under the given path(s) are shelved; everything else in the working tree -- including untracked files that don't match -- is left completely untouched. - Unlike the unscoped case (which stores the COMPLETE working-tree manifest, by design, for unambiguous restoration across branch switches), a scoped entry's snapshot/deleted fields store ONLY the matched subset. This is a deliberate, intentional divergence: it's what makes popping a scoped entry safe even if other files were edited after it was saved -- the entry simply never references them. - The on-disk revert for a scoped save deliberately does NOT reuse apply_manifest(): that function requires every object in its target manifest to already exist in the store and unconditionally calls restore_object() for every target entry. An out-of-scope dirty file's current content was never written to the store (untracked, or modified but out of scope) -- passing it through at all, even as a "no-op" value, either crashes on a never-committed object or risks clobbering an unrelated edit. Found this the hard way: my first implementation attempt built full-sized prev/target manifests and crashed immediately on a missing object for an out-of-scope untracked file. Fixed by writing a small dedicated revert routine that only ever touches the matched paths' objects (which are guaranteed to exist: either just-written, in scope, or already in HEAD). - New shared `muse/core/snapshot.py::path_matches_pathspec` / `filter_manifest_by_paths` -- the same exact-or-directory-prefix matching `muse diff --path` already used, now shared instead of duplicated a third time; `diff.py`'s own `_filter_manifest` delegates to it, zero behavior change there (regression-tested). - `ShelfEntry` gains a `paths` field (`[]` for unscoped, the pathspec for scoped) surfaced through `shelf save`/`list`/`read` --json, with graceful backward-compatible defaulting for shelf entries written before this field existed. `run_pop`/`run_apply`/`run_diff` needed ZERO changes -- they already operate purely off whatever `snapshot`/`deleted` an entry stores, so a correctly-scoped entry at save time is automatically correctly scoped at restore time.
12 new tests in tests/test_shelf_path_scoped.py: the exact RED/GREEN use case (modified tracked file + untracked test file), an out-of-scope dirty tracked file left untouched, newly-added-file scoping, deletion scoping, directory-prefix pathspec matching, the no-match case, and explicit unscoped-behavior-unaffected regression guards. Confirmed via genuine RED (removed the --path flag and forced paths=[] unconditionally) -> 9 of 12 failed as expected (argparse rejected --path; 3 pass trivially since they assert unscoped/empty-paths behavior) -> GREEN. 511-test regression sweep across every shelf/diff/snapshot-relevant suite passes with zero regressions to the unscoped default. Live-verified end-to-end via muse-dev: modified-file scoping, newly-added-file scoping, and deletion scoping all confirmed working exactly as designed, including the full RED (shelve) -> pop (GREEN) cycle.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Semantic Changes
58 symbols
Files Changed
+1
~4
1253 in snapshot
0 comments
muse hub commit comment sha256:a8de2a41b39b56b68a4f5024e8475ff463681093380c43b72ed39e256cf6a46c --body "your comment"
No comments yet. Be the first to start the discussion.