gabriel / muse public
fix BREAKING task/dir-to-file-transition-stage-clobber #1 / 1
AI Agent gabriel · 1 hour ago · Sep 22, 2026 · Diff

fix: muse code add silently drops a file replacing a tracked empty directory of the same path

Discovered live in a real user repo: 'muse code add .' reported a file as staged ('new file'), muse status even showed it correctly staged briefly, but after 'muse commit' the file reverted to untracked -- as if nothing had happened. Reproduced deterministically:

mkdir target-path && muse code add target-path && muse commit ... rmdir target-path && echo content > target-path muse code add . # reports 'target-path: new file' AND 'target-path/: deleted dir' muse commit ... # target-path is in NEITHER the manifest NOR directories afterward

Root cause: muse/cli/commands/code_stage.py::run_add stages new/modified files into updated_stage[rel] first (the 'collected' loop), then -- completely unconditionally -- a second pass for committed empty directories no longer present on disk writes updated_stage[rel_dir] = make_entry(EMPTY_DIR_OID, 'D') for every path in deleted_committed_dirs. Since a file and a directory-sentinel share one flat stage keyspace (bare path string -> one entry), and this repo's path had just transitioned from tracked-empty-directory to real file, both passes targeted the identical dict key -- and the second, unconditional write silently clobbered the first. The file's stage entry never existed by the time write_stage() ran; the object was written to the store, but committed nowhere.

Fix, two parts: 1. code_stage.py: the deleted-committed-dirs loop now skips any path already present in updated_stage from this same invocation -- a directory-to-file transition needs exactly one stage entry (the file), not a clobbering second write to the same key. 2. plugins/code/plugin.py: defense-in-depth invariant in snapshot()'s stage branch -- staged_dirs now explicitly excludes every path present in staged_manifest, so a path can never be recorded as both a file and a directory in the same commit even if some other staging code path fails to explicitly record the transition. This is the same corruption shape as musehub#101 (files double-recorded as directories in historical commits) -- this fix closes the live commit-time source of that corruption, not just a historical symptom of it.

Added TestDirToFileTransitionStageClobber to test_cmd_code_add.py, confirmed red before the fix (failed on the exact clobbered-to-D stage entry) and green after. Full surrounding suite -- test_cmd_code_add, test_code_stage, test_code_stage_envelope, test_code_add_resolves_conflicts, test_code_add_supercharge, test_directories_feature, test_cmd_commit, test_cmd_status -- 462/462 passing, zero regressions.

Not fixed here, noted as a separate lower-severity issue: the reverse transition (tracked file replaced by a directory of the same name) crashes loudly with 'Cannot read <path>: [Errno 21] Is a directory' instead of corrupting silently -- annoying but not a data-loss risk, filed separately.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:db2a9e509b0f5de47d8d34e0aa20d1c2f8ce9ae4f9b5bf37693c125bd608579e sha
+2 ~3 symbols
3 changed · 1203 in snapshot files
sha256:66132d17184db30dd572e048bf1819fe693ec1dc4c8d2bc0b4d915b7d6d9d963 snapshot
+2
symbols added
~3
symbols modified
3
files changed
1203
files in snapshot
0
dead code introduced
Semantic Changes 5 symbols
~ muse/cli/commands/code_stage.py .py 1 symbol modified
~ muse/plugins/code/plugin.py .py 2 symbols modified
~ tests/test_cmd_code_add.py .py 2 symbols added
+ TestDirToFileTransitionStageClobber class class TestDirToFileTransitionStageClobber L89–150
+ test_file_replacing_committed_empty_dir_is_staged_and_committed method method test_file_replacing_committed_empty_dir_is_staged_and_committed L103–150
Files Changed
~3
1203 in snapshot
← Older Oldest on task/dir-to-file-transition-stage-clobber
All commits
Newer → Latest on task/dir-to-file-transition-stage-clobber

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:db2a9e509b0f5de47d8d34e0aa20d1c2f8ce9ae4f9b5bf37693c125bd608579e --body "your comment"