# No command can untrack a committed empty-directory sentinel whose physical directory still exists on disk ## Background Found while cleaning up `~/ecosystem/musehub`'s `.museignore` (adding a `.vscode/` pattern). The directory `.vscode` was already tracked two ways: its one file (`.vscode/settings.json`, untracked cleanly via `muse rm --cached`) and, separately, `.vscode` itself as an explicit empty-directory sentinel in the commit's `directories` list. After untracking the file, `muse status` still reported: ```json "deleted": [".vscode/"] ``` The directory still physically exists on disk (never removed) -- `.museignore` now excludes its contents from every live-tree walk, but nothing removes the stale sentinel from the committed snapshot's `directories` list. ## Repro -- every removal path fails ```bash muse rm --cached .vscode --json # fatal: not removing '.vscode' recursively without -r. muse rm -r --cached .vscode --json # fatal: pathspec '.vscode' did not match any tracked files. muse rm -r --cached ".vscode/" --json # same: pathspec '.vscode' did not match any tracked files. muse code add . --json # {"staged": 0, "added": 0, "modified": 0, "deleted": 0, "files": []} # -- silently no-ops; does not detect or stage the removal at all. ``` No command available today can express "stop tracking this directory sentinel" when the directory is still physically present on disk. ## Root cause `muse/cli/commands/code_stage.py`'s `deleted_committed_dirs` detection (the pass that stages `EMPTY_DIR_OID` "D" sentinels for committed empty dirs) only fires when the directory is **gone from disk**: ```python for _rel_dir in sorted(head_dirs): if (root / _rel_dir).is_dir(): continue # still exists on disk ... deleted_committed_dirs.append(_rel_dir) ``` This never considers the case where the directory still exists but its entire contents are now excluded by `.museignore` (added or changed *after* the directory was originally tracked). From the walker's perspective nothing looks different -- the directory is still there -- so there's no signal that the sentinel should be removed. Separately, `muse rm --cached `'s pathspec resolution apparently only matches entries present in the file manifest (`head_manifest`), not directory-only sentinel entries in `directories` -- hence the "did not match any tracked files" error even when passed the exact literal path shown in `muse status`'s own output. ## Why this matters This is the same underlying design gap `muse#101` and `muse#102` are both instances of: the empty-directory sentinel system has several transition edges (file→dir, dir→file, dir still-present-but-now-ignored) that the staging/removal commands don't fully cover. This is the first one found with **no workaround at all** through the CLI -- #101 and #102 both have escape hatches (add real content; wait for the file/dir history to naturally resolve), but there is currently no command that untracks a still-present, now-ignored directory sentinel. ## Scope 1. `code_stage.py`'s empty-dir deletion detection should also fire when a committed empty directory's path is now fully excluded by `.museignore` (not just when physically absent from disk) -- i.e. `muse code add .` should detect and stage the removal automatically. 2. `muse rm --cached ` (with or without `-r`) should recognize and remove a directory-only sentinel entry, independent of whether any files exist under that path in the manifest. 3. Add regression tests for both: (a) ignoring a previously-tracked, still-present directory and running `muse code add .` untracks it; (b) `muse rm --cached` on a directory-only sentinel entry succeeds. ## Acceptance criteria - At least one command can untrack a committed empty-directory sentinel whose directory still physically exists on disk, whether that's automatic detection via `.museignore` changes, an explicit `muse rm --cached` fix, or both. - `~/ecosystem/musehub`'s `.vscode` directory can actually reach a clean `muse status` after this fix, without manual object-store surgery.