diff --git a/fixos/cli/_auto_cleanup.py b/fixos/cli/_auto_cleanup.py index bb86053..e9dc9d8 100644 --- a/fixos/cli/_auto_cleanup.py +++ b/fixos/cli/_auto_cleanup.py @@ -19,6 +19,11 @@ ".cache/pypoetry/cache", ".cache/pypoetry/artifacts", ".cache/Google/Chrome/Default/Cache", + ".cache/go-build", + ".cache/ruff", + ".cache/mypy", + ".cache/Flake8", + ".cache/black", ) _BROWSERS = ("google-chrome", "chromium", "BraveSoftware/Brave-Browser") _BROWSER_LEAVES = ("Cache", "Code Cache", "GPUCache", "DawnCache", "ShaderCache") diff --git a/fixos/diagnostics/quick_snapshot.py b/fixos/diagnostics/quick_snapshot.py index 8df5d77..c66897f 100644 --- a/fixos/diagnostics/quick_snapshot.py +++ b/fixos/diagnostics/quick_snapshot.py @@ -74,7 +74,12 @@ class CacheRule: CacheRule( "pnpm", "pnpm store", - ("~/.local/share/pnpm/store", "~/.pnpm-store"), + ( + "~/.local/share/pnpm/store", + "~/.pnpm-store", + "~/.cache/pnpm", + "~/Library/Caches/pnpm", + ), "pnpm store prune", stack="node", ), @@ -95,6 +100,33 @@ class CacheRule: "poetry cache clear --all pypi", stack="python", ), + CacheRule( + "go-build", + "Go build cache", + ( + "~/.cache/go-build", + "~/Library/Caches/go-build", + "~/AppData/Local/go-build", + ), + "go clean -cache", + stack="go", + ), + CacheRule( + "python-linters", + "Python linters and typecheck cache", + ( + "~/.cache/ruff", + "~/.cache/mypy", + "~/.cache/Flake8", + "~/.cache/flake8", + "~/.cache/black", + "~/Library/Caches/ruff", + "~/Library/Caches/mypy", + "~/AppData/Local/ruff/Cache", + ), + "rm -rf ~/.cache/ruff ~/.cache/mypy ~/.cache/Flake8 ~/.cache/black", + stack="python", + ), CacheRule( "chrome", "Chrome cache", diff --git a/project/TICKETS.md b/project/TICKETS.md index 2464515..aeb260a 100644 --- a/project/TICKETS.md +++ b/project/TICKETS.md @@ -105,6 +105,7 @@ This file indexes governance tickets without taking ownership of | **ticket-103** | [`README.md`](./ticket-103/README.md) | - | - | - | - | - | | **ticket-104** | [`README.md`](./ticket-104/README.md) | - | - | - | - | - | | **ticket-105** | [`README.md`](./ticket-105/README.md) | - | - | - | - | - | +| **ticket-106** | [`README.md`](./ticket-106/README.md) | - | - | - | - | - | | **ticket-107** | [`README.md`](./ticket-107/README.md) | - | - | - | - | - | | **ticket-108** | [`README.md`](./ticket-108/README.md) | - | - | - | - | - | diff --git a/project/ticket-106/README.md b/project/ticket-106/README.md new file mode 100644 index 0000000..31271e8 --- /dev/null +++ b/project/ticket-106/README.md @@ -0,0 +1,22 @@ +# Ticket 106: Expand additional_cache_candidates and quick_snapshot CACHE_RULES for developer caches + +- **ID**: ticket-106 +- **Owner**: agent:antigravity +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-10-26 + +## Goal and scope + +Expand `_CACHE_PATHS` in `fixos/cli/_auto_cleanup.py` and `CACHE_RULES` in `fixos/diagnostics/quick_snapshot.py` to cover regenerable developer caches (`.cache/go-build`, `.cache/ruff`, `.cache/mypy`, `.cache/Flake8`, `.cache/black`, and `~/.cache/pnpm`) while ensuring strict protections against symlinks, nested mounts, browser profiles, and model weights (`.cache/huggingface`). Unit tests in `tests/unit/test_auto_cleanup.py` verify that developer caches are reclaimed and protected paths remain untouched. + +## Acceptance criteria + +- [x] AC-01: Add safe regenerable developer cache leaves (`.cache/go-build`, `.cache/ruff`, `.cache/mypy`, `.cache/Flake8`, `.cache/black`) to `_CACHE_PATHS` in `fixos/cli/_auto_cleanup.py` preserving strict `cache_path_allowed` guards. +- [x] AC-02: Align `CACHE_RULES` in `fixos/diagnostics/quick_snapshot.py` so `go-build` and Python linter/typecheck caches are reflected in `safe_reclaim` estimates. +- [x] AC-03: Unit tests in `tests/unit/test_auto_cleanup.py` verify that new regenerable developer caches are cleaned and protected paths (models, browser data) remain untouched. + +## Tracking boundary + +This directory contains the minimal reviewed intent. Optional participant prose +and raw command logs are not required delivery output. diff --git a/project/ticket-106/intent.json b/project/ticket-106/intent.json new file mode 100644 index 0000000..06160c0 --- /dev/null +++ b/project/ticket-106/intent.json @@ -0,0 +1,82 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-106", + "summary": "Expand additional_cache_candidates and quick_snapshot CACHE_RULES for developer caches", + "workstream": "application", + "classification": { + "kind": "FEATURE", + "priority": "P2", + "origin": "requested" + }, + "allowedPaths": [ + "project/ticket-106/**", + "TODO.md", + "project/TICKETS.md", + "fixos/cli/_auto_cleanup.py", + "fixos/diagnostics/quick_snapshot.py", + "tests/unit/test_auto_cleanup.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [ + "python" + ], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "98e8f0214df0900ff3dc16b8a42a102de5e67e3e", + "targetBranch": "main", + "outcome": "Expand _CACHE_PATHS in fixos/cli/_auto_cleanup.py and CACHE_RULES in fixos/diagnostics/quick_snapshot.py to cover safe regenerable developer caches (.cache/go-build, .cache/ruff, .cache/mypy, .cache/Flake8, .cache/black) while preserving strict path guards for model weights and browser profile data, with comprehensive unit tests.", + "nonGoals": [ + "Never touch model weights in .cache/huggingface or browser profile databases/bookmarks/cookies." + ], + "complexity": "S", + "estimatedMinutes": 30, + "budgets": { + "maxImplementationFiles": 3, + "maxAffectedComponents": 2, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Add safe regenerable developer cache leaf paths (.cache/go-build, .cache/ruff, .cache/mypy, .cache/Flake8, .cache/black) to _CACHE_PATHS in _auto_cleanup.py and align CACHE_RULES in quick_snapshot.py with go-build and python-linters rules plus pnpm ~/.cache/pnpm coverage; verify path protections and safe_reclaim accounting in unit tests.", + "components": [ + { + "name": "developer-cache-cleanup", + "paths": [ + "fixos/cli/_auto_cleanup.py", + "fixos/diagnostics/quick_snapshot.py" + ] + }, + { + "name": "tests", + "paths": [ + "tests/unit/test_auto_cleanup.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert ticket-106 commit via protected publication." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "pytest tests/unit/test_auto_cleanup.py tests/unit/test_quick_snapshot.py -q" + ], + "evidence": "project/ticket-106/README.md" + } + ] + } +} diff --git a/tests/unit/test_auto_cleanup.py b/tests/unit/test_auto_cleanup.py index 34d5f17..17641a6 100644 --- a/tests/unit/test_auto_cleanup.py +++ b/tests/unit/test_auto_cleanup.py @@ -1,5 +1,6 @@ """Safety and reporting regressions for expanded autonomous cleanup.""" +import inspect import json import subprocess from pathlib import Path @@ -14,6 +15,12 @@ from fixos.cli.main import cli +def _cli_runner() -> CliRunner: + if "mix_stderr" in inspect.signature(CliRunner.__init__).parameters: + return CliRunner(mix_stderr=False) + return CliRunner() + + def put(home, relative, content="cached"): path = home / relative path.parent.mkdir(parents=True, exist_ok=True) @@ -32,6 +39,11 @@ def test_cleanup_keeps_downloads_profiles_models_and_unknown_cache( ".cache/pypoetry/artifacts/pkg.whl", ".cache/google-chrome/Default/Cache/data", ".cargo/registry/cache/repo/a.crate", + ".cache/go-build/leaf/cache", + ".cache/ruff/leaf/cache", + ".cache/mypy/leaf/cache", + ".cache/Flake8/leaf/cache", + ".cache/black/leaf/cache", ) ] protected = [ @@ -168,8 +180,8 @@ def auto_cli(monkeypatch): "thumbnails", ): monkeypatch.setattr(space, "_cleanup_" + name, Mock()) - extra = Mock(side_effect=lambda **k: print("cache progress")) - docker = Mock(side_effect=lambda **k: print("docker progress")) + extra = Mock(side_effect=lambda **k: print("cache progress", flush=True)) + docker = Mock(side_effect=lambda **k: print("docker progress", flush=True)) monkeypatch.setattr(auto, "cleanup_additional_caches", extra) monkeypatch.setattr(auto, "cleanup_docker_builder", docker) return extra, docker @@ -204,7 +216,7 @@ def stat(path, *args, **kwargs): return stat_original(path, *args, **kwargs) monkeypatch.setattr(os, "stat", stat) - result = CliRunner().invoke(cli, ["auto", "--json"]) + result = _cli_runner().invoke(cli, ["auto", "--json"]) assert result.exit_code == 0, result.output payload = json.loads(result.stdout) assert payload["disk_freed_bytes"] == 1024**3 @@ -291,7 +303,7 @@ def test_interactive_cache_still_removes_selected_regular_unknown_cache( def test_auto_executes_7d_state_and_projects_cleanup_by_default(auto_cli): - result = CliRunner().invoke(cli, ["auto", "--json"]) + result = _cli_runner().invoke(cli, ["auto", "--json"]) assert result.exit_code == 0, result.output payload = json.loads(result.stdout) assert "state-old" in payload["steps_executed"] @@ -304,7 +316,7 @@ def test_auto_executes_7d_state_and_projects_cleanup_by_default(auto_cli): def test_auto_respects_no_state_old_and_no_projects_old(auto_cli): - result = CliRunner().invoke( + result = _cli_runner().invoke( cli, ["auto", "--json", "--no-state-old", "--no-projects-old"] ) assert result.exit_code == 0, result.output @@ -312,3 +324,107 @@ def test_auto_respects_no_state_old_and_no_projects_old(auto_cli): assert "state-old" not in payload["steps_executed"] assert "projects-old" not in payload["steps_executed"] + +def test_cleanup_removes_developer_caches_and_preserves_models_and_browser_data( + tmp_path, monkeypatch +): + monkeypatch.setattr(Path, "home", lambda: tmp_path) + caches = [ + put(tmp_path, p) + for p in ( + ".cache/go-build/leaf/cache", + ".cache/ruff/leaf/cache", + ".cache/mypy/leaf/cache", + ".cache/Flake8/leaf/cache", + ".cache/black/leaf/cache", + ) + ] + protected = [ + put(tmp_path, p) + for p in ( + ".cache/huggingface/hub/models--bert-base-uncased/model.safetensors", + ".cache/google-chrome/Default/History", + ".cache/google-chrome/Default/Cookies", + ".config/google-chrome/Default/Bookmarks", + ".cache/unknown_tool/cache", + ) + ] + auto.cleanup_additional_caches(dry_run=False, yes=True) + assert all(not p.exists() for p in caches) + assert all(p.exists() for p in protected) + + +def test_additional_cache_candidates_includes_developer_caches(tmp_path): + for p in ( + ".cache/go-build/data", + ".cache/ruff/data", + ".cache/mypy/data", + ".cache/Flake8/data", + ".cache/black/data", + ): + put(tmp_path, p, "data") + candidates = auto.additional_cache_candidates(tmp_path) + descriptions = {c["description"] for c in candidates} + assert "~/.cache/go-build" in descriptions + assert "~/.cache/ruff" in descriptions + assert "~/.cache/mypy" in descriptions + assert "~/.cache/Flake8" in descriptions + assert "~/.cache/black" in descriptions + + +def test_quick_snapshot_reflects_go_and_python_caches_in_safe_reclaim( + tmp_path, monkeypatch +): + from datetime import datetime, timezone + + from fixos.diagnostics import quick_snapshot as qs + + put(tmp_path, ".cache/go-build/sample.bin", "x" * 2048) + put(tmp_path, ".cache/ruff/sample.bin", "y" * 4096) + put(tmp_path, ".cache/mypy/sample.bin", "z" * 1024) + + monkeypatch.setattr( + qs, + "_resource_snapshot", + lambda: { + "cpu": { + "percent": 10, + "logical_cpus": 4, + "load": [0.1, 0.1, 0.1], + "load_1_normalized_percent": 2, + }, + "memory": { + "total_bytes": 1000, + "used_bytes": 300, + "available_bytes": 700, + "percent": 30, + }, + "swap": {"total_bytes": 1000, "used_bytes": 0, "percent": 0}, + "disk": { + "mount": "/", + "total_bytes": 10 * 1024**3, + "used_bytes": 1024**3, + "free_bytes": 9 * 1024**3, + "percent": 10, + }, + "top_processes": [], + }, + ) + monkeypatch.setattr(qs, "_cached_docker_review", lambda now, **_: None) + + snapshot = qs.collect_quick_snapshot( + state_dir=tmp_path, + home=tmp_path, + cwd=tmp_path, + save=False, + now=datetime.now(timezone.utc), + ) + + safe_items = snapshot["safe_reclaim"]["items"] + item_ids = {item["id"] for item in safe_items} + assert "go-build" in item_ids + assert "python-linters" in item_ids + assert snapshot["safe_reclaim"]["estimated_max_bytes"] > 0 + + +