# Model Usage Tracking — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Track which model files ComfyUI uses in prompts and surface never-used models so the user can delete them. **Architecture:** Extend the existing SQLite DB with a `model_usage` table. At prompt time, inspect each node's `INPUT_TYPES()` to find folder-dropdown inputs, look up the selected filename in a `folder_paths` reverse map, and record usage. A new API endpoint returns per-type grouped stats. The existing dialog gets two tabs: Nodes (unchanged) and Models (new). **Tech Stack:** Python, SQLite (via `sqlite3`), ComfyUI's `folder_paths` + `nodes` modules, vanilla JS. --- ### Task 1: Extend tracker.py — model_usage schema + methods **Files:** - Modify: `tracker.py` - Create: `tests/test_model_tracker.py` **Step 1: Add model_usage to SCHEMA** In `tracker.py`, extend the `SCHEMA` string — append after the existing `CREATE INDEX` lines: ```python SCHEMA = """ CREATE TABLE IF NOT EXISTS node_usage ( class_type TEXT PRIMARY KEY, package TEXT NOT NULL, count INTEGER NOT NULL DEFAULT 0, first_seen TEXT NOT NULL, last_seen TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS prompt_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, class_types TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS model_usage ( model_name TEXT PRIMARY KEY, model_type TEXT NOT NULL, count INTEGER NOT NULL DEFAULT 0, first_seen TEXT NOT NULL, last_seen TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_node_usage_package ON node_usage(package); CREATE INDEX IF NOT EXISTS idx_prompt_log_timestamp ON prompt_log(timestamp); CREATE INDEX IF NOT EXISTS idx_model_usage_type ON model_usage(model_type); """ ``` **Step 2: Write failing test** Create `tests/test_model_tracker.py`: ```python import pytest import tempfile import os from tracker import UsageTracker @pytest.fixture def tracker(tmp_path): return UsageTracker(db_path=str(tmp_path / "test.db")) def test_record_and_retrieve_model_usage(tracker): tracker.record_model_usage([("dreamshaper.safetensors", "checkpoints")]) tracker.record_model_usage([("dreamshaper.safetensors", "checkpoints")]) raw = tracker.get_raw_model_stats() assert len(raw) == 1 assert raw[0]["model_name"] == "dreamshaper.safetensors" assert raw[0]["model_type"] == "checkpoints" assert raw[0]["count"] == 2 def test_record_multiple_models(tracker): tracker.record_model_usage([ ("dreamshaper.safetensors", "checkpoints"), ("vae.safetensors", "vae"), ]) raw = tracker.get_raw_model_stats() assert len(raw) == 2 def test_reset_clears_model_usage(tracker): tracker.record_model_usage([("model.safetensors", "checkpoints")]) tracker.reset() assert tracker.get_raw_model_stats() == [] def test_empty_models_returns_empty(tracker): assert tracker.get_raw_model_stats() == [] ``` **Step 3: Run test to confirm it fails** ```bash cd /media/p5/Comfyui_nodes_stats python -m pytest tests/test_model_tracker.py -v ``` Expected: `AttributeError: 'UsageTracker' object has no attribute 'record_model_usage'` **Step 4: Add methods to UsageTracker** In `tracker.py`, add after `record_usage()`: ```python def record_model_usage(self, models): """Record usage of model files from a single prompt. models: list of (model_name, model_type) tuples """ if not models: return now = datetime.now(timezone.utc).isoformat() with self._lock: self._ensure_db() conn = self._connect() try: for model_name, model_type in models: conn.execute( """INSERT INTO model_usage (model_name, model_type, count, first_seen, last_seen) VALUES (?, ?, 1, ?, ?) ON CONFLICT(model_name) DO UPDATE SET count = count + 1, last_seen = excluded.last_seen""", (model_name, model_type, now, now), ) conn.commit() finally: conn.close() def get_raw_model_stats(self): """Return raw per-model usage rows from DB.""" with self._lock: self._ensure_db() conn = self._connect() try: conn.row_factory = sqlite3.Row rows = conn.execute( "SELECT model_name, model_type, count, first_seen, last_seen " "FROM model_usage ORDER BY count DESC" ).fetchall() return [dict(r) for r in rows] finally: conn.close() ``` In `reset()`, add before `conn.commit()`: ```python conn.execute("DELETE FROM model_usage") ``` **Step 5: Add get_model_stats() method** This method merges DB data with installed models (passed in from caller, so it stays testable): ```python def get_model_stats(self, installed_by_type): """Return per-type grouped model stats with tier classification. installed_by_type: {model_type: [model_name, ...]} from ModelMapper """ db_rows = self.get_raw_model_stats() db_models = {r["model_name"]: r for r in db_rows} now = datetime.now(timezone.utc) one_month_ago = (now - timedelta(days=30)).isoformat() two_months_ago = (now - timedelta(days=60)).isoformat() tracking_start = self._get_first_prompt_time() STATUS_ORDER = { "safe_to_remove": 0, "consider_removing": 1, "unused_new": 2, "used": 3, "uninstalled": 4, } result_by_type = {} # Process installed models for model_type, filenames in installed_by_type.items(): entries = [] for model_name in filenames: if model_name in db_models: row = db_models[model_name] last_seen = row["last_seen"] if last_seen < two_months_ago: status = "safe_to_remove" elif last_seen < one_month_ago: status = "consider_removing" else: status = "used" entry = { "model_name": model_name, "model_type": model_type, "count": row["count"], "first_seen": row["first_seen"], "last_seen": last_seen, "installed": True, "status": status, } else: if tracking_start is None: status = "unused_new" elif tracking_start < two_months_ago: status = "safe_to_remove" elif tracking_start < one_month_ago: status = "consider_removing" else: status = "unused_new" entry = { "model_name": model_name, "model_type": model_type, "count": 0, "first_seen": None, "last_seen": None, "installed": True, "status": status, } entries.append(entry) result_by_type[model_type] = entries # Add uninstalled (in DB but not on disk) installed_names = { name for names in installed_by_type.values() for name in names } for model_name, row in db_models.items(): if model_name not in installed_names: model_type = row["model_type"] result_by_type.setdefault(model_type, []).append({ "model_name": model_name, "model_type": model_type, "count": row["count"], "first_seen": row["first_seen"], "last_seen": row["last_seen"], "installed": False, "status": "uninstalled", }) # Sort each type's models by status tier then name result = [] for model_type in sorted(result_by_type): models = result_by_type[model_type] models.sort(key=lambda m: (STATUS_ORDER.get(m["status"], 5), m["model_name"])) result.append({"model_type": model_type, "models": models}) return result ``` **Step 6: Add tests for get_model_stats()** Append to `tests/test_model_tracker.py`: ```python from datetime import datetime, timezone, timedelta from unittest.mock import patch def test_get_model_stats_used(tracker): tracker.record_model_usage([("model.safetensors", "checkpoints")]) installed = {"checkpoints": ["model.safetensors"]} result = tracker.get_model_stats(installed) assert len(result) == 1 assert result[0]["model_type"] == "checkpoints" assert result[0]["models"][0]["status"] == "used" assert result[0]["models"][0]["count"] == 1 def test_get_model_stats_never_used_new(tracker): installed = {"checkpoints": ["unused.safetensors"]} result = tracker.get_model_stats(installed) assert result[0]["models"][0]["status"] == "unused_new" assert result[0]["models"][0]["count"] == 0 def test_get_model_stats_uninstalled(tracker): tracker.record_model_usage([("gone.safetensors", "checkpoints")]) installed = {} # no longer on disk result = tracker.get_model_stats(installed) assert result[0]["models"][0]["status"] == "uninstalled" assert result[0]["models"][0]["installed"] is False def test_get_model_stats_sorted_by_status(tracker): tracker.record_model_usage([("active.safetensors", "checkpoints")]) installed = {"checkpoints": ["active.safetensors", "unused.safetensors"]} result = tracker.get_model_stats(installed) models = result[0]["models"] statuses = [m["status"] for m in models] # unused_new (2) comes before used (3) in STATUS_ORDER assert statuses.index("unused_new") < statuses.index("used") ``` **Step 7: Run all tests** ```bash python -m pytest tests/test_model_tracker.py -v ``` Expected: all PASS **Step 8: Commit** ```bash git add tracker.py tests/test_model_tracker.py git commit -m "feat: add model_usage table and tracker methods" ``` --- ### Task 2: Add ModelMapper to mapper.py **Files:** - Modify: `mapper.py` - Create: `tests/test_model_mapper.py` **Step 1: Write failing test** Create `tests/test_model_mapper.py`: ```python import pytest from unittest.mock import patch, MagicMock from mapper import ModelMapper FAKE_FOLDER_NAMES = { "checkpoints": ([], {}), "vae": ([], {}), "loras": ([], {}), "configs": ([], {}), } FAKE_FILES = { "checkpoints": ["dream.safetensors", "v15.ckpt"], "vae": ["vae.safetensors"], "loras": ["style.safetensors"], } def _make_mapper(): m = ModelMapper() with patch("folder_paths.folder_names_and_paths", FAKE_FOLDER_NAMES), \ patch("folder_paths.get_filename_list", side_effect=lambda t: FAKE_FILES.get(t, [])): m._build() return m def test_get_model_type_known(monkeypatch): m = _make_mapper() assert m.get_model_type("dream.safetensors") == "checkpoints" assert m.get_model_type("vae.safetensors") == "vae" def test_loras_excluded(monkeypatch): m = _make_mapper() assert m.get_model_type("style.safetensors") is None def test_get_all_models(monkeypatch): m = _make_mapper() all_models = m.get_all_models() assert "checkpoints" in all_models assert "vae" in all_models assert "loras" not in all_models assert "dream.safetensors" in all_models["checkpoints"] def test_unknown_filename_returns_none(): m = _make_mapper() assert m.get_model_type("nonexistent.ckpt") is None ``` **Step 2: Run test to confirm it fails** ```bash python -m pytest tests/test_model_mapper.py -v ``` Expected: `ImportError: cannot import name 'ModelMapper' from 'mapper'` **Step 3: Add ModelMapper to mapper.py** Append to `mapper.py` after the existing `NodePackageMapper` class: ```python # Folder types that are not model files and should not be tracked EXCLUDED_FOLDER_TYPES = { "loras", "configs", "custom_nodes", "temp", "output", "input", "annotators", "assets", } class ModelMapper: """Tracks which folder_paths model types exist and resolves filenames to types.""" def __init__(self): self._folder_files = None # {folder_type: frozenset(filenames)} self._reverse = None # {filename: folder_type} def _build(self): try: import folder_paths self._folder_files = {} for folder_type in folder_paths.folder_names_and_paths: if folder_type in EXCLUDED_FOLDER_TYPES: continue try: files = folder_paths.get_filename_list(folder_type) except Exception: files = [] if files: self._folder_files[folder_type] = frozenset(files) # Reverse map: filename -> folder_type (last write wins on collision) self._reverse = {} for folder_type, files in self._folder_files.items(): for f in files: self._reverse[f] = folder_type except Exception: logger.warning("ModelMapper: failed to build model map", exc_info=True) self._folder_files = {} self._reverse = {} def _ensure(self): if self._folder_files is None: self._build() def get_model_type(self, filename): """Return the folder type for a filename, or None if not tracked.""" self._ensure() return self._reverse.get(filename) def get_all_models(self): """Return {folder_type: [filename, ...]} for all tracked types.""" self._ensure() return {k: sorted(v) for k, v in self._folder_files.items()} def extract_models_from_prompt(self, prompt): """Scan a prompt dict and return (model_name, model_type) pairs. For each node, inspects INPUT_TYPES() to find list-type (folder dropdown) inputs, then resolves the selected value against the folder_paths reverse map. """ self._ensure() try: import nodes as comfy_nodes except ImportError: return [] seen = set() results = [] for node_data in prompt.values(): class_type = node_data.get("class_type") node_inputs = node_data.get("inputs", {}) if not class_type or not node_inputs: continue node_cls = comfy_nodes.NODE_CLASS_MAPPINGS.get(class_type) if node_cls is None: continue try: input_types = node_cls.INPUT_TYPES() except Exception: continue for category in ("required", "optional"): for input_name, input_def in input_types.get(category, {}).items(): if not isinstance(input_def, (list, tuple)) or not input_def: continue # ComfyUI folder dropdowns have a list as their type if not isinstance(input_def[0], list): continue value = node_inputs.get(input_name) if not isinstance(value, str) or value in seen: continue model_type = self.get_model_type(value) if model_type: seen.add(value) results.append((value, model_type)) return results def invalidate(self): """Force rebuild on next access.""" self._folder_files = None self._reverse = None ``` **Step 4: Add extract_models_from_prompt test** Append to `tests/test_model_mapper.py`: ```python def test_extract_models_from_prompt(): m = _make_mapper() fake_node_cls = MagicMock() fake_node_cls.INPUT_TYPES.return_value = { "required": { "ckpt_name": (["dream.safetensors", "v15.ckpt"],), "steps": ("INT", {"default": 20}), } } fake_prompt = { "1": { "class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "dream.safetensors", "steps": 20}, } } with patch("nodes.NODE_CLASS_MAPPINGS", {"CheckpointLoaderSimple": fake_node_cls}): results = m.extract_models_from_prompt(fake_prompt) assert ("dream.safetensors", "checkpoints") in results def test_extract_models_skips_non_list_inputs(): m = _make_mapper() fake_node_cls = MagicMock() fake_node_cls.INPUT_TYPES.return_value = { "required": { "text": ("STRING", {}), } } fake_prompt = {"1": {"class_type": "CLIPTextEncode", "inputs": {"text": "hello"}}} with patch("nodes.NODE_CLASS_MAPPINGS", {"CLIPTextEncode": fake_node_cls}): results = m.extract_models_from_prompt(fake_prompt) assert results == [] ``` **Step 5: Run all tests** ```bash python -m pytest tests/test_model_mapper.py -v ``` Expected: all PASS **Step 6: Commit** ```bash git add mapper.py tests/test_model_mapper.py git commit -m "feat: add ModelMapper with folder_paths introspection" ``` --- ### Task 3: Extend __init__.py — prompt handler + API endpoint **Files:** - Modify: `__init__.py` No automated tests here (requires live ComfyUI server). Test manually after Task 4. **Step 1: Import ModelMapper and create instance** At the top of `__init__.py`, after existing imports: ```python from .mapper import NodePackageMapper, ModelMapper ``` After `tracker = UsageTracker()`, add: ```python model_mapper = ModelMapper() ``` **Step 2: Extend on_prompt_handler to extract models** Replace the existing `on_prompt_handler` function: ```python def on_prompt_handler(json_data): """Called on every prompt submission. Extracts class_types and model files.""" try: prompt = json_data.get("prompt", {}) class_types = set() for node_id, node_data in prompt.items(): ct = node_data.get("class_type") if ct: class_types.add(ct) if class_types: models = model_mapper.extract_models_from_prompt(prompt) threading.Thread( target=_record_prompt, args=(class_types, models), daemon=True, ).start() except Exception: logger.warning("nodes-stats: error recording usage", exc_info=True) return json_data def _record_prompt(class_types, models): tracker.record_usage(class_types, mapper) if models: tracker.record_model_usage(models) ``` **Step 3: Extend reset endpoint to invalidate model_mapper** In the `reset_stats` route handler, add `model_mapper.invalidate()` after `mapper.invalidate()`: ```python @routes.post("/nodes-stats/reset") async def reset_stats(request): try: tracker.reset() mapper.invalidate() model_mapper.invalidate() return web.json_response({"status": "ok"}) except Exception: logger.error("nodes-stats: error resetting stats", exc_info=True) return web.json_response({"error": "internal error"}, status=500) ``` **Step 4: Add /nodes-stats/models endpoint** After the existing `/nodes-stats/usage` route: ```python @routes.get("/nodes-stats/models") async def get_model_stats(request): try: installed_by_type = model_mapper.get_all_models() stats = tracker.get_model_stats(installed_by_type) return web.json_response(stats) except Exception: logger.error("nodes-stats: error getting model stats", exc_info=True) return web.json_response({"error": "internal error"}, status=500) ``` **Step 5: Commit** ```bash git add __init__.py git commit -m "feat: extend prompt handler and add /nodes-stats/models endpoint" ``` --- ### Task 4: Frontend — tab switcher + Models tab **Files:** - Modify: `js/nodes_stats.js` **Step 1: Update showStatsDialog to fetch models data** Replace the fetch at the top of `showStatsDialog()`: ```javascript async function showStatsDialog() { let data, modelData; try { const [pkgResp, modelResp] = await Promise.all([ fetch("/nodes-stats/packages"), fetch("/nodes-stats/models"), ]); if (!pkgResp.ok) { alert("Failed to load node stats: HTTP " + pkgResp.status); return; } if (!modelResp.ok) { alert("Failed to load model stats: HTTP " + modelResp.status); return; } data = await pkgResp.json(); modelData = await modelResp.json(); if (!Array.isArray(data) || !Array.isArray(modelData)) { alert("Failed to load stats: unexpected response format"); return; } } catch (e) { alert("Failed to load stats: " + e.message); return; } // ... rest of function ``` **Step 2: Replace dialog html construction with tabbed layout** After the existing `let html = ...` header block (the title + close button), replace the rest of the dialog HTML building with: ```javascript // Tab switcher html += `
`; // Nodes tab content (existing content, wrapped) html += `
`; html += buildNodesTabContent(custom); html += `
`; // Models tab content html += ``; dialog.innerHTML = html; overlay.appendChild(dialog); document.body.appendChild(overlay); // Tab switch function (scoped to dialog) window.nsShowTab = function(tab) { document.getElementById("ns-content-nodes").style.display = tab === "nodes" ? "" : "none"; document.getElementById("ns-content-models").style.display = tab === "models" ? "" : "none"; const nodeBtn = document.getElementById("ns-tab-nodes"); const modelBtn = document.getElementById("ns-tab-models"); nodeBtn.style.borderBottomColor = tab === "nodes" ? "#4a4" : "transparent"; nodeBtn.style.color = tab === "nodes" ? "#4a4" : "#888"; nodeBtn.style.fontWeight = tab === "nodes" ? "bold" : "normal"; modelBtn.style.borderBottomColor = tab === "models" ? "#4a4" : "transparent"; modelBtn.style.color = tab === "models" ? "#4a4" : "#888"; modelBtn.style.fontWeight = tab === "models" ? "bold" : "normal"; }; ``` **Step 3: Extract existing content into buildNodesTabContent()** Move the existing badge bar + section rendering into a new function. Extract the block that builds the summary badges and sections (everything from the `
` badges to the end of the section tables) into: ```javascript function buildNodesTabContent(custom) { const safeToRemove = custom.filter((p) => p.status === "safe_to_remove"); const considerRemoving = custom.filter((p) => p.status === "consider_removing"); const unusedNew = custom.filter((p) => p.status === "unused_new"); const used = custom.filter((p) => p.status === "used"); const uninstalled = custom.filter((p) => p.status === "uninstalled"); let html = `
${safeToRemove.length} safe to remove
${considerRemoving.length} consider removing
${unusedNew.length} unused <1 month
${used.length} used
`; if (safeToRemove.length > 0) html += sectionHeader("Safe to Remove", "Unused for 2+ months", "#e44") + buildTable(safeToRemove, "safe_to_remove"); if (considerRemoving.length > 0) html += sectionHeader("Consider Removing", "Unused for 1-2 months", "#e90") + buildTable(considerRemoving, "consider_removing"); if (unusedNew.length > 0) html += sectionHeader("Recently Unused", "Unused for less than 1 month", "#68f") + buildTable(unusedNew, "unused_new"); if (used.length > 0) html += sectionHeader("Used", "", "#4a4") + buildTable(used, "used"); if (uninstalled.length > 0) html += sectionHeader("Uninstalled", "Previously tracked, no longer installed", "#555") + buildTable(uninstalled, "uninstalled"); return html; } ``` **Step 4: Write buildModelsTabContent()** Add this new function: ```javascript function buildModelsTabContent(modelData) { // Flatten for summary counts const allModels = modelData.flatMap((g) => g.models); const safeCount = allModels.filter((m) => m.status === "safe_to_remove").length; const considerCount = allModels.filter((m) => m.status === "consider_removing").length; const unusedNewCount = allModels.filter((m) => m.status === "unused_new").length; const usedCount = allModels.filter((m) => m.status === "used").length; let html = `
${safeCount} safe to remove
${considerCount} consider removing
${unusedNewCount} unused <1 month
${usedCount} used
`; if (allModels.length === 0) { html += `

No models tracked yet. Run a workflow to start.

`; return html; } for (const group of modelData) { if (group.models.length === 0) continue; const title = group.model_type.charAt(0).toUpperCase() + group.model_type.slice(1).replace(/_/g, " "); html += sectionHeader(title, `${group.models.length} model${group.models.length !== 1 ? "s" : ""}`, "#4a4"); html += buildModelTable(group.models); } return html; } function buildModelTable(models) { let html = ``; for (const m of models) { const { bg, hover } = STATUS_COLORS[m.status] || STATUS_COLORS.used; const lastSeen = m.last_seen ? new Date(m.last_seen).toLocaleDateString() : "—"; const statusLabel = { safe_to_remove: { text: "safe to remove", color: "#e44" }, consider_removing: { text: "consider removing", color: "#e90" }, unused_new: { text: "unused <1mo", color: "#68f" }, used: { text: "used", color: "#4a4" }, uninstalled: { text: "uninstalled", color: "#555" }, }[m.status] || { text: m.status, color: "#888" }; html += ``; } html += `
Model Executions Last Used Status
${escapeHtml(m.model_name)} ${m.count} ${lastSeen} ${statusLabel.text}
`; return html; } ``` **Step 5: Remove duplicated variable declarations** The variables `safeToRemove`, `considerRemoving`, `unusedNew`, `used`, `uninstalled` that were at the top of `showStatsDialog` are now inside `buildNodesTabContent` — remove them from `showStatsDialog`. Also remove the easter egg badge wiring from after `dialog.innerHTML = html` since it's now inside the nodes tab. Move it after the `nsShowTab` assignment, but target the badge which is inside `ns-content-nodes`: ```javascript // Easter egg: click "used" badge 5 times to show podium let eggClicks = 0; let eggTimer = null; const usedBadge = document.getElementById("nodes-stats-used-badge"); if (usedBadge) { usedBadge.addEventListener("click", () => { eggClicks++; clearTimeout(eggTimer); eggTimer = setTimeout(() => (eggClicks = 0), 1500); if (eggClicks >= 5) { eggClicks = 0; const allNodes = custom .flatMap((p) => p.nodes.map((n) => ({ ...n, pkg: p.package }))) .sort((a, b) => b.count - a.count); showPodium(allNodes.slice(0, 3), overlay); } }); } ``` Note: `custom` needs to be accessible here — declare it at the top of `showStatsDialog` before the html building: ```javascript const custom = data.filter((p) => p.package !== "__builtin__"); ``` **Step 6: Wire close button and pkg-row toggles** These are already present; make sure they remain after the new `dialog.innerHTML = html` assignment: ```javascript document.getElementById("nodes-stats-close").addEventListener("click", () => overlay.remove()); dialog.querySelectorAll(".pkg-row").forEach((row) => { row.addEventListener("click", () => { const detail = row.nextElementSibling; if (detail && detail.classList.contains("pkg-detail")) { detail.style.display = detail.style.display === "none" ? "table-row" : "none"; const arrow = row.querySelector(".arrow"); if (arrow) arrow.textContent = detail.style.display === "none" ? "▶" : "▼"; } }); }); ``` **Step 7: Manual test** 1. Restart ComfyUI 2. Open a workflow that uses a checkpoint (e.g. CheckpointLoaderSimple) 3. Queue a prompt 4. Click the Node Stats button → should see "Nodes" and "Models" tabs 5. Switch to Models tab → checkpoint should appear under "Checkpoints" with count ≥ 1 6. Other installed checkpoints with no usage should show as `unused_new` (or `safe_to_remove` if tracking has been running >2mo) **Step 8: Commit** ```bash git add js/nodes_stats.js git commit -m "feat: add Models tab with per-type usage stats" ``` --- ### Task 5: Final check and cleanup **Step 1: Run full test suite** ```bash python -m pytest tests/ -v ``` Expected: all PASS **Step 2: Verify reset clears model data** ```bash curl -X POST http://localhost:8188/nodes-stats/reset curl http://localhost:8188/nodes-stats/models | python3 -m json.tool # Expect: models with count=0 and status=unused_new (none tracked yet) ``` **Step 3: Commit if any cleanup needed** ```bash git add -p git commit -m "chore: cleanup after model tracking implementation" ```