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:
"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
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:
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 <path>'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
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.muse rm --cached <dir>(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.- Add regression tests for both: (a) ignoring a previously-tracked,
still-present directory and running
muse code add .untracks it; (b)muse rm --cachedon 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
.museignorechanges, an explicitmuse rm --cachedfix, or both. ~/ecosystem/musehub's.vscodedirectory can actually reach a cleanmuse statusafter this fix, without manual object-store surgery.