gabriel / muse public
Open #76 Bug
filed by gabriel human · 15 days ago

muse status misclassifies directories containing only ignored files as untracked

0 Anchors
Blast radius
Churn 30d
0 Proposals

muse status misclassifies directories containing only ignored files as untracked

Background

Found live while auditing ~/ecosystem for stray untracked files. After adding .claude/scheduled_tasks.lock to .museignore, muse status continued to report untracked directory: .claude/ even though muse check-ignore .claude/scheduled_tasks.lock confirms the file is correctly matched and ignored, and muse diff --json reports zero changes (has_changes: false). Two commands disagree about the same working tree because they use different code paths — status goes through the code plugin's drift() method, diff does not.

Root cause — traced to muse/plugins/code/plugin.py's drift() method

for dirpath, dirnames, filenames in os.walk(root_str, followlinks=False):
    ...
    on_disk_dirs.add(cur_rel)              # every dir os.walk visits, unconditionally
    dirnames[:] = sorted(
        d for d in dirnames
        if d not in _ALWAYS_IGNORE_DIRS
        and not is_ignored(f"{cur_rel}/{d}/_" if cur_rel else f"{d}/_", patterns)
        ...
    )
    for fname in sorted(filenames):
        ...
        if is_ignored(rel, patterns):
            continue                        # file excluded from workdir_files
        workdir_files[rel] = ...

...
dirs_with_workdir_files: set[str] = set()   # derived from workdir_files only
for f in workdir_files:
    ...
empty_on_disk_dirs: set[str] = on_disk_dirs - dirs_with_workdir_files
...
dir_added = sorted(empty_on_disk_dirs - committed_empty_dirs - staged_added_dirs)

A directory is only pruned from the walk (excluded from on_disk_dirs) if the directory name itself matches an ignore pattern (e.g. __pycache__/). A file-level pattern like .claude/scheduled_tasks.lock doesn't prune the directory — os.walk still descends into .claude/, adds it to on_disk_dirs, then correctly excludes the one file inside from workdir_files. Since dirs_with_workdir_files is derived only from workdir_files, .claude never lands in it. The subtraction on_disk_dirs - dirs_with_workdir_files then classifies .claude/ as an empty on-disk directory — even though it's not empty, its only content is legitimately ignored — and it flows into dir_added, which muse status renders as untracked directory: .claude/.

Confirmed via isolated reproduction, not just read from source:

$ mkdir onlyignored && echo junk > onlyignored/lockfile.txt
$ echo '[global]\npatterns = ["onlyignored/lockfile.txt"]' > .museignore
$ muse status --json | jq '.untracked'
["onlyignored/"]
$ muse diff --json | jq '.has_changes'
false

Goal

A directory whose contents are entirely composed of individually-ignored files must never be reported as an untracked/empty directory by muse status. The "empty on-disk directory" classification (used for muse code add's empty-dir tracking feature) must mean zero files at all, ignored or not — not "zero non-ignored files."

"Done" means: status's untracked-directory reporting and diff's change-detection agree on every case — a directory containing only ignored content is invisible to both, exactly like a directory containing zero files whose parent is itself ignored.

Out of Scope

  • Any change to muse diff's own logic — it already handles this correctly; this issue is scoped entirely to drift()'s directory classification.
  • The pre-existing empty-directory-tracking feature's semantics for genuinely empty directories (zero files, no ignore involved) — unaffected, not touched.

Phases

Phase 1 — Red: reproduce with a real test

  • STATUSDIR_01 — Directory containing only a file matched by a file-level (not directory-level) ignore pattern: muse status's untracked list must not contain that directory. Must fail against current code (reproducing the exact .claude//onlyignored/ case above) and pass after the fix.
  • STATUSDIR_02 — Same setup, but assert muse diff --json and muse status --json agree: if diff.has_changes is false, status's untracked list must be empty for that directory too. Pins the cross-command consistency directly, not just one command's output.
  • STATUSDIR_03 — Regression guard: a genuinely empty directory (zero files, no ignore pattern involved) must still be correctly reported as an untracked/addable empty directory — confirms the fix doesn't break the existing empty-dir-tracking feature.
  • STATUSDIR_04 — A directory with a mix of one ignored file and one real (non-ignored) file: must appear in untracked only because of the real file, and muse code add on it must stage only the real file.

Deliverable: all four tests exist and STATUSDIR_01/02 fail against current code (red) before any Phase 2 code changes.

Phase 2 — Fix: distinguish "zero files" from "zero non-ignored files"

  • Track a new set — directories containing any file encountered during the walk, ignored or not (populated before the is_ignored check, not after) — separately from dirs_with_workdir_files.
  • empty_on_disk_dirs must subtract this new "has any file" set, not just dirs_with_workdir_files, so a directory with only ignored content is excluded from the empty-dir candidate set entirely.
  • No change to directory-name-level ignore pruning (the dirnames[:] = ... filter) — that mechanism is correct and unrelated to this bug.

Deliverable: STATUSDIR_01 through STATUSDIR_04 all green.

Phase 3 — Regression sweep

  • Full run of the existing directory-tracking test files (test_status_ directory_tracking.py, test_directory_tracking.py, test_dir_lifecycle.py, test_directories_feature.py) — zero new failures.
  • muse code test --json from the muse repo root for downstream impact.

Deliverable: full regression pass, zero new failures.


Acceptance Criteria

  • muse status never reports a directory as untracked/empty when its only contents are individually ignored files.
  • muse status and muse diff agree on working-tree cleanliness for every case in Phases 1–3's tests.
  • Genuinely empty directories (no ignore involved) are still correctly tracked as addable — the pre-existing feature is unaffected.
  • All STATUSDIR_0104 pass; full existing directory-tracking test suite remains green.

Implementation Order

Phase 1 → Phase 2 → Phase 3. TDD by the book — red tests written and confirmed failing before any fix code.

Activity
gabriel opened this issue 15 days ago
No activity yet. Use the CLI to comment.