diff --git a/plugins/disk-cleanup/disk_cleanup.py b/plugins/disk-cleanup/disk_cleanup.py index bd07f844bb4eb..8432c5a7bad3b 100755 --- a/plugins/disk-cleanup/disk_cleanup.py +++ b/plugins/disk-cleanup/disk_cleanup.py @@ -148,6 +148,9 @@ _EMPTY_DIR_PROTECTED_TOP_LEVEL = frozenset({ "logs", "memories", "sessions", "cron", "cronjobs", "cache", "skills", "plugins", "disk-cleanup", "optional-skills", "hermes-agent", "backups", "profiles", ".worktrees", + # User-authored project trees — never sweep empty directories + # inside these (#75403). + "patches", "projects", "skins", "themes", "contributors", }) _EMPTY_DIR_SWEEP_PRUNE_DIRS = frozenset({ @@ -336,6 +339,21 @@ def quick() -> Dict[str, Any]: # Drop the stale entry — it was misclassified. continue + # ---- stale-state migration for 'test' category (fixes #75403) ---- + # Old tracked.json entries may carry a "test" category for paths + # that are now under protected project directories (patches/, + # projects/, etc.). guess_category() was tightened in the fix for + # #75403, but existing entries are never re-validated. Re-classify + # here so stale entries for protected paths are not deleted. + if cat == "test": + re_cat = guess_category(p) + if re_cat != "test": + _log( + f"SKIP stale test entry: {p} " + f"(re-classified as {re_cat!r} — under protected tree)" + ) + continue + # Hard safety net: never delete cron control-plane state even if # the category somehow slipped through re-validation above. if _is_protected_cron_path(p):